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

Какие знаешь методы проверки гипотез кроме A/B теста?

2.0 Middle🔥 141 комментариев
#A/B тестирование#Статистика и математика

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Методы проверки гипотез кроме A/B тестов

A/B тест — это мощный инструмент, но далеко не единственный способ проверить гипотезу. В зависимости от контекста, бюджета, временных рамок и типа гипотезы, я использую разные методы. Расскажу о них с примерами.

1. Quasi-Experimental Design (差分差分анализ / Difference-in-Differences)

Когда использовать: Когда нельзя сделать RCT (случайное распределение), но есть natural experiment: например, feature выкатили в одном регионе раньше, чем в другом.

Как работает:

import pandas as pd
import numpy as np
from scipy import stats

def difference_in_differences_analysis():
    """
    DiD анализ: сравнить тренд в treated group vs control group
    
    Пример: Компания выкатила новую checkout в регионе A в апреле.
    В регионе B эта feature вышла только в июне.
    Хотим знать: повысилась ли конверсия?
    """
    
    # Данные: 3 месяца до, 3 месяца после для обоих регионов
    data = {
        'region': ['A']*6 + ['B']*6,
        'month': [1,2,3,4,5,6]*2,
        'treated': [0,0,0,1,1,1] + [0,0,0,0,0,0],
        'conversion_rate': [
            0.20, 0.21, 0.20,  # Region A: pre-treatment (стабильно)
            0.24, 0.25, 0.26,  # Region A: post-treatment (рост)
            0.22, 0.21, 0.22,  # Region B: pre-treatment (стабильно)
            0.21, 0.22, 0.23   # Region B: post-treatment (небольшой рост, но меньше чем A)
        ]
    }
    
    df = pd.DataFrame(data)
    
    # Шаг 1: Рассчитать средние значения
    treated_before = df[(df['treated'] == 0) & (df['region'] == 'A')]['conversion_rate'].mean()
    treated_after = df[(df['treated'] == 1) & (df['region'] == 'A')]['conversion_rate'].mean()
    control_before = df[(df['treated'] == 0) & (df['region'] == 'B')]['conversion_rate'].mean()
    control_after = df[(df['treated'] == 0) & (df['region'] == 'B')]['conversion_rate'].mean()  # Нету treatment в B
    
    # Шаг 2: DiD estimator = (treated_after - treated_before) - (control_after - control_before)
    treatment_effect = (treated_after - treated_before) - (control_after - control_before)
    
    print(f"Region A: {treated_before:.1%} -> {treated_after:.1%} (изменение: +{(treated_after-treated_before)*100:.1f}pp)")
    print(f"Region B: {control_before:.1%} -> {control_after:.1%} (изменение: +{(control_after-control_before)*100:.1f}pp)")
    print(f"\nTreatment effect (каузальный эффект): +{treatment_effect*100:.1f}pp")
    
    return treatment_effect

treatment_effect = difference_in_differences_analysis()

Преимущества:

  • Не нужно случайное распределение
  • Работает с naturally occurring groups
  • Контролирует за общими тенденциями (что растёт/падает везде)

Недостатки:

  • Требует strong assumption: параллельные тренды (control group растёт так же, как treated бы рос без treatment)
  • Сложнее интерпретировать

2. Propensity Score Matching (PSM)

Когда использовать: Когда users сами выбирают treatment (self-selection bias). Например, хотим сравнить customers, которые вручную включили новый feature vs которые не включили.

Как работает:

from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import NearestNeighbors

