Критерий Манна-Уитни — самый главный враг A/B-тестов

Всем привет! Меня зовут Дима Лунин, я аналитик в компании Авито. В этой статье я расскажу про критерий Манна-Уитни и проблемы при его использовании. 

Этот критерий очень популярен. Во многих компаниях и на обучающих курсах рассказывают про два его важных преимущества:

  1. Манн-Уитни — непараметрический собрат T-test. Если данные в A/B-тесте не из нормального распределения, то T-test использовать нельзя. На помощь приходит Манн-Уитни.

  2. Манн-Уитни — робастный критерий. В данных часто бывает много шумов и выбросов, поэтому T-test неприменим по соображениям мощности, чувствительности или ненормальности данных. А Манн-Уитни в этот момент отлично срабатывает и более вероятно находит статистически значимый эффект.

Если вы анализировали A/B-тест, где вас интересовал прирост или падение какой-то метрики, то наверняка использовали критерий Манна-Уитни. Я хочу рассказать про подводные камни этого критерия, и почему мы в компании его не используем. А в конце вы поймёте, откуда взялся такой холиварный заголовок:)

План статьи такой:

  1. Я теоретически покажу, что проверяет Манн-Уитни и почему это не имеет ничего общего с ростом медиан и среднего значения. А ещё развею миф, что Манн-Уитни проверяет эту гипотезу:

H₀: P(T > C) = ½» src=«https://habrastorage.org/getpro/habr/upload_files/c77/b3f/506/c77b3f506448766ec801afc17588999d.svg» /></p>

<ol><li><p>На примере искусственных и реальных данных продемонстрирую, что Манн-Уитни работает не так, как вы ожидаете. Его «преимущество» в большем числе прокрасов относительно T-test, и это — главный минус этого критерия, поскольку прокрасы ложные.</p></li><li><p>Расскажу про логарифмирование метрики и продемонстрирую, почему это плохая идея.</p></li><li><p>Покажу теоретические случаи, когда Манн-Уитни применим и его можно использовать для сравнения средних.</p></li></ol>

<p>Отвечу на вопрос: как тогда жить и какой критерий использовать для анализа A/B-тестов.</p>

<h3>Что проверяет критерий Манна-Уитни? </h3>

<p>Предлагаю ответить на вопрос: <strong>можем ли мы проверять равенство средних или медиан в тесте и в контроле с помощью критерия Манна-Уитни? </strong> Или проверять, что одна выборка «не сдвинута» относительно другой выборки? Или, если на языке математики: </p>

<p><img alt= C) = ½» src=«https://habrastorage.org/getpro/habr/upload_files/497/c4f/b46/497c4fb4626a0fd50652ab7a413cc674.svg» />

Например, у нас в A/B-тесте две выборки:

  • U[−1, 1] — равномерное распределение от −1 до 1

  • U[−100, 100] — равномерное распределение от −100 до 100

Про эти два распределения мы знаем, что у них равны средние и медианы, и что они симметричны относительно 0. Кроме того, вероятность, что сгенерированное значение в первой выборке будет больше значения во второй выборке, равна ½. Или, если сформулировать математически, P (T > C) = ½, где T и C — выборки теста и контроля.

Я хочу проверить, работает ли здесь корректно Манн-Уитни. Для этого запустим эксперимент 1000 раз и посмотрим на количество отвержений нулевой гипотезы у критерия. Подробнее про этот метод можно прочесть в моей статье про улучшение A/B тестов. Если бы критерий Манна-Уитни работал как надо и проверял одну из гипотез, то по определению уровня значимости он бы ошибался на этих тестах в 5% случаев, с некоторым разбросом из-за шума.  

Проверим корректность примера на уровне значимости для T-test критерия:

# Подключим библиотеки
import scipy.stats as sps
from tqdm.notebook import tqdm # tqdm – библиотека для визуализации прогресса в цикле
from statsmodels.stats.proportion import proportion_confint
import numpy as np
 
# Заводим счетчики количества отвергнутых гипотез для Манна-Уитни и для t-test
mann_bad_cnt = 0
ttest_bad_cnt = 0
 
# Прогоняем критерии 1000 раз
sz = 1000
for i in tqdm(range(sz)):
    # Генерируем распределение
    test = sps.uniform(loc=-1, scale=2).rvs(1000) # U[-1, 1]
    control = sps.uniform(loc=-100, scale=200).rvs(1000) # U[-100, 100]
     
    # Считаем pvalue
    mann_pvalue = sps.mannwhitneyu(control, test, alternative='two-sided').pvalue
    ttest_pvalue = sps.ttest_ind(control, test, alternative='two-sided').pvalue
     
    # отвергаем критерий на уровне 5%
    if mann_pvalue < 0.05:
        mann_bad_cnt += 1
 
    if ttest_pvalue < 0.05:
        ttest_bad_cnt += 1
 
