Что такое seasonality (сезонность) и как её учитывать при анализе метрик?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Сезонность (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 года для годовой сезонности)
Сезонность — это не шум, а важный паттерн, который нужно понимать и учитывать при анализе любых временных рядов.