Все материалы
Продуктовая аналитикагайдсредний

Временные ряды: скользящее среднее, сглаживание и ARIMA

Анализ временных рядов на дневном DAU: тренд, сезонность и шум, скользящее среднее в SQL, экспоненциальное сглаживание и метод Хольта в Python, идея ARIMA.

КейсПрактика14 октября 2026 г.19 мин

В понедельник менеджер присылает скриншот: в субботу DAU упал на 10,8% к пятнице, что сломалось? Скорее всего, ничего — это суббота. Тот же день к прошлой субботе дал +8,1%. Дневная метрика почти всегда складывается из трёх слоёв: медленного роста, недельного ритма и случайного шума. Пока они смешаны, любое движение графика выглядит событием. Разберём анализ временных рядов на дневном DAU учебного сервиса «Маяк»: собрать ряд без ловушек, посчитать скользящее среднее в SQL, проверить недельную сезонность, построить прогноз экспоненциальным сглаживанием и методом Хольта и сравнить его с наивными. В конце — что такое ARIMA и когда она нужна.

Коротко

Временной ряд — это значения одной метрики, упорядоченные по равным интервалам времени: DAU по дням, выручка по неделям, заказы по часам. В отличие от обычной выборки, соседние наблюдения здесь зависят друг от друга, а порядок несёт информацию.

  • Ряд раскладывают на тренд, сезонность и остаток. Если сезонные колебания постоянны в штуках, декомпозиция аддитивная, если в процентах — мультипликативная.
  • Скользящее среднее за 7 дней убирает недельную сезонность: в каждое окно попадает ровно по одному понедельнику, вторнику и так далее.
  • Изменение день к дню на дневной метрике в основном показывает день недели. Сравнивайте неделю к неделе или скользящие средние.
  • Экспоненциальное сглаживание прогнозирует уровень, метод Хольта — уровень и наклон. На растущем DAU «Маяка» Хольт с поправкой на день недели ошибся в среднем на 3,1% против 6,4% у прогноза «как вчера».
  • ARIMA моделирует ряд через его прошлые значения и прошлые ошибки после дифференцирования. Её берут, когда простые методы уже проверены и проиграли.

Что такое временной ряд: тренд, сезонность и шум

Тренд — долгосрочное направление ряда: продукт растёт, стагнирует или теряет аудиторию. Сезонность — повторяющийся рисунок с известным периодом: неделя для дневных продуктовых метрик, год для розницы, сутки для почасовой нагрузки. Остаток, или шум, — всё, что осталось после вычитания первых двух. Иногда выделяют ещё цикл — колебания без фиксированного периода, но на ряду в несколько месяцев его не отличить от тренда.

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

Аддитивная и мультипликативная декомпозиция
yₜ = Tₜ + Sₜ + Rₜ yₜ = Tₜ × Sₜ × Rₜ

T — тренд, S — сезонная компонента, R — остаток. Логарифм второй формулы даёт первую: log y = log T + log S + log R.

Компоненты ряда и чем их выделить
КомпонентаЧто показываетВ дневном DAUЧем выделить
ТрендНаправление за недели и месяцыРост аудитории после запускаСкользящее среднее за 7 дней, метод Хольта
СезонностьПовтор с известным периодомБудни выше, выходные нижеСредний профиль дня недели относительно тренда
ОстатокСлучайные отклоненияПятница, которая вышла слабее обычнойРяд минус тренд и сезонность
Разовые событияСдвиг в конкретный деньРелиз, сбой, праздник, рассылкаКалендарь событий рядом с графиком

Как собрать ряд DAU и почему последний день надо отрезать

Дневной ряд DAU — одна строка на календарный день: дата и число разных пользователей, которые открыли приложение. В учебной базе «Маяк» это событие app_open в таблице events, время записано в UTC+3, и открытие пишется не чаще раза в день на пользователя. Как определить активность для своего продукта и не посчитать события вместо людей, разобрано в статье как считать DAU.

