Час 23 рядом с часом 0: как проверить циклический признак для модели
Модель прогнозирует нагрузку на поддержку по часу суток. Если передать час обычным числом, линейная регрессия видит расстояние между 23 и 0 равным двадцати трём. Между тем это соседние часы. Получается искусственный разрыв ровно там, где поток обращений может меняться плавно.
Причина находится в представлении признака. Час — положение внутри повторяющегося цикла. Для модели, использующей расстояния или линейные комбинации, полезно передать эту геометрию явно. Два числа, синус и косинус угла, располагают часы на окружности и соединяют конец суток с началом.
Делим на длину периода
Для часа от 0 до 23 период равен 24. Угол вычисляется как 2π, умноженные на час и делённые на 24. Деление на 23 ошибочно склеит часы 0 и 23 в одну точку, хотя между ними есть час. Для дня недели от 0 до 6 аналогичный период равен 7.
Один синус недостаточен: он принимает одинаковые значения в разных частях круга. Косинус вместе с синусом задаёт положение однозначно в пределах периода. После этого линейная модель может описывать плавный суточный подъём и спад с настраиваемой фазой. Свободный член задаёт общий уровень нагрузки.
Такая пара предполагает гладкую периодичность. Она не обязана описывать резкое открытие офиса в девять утра или два независимых пика. В этих случаях можно сравнить категории часов, дополнительные гармоники или другой класс модели. Сначала проверим базовую гипотезу на понятном примере.
Обучение на прошлых днях, проверка на следующих
Сгенерируем двенадцать суток с суточной волной нагрузки и небольшим шумом. Первые десять дней используем для обучения, последние два — для проверки. Обе линейные модели получают одинаковые строки: одна обычный час, вторая синус и косинус. Это позволяет увидеть влияние представления признака, сохранив способ обучения.
import numpy as np
rng = np.random.default_rng(11)
hour = np.tile(np.arange(24), 12)
day = np.repeat(np.arange(12), 24)
angle = 2*np.pi*hour/24
target = 25 + 9*np.cos(angle-.4) + rng.normal(0, 1.2, len(hour))
X_raw = np.column_stack([np.ones(len(hour)), hour])
X_cycle = np.column_stack([np.ones(len(hour)), np.sin(angle), np.cos(angle)])
train, test = day < 10, day >= 10
results = {}
for name, X in [('raw_hour',X_raw),('cyclic',X_cycle)]:
coef = np.linalg.lstsq(X[train], target[train], rcond=None)[0]
pred = X[test]@coef
mae = float(np.abs(target[test]-pred).mean())
midnight = np.isin(hour[test],[23,0,1])
edge_mae = float(np.abs(target[test][midnight]-pred[midnight]).mean())
results[name] = mae
print(name, 'MAE', round(mae,3), 'midnight_MAE', round(edge_mae,3))
points = np.column_stack([np.sin(2*np.pi*np.arange(24)/24),
np.cos(2*np.pi*np.arange(24)/24)])
d_23_0 = np.linalg.norm(points[23]-points[0])
d_0_1 = np.linalg.norm(points[0]-points[1])
print('neighbor_distances', round(float(d_23_0),6), round(float(d_0_1),6))
assert np.isclose(d_23_0,d_0_1)
assert results['cyclic'] < results['raw_hour']Расстояния между 23 и 0 и между 0 и 1 совпадают: около 0,261052. Это проверка представления времени. Отдельно код измеряет настоящую ошибку двух обученных регрессий на будущих сутках. На заданной плавной волне циклические признаки подходят лучше, что проверяется сравнением вычисленных MAE.
Хороший результат здесь следует из структуры синтетической задачи. На рабочем потоке сделайте такое же сравнение на нескольких временных окнах. Отдельно посмотрите часы около полуночи, начало рабочего дня и выходные. Уменьшение общей MAE может скрыть ухудшение периода, где отдел особенно чувствителен к нехватке сотрудников.
Какой именно час нужен бизнесу
Для расписания офиса важен местный час, а не час UTC. Приведите timestamp к нужному часовому поясу до извлечения часа. Если клиенты находятся в разных регионах, единый час сервера смешает утро одной аудитории с вечером другой. Часовой пояс или регион может стать отдельным признаком.
При сезонном переводе часов местные сутки иногда имеют необычную длительность, а одинаковое локальное время может встретиться дважды. Храните исходный момент времени с поясом для соединения событий. Циклический час описывает поведение по циферблату, но не заменяет timestamp и не должен становиться уникальным ключом записи.
Когда добавить взаимодействия
У рабочей субботы и обычного понедельника суточная форма нагрузки может различаться. Если линейной модели просто добавить флаг выходного, она сдвинет общий уровень, но сохранит ту же форму волны. Для разных форм нужны взаимодействия этого флага с синусом и косинусом либо более гибкое представление.
Для двух суточных пиков можно добавить вторую гармонику: синус и косинус удвоенного угла. Число гармоник выбирайте на валидации. Слишком гибкое представление способно подстроиться под шум, особенно при короткой истории. В разборе временных признаков scikit-learn также сравниваются категории часов и периодические сплайны.
Подготовку циклических признаков сохраните вместе с моделью: длину периода, исходное поле, часовой пояс и порядок выходных столбцов. Тогда batch и API будут получать одинаковые числа. Для проверки достаточно нескольких опорных часов и сравнения ответов общего pipeline, включая границу суток.
Для запущенного примера MAE обычного часа составляет около 5,062, циклического — 0,875. В часах 23, 0 и 1 ошибки равны примерно 6,662 и 0,733 соответственно. Числа вычисляются на последних двух сутках, а коэффициенты получены на предыдущих десяти. Сохраните такой же раздельный отчёт в своём эксперименте: он покажет, помогло ли преобразование именно на границе, которая вызвала исходную проблему.
В платном курсе «ML Engineer / Data Scientist: табличные модели в продакшн» вы отработаете feature engineering, валидацию без утечек и сравнение моделей, затем соберёте воспроизводимый inference pipeline. Откройте программу курса, чтобы научиться проверять полезность новых признаков и переносить удачные преобразования в batch-инференс и REST API.
Продолжить на практике
- ML Engineer / Data Scientist: табличные модели в продакшн ↗
Практический курс для тех, кто хочет получить навыки ML Engineer / Data Scientist на реальных табличных задачах. В курсе Вы не просто обучите CatBoost/LightGBM, а соберёте полноценный production-style пайплайн: подготовка данных, feature engineering (генерация признаков), корректная валидация, защита от leakage (утечек), тюнинг через Optuna, интерпретация через SHAP, batch-инференс и REST API для предсказаний.
