В CVR пользователь может конвертироваться через минуты, дни или недели после клика, поэтому разметка «как есть» часто превращает будущие positive в ложные negative. Это критично для рекламы, рекомендаций, marketplace-воронок и A/B-тестов, где модель обучается на неполных логах.
Проблема: лейбл может быть еще не созрел
Для клика в момент
click_time = t мы хотим оценить:P(conversion | click, x)Но на дату сборки датасета
T мы знаем только одно из двух:* конверсия уже произошла до
T* конверсии пока не видно
Второй случай не равен
converted = 0. Это censored observation: объект наблюдался недостаточно долго.Типичная ошибка:
converted = 1, если conversion_time - click_time <= 7d
converted = 0, иначе
Так свежие клики получают искусственно заниженный CVR, модель учится на ложных negative, offline-метрики зависят от «зрелости» среза, а production-калибровка плывет: модель предсказывает full-window CVR, а мониторинг видит partial-window CVR.
Baseline: обучаться только на mature data
Если целевой горизонт - конверсия за 7 дней, а данные доступны до
2025-01-31, то в train стоит брать клики не позже 2025-01-24.Плюсы:
* честные лейблы
* простая валидация
* легко дебажить пайплайн и leakage
Минусы:
* теряем свежие данные
* хуже адаптация к сезонности и изменению трафика
* при длинном conversion lag train сильно устаревает
Практический совет: явно храните в feature store или training dataset поля
event_time, label_observed_until, horizon и label_age. Без них невозможно воспроизвести разметку и понять, почему CVR изменился после очередного retraining.Более сильный подход: моделировать задержку
Можно разложить задачу на вероятность конверсии и распределение delay:
P(y = 1, delay <= H | x)Например:
* CVR-модель оценивает вероятность самой конверсии
* delay-модель оценивает
P(delay <= age | y=1, x)Это ближе к survival analysis: есть событие, time-to-event и censored observations. Такой подход особенно полезен, если задержка зависит от категории товара, канала, географии, цены, устройства или ретаргетинга.
Альтернатива - дискретный hazard:
P(conversion at day k | no conversion before day k, x)Тогда клик, наблюдавшийся только 2 дня, все еще полезен для обучения первых двух шагов, а не выбрасывается целиком. Trade-off: модель и inference становятся сложнее, зато меньше потерь данных и честнее работа с длинным хвостом конверсий.
Оценка: test тоже должен быть mature
Если
horizon = 7d, то holdout должен содержать только объекты, для которых прошло минимум 7 дней после клика. Иначе вы измеряете не качество модели, а незрелость лейблов.Хорошая схема:
train: clicks [D0, D1]
validation: clicks [D2, D3]
label cutoff: >= D3 + horizon
В production дополнительно смотрите метрики по delay buckets:
*
0-1h*
1-24h*
1-3d*
3-7d*
7d+Так видно, где ломается система: в быстрых конверсиях, длинном хвосте, data freshness, attribution или из-за цензурированных лейблов. Для A/B-тестов это особенно важно: ранний readout может завысить эффект модели, которая хорошо ловит быстрые конверсии, но проигрывает на полном окне.
Вывод:
Delayed feedback в CVR - это не косметика разметки, а инженерное ограничение ML-системы: без mature labels, явного label cutoff и корректной валидации модель оптимизирует артефакты логирования вместо реальной конверсии.