Первым делом смотрим на края ряда. Выгрузка базы закрыта 30 августа в 13:00, и последний день выглядит как обвал: 221 пользователь против 506 накануне, −56,3%. Время последнего открытия, 12:59, выдаёт причину — день не закончился. Неполный день портит всё, что стоит дальше: скользящее среднее на 30 августа опустилось бы с 518,1 до 481,9, а прогноз начал бы с ложного падения. Поэтому ряд берём по 29 августа включительно, условием event_time < timestamp '2026-08-30'. В рабочей базе так каждое утро: сегодняшний день неполный, а при задержке событий — и вчерашний.

Результат запроса
ДеньDAUПервое открытиеПоследнее открытие
27 августа54106:0520:54
28 августа56706:1720:59
29 августа50606:0620:54
30 августа22106:0412:59
Края ряда: последний день выгрузки обрывается в 12:59
select
  cast(event_time as date) as day,
  count(distinct user_id)  as dau,
  min(event_time)          as first_open,
  max(event_time)          as last_open
from events
where event_name = 'app_open'
  and event_time >= timestamp '2026-08-27'
group by cast(event_time as date)
order by day;

Скользящее среднее за 7 дней в SQL

Скользящее среднее за 7 дней — среднее значение метрики за текущий день и шесть предыдущих. В SQL это оконная функция AVG(...) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW). Запрос ниже сначала строит календарь через generate_series, затем присоединяет к нему дневные значения. В «Маяке» пропусков нет, все 90 дней на месте, но календарь — страховка: рамка ROWS считает строки, а не дни, и если день выпал из данных, окно молча захватит восьмой. Подробнее — в статьях про ряд дат через generate_series и про рамку окна ROWS и RANGE.

Запрос выполняется без изменений в DuckDB и PostgreSQL и возвращает одинаковые 90 строк. Столбец days_in_window показывает, сколько дней попало в окно: в первые шесть дней их меньше семи, и среднее считается по тому, что есть, — 32,0 за 1 июня, 40,0 за 2 июня. Такие значения лучше не показывать или помечать.

Почему окно именно 7, а не 5 или 10? В окно длиной ровно в период сезонности каждый день недели попадает один раз. Сезонные отклонения за неделю в сумме дают почти ноль, и в среднем остаются тренд и сглаженный шум. Окно в 5 дней то включает выходные, то нет, и среднее качается с недельным ритмом.

У такого окна есть цена — запаздывание. Среднее за последние 7 дней описывает середину окна, то есть ситуацию примерно трёхдневной давности. На растущем ряду оно всегда ниже текущего уровня: 29 августа среднее 518,1, а четыре дня подряд до этого DAU был выше 510. Для отчётов это приемлемо, для прогноза нет — к нему вернёмся ниже.

DAU и скользящее среднее за 7 дней, 2–29 августа 2026

Учебная база «Маяк», результат запроса выше. Подписи — числа августа; 2, 9, 16 и 23 августа — воскресенья. Дневная линия проседает каждые выходные, скользящее среднее растёт ровно.

DAUСкользящее среднее за 7 дней
Ряд DAU по календарю и скользящее среднее за 7 дней
with calendar as (
  select cast(x as date) as day
  from generate_series(timestamp '2026-06-01', timestamp '2026-08-29', interval '1 day') as g(x)
),
daily as (
  select cast(event_time as date) as day, count(distinct user_id) as dau
  from events
  where event_name = 'app_open'
    and event_time >= timestamp '2026-06-01'
    and event_time <  timestamp '2026-08-30'   -- 30 августа неполный
  group by cast(event_time as date)
)
select
  c.day,
  coalesce(d.dau, 0) as dau,
  round(cast(avg(coalesce(d.dau, 0)) over (
    order by c.day rows between 6 preceding and current row
  ) as numeric(18, 6)), 1) as ma7,
  count(*) over (order by c.day rows between 6 preceding and current row) as days_in_window
from calendar c
left join daily d on d.day = c.day
order by c.day;

То же в pandas: rolling и граница окна