def propensity_score_matching():
    """
    PSM: найти "двойников" для каждого treated user в control group
    и сравнить outcomes
    """
    
    # Шаг 1: Предсказать вероятность treatment для всех users
    # (зависит ли adoption feature от характеристик пользователя?)
    
    X = df[['user_lifetime_days', 'num_purchases', 'avg_order_value']]
    y = df['adopted_feature']  # 0 или 1
    
    model = LogisticRegression()
    model.fit(X, y)
    df['propensity_score'] = model.predict_proba(X)[:, 1]
    
    # Шаг 2: Для каждого treated user найти control user с похожим propensity score
    treated = df[df['adopted_feature'] == 1]
    control = df[df['adopted_feature'] == 0]
    
    matched_pairs = []
    for _, treated_user in treated.iterrows():
        # Найти ближайшего соседа по propensity score
        distances = abs(control['propensity_score'] - treated_user['propensity_score'])
        closest_match_idx = distances.idxmin()
        
        matched_pairs.append({
            'treated_user_id': treated_user['user_id'],
            'control_user_id': control.loc[closest_match_idx, 'user_id'],
            'propensity_score_diff': distances[closest_match_idx]
        })
    
    # Шаг 3: Сравнить outcomes в matched pairs
    matched_df = pd.DataFrame(matched_pairs)
    
    treated_outcomes = treated.set_index('user_id').loc[matched_df['treated_user_id'], 'revenue'].values
    control_outcomes = control.set_index('user_id').loc[matched_df['control_user_id'], 'revenue'].values
    
    ate = (treated_outcomes - control_outcomes).mean()  # Average Treatment Effect
    print(f"Average Treatment Effect: ${ate:.2f}")
    
    return ate

Преимущества:

  • Решает selection bias
  • Работает с observational data (не нужен controlled experiment)

Недостатки:

  • Полагается на assumption: нет unmeasured confounders
  • Может потерять часть данных (not all users имеют matches)

3. Regression Discontinuity Design (RDD)

Когда использовать: Когда treatment назначается по порогу какой-то переменной. Например, скидка даётся всем, чья корзина > $100.

Как работает:

import matplotlib.pyplot as plt
from scipy.interpolate import UnivariateSpline

