12 · A winning core method: collaborative-filtering scores as features
Goal
Calculate an ItemCF similarity by hand, compare with original code, and explain a real recommendation. Feature engineering here means inspectable numbers: item counts by market, rating statistics, and scores from models trained on different market corpora.
Setup
Editable implementation · Original winning icf_v2 file.
English companion. All instructional Markdown is translated. Experiment code, Chinese language examples, execution counts, and saved outputs remain as in the source edition; historical output text may therefore be Chinese. The initial setup cell anchors the working directory and Python import path for this companion's deeper folder; it does not change the experiment. Saved outputs are historical evidence, not a new run. Run from the top after preparing the required data and models.
# English companions live one directory deeper; keep the original experiment code unchanged.
from pathlib import Path as _TranslationPath
import os as _translation_os
import sys as _translation_sys
_translation_root = next((p for p in [_TranslationPath.cwd(), *_TranslationPath.cwd().parents]
if (p / "reclab").is_dir()), None)
if _translation_root is None:
raise RuntimeError("Open this notebook from inside the Search Recommendation Ads Lab checkout.")
_translation_os.chdir(_translation_root)
if str(_translation_root) not in _translation_sys.path:
_translation_sys.path.insert(0, str(_translation_root))
from pathlib import Path
import sys, json, inspect
ROOT = Path.cwd()
if not (ROOT / "reclab").is_dir():
ROOT = ROOT.parent
sys.path.insert(0, str(ROOT))
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, Code
from reclab.competition import data
from reclab.competition.teaching import load_run, style, bar_scores
style()
pd.set_option("display.max_columns", 12)
pd.set_option("display.max_rows", 40)
run, rows, features = load_run()
print("实际训练快照:", run.name)Saved notebook output · not executed on this page
实际训练快照: 20261030T115556803593-cloud-restore
Step 1 · Calculate IUF by hand
For binary history matrix B and user u with history length n_u:
S_ij = sum_u[B_ui B_uj / log(1+n_u)] / sqrt(count_i count_j), with diagonal set to 0.
Highly active users connect many items, so their co-occurrences receive lower weight. A user's candidate score sums similarities from history items to the candidate.
B = np.array([[1., 1., 0.], [0., 1., 1.], [1., 1., 1.]])
weights = 1 / np.log1p(B.sum(axis=1))
cooccur = B.T @ (weights[:, None] * B)
counts = B.sum(axis=0)
similarity = cooccur / np.sqrt(counts[:, None] * counts[None, :])
np.fill_diagonal(similarity, 0)
manual_01 = (1 / np.log(3) + 1 / np.log(4)) / np.sqrt(2 * 3)
assert np.isclose(similarity[0, 1], manual_01)
display(pd.DataFrame(similarity, index=["item A", "item B", "item C"], columns=["item A", "item B", "item C"]))
print("用户历史[A,B] -> C:", similarity[0, 2] + similarity[1, 2])Saved notebook output · not executed on this page
| item A | item B | item C | |
|---|---|---|---|
| item A | 0.000000 | 0.666093 | 0.360674 |
| item B | 0.666093 | 0.000000 | 0.666093 |
| item C | 0.360674 | 0.666093 | 0.000000 |
用户历史[A,B] -> C: 1.026766260457602
Step 2 · Numerical comparison with pinned winning code
Extract only the checksum-verified original class for comparison. Do not execute its script entry point, installer, or path configuration. IUF and rating cosine are different scores. During queries, the original rating-cosine method also sums history contributions without rating weights.
from reclab.competition.reference import verify_references
parity = verify_references()
display(parity)
from reclab.competition.memory import ItemCF
display(Code(inspect.getsource(ItemCF.fit), language="python"))Saved notebook output · not executed on this page
lgn is already to go(dropout:False)
{'memory': {'iuf': {'max_score_error': 1.6360444821117426e-07, 'pairs': 384},
'rating': {'max_score_error': 8.096896113585217e-07, 'pairs': 384}},
'graph': {'loss_error': 0.0, 'gradient_max_error': 4.656612873077393e-10},
'data': 't1 real training histories',
'users': 8} def fit(self, frame):
self.users, self.items, ratings = encode(frame)
binary = ratings.copy()
binary.data[:] = 1
self.history = binary
values = ratings if self.kind == "rating" else binary
if self.kind == "iuf":
weight = 1 / np.log1p(np.diff(binary.indptr))
weighted = values.multiply(weight[:, None]).tocsr()
else:
weighted = values
sim = (values.T @ weighted).tocsr()
norm = np.sqrt(np.asarray(values.power(2).sum(axis=0)).ravel())
inv = 1 / np.maximum(norm, 1e-12)
sim = sim.multiply(inv[:, None]).multiply(inv[None, :]).tocsr()
sim.setdiag(0)
sim.eliminate_zeros()
if self.top_k:
for row in range(sim.shape[0]):
a, b = sim.indptr[row:row + 2]
if b - a > self.top_k:
ids = np.lexsort((sim.indices[a:b], -sim.data[a:b]))[self.top_k:]
sim.data[a + ids] = 0
sim.eliminate_zeros()
self.similarity = sim
self.popularity = np.asarray(binary.sum(axis=0)).ravel()
return selfStep 3 · Decompose a real candidate's score
Load artifacts actually trained on the full data without retraining existing models. Similarity contributions must sum to the candidate score.
Change USER to another user in the same market.
from reclab.competition.experiment import load_memory
model = load_memory(run / "memory/t1_local")
USER = rows.query("market == 't1'").userId.iloc[0]
subset = rows[(rows.market == "t1") & rows.userId.eq(USER)].copy()
subset["score"] = model.score(subset)
display(subset.sort_values("score", ascending=False).head(10))
ITEM = subset.sort_values(["score", "itemId"], ascending=[False, True]).itemId.iloc[0]
explanation = pd.DataFrame(model.explain(USER, ITEM))
display(explanation.head(10))
assert np.isclose(explanation.similarity.sum(), subset.loc[subset.itemId.eq(ITEM), "score"].iloc[0])Saved notebook output · not executed on this page
| userId | itemId | market | label | partition | score | |
|---|---|---|---|---|---|---|
| 80 | t1U1005570 | P1000949 | t1 | 0 | fit | 0.091353 |
| 0 | t1U1005570 | P1005811 | t1 | 0 | fit | 0.075901 |
| 73 | t1U1005570 | P1021121 | t1 | 0 | fit | 0.069835 |
| 61 | t1U1005570 | P1033291 | t1 | 0 | fit | 0.068454 |
| 57 | t1U1005570 | P1012526 | t1 | 0 | fit | 0.050017 |
| 79 | t1U1005570 | P1008851 | t1 | 0 | fit | 0.039970 |
| 56 | t1U1005570 | P1035707 | t1 | 0 | fit | 0.036421 |
| 43 | t1U1005570 | P1021561 | t1 | 0 | fit | 0.032723 |
| 71 | t1U1005570 | P1012765 | t1 | 0 | fit | 0.032284 |
| 66 | t1U1005570 | P1029597 | t1 | 0 | fit | 0.031917 |
| history_item | similarity | |
|---|---|---|
| 0 | P1031263 | 0.068887 |
| 1 | P1020517 | 0.012866 |
| 2 | P1021106 | 0.006505 |
| 3 | P1014278 | 0.003095 |
| 4 | P1030052 | 0.000000 |
Step 4 · What features describe one user–item pair?
A row combines item counts across markets, t1's own statistics, and scores from several models. IDs and labels are excluded from ranking features. There is no real attribute table; this reproduction does not fabricate categories or user profiles.
position = subset.index[0]
display(features.loc[position].rename("value").to_frame())
assert not {"label", "userId", "itemId", "partition"} & set(features)
table = pd.read_csv(run / "metrics.csv")
selected = table[(table.partition == "select") & (table.market.isin(["t1", "t2"])) & table.model.isin(["icf_local", "icf_s1", "icf_s3", "icf_all_iuf"])]
display(selected.pivot(index="model", columns="market", values="ndcg10"))Saved notebook output · not executed on this page
| value | |
|---|---|
| s1_train_item_count | 7.000000 |
| s1_train_5core_item_count | 0.000000 |
| s2_train_item_count | 7.000000 |
| s2_train_5core_item_count | 2.000000 |
| s3_train_item_count | 11.000000 |
| s3_train_5core_item_count | 2.000000 |
| t1_train_item_count | 19.000000 |
| t1_train_5core_item_count | 11.000000 |
| target_user_rating_count | 5.000000 |
| target_user_rating_mean | 4.800000 |
| target_item_rating_count | 19.000000 |
| target_item_rating_mean | 3.736842 |
| t2_train_item_count | 6.000000 |
| t2_train_5core_item_count | 0.000000 |
| icf_local | 0.075901 |
| icf_s1 | 0.046836 |
| icf_s3 | 0.055958 |
| icf_all_iuf | 0.030729 |
| icf_all_rating | 0.093117 |
| graph_layers0 | -0.083863 |
| graph_layers2 | 0.229921 |
| market | t1 | t2 |
|---|---|---|
| model | ||
| icf_all_iuf | 0.684087 | 0.586053 |
| icf_local | 0.673879 | 0.562058 |
| icf_s1 | 0.683179 | 0.582342 |
| icf_s3 | 0.675800 | 0.565388 |
columns = ["icf_local", "icf_s1", "icf_s3", "icf_all_iuf", "icf_all_rating"]
correlation = features[columns].corr()
fig, ax = plt.subplots(figsize=(7, 5))
image = ax.imshow(correlation, vmin=-1, vmax=1, cmap="BrBG")
ax.set_xticks(range(len(columns)), columns, rotation=35, ha="right")
ax.set_yticks(range(len(columns)), columns)
for i in range(len(columns)):
for j in range(len(columns)):
ax.text(j, i, f"{correlation.iloc[i,j]:.2f}", ha="center", va="center", color="white" if abs(correlation.iloc[i,j]) > .65 else "black")
ax.set_title("Pre-rank feature correlation · all official validation candidates")
fig.colorbar(image, ax=ax, label="Pearson correlation")
fig.tight_layout()
plt.show()Saved notebook output · not executed on this page
<Figure size 770x550 with 2 Axes>

Compare actual scores with the paper
Below are ItemCF results on the full official validation set, trained without target validation labels. Numbers from the paper's Table 4 are fixed references, never mixed with in-training LightGBM scores. Combined results are weighted by user count. Local item-ID tie-breaking is deterministic; bit-for-bit agreement with the original solution is not required.
full = table[(table.partition == "all_official_valid") & (table.model == "icf_local")][["market", "users", "ndcg10"]].copy()
full["paper_table4"] = full.market.map({"t1": .6776, "t2": .5589, "combined": .5980})
full["local_minus_paper"] = full.ndcg10 - full.paper_table4
display(full)
print("本地合并 nDCG@10:", round(float(full.loc[full.market.eq("combined"), "ndcg10"].iloc[0]), 6))Saved notebook output · not executed on this page
| market | users | ndcg10 | paper_table4 | local_minus_paper | |
|---|---|---|---|---|---|
| 15 | t1 | 2697 | 0.677494 | 0.6776 | -0.000106 |
| 16 | t2 | 5482 | 0.558946 | 0.5589 | 0.000046 |
| 17 | combined | 8179 | 0.598037 | 0.5980 | 0.000037 |
本地合并 nDCG@10: 0.598037
Check and continue
Must one of two features with correlation near 1 be removed?