В pandas скользящее среднее считает rolling(7).mean(), и здесь первая ловушка: по умолчанию pandas требует полного окна и для первых шести дней возвращает NaN. SQL в той же ситуации усредняет то, что есть. Чтобы результаты совпали, нужен min_periods=1; чтобы честно не показывать неполные окна — оставьте NaN.

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

Результат запуска: 1–10 июня
Вариант1 июня2 июня4 июня7 июня10 июня
rolling(7)NaNNaNNaN67,390,9
rolling(7, min_periods=1)32,040,056,867,390,9
rolling(7, center=True)NaNNaN67,390,9NaN
pythonpandas: полное окно, неполное окно и центрированное окно на первых 10 днях
import pandas as pd

dau = pd.Series([32, 48, 67, 80, 100, 76, 68, 95, 96, 121],
                index=pd.date_range('2026-06-01', periods=10, freq='D'))
print(dau.rolling(7).mean().round(1).tolist())                 # первые 6 дней — NaN
print(dau.rolling(7, min_periods=1).mean().round(1).tolist())  # как AVG OVER в SQL
print(dau.rolling(7, center=True).mean().round(1).tolist())    # центрированное окно

Есть ли недельная сезонность: профиль дня недели

Чтобы увидеть сезонность, сначала убирают тренд, иначе на растущем ряду поздние дни недели окажутся выше ранних просто потому, что наступили позже. Для каждого дня берём центрированное среднее за 7 дней как оценку тренда, считаем отклонение DAU от него в штуках и в долях и усредняем по дням недели. Берём только дни с полным окном — с 4 июня по 26 августа, ровно 12 недель.

Недельная сезонность в данных есть, и она заметная. Среда, четверг и пятница выше тренда на 16–19 пользователей, суббота ниже на 13, воскресенье — на 29. В долях воскресенье даёт 0,885 от тренда, то есть −11,5%. Понедельник и вторник близки к тренду. Сумма семи отношений — 7,017, почти ровно 7: профиль сбалансирован, как и должно быть.

Теперь вопрос про вид декомпозиции. До 16 июля воскресенье отставало от тренда на 30,7 пользователя, после — на 27,8. В долях это −16,5% и −6,6%. За это время уровень ряда вырос больше чем вдвое, а провал в штуках остался прежним. Это признак аддитивной сезонности: в «Маяке» воскресенье «стоит» примерно 30 пользователей, а не фиксированный процент. По остальным дням картина шумнее; прогноз ниже этот вывод подтвердит.

Результат запроса: по 12 дней на каждый день недели
ДеньРазница с трендомОтношение к трендуРазница до 16 июляРазница послеОтношение до 16 июляОтношение после
Пн−5,00,983−5,2−4,70,9750,990
Вт−5,10,984−2,1−8,10,9850,983
Ср+17,71,058+15,3+20,01,0741,043
Чт+19,21,075+10,0+28,51,0791,072
Пт+16,51,080+22,7+10,31,1361,024
Сб−13,30,952−10,8−15,70,9420,962
Вс−29,20,885−30,7−27,80,8350,934
Средняя разница DAU с трендом по дням недели, пользователей

Учебная база «Маяк», 4 июня — 26 августа 2026; тренд — центрированное среднее за 7 дней.

Разница с трендом
Профиль дня недели: отклонение DAU от центрированного среднего за 7 дней
with daily as (
  select cast(event_time as date) as day, count(distinct user_id) as dau
  from events
  where event_name = 'app_open'
    and event_time >= timestamp '2026-06-01'
    and event_time <  timestamp '2026-08-30'
  group by cast(event_time as date)
),
smoothed as (
  select
    day,
    dau,
    avg(dau) over (order by day rows between 3 preceding and 3 following) as trend,
    count(*) over (order by day rows between 3 preceding and 3 following) as n
  from daily
)
select
  extract(isodow from day) as weekday,   -- 1 = понедельник, 7 = воскресенье
  count(*) as days,
  round(cast(avg(dau - trend) as numeric(18, 6)), 1) as diff,
  round(cast(avg(dau / trend) as numeric(18, 6)), 3) as ratio,
  round(cast(avg(dau - trend) filter (where day <  date '2026-07-16') as numeric(18, 6)), 1) as diff_early,
  round(cast(avg(dau - trend) filter (where day >= date '2026-07-16') as numeric(18, 6)), 1) as diff_late,
  round(cast(avg(dau / trend) filter (where day <  date '2026-07-16') as numeric(18, 6)), 3) as ratio_early,
  round(cast(avg(dau / trend) filter (where day >= date '2026-07-16') as numeric(18, 6)), 3) as ratio_late
from smoothed
where n = 7
group by extract(isodow from day)
order by weekday;

Неделя к неделе или день к дню

Изменение день к дню (DoD) сравнивает разные дни недели и на ряду с недельной сезонностью в основном показывает календарь: за 16–29 августа DoD прыгал от −10,8% до +11,8% и был отрицательным в 6 днях из 14. Неделя к неделе (WoW) сравнивает субботу с субботой: те же две недели дают от −4,0% до +14,5% и только 3 отрицательных дня. Рост продукта виден во втором столбце и почти не виден в первом.

В SQL оба сравнения делает LAG: lag(dau, 1) — вчера, lag(dau, 7) — тот же день неделю назад. Это снова сдвиг на строки, а не на дни, поэтому на ряду с пропусками его считают после календаря из предыдущего раздела. Функция разобрана в статьях LAG и LEAD и оконные функции ROW_NUMBER и LAG.

Самое устойчивое сравнение для еженедельного отчёта — скользящее среднее к самому себе неделю назад. 29 августа оно равно 518,1, 22 августа — 480,6: рост на 7,8% за неделю, без влияния того, какой сегодня день.

Последняя неделя из результата запроса
ДеньDAUДень к днюНеделя к неделе
Вс, 23 августа475+1,5%+6,3%
Пн, 24 августа478+0,6%−4,0%
Вт, 25 августа511+6,9%+11,8%
Ср, 26 августа549+7,4%+7,4%
Чт, 27 августа541−1,5%+12,2%
Пт, 28 августа567+4,8%+13,2%
Сб, 29 августа506−10,8%+8,1%
Изменение день к дню и неделя к неделе за последние две недели
with daily as (
  select cast(event_time as date) as day, count(distinct user_id) as dau
  from events
  where event_name = 'app_open'
    and event_time >= timestamp '2026-06-01'
    and event_time <  timestamp '2026-08-30'
  group by cast(event_time as date)
),
lagged as (
  select
    day,
    dau,
    lag(dau, 1) over (order by day) as prev_day,
    lag(dau, 7) over (order by day) as week_ago
  from daily
)
select
  day,
  extract(isodow from day) as weekday,
  dau,
  round(cast(100.0 * (dau - prev_day) / prev_day as numeric(18, 6)), 1) as dod_pct,
  round(cast(100.0 * (dau - week_ago) / week_ago as numeric(18, 6)), 1) as wow_pct
from lagged
where day >= date '2026-08-16'
order by day;

Экспоненциальное сглаживание и метод Хольта

Скользящее среднее даёт семи дням одинаковый вес и целиком забывает восьмой. Экспоненциальное сглаживание мягче: новая оценка уровня — взвешенная сумма нового значения и прошлой оценки. Вес последнего дня равен α, предыдущего — α(1 − α), ещё более раннего — α(1 − α)², веса убывают как геометрическая прогрессия. Метод предложили в конце 1950-х; в учебнике Хайндмана и Атанасопулоса «Forecasting: Principles and Practice» его истоки связывают с работами Брауна, Хольта и Уинтерса.

Параметр α от 0 до 1 задаёт память. При α близком к 1 модель почти повторяет последнее значение, при малом α долго помнит прошлое и сильно сглаживает. Прогноз простого экспоненциального сглаживания (SES) на любой горизонт — одно и то же число, последний уровень. Для ряда без тренда это разумно, для растущего — систематически занижено.

Метод Хольта добавляет вторую компоненту — наклон, то есть ожидаемый прирост за шаг. Уровень обновляется с учётом наклона, наклон — по изменению уровня со своим параметром β. Прогноз на h дней вперёд — уровень плюс h наклонов, то есть прямая из последней точки. Уинтерс добавил третью, сезонную компоненту (метод Хольта — Уинтерса). Мы сделаем похожее вручную: вычтем недельный профиль, сгладим ряд Хольтом и вернём профиль в прогноз.

Простое экспоненциальное сглаживание и метод Хольта
SES: ℓₜ = α·yₜ + (1 − α)·ℓₜ₋₁; ŷₜ₊ₕ = ℓₜ. Хольт: ℓₜ = α·yₜ + (1 − α)·(ℓₜ₋₁ + bₜ₋₁); bₜ = β·(ℓₜ − ℓₜ₋₁) + (1 − β)·bₜ₋₁; ŷₜ₊ₕ = ℓₜ + h·bₜ

ℓ — уровень, b — наклон (прирост за день), α и β — параметры сглаживания от 0 до 1, h — горизонт прогноза.

Прогноз на 14 дней: сравниваем с наивными методами

Модель учится на прошлом и отвечает на будущее, которого не видела: обучаем на 1 июня — 15 августа (76 дней), прогнозируем 16–29 августа (14 дней) и сравниваем с фактом. Параметры α и β подбираем перебором по сетке с шагом 0,05 только на обучающем отрезке, по ошибке прогноза на день вперёд. Недельный профиль тоже считаем только по обучающим дням. SES и Хольт написаны на numpy без statsmodels: по два десятка строк, зато видно, что внутри, и результат легко сверить с готовой библиотекой.

Любую модель нужно сравнивать с наивными прогнозами, иначе непонятно, заработала ли она свою сложность. Берём три: «как вчера» (последнее значение на все 14 дней), «как неделю назад» (повторяем последнюю неделю, это сезонный наивный прогноз) и «среднее последних 7 дней».

Метрики ошибки — MAE, средняя абсолютная ошибка в пользователях, и MAPE, средняя абсолютная ошибка в процентах от факта. MAPE удобна для рядов разного масштаба, но ломается при факте около нуля и несимметрична. Для DAU в сотни пользователей она работает нормально.

Ошибка прогноза
MAE = (1/n) · Σ |yₜ − ŷₜ| MAPE = (100% / n) · Σ |yₜ − ŷₜ| / yₜ

yₜ — факт, ŷₜ — прогноз, n — число дней проверки (здесь 14).

Результат запуска: ошибка прогноза на 16–29 августа
МетодПараметрыПрогнозMAE, пользователейMAPE
Как вчера—472 на все дни33,66,4%
Как неделю назад—неделя 9–15 августа дважды42,28,2%
Среднее 7 дней—458 на все дни43,18,3%
SESα = 0,85473 на все дни33,16,3%
Хольтα = 0,65, β = 0,05от 482 до 54725,35,2%
Хольт + день недели, аддитивноα = 0,4, β = 0,1от 448 до 54715,43,1%
Хольт + день недели, мультипликативноα = 0,35, β = 0,15от 411 до 56319,84,1%
Факт и прогнозы DAU на 16–29 августа, сделанные 15 августа

Учебная база «Маяк», расчёт numpy/pandas из блока выше. Подписи — числа августа.

ФактХольт + день неделиХольтСреднее 7 дней
pythonSES, Хольт и Хольт с недельным профилем против наивных прогнозов на 16–29 августа
import numpy as np
import pandas as pd

# DAU по дням, 1 июня — 29 августа 2026: результат запроса из раздела про скользящее среднее
dau = [32, 48, 67, 80, 100, 76, 68, 95, 96, 121, 131, 128, 123, 109, 139, 151,
       167, 172, 194, 175, 162, 193, 202, 227, 222, 239, 202, 196, 230, 232,
       268, 270, 283, 257, 248, 257, 276, 285, 277, 325, 275, 248, 315, 330,
       361, 397, 377, 336, 324, 380, 370, 374, 413, 370, 365, 370, 367, 420,
       410, 436, 455, 409, 394, 435, 408, 470, 484, 429, 430, 424, 435, 434,
       479, 484, 478, 472, 447, 498, 457, 511, 482, 501, 468, 475, 478, 511,
       549, 541, 567, 506]
