Собесов

FindMyKids — два независимых A/B-теста: цена и позиция функций

Кейсы и метрикиAB analysisСредняяMiddle

Условие

В сервисе 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. Формализовать гипотезу:
    • Эксп. 1 (цена 400→160): ↑ конверсия в покупку, ↓ ARPPU, итог ARPU = ?
    • Эксп. 2 (свап позиций): сместит ли клики/использование между функциями A и Б?
  2. Проверить sanity (split ratio, SRM, эквивалентность пред-периода).
  3. Метрики:
    • Основные: ARPU = revenue / users в группе, CR в покупку (бинарка), engagement = % users using function X.
    • Гарды: retention (Day-1, Day-7), отток.
  4. Стат-тесты:
    • 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 + Б) может остаться тем же, но перераспределиться. Поэтому:

  1. Сравнить total events function% per user — общий engagement.
  2. Сравнить отдельно usage A и Б — кто выигрывает позицию function1.
  3. Сверить с retention/ARPU: если позиция первой функции сильно влияет на retention, эффект может быть глобальным.

Часто решающий аргумент — какая функция важнее для бизнеса: если Б монетизирует лучше, и B растёт на новой позиции — keep new.

Проверка / интерпретация

  • SRM (Sample Ratio Mismatch): доли групп ≈ задумано. Если нет — баг сплита.
  • Distribution check: дисперсия выручки сильно вытянута → используйте bootstrap, не t-test, или преобразование (log1p).
  • Multiple testing: смотрим ARPU + CR + engagement → α/3 или Holm-Bonferroni.

Подводные камни

  1. Action LIKE 'function%' ловит и function1, и function2 — для эксп. 2 нужен явный фильтр по конкретной функции.
  2. Один пользователь — в нескольких экспериментах одновременно. Если эксп. 2 не сбалансирован по группам эксп. 1, есть confounding. Проверьте кросс-таблицу.
  3. ARPU vs ARPPU: оба нужны. ARPU = деньги/всех; ARPPU = деньги/платящих. Цена → ↑CR, ↓ARPPU; ARPU = их произведение.
  4. Payment имеет 2 типа покупок — суммируйте оба или анализируйте раздельно.
  5. Retention 1 не наблюдаем за период теста, если короткий — нет статистики на гарде.

Эталонный ответ

Эксп. 1: основная метрика — ARPU; Welch t-test, bootstrap CI, z-test для CR; гард — engagement и retention. Решение по знаку и значимости ARPU.

Эксп. 2: метрика — engagement по конкретной функции (A и Б отдельно), плюс total engagement; цель — увидеть cannibalization vs uplift, и решить, какая функция приоритетнее на function1.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти