Условие
Дана таблица заказов 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 — явное требование).
Решение
Подход
«Сезонность» — это повторяющиеся паттерны на разных временных шкалах:
- Внутринедельная: пятница/суббота vs будни.
- Внутримесячная: начало месяца (зарплата) vs середина.
- Годовая: лето vs зима, праздники.
- Внутридневная: ланч 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–2 месяцам нельзя. Только weekly + intra-week можно надёжно увидеть.
- Тренд × сезонность. Если объём растёт, средний DoW-индекс может маскироваться. Всегда сначала detrend (через STL), потом смотрим.
- Outliers (праздники, акции). Один день с акцией +200% испортит средние. Использовать медиану / робастный STL (
robust=True). - Маленькие страны = большой шум. В Гане может быть 10 заказов в день, и weekly pattern — это шум. Считайте только при минимальном объёме (например, > 50 заказов/день в среднем).
- Cancelled & failed orders. В метрике
ordersлучше использоватьdelivered, иначе ошибки маршрутизации искажают сезонность. - Платежи cash vs cashless в разных странах могут иметь разные паттерны — отдельная сегментация.
- COVID-эффект. Конец февраля 2020 —
lockdownначинался в Италии. Если данные включают эти даты, падение трафика — не «нет сезонности», а резкий шок. - 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 = вклад дня недели.
Эталонный ответ
Структура отчёта:
- Визуальный анализ дневных рядов по каждой стране (line plots).
- DoW-индекс: pivot-таблица «среднее по дню недели / общее среднее».
- Статистический тест (Kruskal-Wallis по DoW).
- STL-декомпозиция для стран с достаточным объёмом.
- Вывод по странам: где сезонность выражена сильно (Portugal, Estonia), где умеренно (Finland), где слабо (Ghana). Указать ограничения данных (COVID-end-of-Feb).
Главное — отделить факт сезонности от структурных шоков в самом конце выгрузки.