Монте-Карло

Прикладний статистичний аналіз

Ігор Мірошниченко

КНУ імені Тараса Шевченка, ФІТ

Перевірка критеріїв

Постановка

Монте-Карло — що є дуже потужний інструментом у статистиці.

З його допомогою ми відповімо з вами на запитання:

  • Як перевірити ваш критерій? Валідний він на практиці чи ні?
    • Наприклад, чи працює \(t\)-test на малих розмірах вибірок?
  • У вас є 2 різні критерії. Як зрозуміти, який критерій краще підходить для вашої задачі?

Згадаємо \(t\)-критерій

маленька вибірка велика вибірка
нормальний розподіл t-test t-test
будь-який розподіл t-test

Що означає, що критерій коректний?

  • Критерій рівня значущості \(\alpha\) означає, що ймовірність невірно відкинути нульову гіпотезу \(\le \alpha\).
  • А це зі свого боку означає, що якщо нескінченно багато разів повторити один і той самий експеримент, у якому правильна нульова гіпотеза, генеруючи наново експеримент, то кількість хибнопозитивних спрацьовувань буде меншою за \(\le \alpha\) відсотків.

Процедура

Коротко:

  1. Створюємо код критерію, який ми будемо перевіряти.
  2. Генеруємо якомога більше експериментів, де вірна \(H_0\).
  3. Проганяємо на них придуманий критерій.
  4. Перевіряємо, чи правда, що тільки в \(\alpha\) відсотків випадків критерій відкидається?

Розгорнуто:

  1. Насамперед треба вибрати розподіл, який буде описувати наші дані. Наприклад, якщо у нас метрика конверсії, то це бернуллівський розподіл, а якщо метрика — виторг, то краще використовувати експоненціальний розподіл як найпростіше наближення.
  2. Завести лічильник bad_cnt = 0.
  3. Далі в циклі розміру \(N\), де \(N\) — натуральне число від 1000 до нескінченності (чим воно більше, тим краще):
    • Симулювати створення вибірки з розподілу, обраного на першому кроці. Так, щоб вірною була \(H_0\).
      • А в разі AB-тесту симулювати треба не 1 вибірку, а 2: для тесту і контролю.
    • Запустити на згенерованих даних критерій, що перевіряється.
    • Далі перевірити, pvalue < alpha. Якщо так, то збільшити лічильник bad_cnt на 1. Тут ми перевіряємо, чи помилився критерій на поточній симуляції, чи ні.
  4. Порахувати конверсію bad_cnt / N.
    • Якщо вона приблизно збігається з \(\alpha\), то все добре.
    • Якщо вона менша за \(\alpha\), то в принципі це адекватний критерій на практиці, просто він буде менш потужний, ніж критерій, що помиляється рівно в \(\alpha\) відсотку випадків.
      • Але на практиці варто перевірити: а теоретично така ситуація можлива? Чи це помилка в коді критерію?
    • Якщо критерій помиляється більше, ніж у \(\alpha\), то значить він некоректний і ним не можна користуватися. Використовуючи такий критерій, ви будете помилятися частіше, ніж треба, а отже, ваша компанія втрачатиме більше грошей.

\(t\)-test: мала вибірка, нормальний розподіл

numpy.random.seed(42)

bad_cnt = 0
N = 10000
alpha = 0.05

sample_dist = norm(loc=2, scale=3)
mu0=sample_dist.expect()
for i in range(N):
    # Генерую вибірку тесту і контролю
    test    = sample_dist.rvs(5)
    control = sample_dist.rvs(5)

    # Запускаю критерій і рахую p-value
    pvalue = ttest_ind(test, control, alternative='two-sided').pvalue
    
    # Перевіряю, що pvalue < alpha
    bad_cnt += (pvalue < alpha)

print(f"FPR: {round(bad_cnt / N, 4)}")
FPR: 0.0519
😩
😃Довірчий інтервал для FPR!
  1. Порахувати отриманий FPR і побудувати довірчий інтервал для нього. Якщо \(\alpha\) лежить у ньому, значить усе добре, а інакше розбираємося, що пішло не так.
    • Довірчий інтервал можна побудувати різними способами: наприклад, використовуючи ідеї побудови довірчих інтервалів із другої лекції.
    • Але можна зробити простіше: довірчий інтервал Вілсона: він не такий точний, як ми виводили раніше, зате він швидший і працює з «коробки». Його не треба реалізовувати самому.
proportion_confint(
    count = bad_cnt, nobs = N, alpha=0.05, method='wilson'
    )
(0.04772180742973847, 0.05642233191006188)

\(t\)-test: мала вибірка, експ. розподіл

numpy.random.seed(42)

bad_cnt = 0
N = 10000
alpha = 0.05

sample_dist = expon(scale=10)
mu0=sample_dist.expect()

