← Назад к вопросам

Что такое seasonality (сезонность) и как её учитывать при анализе метрик?

2.0 Middle🔥 211 комментариев
#Метрики продукта#Статистика и математика

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Сезонность (Seasonality) в анализе метрик

Сезонность — это повторяющийся паттерн в данных, связанный с определенным периодом времени (день недели, месяц, сезон, праздники и т.д.). Игнорирование сезонности может привести к неправильным выводам об эффективности продукта и бизнеса.

Типы сезонности

1. Недельная сезонность

Выходные отличаются от будних дней.

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'date': pd.date_range('2024-01-01', periods=30, freq='D'),
    'users': [100, 120, 110, 105, 95, 200, 210] * 4 + [100, 120, 110, 105, 95, 200]  # Паттерн повторяется
})

df['day_of_week'] = df['date'].dt.day_name()
df['is_weekend'] = df['date'].dt.dayofweek.isin([5, 6])

# Среднее за день недели
weekly_pattern = df.groupby('day_of_week')['users'].mean()
print(weekly_pattern)
# Monday     105.00
# Tuesday    95.00
# Wednesday  100.00
# Thursday   107.50
# Friday     110.00
# Saturday   205.00
# Sunday     210.00

2. Месячная/квартальная сезонность

Начало, середина и конец месяца отличаются.

# День месяца влияет на покупки
df['day_of_month'] = df['date'].dt.day

monthly_pattern = df.groupby('day_of_month')['users'].mean()
print(monthly_pattern.head(10))
# 1     95.0
# 2     98.0
# 3     102.0
# ...

3. Годовая сезонность

Сезоны года, праздники и события.

Примеры:

  • Черная пятница (ноябрь) — скачок продаж
  • Новый год (январь) — скачок использования фитнес приложений
  • Лето — рост использования туристических сервисов
  • Праздники — пики трафика

4. События/специальные дни

holidays = {
    '2024-01-01': 'New Year',
    '2024-02-14': 'Valentine\'s Day',
    '2024-12-25': 'Christmas'
}

df['is_holiday'] = df['date'].isin(pd.to_datetime(list(holidays.keys())))

holiday_comparison = df.groupby('is_holiday')['users'].mean()
print(holiday_comparison)
# is_holiday
# False    150.0
# True     450.0  (праздники дают 3x скачок)

Проблемы при игнорировании сезонности

Проблема 1: Ложные выводы об улучшении

# Сравниваем две недели
week1 = df[(df['date'] >= '2024-01-01') & (df['date'] < '2024-01-08')]['users'].sum()
week2 = df[(df['date'] >= '2024-01-08') & (df['date'] < '2024-01-15')]['users'].sum()

print(f"Week 1: {week1}, Week 2: {week2}")
print(f"Growth: {(week2 - week1) / week1 * 100:.1f}%")

# Результат может быть +30%, но это просто потому что week2 
# содержит выходные в других днях недели!

Проблема 2: Неправильный расчет тренда

# Интернет магазин имеет четкую недельную сезонность:
# Пт-Сб: +50% трафика
# Пн: -30% трафика

# Если сравниваем Пн неделю A с Пт неделей B, 
# выводы будут совсем неправильные

Методы учета сезонности

Метод 1: Сравнение с тем же периодом года назад (Year-over-Year)

Это самый надежный способ.

df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month

# Сравниваем январь 2024 с январем 2023
jan_2024 = df[(df['year'] == 2024) & (df['month'] == 1)]['users'].sum()
jan_2023 = df[(df['year'] == 2023) & (df['month'] == 1)]['users'].sum()

yoy_growth = (jan_2024 - jan_2023) / jan_2023 * 100
print(f"YoY Growth: {yoy_growth:.1f}%")

Метод 2: Сравнение с тем же днем недели

# Компарируем каждый понедельник
mondays_current = df[df['date'].dt.day_name() == 'Monday']['users'].mean()
mondays_previous = df[df['date'].dt.day_name() == 'Monday']['users'].shift(1).mean()

print(f"Monday growth: {(mondays_current - mondays_previous) / mondays_previous * 100:.1f}%")

Метод 3: Deseasonalization (десезонализация)

Удаляем сезонный компонент из данных.

import pandas as pd
import numpy as np
from scipy import stats

# Расчет сезонного индекса
monthly_avg = df.groupby(df['date'].dt.month)['users'].mean().mean()  # Общее среднее
seasonal_index = df.groupby(df['date'].dt.month)['users'].mean() / monthly_avg

print(seasonal_index)
# 1     0.85  (январь на 15% ниже среднего)
# 2     0.95
# ...
# 12    1.20  (декабрь на 20% выше среднего)

# Десезонализуем
df['month_num'] = df['date'].dt.month
df['deseasoned'] = df['users'] / df['month_num'].map(seasonal_index)

# Теперь в десезонализованных данных нет сезонного тренда