# Строим доверительный интервал для уровня значимости критерия (или для FPR критерия)
left_mann_level, right_mann_level = proportion_confint(count = mann_bad_cnt, nobs = sz, alpha=0.05, method='wilson')
left_ttest_level, right_ttest_level = proportion_confint(count = ttest_bad_cnt, nobs = sz, alpha=0.05, method='wilson')
# Выводим результаты
print(f"Mann-whitneyu significance level: {round(mann_bad_cnt / sz, 4)}, [{round(left_mann_level, 4)}, {round(right_mann_level, 4)}]")
print(f"T-test significance level: {round(ttest_bad_cnt / sz, 4)}, [{round(left_ttest_level, 4)}, {round(right_ttest_level, 4)}]")

Результат:

Mann-whitneyu significance level: 0.114, [0.0958, 0.1352]
T-test significance level: 0.041, [0.0304, 0.0551]

T-test здесь корректно работает: он ошибается в 5% случаев, как и заявлено. А значит, пример валиден.

Манн-Уитни ошибается в 11% случаев. Если бы он проверял равенство средних, медиан или P (T > C) = ½, то должен был ошибиться только в 5%, как T-test. Но процент ошибок оказался в два раза больше. А значит, что эти гипотезы неверны для Манна-Уитни:

T и C — выборки теста и контроляT и C — выборки теста и контроля

Этот метод можно использовать для проверки только такой гипотезы:

F — функция распределения метрики у пользователей в контроле и в тесте, T и C — выборки теста и контроляF — функция распределения метрики у пользователей в контроле и в тесте, T и C — выборки теста и контроля

Этот критерий проверяет, что выборки теста и контроля взяты из одного распределения. Если он считает, что выборки из разных распределений — отвергает гипотезу. Это обычный критерий однородности.

Для демонстрации корректности я предлагаю снова запустить 1000 раз тесты, но выборки теста и контроля в этот раз взяты из одного распределения:  U[-100, 100] (равномерное распределение от -100 до 100).

Проверка

# Подключим библиотеки
import scipy.stats as sps
from tqdm.notebook import tqdm # tqdm – библиотека для визуализации прогресса в цикле
from statsmodels.stats.proportion import proportion_confint
import numpy as np
 
# Заводим счетчики количества отвергнутых гипотез для Манна-Уитни и для t-test
mann_bad_cnt = 0
ttest_bad_cnt = 0
 
# Прогоняем критерии 1000 раз
sz = 1000
for i in tqdm(range(sz)):
    # Генерируем распределение
    test = sps.uniform(loc=-100, scale=200).rvs(1000) # U[-100, 100]
    control = sps.uniform(loc=-100, scale=200).rvs(1000) # U[-100, 100]
     
    # Считаем pvalue
    mann_pvalue = sps.mannwhitneyu(control, test, alternative='two-sided').pvalue
    ttest_pvalue = sps.ttest_ind(control, test, alternative='two-sided').pvalue
     
    # отвергаем критерий на уровне 5%
    if mann_pvalue < 0.05:
        mann_bad_cnt += 1
 
    if ttest_pvalue < 0.05:
        ttest_bad_cnt += 1
 
# Строим доверительный интервал для уровня значимости критерия (или для FPR критерия)
left_mann_level, right_mann_level = proportion_confint(count = mann_bad_cnt, nobs = sz, alpha=0.05, method='wilson')
left_ttest_level, right_ttest_level = proportion_confint(count = ttest_bad_cnt, nobs = sz, alpha=0.05, method='wilson')
# Выводим результаты
print(f"Mann-whitneyu significance level: {round(mann_bad_cnt / sz, 4)}, [{round(left_mann_level, 4)}, {round(right_mann_level, 4)}]")
print(f"T-test significance level: {round(ttest_bad_cnt / sz, 4)}, [{round(left_ttest_level, 4)}, {round(right_ttest_level, 4)}]")

Результаты:

Mann-whitneyu significance level: 0.045, [0.0338, 0.0597]
T-test significance level: 0.043, [0.0321, 0.0574]

Манн-Уитни не может проверить ничего, кроме равенства распределений. Этот критерий не подходит для сравнения средних или медиан. 

Разберёмся, почему Манна-Уитни нельзя применять для сравнения средних, медиан и P (T > C) = ½. Для начала посмотрим на статистику, которая считается внутри критерия:

0c4ce114394b72dbaf3a39782ec0ac15.png

Для неё считаются те критические области, при попадании в которые статистики U критерий отвергнется.