for i in range(N):
    # Генерую вибірку тесту і контролю
    test    = sample_dist.rvs(5)
    control = sample_dist.rvs(5)

    # Запускаю критерій і рахую p-value
    pvalue = ttest_ind(test, control, alternative='two-sided').pvalue
    
    # Перевіряю, що pvalue < alpha
    bad_cnt += (pvalue < alpha)

print(f"FPR: {round(bad_cnt / N, 4)}")
FPR: 0.0389
proportion_confint(
    count = bad_cnt, nobs = N, alpha=0.05, method='wilson'
    )
(0.03528393401149463, 0.042870189285943314)
🤨

\(t\)-test: мала вибірка, різні розподіли

numpy.random.seed(42)

test_dist = expon(scale = 10)
control_dist = expon(loc=5, scale = 5)

x = numpy.linspace(0, 100, 1000)

pyplot.figure(figsize=(10, 5))
pyplot.title('Приклад розподілів', fontsize=12)
pyplot.plot(x, test_dist.pdf(x), label='test', color=red)
pyplot.plot(x, control_dist.pdf(x), label='control', color=blue)
pyplot.xlabel('Виручка')
pyplot.ylabel('Щільність')
pyplot.legend(fontsize=12)
pyplot.grid(linewidth=0.2)
pyplot.show()

print(f"mean_t: {test_dist.mean()}, mean_c: {control_dist.mean()}")

mean_t: 10.0, mean_c: 10.0

\(t\)-test: мала вибірка, різні розподіли

def check_criterion(test_dist, control_dist, sample_size, N_exps=10000, to_print=True):
    """
        Функція для перевірки t-test критерію для AB-тесту
        Повертає довірчий інтервал для FPR, якщо прапор to_print = False. Інакше друкує результат.
    
        Параметри:
            - test_dist: Розподіл тестової вибірки в експерименті
            - control_dist: Розподіл контрольної вибірки в експерименті
            - sample_size: розмір вибірки тесту і контролю
            - N_exps: кількість експериментів, за якими потім рахується FPR
            - to_print: друкувати результат чи ні. Якщо ні, то функція повертає довірчий інтервал для FPR.
    """
    
    numpy.random.seed(35)
    bad_cnt = 0
    alpha = 0.05

    for i in range(N_exps):
        # Генерую вибірку
        test = test_dist.rvs(sample_size)
        control = control_dist.rvs(sample_size)

        # Запускаю критерій і рахую p-value
        pvalue = ttest_ind(test, control, equal_var=False, alternative='two-sided').pvalue

        # Перевіряю, що pvalue < alpha
        bad_cnt += (pvalue < alpha)

    if to_print:
        print(f"FPR: {round(bad_cnt / N_exps, 4)}")
        print(f"CI={proportion_confint(count = bad_cnt, nobs = N_exps, alpha=0.05, method='wilson')}")
    else:
        return proportion_confint(count = bad_cnt, nobs = N_exps, alpha=0.05, method='wilson')


check_criterion(test_dist=test_dist, control_dist=control_dist, sample_size=10)
FPR: 0.0689
CI=(0.0640994596515586, 0.07403162374363877)

Мінімальний розмір вибірки для \(t\)-test

scale = numpy.arange(20, 110, 20)
for N in scale:
    left, right = check_criterion(test_dist=test_dist, control_dist=control_dist, sample_size=N, N_exps=10000, to_print=False)
    if left < alpha < right:
        print(f"Min sample size: {N}")
        break
Min sample size: 60


check_criterion(test_dist=test_dist, control_dist=control_dist, sample_size=60)
FPR: 0.0527
CI=(0.0484900114917122, 0.05725351345069507)

Якщо треба більша точність, то можна збільшити кількість експериментів N_exps.

Висновок

Щоб перевірити критерій, треба вміти багато разів проводити один і той самий експеримент.

  • Чи правильно реалізовано критерій?
    • Перевірте його! Можна на спеціально змодельованих даних.
  • Чи можна використовувати цей критерій для нашої задачі?
    • Перевірте його! Але тільки потрібно правильно згенерувати експеримент.
  • Як знайти мінімальний розмір вибірки у \(t\)-test?
    • Перевірте \(t\)-test на різних розмірах вибірки. З того моменту, як \(\alpha\) лежить у довірчому інтервалі — можемо вважати, що \(t\)-test буде працювати.
маленька вибірка велика вибірка
нормальний розподіл t-test t-test
будь-який розподіл Монте-Карло t-test

Як імітувати експеримент?

