RECOMMENDATION / NOTEBOOK LESSON

12 · A winning core method: collaborative-filtering scores as features

Reproduce ItemCF formulas and inspect real cross-market ranking features.

Real-data experimentRead-only · saved experiment

Saved outputs come from recorded teaching runs; opening this page does not rerun an experiment. The code and original outputs are preserved; original-language data and annotations may remain.

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.

PYTHON · 01
# 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))
PYTHON · 02
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.

PYTHON · 03
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.

PYTHON · 04
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 self

Step 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.

PYTHON · 05
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.

PYTHON · 06
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
PYTHON · 07
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>
Saved notebook figure

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.

PYTHON · 08
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?

ReasoningCorrelation alone cannot decide. Run a fixed-configuration ablation on select users; the features may complement each other in a market or a long-tail segment. Do not repeatedly choose features by inspecting audit.
Changing weights in `ItemCF.fit` or setting `top_k` changes scores and requires regenerating features. Continue to 13 for trainable representations.

GO TO THE SOURCE

Sources for this lesson

Official task and dataEnglish

WSDM Cup 2022 · Official XMRec Task

Official task, data and evaluation protocol

Official cross-market recommendation task. Only local public-split results are reported, not hidden leaderboard scores.

Solution authors’ codeEnglish

OPDAI · XMRec Winning Solution

Core methods from the authors’ solution

Selected sparse ItemCF, graph retrieval and ranking methods. Not the complete winning ensemble.

Completion marks are optional and stored only in this browser. Reading a page does not mark it complete.