Собесов

Сценарий: CI для ratio metric (CTR, ARPU) — delta method

Статистика и теорверConfidence intervalsСложнаяSenior

Условие

CTR = clicks / views на user-level. Считаем по 10 000 пользователей. Как построить CI?

Решение

Проблема ratio metric

CTR на user level — это sum(clicks) / sum(views) по всем юзерам. Не среднее CTR per user (это разные числа).

Прямой t-test на ratio неверен, потому что:

  • Numerator (clicks) и denominator (views) коррелированы.
  • Variance ratio не равна Var(numerator) / mean(denominator)².

Delta method

Для функции g(X, Y) = X / Y:

Var(X/Y) ≈ (1/μ_Y²) × Var(X) + (μ_X² / μ_Y⁴) × Var(Y) - 2(μ_X/μ_Y³) × Cov(X, Y)

Где μ — средние, X — clicks, Y — views.

import numpy as np
 
clicks = df['clicks']
views = df['views']
 
mean_c, mean_v = clicks.mean(), views.mean()
var_c, var_v = clicks.var(ddof=1), views.var(ddof=1)
cov_cv = np.cov(clicks, views, ddof=1)[0, 1]
 
n = len(df)
ctr = mean_c / mean_v
var_ctr = (var_c / mean_v**2 - 2*mean_c*cov_cv / mean_v**3 + mean_c**2*var_v / mean_v**4) / n
se_ctr = np.sqrt(var_ctr)
 
ci = (ctr - 1.96 * se_ctr, ctr + 1.96 * se_ctr)

Альтернатива — bootstrap

def ratio_ci_bootstrap(df, n_boot=10_000):
    ratios = []
    n = len(df)
    for _ in range(n_boot):
        idx = np.random.randint(0, n, n)
        sub = df.iloc[idx]
        ratios.append(sub['clicks'].sum() / sub['views'].sum())
    return np.percentile(ratios, [2.5, 97.5])

Bootstrap дороже, но меньше предположений.

A/B-тест для ratio

# Версия A: clicks_a, views_a; Версия B: clicks_b, views_b
def delta_variance(c, v):
    mc, mv = c.mean(), v.mean()
    var_c, var_v = c.var(ddof=1), v.var(ddof=1)
    cov_cv = np.cov(c, v, ddof=1)[0, 1]
    n = len(c)
    return (var_c / mv**2 - 2*mc*cov_cv / mv**3 + mc**2*var_v / mv**4) / n
 
var_a = delta_variance(c_a, v_a)
var_b = delta_variance(c_b, v_b)
ctr_a, ctr_b = c_a.sum() / v_a.sum(), c_b.sum() / v_b.sum()
 
z = (ctr_a - ctr_b) / np.sqrt(var_a + var_b)
p_value = 2 * (1 - stats.norm.cdf(abs(z)))

Когда unit of analysis = views

Если каждый view независим (не клустеризован по юзерам), CTR — это просто Binomial → Wilson CI. Но в реальности views у одного юзера зависимы (поведение коррелирует), поэтому unit = user.

Clustered std error

Альтернативный подход — кластеризованные SE с регрессией:

import statsmodels.formula.api as smf
model = smf.ols('clicks ~ 1', data=df).fit(
    cov_type='cluster',
    cov_kwds={'groups': df['user_id']}
)

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

  1. Naive t-test(ctr_per_user) ≠ delta method. На user-level разные числа.
  2. Bootstrap должен resample users (целые блоки), не отдельные events.
  3. Delta method предполагает CLT — не работает на малых n или очень скошенных распределениях.
  4. Cov(clicks, views) обычно положительный — игнорирование сильно завышает SE.
  5. Watch out for users с 0 views — деление на ноль, исключать или агрегировать.

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

Delta method: Var(X/Y) ≈ (1/μ_Y²)Var(X) − 2(μ_X/μ_Y³)Cov(X,Y) + (μ_X²/μ_Y⁴)Var(Y). Или bootstrap на user-level. Naive t-test на user-CTR — неверно.

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

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

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