Условие
В сервисе FindMyKids проведено два независимых эксперимента. Дано три таблицы в MySQL:
Action(uid, ts, action)— события пользователя;firstOpen= первое открытие после установки,function%= использование какой-либо функции.Experiments(experiment, group, uid)— пользователь может быть в нескольких экспериментах одновременно.Payment(uid, paymentType, payment)— два возможных типа покупок одновременно.
Эксперимент 1: снижение цены первого продукта с 400 у.е. до 160 у.е.
Эксперимент 2: смена позиций функций A и Б — A была на function1, стала на function2; Б — наоборот.
Нужно проанализировать каждый и решить, какой вариант оставить (новый или старый), опираясь в т.ч. на ARPU и вовлечение в функции. SQL — для агрегации, отчёт — на Python (pandas/matplotlib).
Решение
Каркас анализа (общий для обоих)
- Формализовать гипотезу:
- Эксп. 1 (цена 400→160): ↑ конверсия в покупку, ↓ ARPPU, итог ARPU = ?
- Эксп. 2 (свап позиций): сместит ли клики/использование между функциями A и Б?
- Проверить sanity (split ratio, SRM, эквивалентность пред-периода).
- Метрики:
- Основные: ARPU = revenue / users в группе, CR в покупку (бинарка), engagement =
% users using function X. - Гарды: retention (Day-1, Day-7), отток.
- Основные: ARPU = revenue / users в группе, CR в покупку (бинарка), engagement =
- Стат-тесты:
- ARPU — bootstrap или Welch t-test (на user-level).
- CR — z-test двух пропорций.
- Engagement — z-test пропорций или хи-квадрат.
SQL для эксперимента 1
-- Метрики на уровне пользователя в эксп. 1
WITH e AS (
SELECT uid, `group`
FROM Experiments WHERE experiment = 'price_first_product'
),
pay AS (
SELECT uid, SUM(payment) AS rev,
COUNT(*) AS purchases
FROM Payment GROUP BY uid
),
fn AS (
SELECT uid,
MAX(action LIKE 'function%') AS used_any_fn
FROM Action GROUP BY uid
)
SELECT e.`group`,
COUNT(*) AS users,
AVG(COALESCE(pay.rev, 0)) AS arpu,
SUM(CASE WHEN pay.rev > 0 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) AS cr_pay,
AVG(COALESCE(pay.rev, 0) /
NULLIF(pay.purchases, 0)) AS arppu,
AVG(COALESCE(fn.used_any_fn, 0)) AS engagement
FROM e LEFT JOIN pay USING (uid)
LEFT JOIN fn USING (uid)
GROUP BY e.`group`;Python (Stat-test и решение)
import pandas as pd, numpy as np
from scipy import stats
from statsmodels.stats.proportion import proportions_ztest
df = pd.read_sql(query, conn) # user-level: uid, group, rev, paid, used_function
# 1) ARPU — Welch t-test
g0 = df[df['group']=='control']['rev']
g1 = df[df['group']=='test']['rev']
t, p_arpu = stats.ttest_ind(g1, g0, equal_var=False)
# Bootstrap CI на разность
def boot(a, b, n=10000):
n_a, n_b = len(a), len(b)
diffs = [b.sample(n_b, replace=True).mean() - a.sample(n_a, replace=True).mean()
for _ in range(n)]
return np.percentile(diffs, [2.5, 97.5])
# 2) CR в покупку — z-test пропорций
n0, n1 = len(g0), len(g1)
x0 = (g0>0).sum(); x1 = (g1>0).sum()
z, p_cr = proportions_ztest([x1, x0], [n1, n0])
# 3) Engagement — пропорции
e0 = df[df['group']=='control']['used_function'].mean()
e1 = df[df['group']=='test']['used_function'].mean()Логика решения для Эксп. 1 (price drop)
- Если
ARPU_test > ARPU_control(стат. значимо) иengagement не упал(гард) → выкатываем 160 у.е. - Если
CR_test > CR_control, ноARPPUупал так, чтоARPU_test < ARPU_control→ не выкатываем (классика «дешёвая монетизация — больше юзеров, меньше денег»). - Если разница не значима — оставляем как было (баланс status quo при отсутствии данных).
Логика для Эксп. 2 (swap)
Здесь главный вопрос — canibalization: при свапе позиций суммарное использование (A + Б) может остаться тем же, но перераспределиться. Поэтому:
- Сравнить total
events function%per user — общий engagement. - Сравнить отдельно usage A и Б — кто выигрывает позицию
function1. - Сверить с retention/ARPU: если позиция первой функции сильно влияет на retention, эффект может быть глобальным.
Часто решающий аргумент — какая функция важнее для бизнеса: если Б монетизирует лучше, и B растёт на новой позиции — keep new.
Проверка / интерпретация
- SRM (Sample Ratio Mismatch): доли групп ≈ задумано. Если нет — баг сплита.
- Distribution check: дисперсия выручки сильно вытянута → используйте bootstrap, не t-test, или преобразование (log1p).
- Multiple testing: смотрим ARPU + CR + engagement → α/3 или Holm-Bonferroni.
Подводные камни
Action LIKE 'function%'ловит иfunction1, иfunction2— для эксп. 2 нужен явный фильтр по конкретной функции.- Один пользователь — в нескольких экспериментах одновременно. Если эксп. 2 не сбалансирован по группам эксп. 1, есть confounding. Проверьте кросс-таблицу.
- ARPU vs ARPPU: оба нужны. ARPU = деньги/всех; ARPPU = деньги/платящих. Цена → ↑CR, ↓ARPPU; ARPU = их произведение.
Paymentимеет 2 типа покупок — суммируйте оба или анализируйте раздельно.- Retention 1 не наблюдаем за период теста, если короткий — нет статистики на гарде.
Эталонный ответ
Эксп. 1: основная метрика — ARPU; Welch t-test, bootstrap CI, z-test для CR; гард — engagement и retention. Решение по знаку и значимости ARPU.
Эксп. 2: метрика — engagement по конкретной функции (A и Б отдельно), плюс total engagement; цель — увидеть cannibalization vs uplift, и решить, какая функция приоритетнее на function1.