y = pd.Series(dau, index=pd.date_range('2026-06-01', periods=len(dau), freq='D'))
train, test = y[:'2026-08-15'], y['2026-08-16':]   # 76 дней на подбор, 14 на проверку
x, fact = train.to_numpy(float), test.to_numpy(float)
h = np.arange(1, len(test) + 1)                    # горизонт: 1…14 дней вперёд


def ses(x, alpha):
    level, fitted = x[0], []
    for v in x:
        fitted.append(level)                       # прогноз дня по прошлым дням
        level = alpha * v + (1 - alpha) * level
    return level, np.array(fitted)


def holt(x, alpha, beta):
    level, trend, fitted = x[0], x[1] - x[0], []
    for v in x:
        fitted.append(level + trend)
        prev = level
        level = alpha * v + (1 - alpha) * (level + trend)
        trend = beta * (level - prev) + (1 - beta) * trend
    return level, trend, np.array(fitted)


# параметры подбираем только на обучающем отрезке: по ошибке прогноза на день вперёд
grid = np.round(np.arange(0.05, 1.0, 0.05), 2)
fit_ses = lambda x: min(grid, key=lambda a: np.sum((x[1:] - ses(x, a)[1][1:]) ** 2))
fit_holt = lambda x: min(((a, b) for a in grid for b in grid),
                         key=lambda p: np.sum((x[2:] - holt(x, *p)[2][2:]) ** 2))

a = fit_ses(x)
level, _ = ses(x, a)
ph = fit_holt(x)
lh, th, _ = holt(x, *ph)

# недельный профиль тоже только по обучающему отрезку
trend7 = train.rolling(7, center=True).mean()
dev = (train - trend7).dropna()                    # аддитивный: «плюс-минус N человек»
add = dev.groupby(dev.index.dayofweek).mean()
add -= add.mean()
rat = (train / trend7).dropna()                    # мультипликативный: «плюс-минус N %»
mul = rat.groupby(rat.index.dayofweek).mean()
mul /= mul.mean()
wd, wd_test = train.index.dayofweek, test.index.dayofweek

xa = x - add.loc[wd].to_numpy()
pa = fit_holt(xa)
la, ta, _ = holt(xa, *pa)
xm = x / mul.loc[wd].to_numpy()
pm = fit_holt(xm)
lm, tm, _ = holt(xm, *pm)

forecasts = {
    'вчера': np.full(len(test), x[-1]),
    'неделю назад': np.resize(x[-7:], len(test)),
    'среднее 7 дней': np.full(len(test), x[-7:].mean()),
    f'SES, α={a}': np.full(len(test), level),
    f'Хольт, α={ph[0]}, β={ph[1]}': lh + h * th,
    f'Хольт + день недели, аддитивно, α={pa[0]}, β={pa[1]}': la + h * ta + add.loc[wd_test].to_numpy(),
    f'Хольт + день недели, мультипликативно, α={pm[0]}, β={pm[1]}': (lm + h * tm) * mul.loc[wd_test].to_numpy(),
}
print('факт      ', fact.astype(int).tolist())
for name, f in forecasts.items():
    mae = np.mean(np.abs(fact - f))
    mape = 100 * np.mean(np.abs(fact - f) / fact)
    print(f'{name:<52} MAE {mae:5.1f}  MAPE {mape:4.1f}%  {np.round(f).astype(int).tolist()}')
print('Хольт: уровень', round(lh, 1), 'тренд', round(th, 2), 'в день')
print('аддитивный профиль, пн…вс:', add.round(1).tolist())

Что показало сравнение прогнозов

Хуже всех — «среднее 7 дней» и «неделю назад»: на растущем ряду они смотрят в прошлое и занижают почти каждый день проверки. Простое сглаживание подобрало α = 0,85 и превратилось в прогноз «как вчера»: 473 против 472. Модели, которая умеет только уровень, на ряду с трендом выгоднее всего держаться за последнее значение.

