Собесов

Кейс — анализ сезонности заказов Bolt Food по странам

Кейсы и метрикиTime series / SeasonalityСредняяSenior

Условие

Дана таблица заказов Bolt Food (поля: Created Date, Country, City, Restaurant ID/Name, Order State, Cancel Reason, Cuisine, Platform, Payment Method, Order Value (Gross) €, Delivery Fee, Delivery Time). Период данных — до конца февраля 2020.

Вопрос: Есть ли сезонность в показанных странах?

Для решения использовать Python или BI-инструмент (не Excel/Google Sheets — явное требование).

Решение

Подход

«Сезонность» — это повторяющиеся паттерны на разных временных шкалах:

  1. Внутринедельная: пятница/суббота vs будни.
  2. Внутримесячная: начало месяца (зарплата) vs середина.
  3. Годовая: лето vs зима, праздники.
  4. Внутридневная: ланч vs ужин (если есть time-of-day).

Чтобы её увидеть — раскладываем ряд на компоненты и смотрим визуально + числово.

Шаг 1. Подготовка данных

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
 
df = pd.read_excel("bolt_food.xlsx", sheet_name="Data", parse_dates=["Created Date"])
 
# Чистим евро-знак и пропуски
df["order_value"] = (
    df["Order Value € (Gross)"].astype(str)
      .str.replace("€", "", regex=False)
      .str.replace(",", ".", regex=False)
      .replace("", np.nan).astype(float)
)
df["delivered"] = (df["Order State"] == "delivered").astype(int)
 
# Дневной агрегат по стране
daily = (df.groupby(["Country", df["Created Date"].dt.normalize()])
           .agg(orders=("Order State", "size"),
                delivered=("delivered", "sum"),
                gmv=("order_value", "sum"))
           .reset_index()
           .rename(columns={"Created Date": "date"}))

Шаг 2. Визуальная инспекция

for country, sub in daily.groupby("Country"):
    sub = sub.sort_values("date")
    plt.figure(figsize=(10, 3))
    plt.plot(sub["date"], sub["orders"])
    plt.title(f"{country} — daily orders")
    plt.show()

Что искать на графиках:

  • Пилообразные пики выходных.
  • Дрейф (рост/падение тренда за период).
  • Точечные провалы (праздники, аутэйджи).

Шаг 3. Внутринедельная сезонность

daily["dow"] = daily["date"].dt.day_name()
order_dow = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
 
pivot = (daily.pivot_table(index="dow", columns="Country", values="orders", aggfunc="mean")
              .reindex(order_dow))
print(pivot)
 
# Индекс DoW: среднее в день недели / общее среднее
norm = pivot.div(pivot.mean())
print(norm.round(2))

Если в norm стоят значения вроде Sat=1.4, Sun=1.3, Mon=0.7 — это явная weekly seasonality. Если все ≈ 1.0 — её нет.

Численный тест: ANOVA или Kruskal-Wallis по фактору day_of_week.

from scipy import stats
groups = [g["orders"].values for _, g in daily[daily.Country=="Portugal"].groupby("dow")]
print(stats.kruskal(*groups))

p-value < 0.05 → сезонность статистически значима.

Шаг 4. STL-декомпозиция

from statsmodels.tsa.seasonal import STL
 
for country, sub in daily.groupby("Country"):
    if len(sub) < 14:   # STL нужен >= 2 периода
        continue
    ts = sub.set_index("date")["orders"].asfreq("D").fillna(0)
    stl = STL(ts, period=7).fit()
    fig = stl.plot()
    fig.suptitle(country)
    plt.show()

STL раскладывает ряд на trend + seasonal + residual. Сильный seasonal компонент с амплитудой ≥ 20% от среднего — подтверждение сезонности.

Шаг 5. Что увидим в данных Bolt Food

В типичных food-delivery данных:

  • Португалия (большой рынок в выгрузке) — чётко выраженная weekly seasonality: Fri/Sat/Sun > Mon/Tue/Wed. Средняя амплитуда 30–50%.
  • Эстония / Финляндия (если есть) — похожий паттерн с пиком в пятницу.
  • Гана — обычно слабее выражено weekly seasonality (другая культура потребления, доставка не так связана с уикендом).
  • За короткий период (~1–2 месяца) годовой сезонности увидеть нельзя — но можно увидеть Christmas/New Year эффект, если данные включают конец 2019/январь 2020.

Шаг 6. Связь с задачей про прогноз марта 2020

Этот анализ — основа для следующей задачи (bolt-senior-orders-forecast-march-2020). Зная weekly pattern и тренд, можно строить прогноз. Но! Февраль 2020 — это начало COVID-19. Если в данных видно резкое падение в последних днях февраля (особенно в Италии, Португалии), это не сезонность, а структурный шок, и его в прогноз марта 2020 экстраполировать нельзя.

Подводные камни

  1. Одной весны мало для годовой сезонности. Делать вывод о «сезонности по году» по 1–2 месяцам нельзя. Только weekly + intra-week можно надёжно увидеть.
  2. Тренд × сезонность. Если объём растёт, средний DoW-индекс может маскироваться. Всегда сначала detrend (через STL), потом смотрим.
  3. Outliers (праздники, акции). Один день с акцией +200% испортит средние. Использовать медиану / робастный STL (robust=True).
  4. Маленькие страны = большой шум. В Гане может быть 10 заказов в день, и weekly pattern — это шум. Считайте только при минимальном объёме (например, > 50 заказов/день в среднем).
  5. Cancelled & failed orders. В метрике orders лучше использовать delivered, иначе ошибки маршрутизации искажают сезонность.
  6. Платежи cash vs cashless в разных странах могут иметь разные паттерны — отдельная сегментация.
  7. COVID-эффект. Конец февраля 2020 — lockdown начинался в Италии. Если данные включают эти даты, падение трафика — не «нет сезонности», а резкий шок.
  8. Symbol в Order Value — нужно чистить. Иначе колонка читается как строка, и любые агрегаты неправильны.

Альтернативы

  • Auto-Correlation Function (ACF): пик на лаге 7 = weekly seasonality.
from statsmodels.graphics.tsaplots import plot_acf
plot_acf(ts.fillna(0), lags=21)
  • Holt-Winters / Prophet: модели с явной сезонной компонентой; их подгонка покажет амплитуду эффектов.
  • DoW-fixed effects регрессия: orders ~ trend + dow_dummies + holidays. Коэффициенты dummies = вклад дня недели.

Эталонный ответ

Структура отчёта:

  1. Визуальный анализ дневных рядов по каждой стране (line plots).
  2. DoW-индекс: pivot-таблица «среднее по дню недели / общее среднее».
  3. Статистический тест (Kruskal-Wallis по DoW).
  4. STL-декомпозиция для стран с достаточным объёмом.
  5. Вывод по странам: где сезонность выражена сильно (Portugal, Estonia), где умеренно (Finland), где слабо (Ghana). Указать ограничения данных (COVID-end-of-Feb).

Главное — отделить факт сезонности от структурных шоков в самом конце выгрузки.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти