Регулярные и нерегулярные ряды, гранулярность

Учимся видеть шаг ряда, выбирать гранулярность и приводить разрозненные события к ровной временной сетке.

Гранулярность — это шаг времени между точками ряда: секунда, минута, час, день, неделя или месяц. От неё зависит, какие паттерны вы вообще сможете увидеть.

Зачем выбирать гранулярность осознанно

Один и тот же поток событий можно агрегировать по-разному, и каждый уровень показывает своё. Продажи по секундам — это почти шум. По часам видна суточная активность. По дням проступает недельная сезонность. По месяцам — годовой тренд. Слишком мелкий шаг тонет в шуме, слишком крупный — прячет полезные паттерны. Выбор гранулярности — первое содержательное решение аналитика.

Гранулярность стоит выбирать не «по красоте графика», а под конкретное решение, которое будет принимать бизнес. Если служба доставки планирует число курьеров на каждый час, прогноз по дням бесполезен — нужен почасовой ряд. Если же финансовый отдел верстает бюджет на квартал, почасовые колебания только мешают, и осмысленнее смотреть на месячные итоги. Правило простое: гранулярность ряда должна совпадать с гранулярностью решения. Прогнозировать мельче, чем вы способны действовать, — потраченные впустую усилия; прогнозировать крупнее — значит остаться без нужной детализации.

Есть и техническая сторона. Чем мельче шаг, тем длиннее ряд и тем дороже обучение, но тем больше в нём сезонных слоёв (суточный, недельный). Чем крупнее шаг, тем короче ряд — а на коротких рядах сложным моделям попросту не на чем учиться. Так гранулярность незаметно определяет и выбор модели.

Регулярный против нерегулярного

Регулярный ряд имеет постоянный шаг: ровно один отсчёт в день. Нерегулярный состоит из событий в произвольные моменты: каждая покупка, каждый клик. Большинство классических моделей (сглаживание, ARIMA) требуют регулярной сетки, поэтому сырые события почти всегда сначала ресемплируют — группируют по бакетам и агрегируют.

В жизни данные приходят именно как нерегулярный поток: строки лога с покупками, записи кликов, транзакции. Сама по себе такая таблица — ещё не временной ряд, а «сырьё» для него. Превращение потока событий в ровную сетку отсчётов — обязательный подготовительный шаг, и от того, как вы его выполните, зависит всё последующее. Ошибка на этом этапе тихо протечёт во все модели сразу.

Ресемплинг руками

Пусть есть события продаж с метками в минутах. Сгруппируем их в часовые бакеты и посчитаем суммы. Это и есть ресемплинг к часовой сетке.

from collections import defaultdict

# (час, сумма продажи) — события внутри дня
events = [(9, 100), (9, 50), (10, 200), (12, 30), (12, 70), (12, 10)]

buckets = defaultdict(int)
for hour, amount in events:
    buckets[hour] += amount

# регулярная сетка 9..12, пустые часы = 0
grid = []
for h in range(9, 13):
    grid.append((h, buckets.get(h, 0)))

for h, total in grid:
    print(f"{h}:00 -> {total}")

Вывод:

9:00 -> 150
10:00 -> 200
11:00 -> 0
12:00 -> 110

Важная деталь: час 11:00 не встречался в событиях, но в регулярной сетке он обязан быть — со значением 0. Пропустить его нельзя, иначе шаг ряда «поедет» и модели увидят ложную картину.

Разберём код по шагам. Сначала defaultdict(int) складывает все продажи в пределах одного часа — это и есть агрегация по бакету. Затем отдельный цикл range(9, 13) строит полную сетку часов независимо от того, были в них события или нет; вызов buckets.get(h, 0) подставляет ноль там, где продаж не было. Именно разделение на «агрегацию» и «построение сетки» — суть ресемплинга. Если бы мы просто перебрали ключи словаря, час 11:00 бесследно исчез бы, и ряд из четырёх часов превратился бы в три — с неверным шагом.

Как работает под капотом

Ресемплинг — это два шага: построить полную сетку моментов от начала до конца с нужным шагом, а затем для каждого бакета применить агрегат (сумма, среднее, максимум, последнее значение). Сумма уместна для количеств (продажи, клики), среднее — для измерений (температура, загрузка CPU), последнее значение — для «уровней» (остаток на счёте). Выбор агрегата — содержательный, а не технический.

Отдельный вопрос — что считать «пустым» бакетом. Для потоковых количеств (число заказов в час) отсутствие событий честно означает ноль, и подстановка нуля корректна. Но для измерений-уровней (температура, цена, остаток) ноль был бы ложью: значение не «обнулилось», его просто не измерили. В таких случаях пропуск оставляют как пропуск и заполняют позже — например, переносом последнего известного значения вперёд (forward fill) или интерполяцией. Поэтому связка «агрегат + способ заполнения дыр» всегда выбирается вместе и под смысл величины.

Стоит помнить и про границы бакетов. Договорённость «час 9:00 включает события с 9:00 до 9:59» должна быть единой по всему ряду, иначе одно и то же событие может попасть то в один бакет, то в соседний. Эта мелочь редко обсуждается, но именно из-за неё расходятся цифры в разных отчётах по одним и тем же данным.

Полезная аналогия — фотография в движении. Мелкая гранулярность похожа на короткую выдержку: видно каждое дрожание, но кадр шумный и зернистый. Крупная гранулярность — это длинная выдержка: картинка гладкая, общий силуэт ясен, но мелкие детали смазаны в один тон. Аналитик каждый раз подбирает «выдержку» под то, что хочет разглядеть: суточный ритм нагрузки или годовой тренд выручки. И как в фотографии нельзя одной выдержкой поймать сразу всё, так и одной гранулярностью редко удаётся увидеть и быстрые, и медленные паттерны — поэтому один и тот же поток событий нередко анализируют сразу на нескольких уровнях.

Частые ошибки

  • Забыть про пустые бакеты — сетка перестаёт быть регулярной.
  • Суммировать там, где нужно среднее (например, агрегируя температуру складыванием).
  • Выбрать слишком мелкую гранулярность и утонуть в шуме вместо паттерна.
  • Подставлять ноль в пустой бакет для величины-уровня (температура, остаток), где пропуск нужно заполнять переносом или интерполяцией.
  • Прогнозировать мельче, чем бизнес способен действовать, — детализация, которой никто не воспользуется.

Итоги

  • Гранулярность определяет, какие паттерны видны; выбирайте её под задачу.
  • Регулярный ряд имеет постоянный шаг; нерегулярные события нужно ресемплировать.
  • Пустые бакеты заполняйте явно (часто нулём), чтобы сохранить регулярность.
  • Гранулярность ряда должна совпадать с гранулярностью бизнес-решения.
  • Агрегат и способ заполнения дыр выбираются вместе и под смысл величины.
Проверьте себя
1. Что нужно сделать с пустым часом при ресемплинге событий в часовую сетку?
AПропустить его
BЯвно добавить со значением 0 (или иным нейтральным)
CУдалить соседние часы
DЗаполнить максимумом
2. Какой агрегат уместен при ресемплинге температуры по часам?
AСумма
BСреднее
CПроизведение
DКоличество строк