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

Как проверить, получилось ли увеличить прибыль по среднему чеку?

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. Практический чеклист

  1. Выберите период сравнения — минимум 2 недели, лучше месяц (контролирует недельные циклы)
  2. Исключите аномалии — выбросы, технические сбои, специальные акции
  3. Проверьте нормальность распределения — выберите правильный статтест
  4. Рассчитайте доверительный интервал — 95% по умолчанию
  5. Контролируйте за сезонностью — используйте CUPED или нормализацию
  6. Посмотрите на подгруппы — может быть, рост только для VIP клиентов?
  7. Проверьте размер выборки — минимум 30 заказов в группе
  8. Интерпретируйте результаты — 2% рост не всегда бизнес-важен

Вывод: Увеличение среднего чека считается доказанным, если: (1) p-value меньше 0.05, (2) доверительный интервал не пересекает ноль, (3) эффект практически значим (Cohen's d больше 0.2), (4) результат воспроизводится на подгруппах.