Прогнозирование спроса на такси: временные ряды, лаги и CatBoost

Учебный проект по Data Science: прогноз количества заказов такси на следующий час по историческим данным.

Прогнозирование спроса на такси: временные ряды, лаги и CatBoost

Контекст и цель

В этом проекте я решал задачу прогнозирования количества заказов такси на следующий час по историческим данным. Формально это регрессия, но по сути — классическая работа с временным рядом.

Репозиторий с ноутбуком проекта:

Notebook: forecasting_taxi_orders/project_df_20260228_fin.ipynb

Ссылка на проект: перейти

Постановка задачи

Дано: история заказов с временной меткой.

Нужно: предсказать num_orders на следующий час.

Метрика качества: RMSE (чем меньше — тем лучше).

Для временных рядов критично не допускать утечки будущего в обучение. Поэтому обычный train_test_split здесь не подходит: нужно разделение с сохранением временного порядка.

Данные и предобработка

1) Приведение временной шкалы

Первое, что я сделал в задаче:

  • приводим столбец времени к datetime
  • устанавливаем его индексом
  • приводим ряд к равномерной сетке (например, почасовой)
Почасовой ресемплинг python
1
2
3
4
5
6
7
# df: исходные данные с временем и num_orders

df["datetime"] = pd.to_datetime(df["datetime"])
df = df.set_index("datetime").sort_index()

# приводим к почасовой частоте
hourly = df.resample("1H").sum()

2) Анализ ряда (EDA)

На этапе EDA необходимо проверить:

  • тренд и сезонности (суточная/недельная)
  • выбросы и аномальные пики
  • стабильность дисперсии
Чаще всего в заказах такси видна яркая суточная сезонность (день/ночь) и эффект выходных.

Подготовка признаков: как превратить ряд в табличные данные

Большинство моделей градиентного бустинга (CatBoost/LightGBM/XGBoost) ожидают табличные признаки. Поэтому временной ряд нужно «развернуть» в supervised-формат.

Я использовал два основных семейства признаков:

  1. Лаги — значения ряда в прошлом (t-1, t-2, …)
  2. Скользящие статистики — например, rolling mean
Функция генерации лагов и rolling mean python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def make_features(data: pd.DataFrame, max_lag: int, rolling_window: int) -> pd.DataFrame:
    df_feat = data.copy()

    # календарные признаки
    df_feat["hour"] = df_feat.index.hour
    df_feat["dayofweek"] = df_feat.index.dayofweek

    # лаги
    for lag in range(1, max_lag + 1):
        df_feat[f"lag_{lag}"] = df_feat["num_orders"].shift(lag)

    # скользящее среднее (сдвигаем, чтобы не подглядывать в текущий час)
    df_feat["rolling_mean"] = (
        df_feat["num_orders"].shift(1).rolling(rolling_window).mean()
    )

    return df_feat
Ключевой момент: любой rolling/агрегации нужен shift(1), иначе в признаках окажется текущее значение целевой переменной.

После генерации признаков неизбежно появляются NaN в начале ряда (из-за лагов и окна). Их нужно удалить:

Удаление NaN после генерации признаков python
1
2
features = make_features(hourly, max_lag=24, rolling_window=24)
features = features.dropna()

Разделение train/valid/test без утечки

Правильный подход — последнюю часть ряда оставить под тест, а до неё — обучаться и валидироваться.

Для выбора гиперпараметров можно использовать TimeSeriesSplit.

Обучение моделей и сравнение

Я сравнивал несколько базовых подходов:

  • простая baseline-модель
  • линейная регрессия (как sanity check)
  • бустинги: LightGBM / CatBoost

Почему бустинг — сильный кандидат:

  • хорошо работает на табличных данных
  • «переваривает» много лаговых признаков
  • умеет моделировать нелинейности и взаимодействия (час × день недели × лаги)
Пример обучения модели (схема) python
1
2
3
4
5
6
from sklearn.metrics import mean_squared_error

model.fit(X_train, y_train)
pred = model.predict(X_valid)
rmse = mean_squared_error(y_valid, pred, squared=False)
print("RMSE:", rmse)
Итог проекта — модель, которая проходит требование по качеству на тесте (RMSE ниже заданного порога) и демонстрирует адекватное поведение на пиках/просадках спроса.

Что важно в результате (и что я вынес)

  1. Временной порядок важнее всего. Ошибка в разбиении или в rolling легко даст нереалистично хорошую метрику.
  2. Фичи решают. В задачах прогнозирования на 1 шаг вперёд лаги + rolling часто дают большой прирост.
  3. Календарные признаки — must-have. Без hour и dayofweek модель хуже ловит сезонность.
  4. Baseline обязателен. Он защищает от ситуации «сложная модель хуже, чем простое правило».