Анализ экспериментов с Ratio-метриками



Проводя эксперименты, обычно, мы используем базовые методы: t-test, z-test, bootstrap (если данных не так много). Это для одних метрик, но, предположим, мы работаем с метриками-отношения (представляют из себя деление суммы случайной величины X на сумму случайной величины Y).



В этом случае в группе A и группе B у нас нет дисперсии. В самом деле, рассмотрим средний чек - метрика отношения (сумма GMV / количество заказов), CTR (сумма кликов / количество показов). Получаем некоторую метрику Za и Zb для которой мы знаем значение, но дисперсии метрики не знаем.



Отсюда выход:



а) провести бутстрап, чтобы найти распределение этой метрики и узнать распределение статистики

б) бакетировать (меньше по ресурсам, чем бутстрап).

в) применять другие методы (например, линеаризацию или дельта-метод).

г*) что-то другое...



Бутстрап



Для оценки распределения метрики можем использовать выборочные значения в группе A и группе B и найти разницу, таким образом, мы получим «разницу средних». Из минусов: MDE не рассчитать (при дизайне эксперимента), очень долгий расчет при больших выборках, ресурсов может не хватить.



Расчет для конкретной выборки, можно разницу средних потом посчитать





def bootstrap_ratio(data, nominator, denominator, group_column, group_value, n_iter=10000):



group_data = data[data[group_column] == group_value]



boot_ratios = []

for _ in range(n_iter):

sample = group_data.sample(len(group_data), replace=True)

ratio = sample[nominator].sum() / sample[denominator].sum()

boot_ratios.append(ratio)



return np.array(boot_ratios)





Бакетизация



Делим также пользователей по бакетам (с их GMV и количеством заказов), распределение метрики будет нормальным, главное, чтобы в бакетах было достаточное количество наблюдений. Мы делаем n-подвыборок (где n - это количество бакетов). Из минусов: сложность работать с маленькими выборками, зависимость от количества бакетов (тонкая настройка).





def bucketize(data, nominator, denominator, n_buckets=50, random_state=42):



data = data.sample(frac=1, random_state=random_state).reset_index(drop=True)

buckets = np.array_split(data, n_buckets)

bucket_ratios = [bucket[nominator].sum() / bucket[denominator].sum() for bucket in buckets]



return bucket_ratios







Пару слов про дельта-метод и линеаризацию.



В общем-то это об одном и том же. Мы хотим найти дисперсию метрики для того, чтобы применить классические методы (например, t-test). В дельта-методе мы корректируем дисперсию на корреляцию двух случайных величин (числителя и знаменателя). Только есть разница: дельта-метод вычисляет дисперсию на уровне выборки сразу, а линеаризация позволяет Ratio-метрику превратить в поюзерную. Результаты сонаправлены.



Дельта-метод





def calculate_ratio_variance(values_numerator, values_denominator):



mean_num = np.mean(values_numerator)

mean_denom = np.mean(values_denominator)

variance_num = np.var(values_numerator, ddof=1)

variance_denom = np.var(values_denominator, ddof=1)



covariance_num_denom = np.cov(values_numerator, values_denominator)[0, 1]



ratio_variance = (

(variance_num / mean_denom ** 2)

- (2 * (mean_num / mean_denom ** 3) * covariance_num_denom)

+ ((mean_num ** 2 / mean_denom ** 4) * variance_denom)

)



return ratio_variance





Линеаризация





# ratio_control - ratio-метрика в контрольной группе (для теста также рассчитывается)



def calculate_ratio_control(numerator_control, denominator_control):

return sum(numerator_control) / sum(denominator_control)



ratio_control = calculate_ratio_control(numerator_control, denominator_control)



def linearization(numerator, denominator, ratio_control):

return numerator - ratio_control * denominator





Дополнительные материалы для ознакомления: первый, второй, третий, четвертый, пятый



Понравился пост? Давайте наберем 150 🐳🐳🐳, если хотите продолжение, пишите в комментариях, часто ли используете подобные методы?