def regression_discontinuity_design():
    """
    RDD: сравнить users прямо ДО и ПОСЛЕ порога
    """
    
    # Данные: cart value vs conversion
    np.random.seed(42)
    n = 1000
    
    # Users с низкой корзиной (< $100) - no discount
    cart_value_low = np.random.uniform(50, 99, n//2)
    conversion_low = 0.15 + 0.001 * cart_value_low + np.random.normal(0, 0.05, n//2)
    
    # Users с высокой корзиной (>= $100) - 10% discount
    cart_value_high = np.random.uniform(101, 200, n//2)
    conversion_high = 0.20 + 0.001 * cart_value_high + np.random.normal(0, 0.05, n//2)
    # ^^ заметьте: +0.05 это treatment effect (10% discount)
    
    # Combine
    cart_values = np.concatenate([cart_value_low, cart_value_high])
    conversions = np.concatenate([conversion_low, conversion_high])
    
    # Plot
    plt.scatter(cart_values[cart_values < 100], conversions[cart_values < 100], alpha=0.5, label='No discount')
    plt.scatter(cart_values[cart_values >= 100], conversions[cart_values >= 100], alpha=0.5, label='10% discount')
    plt.axvline(x=100, color='red', linestyle='--', label='Threshold')
    plt.xlabel('Cart Value ($)')
    plt.ylabel('Conversion Rate')
    plt.legend()
    plt.title('Regression Discontinuity: Effect of Discount at $100 threshold')
    
    # Рассчитать treatment effect: difference в conversion прямо около порога
    near_threshold = 10  # $10 от порога
    left_side = conversions[(cart_values >= 90) & (cart_values < 100)].mean()
    right_side = conversions[(cart_values >= 100) & (cart_values < 110)].mean()
    
    treatment_effect = right_side - left_side
    print(f"Treatment Effect (discount): +{treatment_effect*100:.1f}pp conversion")
    
    return treatment_effect

Преимущества:

  • Работает когда treatment назначается по правилу
  • Нет selection bias (порог обычно arbitrary)

Недостатки:

  • Нужны достаточно данных прямо около порога
  • Результаты применимы только к users около порога

4. Synthetic Control Method

Когда использовать: Когда нужно оценить эффект treatment в одном месте/группе, используя данные из других мест.

Как работает:

def synthetic_control_method():
    """
    Пример: компания выкатила feature в городе A.
    Хотим знать эффект, но нет perfect control города.
    Синтетический контроль = взвешенная комбинация других городов
    """
    
    # Data: weekly revenue по городам
    weeks = range(1, 21)  # 20 недель
    
    # Pre-treatment (недели 1-10), treatment в неделю 11
    city_a_pre = [100 + np.random.normal(5, 2) for _ in range(10)]
    city_a_post = [100 + 15 + np.random.normal(5, 2) for _ in range(10)]  # treatment effect = +15
    city_a = city_a_pre + city_a_post
    
    city_b = [95 + np.random.normal(4, 2) for _ in range(20)]  # Control (no treatment)
    city_c = [98 + np.random.normal(3, 2) for _ in range(20)]  # Control (no treatment)
    
    # Шаг 1: На pre-period данных найти веса для synthetic control
    X_pre = np.array([city_b[:10], city_c[:10]]).T  # Features: city B и C
    y_pre = np.array(city_a_pre)  # Target: city A
    
    model = np.linalg.lstsq(X_pre, y_pre, rcond=None)[0]
    weight_b, weight_c = model
    
    print(f"Synthetic City A = {weight_b:.2f} * City B + {weight_c:.2f} * City C")
    
    # Шаг 2: Применить те же веса к post-period для counterfactual
    synthetic_control_post = weight_b * np.array(city_b[10:]) + weight_c * np.array(city_c[10:])
    
    # Шаг 3: Treatment effect = actual - synthetic control
    treatment_effect = (np.array(city_a_post) - synthetic_control_post).mean()
    print(f"\nTreatment Effect: +{treatment_effect:.1f}")
    
    return treatment_effect

Преимущества:

  • Работает для single treated unit (city, country, etc.)
  • Можно использовать pre-treatment данные для validation

Недостатки:

  • Требует данных из нескольких control units
  • Чувствительно к выбору control units

5. Interrupted Time Series (ITS) Analysis

Когда использовать: Когда есть длинный временной ряд и treatment происходит в определённый момент времени.

Как работает:

from statsmodels.formula.api import ols

def interrupted_time_series():
    """
    ITS: анализ изменения тренда до и после intervention
    """
    
    # Данные: месячная retention за 24 месяца
    months = np.arange(1, 25)
    
    # Месяцы 1-12: без intervention (тренд: стабилен или растёт слегка)
    retention_pre = 0.30 + 0.002 * months[:12] + np.random.normal(0, 0.01, 12)
    
    # Месяцы 13-24: после intervention (retention улучшилась)
    retention_post = 0.35 + 0.003 * (months[12:] - 12) + np.random.normal(0, 0.01, 12)
    
    retention = np.concatenate([retention_pre, retention_post])
    intervention = [0]*12 + [1]*12  # intervention at month 13
    months_since_intervention = [0]*12 + list(range(1, 13))  # 0, 0, ..., 1, 2, ..., 12
    
    # Regression: retention ~ months + intervention + months_since_intervention
    data = pd.DataFrame({
        'month': months,
        'retention': retention,
        'intervention': intervention,
        'months_since_intervention': months_since_intervention
    })
    
    model = ols('retention ~ month + intervention + months_since_intervention', data=data).fit()
    
    print("ITS Results:")
    print(f"Intercept (baseline): {model.params['Intercept']:.3f}")
    print(f"Pre-intervention trend: {model.params['month']:.5f} per month")
    print(f"Level change at intervention: {model.params['intervention']:.3f} (immediate jump)")
    print(f"Slope change post-intervention: {model.params['months_since_intervention']:.5f} per month")
    
    # Level change показывает immediate effect
    # Slope change показывает, как изменился тренд
    
    return model

Преимущества:

  • Работает с одной временной серией
  • Показывает как immediate effect так и trend change

Недостатки:

  • Нужны данные до и после intervention
  • Чувствительно к confounding (другие события в том же периоде)

6. Causal Impact Analysis (Bayesian Structural Time-Series)

Когда использовать: Когда нужна sophisticated analysis с uncertainty quantification.

from causalimpact import CausalImpact

def causal_impact_analysis():
    """
    Google's CausalImpact library: Bayesian approach
    """
    
    # Data: pre-period (training), post-period (analysis)
    pre_period = ['2024-01-01', '2024-04-30']
    post_period = ['2024-05-01', '2024-08-31']
    
    # Actual series: где мы применили treatment
    actual = df_treated['daily_revenue']  # Time series
    
    # Control series: похожая группа без treatment
    control = df_control['daily_revenue']
    
    # Объединить
    data = pd.concat([actual, control], axis=1)
    data.columns = ['actual', 'control']
    
    # Запустить анализ
    impact = CausalImpact(data, pre_period, post_period)
    print(impact.summary())
    # Output показывает: what was actual, counterfactual (что было бы без treatment), и difference

Преимущества:

  • Probabilistic output (не просто точечная оценка, а credible intervals)
  • Работает с multiple control series

Недостатки:

  • Требует Python library
  • Сложнее интерпретировать

7. Qualitative Research Methods

Когда использовать: Когда нужно понять не только "что", но и "почему".

Методы:

  • User Interviews (5-10 users): глубокое понимание мотивов
  • Focus Groups (6-8 users): групповая дискуссия, разные мнения
  • User Testing (5-20 users): наблюдение как пользователи используют product
  • Surveys (100+ users): масштаб, но менее глубоко
  • Diary Studies (10-20 users): как пользователь использует product в реальной жизни
# Пример: User Testing script
user_testing_guide = """
1. Greeting (5 min): объяснить цель
2. Background (5 min): спросить о их опыте
3. Observation (15 min): дать task, наблюдать (не помогать!)
4. Interview (10 min): спросить "почему ты сделал X?"
5. Wrap-up (5 min): спасибо

Цель: найти pain points и понять mental model пользователя
"""

8. Cohort Analysis для проверки гипотез

Когда использовать: Когда хотим проверить гипотезу на естественных группах пользователей.

-- Пример: улучшилась ли retention после изменения onboarding?
WITH cohorts AS (
  SELECT
    DATE(signup_date) as cohort,
    CASE 
      WHEN DATE(signup_date) < '2024-05-01' THEN 'Old Onboarding'
      ELSE 'New Onboarding'
    END as version,
    user_id,
    DATE(last_activity) as last_activity_date
  FROM users
)
SELECT
  cohort,
  version,
  COUNT(DISTINCT user_id) as cohort_size,
  ROUND(100.0 * COUNT(DISTINCT CASE WHEN DATE_PART('day', last_activity_date - cohort) >= 7 THEN user_id END) / COUNT(DISTINCT user_id), 1) as d7_retention
FROM cohorts
GROUP BY cohort, version
ORDER BY cohort;

Сравнительная таблица методов

МетодТребует Randomization?Требует Control Group?TimeframesСложность
A/B TestДаДа1-4 неделиНизкая
DiDНетДаМесяцыСредняя
PSMНетДаБыстроСредняя
RDDНетНетБыстроСредняя
Synthetic ControlНетДаМесяцыВысокая
ITSНетНетМесяцыСредняя
QualitativeНетНет1-2 неделиСредняя
Cohort AnalysisНетДаБыстроНизкая

Мой выбор метода

  1. Default: A/B тест (если можно randomize)
  2. Если нельзя randomize: DiD или Cohort Analysis
  3. Если нужна глубина: User Testing + Surveys
  4. Если single unit: Synthetic Control или ITS
  5. Если много confounders: PSM

Ключевое: выбирать метод в зависимости от question, data availability и constraints. Не использовать A/B тест если qualitative research даст ответ быстрее.

Какие знаешь методы проверки гипотез кроме A/B теста? | PrepBro