Отсюда видно, что:

  1. Манн-Уитни учитывает расположение элементов выборок относительно друг друга, а не значения элементов. Поэтому он не может сравнивать математические ожидания, даже если не знает абсолютные значения элементов выборки.

  2. Гипотеза H_0: P (T > C) = ½ неверна для данного критерия. Если P (T > C) = ½, то мат. ожидание EU = (произведение размера выборок) / 2. Но при равенстве распределений мат ожидание статистики U будет точно таким же. Почему критерий не сработал на примере с 2 разными равномерными распределениями U[-1, 1] VS U[-100, 100], но сработал при сравнении распределений U[-100, 100] VS U[-100, 100]? Суть в том, что мы не знаем распределение статистики U. Когда тест и контроль из одного распределения, мы знаем, что статистика U распределена нормально. Но если это не так, то теорема перестаёт работать. Тогда мы не знаем, как распределена статистика U, и не можем посчитать p-value.

  3. По этой же причине критерий не может проверять равенство медиан.

Мы рассмотрели, почему Манна-Уитни не следует применять для проверки равенства средних, медиан и смущённости распределений. Но на практике его все равно часто применяют для этих гипотез, особенно для равенства средних. Что плохого может случиться в этом случае? Правда ли, что в таком случае мы завысим ошибку 1 рода в 2 раза и будем ошибаться в 11% вместо 5%?

Манн-Уитни при анализе реальных A/B-тестов: подводные камни

На реальных задачах проблемы Манна-Уитни будут куда серьёзнее, чем завышение alpha в два раза. Все последующие примеры будут основаны на кейсах, которые встречаются на практике в Авито. Я продемонстрирую работу критерия Манна-Уитни на искусственно сгенерированных данных, моделирующих один из наших экспериментов, а потом продемонстрирую результаты на настоящих данных из Авито.

Допустим, мы проводим A/B-тест с целью улучшить какую-то метрику. Тогда наше изменение чаще всего приводит к двум ситуациям:

  • Части пользователей понравилось наше воздействие и нулей в выборке стало меньше. Например, мы дали скидки на услуги, платящих пользователей стало больше.

  •  Другой части пользователей не понравилось наше изменение и они оттекли по метрике — стало больше нулей.

В обоих этих случаях я покажу, как может работать критерий. 

Кейс 1: прирост количества пользователей

Я разберу этот кейс на примере выдачи скидки. Количество платящих пользователей увеличилось, но ARPPU упал. Наша ключевая метрика — выручка. Мы хотим понять: начнём ли мы больше зарабатывать со скидками. Или выросло ли ARPU, математическое ожидание выручки — что эквивалентно. 

Сразу зафиксируем проверяемую гипотезу о равенстве средних: H0 — средний чек не изменился или упал vs. H1 — средний чек вырос. Мы уже знаем, что Манн-Уитни может ошибаться в этой задаче. Но насколько?

Искусственно насимулируем эксперимент, который мог бы произойти в реальности. В этот раз это будут не два равномерных распределения, а что-то более приближенное к реальности, и что вы сможете проверить самостоятельно, запустив код.

  1. Пусть распределение ненулевой части метрики подчиняется экспоненциальному распределению. Для выручки это хорошее приближение.

f53ee2472d85f537eeac5467125936e3.png

  1. Пусть нашими скидками мы увеличили число платящих пользователей на 5%.

    • Изначально в контроле было 50% нулей. Половина пользователей ничего не покупали на сайте.

    • В тесте стало 55% платящих пользователей.

  2. Раньше пользователь в среднем платил 7 рублей, а сейчас из-за скидки он платит 6 ₽, скидка была примерно 15%. Тогда математическое ожидание выручки в контроле было 3.5 ₽ (7×0.5), а в тесте — 3.3 ₽ (6×0.55).

То есть платящих пользователей стало больше, а средний чек упал. А значит, верна H0 и процент ложноположительных срабатываний не может быть больше 5% у корректного критерия. Для проверки мы снова запустим тест 1000 раз и проверим, что здесь покажут T-test с Манном-Уитни. 

Демонстрация результатов:

# Подключим библиотеки
import scipy.stats as sps
from tqdm.notebook import tqdm # tqdm – библиотека для визуализации прогресса в цикле
from statsmodels.stats.proportion import proportion_confint
import numpy as np
 
 
# Заводим счетчики количества отвергнутых гипотез для Манна-Уитни и для t-test
mann_bad_cnt = 0
ttest_bad_cnt = 0
sz = 10000
 
