Признаки на момент прогноза: как собрать историческую таблицу без будущего
Для исторического заказа нужен признак, известный в момент прогноза. Покажем point-in-time соединение в pandas и разберём, почему время события иногда не подходит для такого JOIN.
Последняя запись сегодня не годится для вчерашнего прогноза
Вы обучаете модель повторной покупки и добавляете клиенту его текущий баланс бонусов. Таблица выглядит корректно: один клиент, одно число, пропусков мало. Но баланс успел измениться после той даты, когда модель должна была дать прогноз. Обучение получает сведения из будущего, а при запуске эти сведения недоступны.
Для каждой строки обучения зафиксируйте момент решения. Для каждого значения признака сохраните время, с которого оно стало доступно системе. Затем выберите последнюю запись того же клиента, доступную не позже решения. Такое восстановление состояния часто называют point-in-time join. По смыслу это чтение исторического снимка, даже если физически он хранится в журнале изменений.
Время доступности отличается от времени события. Покупка могла произойти в 10:00, а попасть в витрину в 10:20. Прогноз в 10:10 не мог использовать её. Если история хранит только время покупки, аккуратное соединение по нему всё равно даст утечку. Нужна дата поступления либо воспроизводимая историческая версия витрины.
Присоединим доступную версию через merge_asof
В pandas merge_asof умеет искать ближайшую предыдущую запись. Направление backward означает «время справа не больше времени слева», а by ограничивает поиск клиентом. Оба набора сортируются по временному ключу глобально. Пример использует минуты одной условной даты, чтобы результат легко проверить.
Клиенту a в минуту 10 доступны 100 бонусов. Запись о 500 бонусах появляется в минуту 12 и годится только для более позднего прогноза. Клиент b в минуту 9 ещё не имеет доступной записи. Для него оставляем пропуск: подстановка более позднего значения снова нарушила бы задачу.
import pandas as pd
decisions = pd.DataFrame({"id": ["a", "a", "b"], "at": [10, 15, 9]})
features = pd.DataFrame({"id": ["a", "a", "b"],
"available_at": [5, 12, 11],
"bonus": [100, 500, 70]})
joined = pd.merge_asof(
decisions.sort_values("at"), features.sort_values("available_at"),
left_on="at", right_on="available_at", by="id", direction="backward"
)
print(joined.to_string(index=False))
a = joined[joined.id == "a"]
assert a.bonus.tolist() == [100, 500]
assert joined.loc[joined.id == "b", "bonus"].isna().all()
assert (joined.available_at.dropna() <= joined.loc[joined.available_at.notna(), "at"]).all()
В результате a получает 100 в минуту 10 и 500 в минуту 15. У b в минуту 9 будет NaN. Это отсутствие исторического знания, а не доказанный нулевой баланс. Решение о заполнении пропуска должно соответствовать предметному смыслу и входам будущего сервиса.
Отдельно определите допустимую давность признака. Если баланс обновлялся год назад, формально это последняя предыдущая запись, но бизнесу может требоваться более свежий снимок. Параметр tolerance задаёт максимальное расстояние между временами; при datetime-ключах задавайте его через Timedelta. Возраст полезно также сохранить отдельным столбцом и сравнивать его между обучением и эксплуатацией.
Проверьте одинаковые временные метки. Когда два обновления одного клиента имеют одно время, нужна последовательность событий или другой ключ, задающий последнюю версию. Простая сортировка по времени не определит, какое значение истинное. Предварительно приведите журнал к однозначной записи на клиента и момент доступности.
В итоговую проверку включите несколько ручных историй: обновление до решения, после него, отсутствие истории и запоздалое поступление. Сверьте их с тем, что реально вернул бы сервис в тот момент. Хорошая временная валидация не исправляет признаки, уже собранные из будущего; сначала восстановите доступные данные, затем стройте окна обучения.
Для следующего шага пригодится временная валидация с учётом горизонта.
В платном курсе «ML Engineer / Data Scientist: табличные модели в продакшн» вы подготовите табличные данные, создадите признаки и проверите валидацию на утечки. Такая практика связывает способ сборки исторической таблицы с качеством будущего прогноза. Откройте программу курса.
Продолжить на практике
- ML Engineer / Data Scientist: табличные модели в продакшн ↗
Практический курс для тех, кто хочет получить навыки ML Engineer / Data Scientist на реальных табличных задачах. В курсе Вы не просто обучите CatBoost/LightGBM, а соберёте полноценный production-style пайплайн: подготовка данных, feature engineering (генерация признаков), корректная валидация, защита от leakage (утечек), тюнинг через Optuna, интерпретация через SHAP, batch-инференс и REST API для предсказаний.
