Условие
Мы придумали MLфичу — автоматическая сборка крутого видео на YouTube. Хотим продавать как отдельную подписку (другие подобные фичи мы успешно продавали).
Хотим протестировать 3 ценовые опции: 10, $15 в месяц. Время теста — месяц. Дизайн эксперимента и анализ — на вас.
Вопросы:
- Опишите setup эксперимента: гипотезу (с привязкой к выбору лучшей цены в терминах A/B), ключевую метрику, статкритерий.
- Рассчитайте размер групп при базовой конверсии в подписку 10%, MDE = 5%, alpha = 5%, beta = 20%.
Решение
Подход
Это 3-арм A/B/C тест с разными ценами. Главная сложность: decision-метрика — не конверсия, а revenue per user (RPU), потому что более низкая цена даёт высокую конверсию, но низкий чек.
Шаг 1. Формулировка гипотезы
H0: средний доход на пользователя одинаков для всех трёх цен. H1: хотя бы одна цена даёт больший RPU.
В терминах A/B: ищем revenue-максимизирующую цену, не конверсию-максимизирующую.
Ключевая метрика
RPU (revenue per user) = price * conversion_rate. Это бинарная метрика, умноженная на константу — статистически проще, чем «выручка за месяц с учётом отмен», но требует определиться, что считать конверсией.
Дополнительные метрики
| Метрика | Зачем |
|---|---|
| Conversion rate | для каждой цены — отдельно (для проверки модели) |
| Cancellation rate | дешёвая цена → больше «попробовали и ушли» |
| LTV-90 | если можно ждать дольше — лучше metric, чем RPU |
| Cancel within 7 days | гард — нельзя «купить» конверсию ценой, которую люди потом массово отменяют |
| Time to subscribe | UX-сигнал |
Статкритерий
При двух группах — z-test на разность средних RPU (через delta-method или bootstrap, потому что RPU — функция бинарной величины и константы; либо просто z-test для пропорций с поправкой на цену).
При трёх группах — два варианта:
- ANOVA / Kruskal-Wallis — глобальный тест «есть ли разница хоть где-то». Требует попарных сравнений потом, с поправкой Бонферрони ().
- Pairwise z-tests на 3 пары (
A-B, B-C, A-C) с поправкой Бонферрони / Холма-Бонферрони (α/3для строгого FWER) или Benjamini-Hochberg (для FDR).
Для multi-arm pricing — лучше попарный z-test на RPU с BH-коррекцией, потому что нас интересуют попарные сравнения, а не общий тест.
Шаг 2. Размер выборки
Если базовая конверсия p0 = 0.10, MDE = 5% относительный (то есть 0.10 → 0.105 — крошечный эффект):
from statsmodels.stats.power import NormalIndPower
from statsmodels.stats.proportion import proportion_effectsize
p0 = 0.10
mde_relative = 0.05
p1 = p0 * (1 + mde_relative) # 0.105
es = proportion_effectsize(p1, p0)
n = NormalIndPower().solve_power(
effect_size=es, alpha=0.05, power=0.8, alternative="two-sided"
)
print(f"n per group ≈ {n:.0f}") # ~94 800~94 800 на группу × 3 группы ≈ 285 000 total. Для месячного теста при 10% базовой конверсии и активной аудитории — может быть подъёмно для крупного приложения, но не для маленького.
Если MDE — 5% абсолютный (0.10 → 0.15 — огромный эффект):
p0, p1 = 0.10, 0.15
es = proportion_effectsize(p1, p0)
n = NormalIndPower().solve_power(effect_size=es, alpha=0.05, power=0.8)
# ~720 на группу«MDE = 5%» — формулировка двусмысленная. Уточните у заказчика, что именно: 5% относительный или абсолютный.
Поправка на multiple comparisons
При 3 попарных сравнениях с Бонферрони: α' = 0.05 / 3 = 0.0167. Размер выборки растёт примерно в (z_{0.0083} / z_{0.025})^2 ≈ 1.4 раз. То есть для абсолютного MDE = 5% нужно ~1000 на группу. Для относительного 5% — ~133 000 на группу.
Шаг 3. Дизайн
from statsmodels.stats.power import NormalIndPower
from statsmodels.stats.proportion import proportion_effectsize, proportions_ztest
from statsmodels.stats.multitest import multipletests
import numpy as np
# 1. Размер группы (с поправкой Бонферрони на 3 сравнения)
p0, p1 = 0.10, 0.15 # абсолютный MDE 5pp
es = proportion_effectsize(p1, p0)
n_unadj = NormalIndPower().solve_power(es, alpha=0.05, power=0.8)
# Поправка на 3 сравнения: alpha = 0.05/3
n_adj = NormalIndPower().solve_power(es, alpha=0.05/3, power=0.8)
print(f"n per group (без поправки): {n_unadj:.0f}")
print(f"n per group (Bonferroni): {n_adj:.0f}")
# 2. После эксперимента: попарные сравнения
def run_pairwise(groups, prefix=""):
pvals, pairs = [], []
for i, j in [(0, 1), (1, 2), (0, 2)]:
z, p = proportions_ztest(
count=[groups[i].sum(), groups[j].sum()],
nobs=[len(groups[i]), len(groups[j])])
pairs.append((i, j)); pvals.append(p)
rejected, p_adj, _, _ = multipletests(pvals, alpha=0.05, method="bonferroni")
return list(zip(pairs, p_adj, rejected))Анализ результатов
# Решение принимаем по RPU
def rpu(group, price):
return group.mean() * price
# Скажем, RPU($5) = 0.20 * 5 = $1.00
# RPU($10) = 0.10 * 10 = $1.00
# RPU($15) = 0.05 * 15 = $0.75
# Считаем CI на RPU через bootstrap
def bootstrap_rpu(conv, price, n_iter=10000, seed=42):
rng = np.random.default_rng(seed)
samples = []
for _ in range(n_iter):
idx = rng.integers(0, len(conv), len(conv))
samples.append(conv[idx].mean() * price)
return np.percentile(samples, [2.5, 97.5])Решение:
- Сравниваем
RPU(price)— более стабильная картина. - Если разница в RPU между двумя ценами не значима → выбираем по доп. соображениям (cancel rate, LTV-90, бренд-позиционирование).
Подводные камни
- «Конверсия» — главная метрика? Нет. Низкая цена даёт высокую конверсию, но мизерный RPU. Decision = RPU, конверсия — sanity-check.
- Multiple comparisons. Без поправки FWER при 3 сравнениях — раздутый FPR.
- Cannibalization. Если у нас уже есть платные функции, дешёвая ($5) подписка на новую может «съесть» платящих с других продуктов. Проверяйте.
- Cancel rate. Низкая цена → больше «попробовать и забыть». RPU может быть высоким за месяц, но плохая ретенция → LTV рухнет.
- Anchoring effect. Если показывать пользователю все 3 цены сразу на пейволле — это другой эксперимент (про anchoring), не про лучшую цену. Здесь — каждый видит ОДНУ цену.
- Network effects. Если юзеры обсуждают цену — A/B нечистый.
- Sample Ratio Mismatch. Сплит на 3 группы — особое внимание SRM (chi2 со степенями = 2).
- Длительность. Месяц — мало для retention/cancel метрик. RPU считаем за месяц, но cancel rate смотрим за 3 месяца.
- Sticky assignment. Юзер должен видеть одну цену всегда (иначе путается).
Альтернативы
- Multi-armed bandit (Thompson sampling) с RPU как reward — оптимизирует выручку в процессе теста, но даёт менее точные оценки.
- Sequential testing (mSPRT) — экономия выборки, но требует careful FWER.
- Conjoint analysis на опросе — дешевле, не A/B, но менее точно.
Эталонный ответ
Setup: 3 группы (по одной цене каждая), sticky-assignment, decision-метрика RPU = price × conversion, попарные z-tests с Бонферрони/BH.
Размер выборки при p0=0.10, MDE=5%:
- 5% относительный: ~94k на группу (без поправки), ~133k с Бонферрони на 3 сравнения. Месяц нереалистичен без массивной аудитории.
- 5% абсолютный: ~720 на группу (без поправки), ~1000 с поправкой. Подъёмно за месяц.
Главное: уточнить «MDE = 5%» (относительный или абсолютный) и принять решение по RPU, а не конверсии.