for i in tqdm(range(sz)):
    test_zero_array = sps.bernoulli(p=0.55).rvs(1000)
    control_zero_array = sps.bernoulli(p=0.5).rvs(1000)
    test = sps.expon(loc=0, scale=6).rvs(1000) * test_zero_array # ET = 3.3
    control = sps.expon(loc=0, scale=7).rvs(1000) * control_zero_array # EC = 3.5
     
    # Проверяем гипотезу
    mann_pvalue = sps.mannwhitneyu(control, test, alternative='less').pvalue
    ttest_pvalue = sps.ttest_ind(control, test, alternative='less').pvalue
    if mann_pvalue < 0.05:
        mann_bad_cnt += 1
 
    if ttest_pvalue < 0.05:
        ttest_bad_cnt += 1
 
# Строим доверительный интервал
left_mann_power, right_mann_power = proportion_confint(count = mann_bad_cnt, nobs = sz, alpha=0.05, method='wilson')
left_ttest_power, right_ttest_power = proportion_confint(count = ttest_bad_cnt, nobs = sz, alpha=0.05, method='wilson')
# Выводим результаты
print(f"Mann-whitneyu LESS power: {round(mann_bad_cnt / sz, 4)}, [{round(left_mann_power, 4)}, {round(right_mann_power, 4)}]")
print(f"T-test LESS power: {round(ttest_bad_cnt / sz, 4)}, [{round(left_ttest_power, 4)}, {round(right_ttest_power, 4)}]")

Результат:

Mann-whitneyu LESS power: 0.3232, [0.3141, 0.3324]
T-test LESS power: 0.0072, [0.0057, 0.0091]

Процент принятий гипотезы H1: «средний чек вырос» у T-test около 0: это нормально, так как мы знаем, что на самом деле среднее в тесте упало, а не выросло — критерий корректен. А у Манна-Уитни процент отвержений 32%, что сильно больше 11%, которые были в первом, изначальном примере.

Представим, что на ваших настоящих данных была примерно такая же картина. Только в реальности вы не симулировали эксперимент, поэтому не знаете, что ваш средний чек упал. У T-test околонулевой процент обнаружения статистически значимого эффекта. Используя его, вы видите, что он ничего не задетектировал, начинаете использовать критерий Манна-Уитни, и в одной трети случаев обнаруживаете эффект, что среднее увеличилось. 

Увидев, что T-test не прокрасился, а Манн-Уитни даёт статистически значимый прирост, многие аналитики доверятся второму критерию и покатят на всех пользователей скидки. И выручка упадёт.

Кейс 2: отток части пользователей

Другой вариант: части пользователей не понравилось наше изменение и они ушли. Допустим, мы захотели нарастить количество сделок на Авито. Сделка у нас — событие, когда продавец нашёл покупателя на своё объявление. Для этого мы добавили новые обязательные параметры в объявление и процесс подачи стал сложнее. Часть продавцов отвалилась, зато остальным стало легче: теперь покупателям с помощью фильтров и поиска проще находить подходящие объявления. Осталось понять, нарастили ли мы количество сделок.

Например, у нас было 100 продавцов и в среднем они совершали 2 сделки на Авито. После того как мы ввели новые параметры, продавцов стало 70. Оставшимся 30 продавцам наша инициатива не понравилась и они ушли. Теперь на одного продавца приходится по 3 сделки, так как пользователям стало легче находить релевантный товар. Раньше было 200 сделок, а теперь 210.

В этот раз все распределения будут взяты не из теоретических соображений, а из реальных данных. Как это сделать, я подробно рассказывал в статье про улучшение A/B-тестов. Я собрал 1000 настоящих А/А-тестов на исторических данных в Авито: взял всех продавцов в разных категориях и регионах за разные промежутки времени и поделил случайно на тест и контроль. Повторил это 1000 раз. 

Теперь я хочу простимулировать эффект от ввода новых обязательных параметров в тестовой группе: я моделирую отток пользователей и прирост числа сделок у оставшихся продавцов. 

Как именно я это сделал:

  • взял в качестве метрики количество сделок;

  • обнулил число сделок 5 процентам случайных продавцов, как будто они ушли с площадки;

  • у оставшихся пользователей в тестовой группе я домножил количество сделок на некий коэффициент больше единицы. В итоге среднее число сделок на продавца стало больше даже с учетом оттока. В примере выше этот коэффициент равнялся бы 1.5: 2 сделки → 3 сделки.

На самом деле количество сделок выросло от введения новых параметров.

Замечание

Да, получились не совсем «реальные» AB-тесты, а лишь приближения к ним. Но в любом случае это наилучшее возможное приближение, где мы знаем реальный эффект.

Теперь построим таблицу процента отвержений нулевой гипотезы в зависимости от альтернативы на этих 1000 A/B-тестах:

в тесте количество сделок больше, чем в контроле

--//-- меньше, чем в контроле

T-test

27%

0.1%

Манн-Уитни

6%

80%

© Habrahabr.ru