Офлайн NDCG вырос, а онлайн click-through rate упал — почините этот скрипт оценки.
Офлайн-оценка показывает крупный прирост NDCG у нового ранкера, но онлайн A/B-тест даёт падение click-through rate. В логе одна строка на показ, выданный текущим продовым ранкером, с полями session_id, меткой времени served_at и бинарной меткой clicked.
Ограничения — лог является единственными доступными данными, сам ранкер заведомо исправен, а реализация метрики ниже численно корректна.
logs = pd.read_parquet("impressions.parquet") # только то, что показал живой ранкер
train, test = train_test_split(logs, test_size=0.2, random_state=0)
model = fit_ranker(train)
def session_ndcg(df, k=10):
scored = df.assign(score=model.predict(df)).sort_values("score", ascending=False)
top = scored.head(k)
gains = top["clicked"] / np.log2(np.arange(2, len(top) + 2))
return gains.sum() / ideal_dcg(df, k)
print(logs.groupby("session_id").apply(session_ndcg).mean())
Найдите три изъяна, из-за которых офлайн NDCG растёт при падающем онлайн click-through rate, и исправьте их.
Скрипт скорит весь лог, включая обучающие строки, режет выборку случайно, и будущие сессии утекают назад, и доверяет логированным кликам, хотя видели только показанное старым ранкером. Нужно резать по времени, скорить отложенные сессии и перевзвешивать клики обратной пропенсити.
- ✗Резать логи взаимодействий случайно вместо разбиения по времени
- ✗Считать метрику на строках, на которых модель обучалась
- ✗Считать логированные клики несмещёнными метками релевантности
- →Как построить контрфактическую оценку, лучше предсказывающую онлайн-прирост?
- →Какой из трёх изъянов, по-вашему, сильнее всего завышает офлайн NDCG?
Скрипт ломается в трёх местах сразу, и все три завышают офлайн-число.
расходятся по обеим частям.
на верхней позиции отражает показ не меньше, чем релевантность.
- Оценка идёт по
logs, а не поtest— модель скорит строки, на которых обучалась. train_test_splitрежет случайно — будущие сессии попадают в обучение, а строки одной сессии- Клики берутся как несмещённые метки — в логе есть только то, что показал старый ранкер, и клик
cutoff = logs["served_at"].quantile(0.8)
train = logs[logs["served_at"] < cutoff]
test = logs[logs["served_at"] >= cutoff]
model = fit_ranker(train)
def session_ndcg(df, k=10):
scored = df.assign(score=model.predict(df)).sort_values("score", ascending=False)
top = scored.head(k)
weights = 1.0 / top["propensity"] # обратная вероятность просмотра
gains = top["clicked"] * weights / np.log2(np.arange(2, len(top) + 2))
return gains.sum() / ideal_dcg(df, k)
print(test.groupby("session_id").apply(session_ndcg).mean())
Пропенсити оценивают по рандомизированным слотам выдачи. Даже после этого офлайн-число остаётся оценкой — окончательное решение принимает A/B-тест.