Є 2 відповіді на це запитання:

  1. Генерація тесту і контролю через штучне моделювання. За допомогою різних розподілів можна спробувати наблизити реальний розподіл на даних. Наприклад:
    • Для генерації виручки використовувати експоненціальний розподіл. Чим більша виручка від користувача — тим менше таких людей.
    • Для генерації конверсійних вибірок (наприклад, клікне/не клікне) використовувати бернулліївську вибірку.
    • Іноді можна брати суміш розподілів: нехай 90% користувачів нашого сайту приносять нульову виручку. Тоді можна перемножити бернуллівський розподіл на експоненціальний для моделювання виручки від користувача.
    • Також для перевірки критерію не обов’язково розподіли в тесті та в контролі мають збігатися. Вони можуть бути різними, але мат. очікування співпаде, як було в прикладі раніше.

Як імітувати експеримент?

  1. Датасети на історичних даних компанії. У багатьох компаній є логування подій. Тоді ми зможемо прямо на реальних даних оцінити дієздатність критерію! І не потрапити в пастку того, що на штучних вибірках критерій валідний, а на реальних даних ні. Наприклад, у нас є дані про транзакції користувачів за кілька років. Це вже один готовий датасет: ви ділите всіх користувачів на тест та контроль і отримуєте один «експеримент» для перевірки вашого критерію.

Монте-Карло на історичних даних

Наші користувачі розміщують оголошення. Кожне оголошення відноситься тільки до однієї категорії товарів і розміщено тільки в одному регіоні. Звідси виникає нехитрий алгоритм:

  • Розіб’ємо всі розміщення користувачів на чотири (або N у загальному випадку) категорії: автомобілі, спецтехніка, послуги та нерухомість. Тепер наш датасет можна розбити на ці підкатегорії: наприклад, в одному датасеті дивитися виручку користувача тільки в цій підкатегорії.
  • Поділимо датасети за місяцями: датасет витрат користувача за листопад, за грудень і так далі.
  • Ще всі метрики можна поділити за суб’єктами країни або за групою суб’єктів.
  • Об’єднаємо всі 3 правила в одне. Наприклад: датасет витрат користувача за листопад у Києві.
  • Тепер у нас є велика кількість датасетів і в кожному з них є користувачі. Поділимо користувачів випадково на тест та контроль і отримаємо фінальні датасети для валідації придуманих статистичних критеріїв.

Який критерій краще?

Постановка

У нас є 2 критерії, які ми хочемо порівняти. Як вибрати кращий?

Порівняти потужність критеріїв!
  • генеруємо експерименти, де вірна \(H_1\);
  • рахуємо TPR для кожного критерію;

Порівняння критеріїв за потужністю

numpy.random.seed(42)

rej_cnt = 0
N = 10000
alpha = 0.05

sample_dist = norm(loc=2, scale=3)
mu = sample_dist.expect()

for i in range(N):
    # Генерую вибірку тесту і контролю
    test = sample_dist.rvs(15)
    control = sample_dist.rvs(15) * 2

    # Запускаю критерій і рахую p-value
    pvalue = ttest_ind(test, control, equal_var=False, alternative='two-sided').pvalue
    
    # Перевіряю, що pvalue < alpha
    rej_cnt += (pvalue < alpha)

print(f"TPR або потужність: {round(rej_cnt / N, 4)}")
TPR або потужність: 0.1938

Порівняння критеріїв за потужністю

numpy.random.seed(42)

rej_cnt = 0
N = 10000
alpha = 0.05

sample_dist = norm(loc=2, scale=3)
mu = sample_dist.expect()

for i in range(N):
    # Генерую вибірку тесту і контролю
    test = sample_dist.rvs(15)
    control = sample_dist.rvs(15) * 3

    # Запускаю критерій і рахую p-value
    pvalue = ttest_ind(test, control, equal_var=False, alternative='two-sided').pvalue
    
    # Перевіряю, що pvalue < alpha
    rej_cnt += (pvalue < alpha)

print(f"TPR або потужність: {round(rej_cnt / N, 4)}")
TPR або потужність: 0.343

Порівняння критеріїв за потужністю

numpy.random.seed(42)

rej_cnt = 0
N = 10000
alpha = 0.05

sample_dist = norm(loc=2, scale=3)
mu = sample_dist.expect()

for i in range(N):
    # Генерую вибірку тесту і контролю
    test = sample_dist.rvs(50)
    control = sample_dist.rvs(50) * 3

    # Запускаю критерій і рахую p-value
    pvalue = ttest_ind(test, control, equal_var=False, alternative='two-sided').pvalue
    
    # Перевіряю, що pvalue < alpha
    rej_cnt += (pvalue < alpha)

print(f"TPR або потужність: {round(rej_cnt / N, 4)}")
TPR або потужність: 0.8361

Дякую за увагу!



Матеріали курсу

ihor.miroshnychenko@knu.ua

Data Mirosh

@ihormiroshnychenko

@aranaur

aranaur.rbind.io