Метод 4: Moving Average (скользящее среднее)

Удаляет шум и выделяет истинный тренд.

import pandas as pd

# 7-дневное скользящее среднее (убирает недельную сезонность)
df['ma_7'] = df['users'].rolling(window=7, center=True).mean()

# 30-дневное скользящее среднее (убирает месячную сезонность)
df['ma_30'] = df['users'].rolling(window=30, center=True).mean()

# Визуализация
df.plot(x='date', y=['users', 'ma_7', 'ma_30'])

Метод 5: Seasonal Decomposition

Разделяем временной ряд на компоненты: тренд, сезонность, остаток.

from statsmodels.tsa.seasonal import seasonal_decompose

# Требует регулярного временного ряда
df.set_index('date', inplace=True)
df = df.asfreq('D')  # Каждый день

# Декомпозиция
decomposition = seasonal_decompose(df['users'], model='additive', period=365)

print(f"Trend: {decomposition.trend}")
print(f"Seasonal: {decomposition.seasonal}")
print(f"Residual: {decomposition.resid}")

# Визуализация
decomposition.plot()

SQL примеры учета сезонности

Пример 1: Year-over-Year анализ

WITH monthly_data AS (
  SELECT 
    DATE_TRUNC('month', created_at) as month,
    SUM(amount) as revenue
  FROM orders
  WHERE created_at >= '2023-01-01'
  GROUP BY DATE_TRUNC('month', created_at)
)
SELECT 
  EXTRACT(MONTH FROM month) as month_num,
  EXTRACT(YEAR FROM month) as year,
  revenue,
  LAG(revenue) OVER (PARTITION BY EXTRACT(MONTH FROM month) ORDER BY month) as prev_year_revenue,
  ROUND(
    (revenue - LAG(revenue) OVER (PARTITION BY EXTRACT(MONTH FROM month) ORDER BY month)) / 
    LAG(revenue) OVER (PARTITION BY EXTRACT(MONTH FROM month) ORDER BY month) * 100,
    2
  ) as yoy_growth
FROM monthly_data
ORDER BY month DESC;

Пример 2: Сравнение с тем же днем недели

WITH daily_stats AS (
  SELECT 
    created_at::date as day,
    TO_CHAR(created_at, 'Day') as day_name,
    COUNT(*) as orders
  FROM orders
  GROUP BY created_at::date, TO_CHAR(created_at, 'Day')
)
SELECT 
  day_name,
  AVG(CASE WHEN day >= CURRENT_DATE - INTERVAL '7 days' THEN orders END) as last_week_avg,
  AVG(CASE WHEN day >= CURRENT_DATE - INTERVAL '14 days' AND day < CURRENT_DATE - INTERVAL '7 days' THEN orders END) as prev_week_avg
FROM daily_stats
GROUP BY day_name;

Практические примеры из Product Analytics

Пример 1: Анализ retention с учетом сезонности

import pandas as pd

df = pd.DataFrame({
    'signup_date': pd.date_range('2023-01-01', periods=365, freq='D').repeat(10),
    'last_active': pd.date_range('2023-01-01', periods=3650, freq='D')
})

# Расчет days_active для каждого пользователя
df['days_active'] = (df['last_active'] - df['signup_date']).dt.days

# Группируем по сезону
def get_season(month):
    if month in [12, 1, 2]:
        return 'Winter'
    elif month in [3, 4, 5]:
        return 'Spring'
    elif month in [6, 7, 8]:
        return 'Summer'
    else:
        return 'Fall'

df['signup_month'] = df['signup_date'].dt.month
df['season'] = df['signup_month'].apply(get_season)

# Retention по сезонам
retention = df.groupby('season')['days_active'].agg(['mean', 'median', 'std'])
print(retention)
# Может оказаться, что летние когорты более активны

Пример 2: Прогноз с учетом сезонности

from statsmodels.tsa.statespace.sarimax import SARIMAX

# SARIMA модель для прогноза с сезонностью
model = SARIMAX(
    df['users'],
    order=(1, 1, 1),        # (p, d, q)
    seasonal_order=(1, 1, 1, 7)  # (P, D, Q, s) где s=7 для недельной сезонности
)

results = model.fit()
forecast = results.get_forecast(steps=7)
print(forecast.predicted_mean)

Красные флаги сезонности

  • Резкие скачки каждый понедельник/четверг
  • Систематический рост/падение в определенные дни месяца
  • Большие скачки в праздничные периоды
  • Различные паттерны в разных временах года

Чек-лист при анализе метрик

  • Проверены дневные и недельные паттерны
  • Учтены выходные и праздники
  • Сравниваются сопоставимые периоды (Пн с Пн, Январь с Январем)
  • Используются скользящие средние для выявления трендов
  • Рассчитаны сезонные индексы
  • Тесты не запускались перед праздниками без планирования
  • Есть достаточно данных (минимум 2 года для годовой сезонности)

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