Раздел 42 · Аналитика данных с нуля

Временные ряды на SQL: тренд, сезонность, скользящее среднее, неделя к неделе и год к году

middle~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Мира · маркетолог магазина настолок «Кубик»

В декабре у нас был рекорд, а в январе заказов на 41% меньше. Мы теряем покупателей? И ещё все говорят, что летом настолки не покупают. Это правда или отговорка для плохих месяцев?

Мира смотрит на два соседних месяца и делает вывод о бизнесе. Так делают почти все, и почти всегда это ошибка. Заказы по дням или месяцам это временной ряд, и у него свои правила чтения. В уроке про ловушки мы чистили данные. Сегодня данные чистые, но врать может само сравнение.

База boardshop: заказы «Кубика» с января 2025 по июнь 2026, полтора года. Заказ считаем без отменённых, status <> 'cancelled'.

Море у берега

Стоишь на пирсе и смотришь на воду. Волны бьют то выше, то ниже, каждые несколько секунд. Под ними за шесть часов поднимается прилив. А летом и зимой у моря разный характер. Если по двум волнам решить, что море уходит, ошибёшься: просто одна волна была выше.

У ряда те же три слоя:

  • тренд, прилив: магазин растёт или усыхает;
  • сезонность, характер моря по временам года: декабрь, лето, выходные;
  • шум, отдельные волны: в один день 14 заказов, в соседний 32.
 month      | orders | к прошлому месяцу | к тому же месяцу 2025
------------+--------+-------------------+-----------------------
 2025-01-01 |    123 |                   |
 2025-07-01 |    257 |             −0,8% |
 2025-08-01 |    231 |            −10,1% |
 2025-11-01 |    590 |            +55,3% |
 2025-12-01 |    720 |            +22,0% |
 2026-01-01 |    427 |            −40,7% |               +247,2%
 2026-06-01 |    397 |            −12,6% |                +53,3%

Январь 2026 на 40,7% ниже декабря и в 3,5 раза выше января 2025. Оба числа верны. Первое меряет сезон, второе рост магазина. Задача урока: научиться разделять эти слои запросом.

Скользящее среднее: убираем волны

Начнём с шума. Заказы по дням скачут: 16 декабря 32 заказа, 18 декабря 14. Сгладить ряд помогает скользящее среднее: среднее за день и шесть предыдущих. Рамку окна ты уже знаешь по уроку 40.

Одна тонкость. Окно ROWS BETWEEN 6 PRECEDING AND CURRENT ROW считает строки, а не дни. Если в какой-то день заказов не было, его строки в группировке нет, и «семь строк» растянутся на восемь дней. Поэтому сначала строим календарь через generate_series:

WITH days AS (
  SELECT generate_series(min(created_at)::date, max(created_at)::date, interval '1 day')::date AS day
  FROM orders
), daily AS (
  SELECT d.day, count(o.id) AS orders
  FROM days d
  LEFT JOIN orders o ON o.created_at::date = d.day AND o.status <> 'cancelled'
  GROUP BY d.day
)
SELECT day, orders,
       round(avg(orders) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW), 2) AS ma7
FROM daily
ORDER BY day;
 day        | orders |  ma7
------------+--------+-------
 2025-12-16 |     32 | 20.86
 2025-12-17 |     23 | 21.43
 2025-12-18 |     14 | 21.29
 ...
 2026-01-04 |     15 | 18.71
 2026-01-05 |      7 | 16.86
 2026-01-08 |      9 | 13.57
 2026-01-11 |     13 | 10.29

Сырые числа прыгают вдвое за два дня, а скользящее среднее держится около 21 весь декабрь и сползает к 10-13 после 7 января. В «Кубике» день без заказов был один, 15 января 2025, но запрос без календаря на нём уже ошибся бы.

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

урок из курса

Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.