Хольт оценил наклон в 5,0 пользователя в день и снизил ошибку до 5,2%. Но его прямая не видит недельного рисунка: во все четыре выходных дня проверки — 16, 22, 23 и 29 августа — прогноз выше факта на 35–44 пользователя. Добавление недельного профиля исправляет именно это: аддитивный вариант дал MAPE 3,1%, вдвое меньше прогноза «как вчера». Мультипликативный профиль оказался хуже, 4,1%: он переносит в август большие июньские проценты провала выходных. Это тот же вывод, что дал профиль дня недели в SQL.

Главное ограничение: это одна точка старта и 14 дней, на другом отрезке порядок методов может поменяться. В рабочей задаче прогноз проверяют из нескольких точек подряд — 1, 8 и 15 августа — и усредняют ошибку. И Хольт продолжает наклон бесконечно: на горизонте в месяцы прямая уйдёт туда, куда продукт не дойдёт, поэтому для дальних прогнозов наклон гасят (затухающий тренд).

ARIMA простыми словами

ARIMA — семейство моделей, которые описывают значение ряда через его прошлые значения и прошлые ошибки прогноза. Название складывается из трёх частей. AR, авторегрессия порядка p: сегодняшнее значение — линейная комбинация p предыдущих. MA порядка q: зависимость от q прошлых ошибок прогноза. Это не скользящее среднее из начала статьи, совпадение названий сбивает с толку многих. I, интегрированность порядка d: сколько раз ряд нужно продифференцировать, прежде чем строить AR и MA.

Дифференцирование нужно ради стационарности: у стационарного ряда нет тренда и сезонности, среднее и разброс не зависят от момента наблюдения. DAU «Маяка» нестационарен дважды: он растёт, и у него недельный ритм. Первая разность yₜ − yₜ₋₁ убирает тренд, сезонная разность yₜ − yₜ₋₇ — недельный рисунок. Модель, которая умеет и то и другое, называют сезонной ARIMA и записывают ARIMA(p,d,q)(P,D,Q) с периодом 7. Подход популяризировала книга Бокса и Дженкинса «Time Series Analysis: Forecasting and Control» 1970 года.

Руками ARIMA никто не пишет. В Python есть statsmodels: классы ARIMA и SARIMAX в модуле statsmodels.tsa, там же SimpleExpSmoothing, Holt, ExponentialSmoothing и декомпозиция STL. Для рядов с несколькими сезонностями и праздниками часто берут Prophet — открытую библиотеку команды Core Data Science компании Facebook для Python и R. Она складывает нелинейный тренд, недельную и годовую сезонность и эффекты праздников. Порядок работы прежний: сначала наивный прогноз и сглаживание, и только если они проиграли на честной проверке, — ARIMA или Prophet. На ряду в 90 дней сложная модель скорее подстроится под шум.

Какой метод взять под задачу
ЗадачаМетодЧто учесть
Сгладить график для отчётаСкользящее среднее за 7 днейОтстаёт на ~3 дня, неполные окна в начале
Разложить историю на компонентыЦентрированное среднее + профиль дня недели, STLПоследние 3 дня без оценки тренда
Прогноз ряда без трендаSESНа растущем ряду занижает
Прогноз ряда с трендомМетод ХольтаПрямая вечна — для дальних горизонтов гасить наклон
Тренд и сезонностьХольт — Уинтерс или Хольт по очищенному рядуАддитивную или мультипликативную сезонность проверить
Сложная автокорреляция, длинная историяARIMA / SARIMA, ProphetСначала обыграть наивный прогноз

Типичные ошибки в анализе временных рядов

Большинство ошибок здесь не про формулы, а про то, какие данные попали в расчёт и что с чем сравнили. Похожие ловушки с составом и календарём собраны в статье почему метрика обманывает, а про выбор самой метрики активности — в разборе DAU и MAU.

  • Прогноз растущего ряда средним. Среднее за неделю на 16–29 августа ошиблось на 8,3% — больше всех методов. Среднее и SES годятся для ряда без тренда, для растущего нужен наклон.
  • Утечка будущего. Центрированное среднее использует три будущих дня, профиль по всему ряду включает дни проверки, параметры, подобранные на тестовом отрезке, подогнаны под ответ. Всё для прогноза считается только по дням до его начала.
  • Неполный последний день. 30 августа в «Маяке» выглядит как падение на 56%, хотя это полдня. Отрезайте текущий день.
  • Разрыв ряда. Если день без событий выпал из выборки, ROWS BETWEEN 6 PRECEDING и lag(dau, 7) сдвигаются на строки, а не на дни. Календарь через generate_series и LEFT JOIN показывает пропуск нулём.
  • Сравнение периодов разной длины. В выгрузке июль длиннее августа на два дня, их суммы несравнимы. Сравнивайте среднее за день или равное число полных недель.
  • Праздники и разовые события. Праздник ведёт себя как выходной не в свой день и портит профиль, если его не пометить. В учебной базе 12 июня, пятница, не выделяется: отношение к тренду 1,058 при среднем для пятниц 1,080. В реальном продукте держите календарь праздников, релизов и рассылок рядом с рядом.

Частые вопросы

Какое окно скользящего среднего выбрать?

Равное периоду сезонности: 7 для дневных данных с недельным ритмом, 24 для почасовых с суточным, 12 для месячных с годовым. Окно другой длины оставит сезонный рисунок в сглаженной линии.

Чем экспоненциальное сглаживание лучше скользящего среднего?

Оно плавно снижает вес старых наблюдений, а не обрезает их, и сразу даёт прогноз. Скользящее среднее описывает прошлое и на растущем ряду всегда отстаёт.

Как понять, что сезонность аддитивная, а не мультипликативная?

Посчитайте сезонные отклонения в штуках и в долях на ранней и поздней части ряда. Что стабильнее, тот вид и ближе. В «Маяке» воскресный провал держится около 30 пользователей, а в процентах слабеет с 16,5% до 6,6% — сезонность аддитивная.

Нужна ли ARIMA для прогноза DAU?

Обычно нет, пока не проверены наивный прогноз и Хольт с поправкой на день недели — на DAU «Маяка» он ошибается на 3,1%. ARIMA оправдана на длинной истории со сложной автокорреляцией.

Почему MAPE иногда вводит в заблуждение?

Она делит ошибку на факт: при факте около нуля процент взлетает, а при факте ноль не определён. Для метрик с малыми значениями используйте MAE или делите суммарную ошибку на суммарный факт.

Итог

Анализ временного ряда начинается с честного ряда: только закрытые дни, календарь без пропусков, явные границы. Скользящее среднее за 7 дней отделяет тренд от недельного ритма, профиль дня недели показывает вид сезонности, сравнение неделя к неделе заменяет «день к дню». Для прогноза на две недели часто хватает Хольта с поправкой на день недели, если он обыграл наивный прогноз на днях, которых не видел.

Все SQL-запросы статьи выполняются на учебной базе «Маяк» как есть — повторите их в песочнице и попробуйте задачи на оконные функции и даты в сборнике SQL-задач.

Продолжить чтение
Вся библиотека
Продуктовая аналитика2 августа 2026 г.23 мин

Почему метрика обманывает: состав, календарь, база и декомпозиция

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

Читать материал
Продуктовая аналитика25 сентября 2026 г.12 мин
Столбики счётчиков событий по дням и плавная кривая ожидаемых значений поверх них

Распределение Пуассона: формула, примеры и проверка на данных

Распределение Пуассона простыми словами: формула Пуассона, пример расчёта вручную, почему λ — это и среднее, и дисперсия, проверка платежей по дням в SQL и Python, сверхдисперсия и пороги алертов.

Читать материал
Продуктовая аналитика26 июля 2026 г.20 мин

Какой статистический метод выбрать: t-тест, хи-квадрат, бутстреп, Байес или регрессия

Навигатор по методам для аналитика: когда сравнивать средние t-тестом, доли и категории хи-квадратом, где нужен бутстреп, байесовский апдейт, линейная или логистическая регрессия — и что каждый из них не скажет.

Читать материал