← Назад к вопросам
Как проверить, получилось ли увеличить прибыль по среднему чеку?
1.2 Junior🔥 131 комментариев
#Статистика и A/B тестирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Проверка увеличения прибыли по среднему чеку
Проверка улучшения среднего чека требует не только статистического анализа, но и учёта бизнес-контекста, сезонности и качества трафика. Рассмотрим полный подход.
1. Базовые метрики
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
class CheckAverageAnalyzer:
def __init__(self, orders_df):
"""
orders_df должен содержать колонки:
- date, order_id, amount, customer_id, segment
"""
self.df = orders_df
def calculate_metrics(self):
"""Базовые метрики"""
return {
"avg_check": self.df["amount"].mean(),
"median_check": self.df["amount"].median(),
"std_check": self.df["amount"].std(),
"total_revenue": self.df["amount"].sum(),
"order_count": len(self.df),
"revenue_per_customer": self.df.groupby("customer_id")["amount"].sum().mean()
}
2. Сравнение периодов: t-тест и Mann-Whitney U
Важно: Выбор теста зависит от распределения данных.
from scipy import stats
class ComparisionTest:
@staticmethod
def is_normal_distribution(data, alpha=0.05):
"""Шапиро-Уилк тест на нормальность"""
stat, p_value = stats.shapiro(data)
return p_value > alpha # True если нормальное
def compare_periods(self, control_checks, treatment_checks):
"""Сравнение двух периодов"""
# Проверяем нормальность
control_normal = self.is_normal_distribution(control_checks)
treatment_normal = self.is_normal_distribution(treatment_checks)
if control_normal and treatment_normal:
# Если оба нормальные — t-тест
stat, p_value = stats.ttest_ind(treatment_checks, control_checks)
test_name = "Independent t-test"
else:
# Если нет — Mann-Whitney U (non-parametric)
stat, p_value = stats.mannwhitneyu(treatment_checks, control_checks)
test_name = "Mann-Whitney U"
return {
"test": test_name,
"statistic": stat,
"p_value": p_value,
"significant": p_value < 0.05,
"control_mean": control_checks.mean(),
"treatment_mean": treatment_checks.mean(),
"difference": treatment_checks.mean() - control_checks.mean(),
"percent_increase": ((treatment_checks.mean() - control_checks.mean()) / control_checks.mean()) * 100
}
3. Доверительные интервалы (Confidence Intervals)
from scipy import stats
def calculate_ci_for_mean(data, confidence=0.95):
"""95% доверительный интервал для среднего"""
n = len(data)
mean = data.mean()
std_error = data.std() / np.sqrt(n)
# t-распределение для малых выборок
t_score = stats.t.ppf((1 + confidence) / 2, df=n-1)
margin_of_error = t_score * std_error
ci_lower = mean - margin_of_error
ci_upper = mean + margin_of_error
return {
"mean": mean,
"ci_lower": ci_lower,
"ci_upper": ci_upper,
"margin_of_error": margin_of_error
}
# Пример
control_ci = calculate_ci_for_mean(control_checks, confidence=0.95)
treatment_ci = calculate_ci_for_mean(treatment_checks, confidence=0.95)
print(f"Контроль: {control_ci['mean']:.2f} [CI: {control_ci['ci_lower']:.2f} - {control_ci['ci_upper']:.2f}]")
print(f"Лечение: {treatment_ci['mean']:.2f} [CI: {treatment_ci['ci_lower']:.2f} - {treatment_ci['ci_upper']:.2f}]")
# Если интервалы не пересекаются — различие значимо
4. Контроль за сезонностью и трендами
Средний чек колеблется в зависимости от сезона, дня недели, праздников.
import pandas as pd
class SeasonalAnalyzer:
def __init__(self, orders_df):
self.df = orders_df.copy()
self.df["date"] = pd.to_datetime(self.df["date"])
self.df["weekday"] = self.df["date"].dt.dayofweek
self.df["month"] = self.df["date"].dt.month
def normalize_by_seasonality(self):
"""Нормализуем средний чек по сезонности"""
# Считаем baseline по дням недели
baseline = self.df.groupby("weekday")["amount"].mean()
# Нормализуем
self.df["normalized_amount"] = self.df.apply(
lambda row: row["amount"] / baseline[row["weekday"]],
axis=1
)
return self.df
def control_holiday_effect(self, holiday_dates):
"""Исключаем дни с праздниками из анализа"""
holiday_mask = self.df["date"].isin(holiday_dates)
return self.df[~holiday_mask]
5. CUPED: Контролируемый эксперимент с предыдущими данными
Снижает дисперсию метрики, делая тест более чувствительным.
def apply_cuped(control_data, treatment_data, control_baseline, treatment_baseline):
"""CUPED для снижения дисперсии"""
# Ковариация между текущим и baseline периодом
control_cov = np.cov(control_baseline, control_data)[0, 1]
control_var = np.var(control_baseline)
# Оптимальный вес
theta = control_cov / control_var if control_var > 0 else 0
# Отрегулированные метрики
control_adjusted = control_data - theta * (control_baseline - control_baseline.mean())
treatment_adjusted = treatment_data - theta * (treatment_baseline - treatment_baseline.mean())
return control_adjusted, treatment_adjusted
# Пример использования
control_adj, treatment_adj = apply_cuped(
control_checks, treatment_checks,
control_baseline_checks, treatment_baseline_checks
)
result = stats.ttest_ind(treatment_adj, control_adj)
print(f"CUPED-adjusted p-value: {result.pvalue}")
6. Сегментация и анализ подгрупп
Рост среднего чека может быть обусловлен разными причинами.
def analyze_by_segments(df, test_period_start, test_period_end):
"""Анализ по сегментам клиентов"""
results = {}
for segment in df["segment"].unique():
segment_df = df[df["segment"] == segment]
control = segment_df[segment_df["date"] < test_period_start]["amount"]
treatment = segment_df[
(segment_df["date"] >= test_period_start) &
(segment_df["date"] <= test_period_end)
]["amount"]
if len(treatment) > 30: # Минимум для статистики
_, p_value = stats.ttest_ind(treatment, control)
results[segment] = {
"control_mean": control.mean(),
"treatment_mean": treatment.mean(),
"lift_pct": ((treatment.mean() - control.mean()) / control.mean()) * 100,
"p_value": p_value,
"significant": p_value < 0.05
}
return pd.DataFrame(results).T
7. Размер эффекта (Effect Size)
def calculate_effect_size(control_data, treatment_data):
"""Cohen's d — стандартизированный размер эффекта"""
n1, n2 = len(control_data), len(treatment_data)
var1, var2 = control_data.var(), treatment_data.var()
# Объединённая стандартная девиация
pooled_std = np.sqrt(((n1-1)*var1 + (n2-1)*var2) / (n1 + n2 - 2))
# Cohen's d
cohens_d = (treatment_data.mean() - control_data.mean()) / pooled_std
# Интерпретация
if abs(cohens_d) < 0.2:
effect = "negligible"
elif abs(cohens_d) < 0.5:
effect = "small"
elif abs(cohens_d) < 0.8:
effect = "medium"
else:
effect = "large"
return {"cohens_d": cohens_d, "effect_size": effect}
8. Практический чеклист
- Выберите период сравнения — минимум 2 недели, лучше месяц (контролирует недельные циклы)
- Исключите аномалии — выбросы, технические сбои, специальные акции
- Проверьте нормальность распределения — выберите правильный статтест
- Рассчитайте доверительный интервал — 95% по умолчанию
- Контролируйте за сезонностью — используйте CUPED или нормализацию
- Посмотрите на подгруппы — может быть, рост только для VIP клиентов?
- Проверьте размер выборки — минимум 30 заказов в группе
- Интерпретируйте результаты — 2% рост не всегда бизнес-важен
Вывод: Увеличение среднего чека считается доказанным, если: (1) p-value меньше 0.05, (2) доверительный интервал не пересекает ноль, (3) эффект практически значим (Cohen's d больше 0.2), (4) результат воспроизводится на подгруппах.