Какие знаешь методы проверки гипотез кроме A/B теста?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Методы проверки гипотез кроме 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 | Нет | Да | Быстро | Низкая |
Мой выбор метода
- Default: A/B тест (если можно randomize)
- Если нельзя randomize: DiD или Cohort Analysis
- Если нужна глубина: User Testing + Surveys
- Если single unit: Synthetic Control или ITS
- Если много confounders: PSM
Ключевое: выбирать метод в зависимости от question, data availability и constraints. Не использовать A/B тест если qualitative research даст ответ быстрее.