\(t\)-критерій Ст’юдента

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

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

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

\(t'\)-тест

📈 Задача

У нашій компанії хочуть перейти з однієї СУБД на іншу. Головним критерієм для переходу є «витрачений час у добі на завантаження нових даних». Якщо раніше для щоденного оновлення бази було потрібно в середньому 10 годин, то хочеться знайти нову СУБД, у якій усе це відбуватиметься швидше, ніж за 7 годин.

Для цього було прийнято рішення перенести всі дані на нову тестовану СУБД. Протягом одного тижня щодня ми порахуємо час завантаження даних, і якщо в середньому на оновлення витрачатиметься менше 7 годин, то ми повністю перейдемо на нову СУБД. Ваше завдання придумати, як перевірити гіпотезу про те, що нова СУБД краща за стару.

Вийшла вибірка:

  • [6.9, 6.45, 6.32, 6.88, 6.19, 7.13, 6.76] — час завантаження в нову БД по днях у годинах.

📊 Гіпотеза

  • \(X_1, X_2, ..., X_7\) — час завантаження в годинах нових даних у СУБД за кожен день експерименту
  • \(X\) з нормального розподілу.
X = numpy.array([6.9, 6.45, 6.32, 6.88, 6.09, 7.13, 6.76])
print(f"Середній час завантаження в СУБД: {round(numpy.mean(X), 2)}")
Середній час завантаження в СУБД: 6.65
  • \(H_0: \bar{X} \geq 7\) vs. \(H_1: \bar{X} < 7\)

📐 \(Z\)-критерій

  • Статистика \(Z(X) = \sqrt{n}\dfrac{\overline X - \mu_0}{\sqrt{\sigma^2}}\)

Тоді треба лише порахувати таку статистику: \[\sqrt{n}\dfrac{\overline X - 7}{\sqrt{\sigma^2}} \overset{H_0}{\sim} \mathcal{N}(0, 1)\]

Важливо

Але є одна проблема: ми не знаємо \(\sigma^2\)!

📐 \(t'\)-тест

\(\widehat{\sigma^2} =S^2 = \dfrac{1}{n - 1}\underset{i=1}{\overset{n}{\sum}}(X_i - \overline X)^2\)

# ddof = 1 -- Це означає, що ділимо не на n, а на n-1 у формулі вище
print(f"Оцінка sigma^2: {numpy.var(X, ddof=1)}")
Оцінка sigma^2: 0.1367238095238095

Давайте введемо новий критерій \(t\)-критерій, у якому ми підставимо:

  • \(T(X) := \sqrt{n}\dfrac{\overline X - \mu_0}{\sqrt{S^2}}\)
  • \(T(X) \overset{H_0}{\sim} \mathcal{N}(0, 1)\) 🤨

Залишилося перевірити: Чи правда, що при \(H_0\) розподіл статистики T — стандартний нормальний?

📊 Розподіл статистики

  • Ми \(M\) раз генеруємо вибірку \(X\) і порахуємо щоразу статистику \(T(X)\).
  • У підсумку ми отримаємо вибірку розміру \(M\) для \(T(X)\) і зможемо побудувати гістограму розподілу.
  • Окремо побудуємо розподіл \(\mathcal{N}(0, 1)\).
  • Якщо емпіричний розподіл візуально збіжиться з теоретичним нормальним, значить, усе добре. А якщо ні, то так просто ми не можемо замінити \(\sigma^2\) на \(S^2\).
    • Додатково подивимося, що буде, якщо замінити \(T(X)\) на \(Z(X)\).

🐍 Python та 📊 Розподіл статистики

def sample_statistics(number_of_experiments, statistic_function, sample_size, sample_distr):
    """
        Функція для генерації вибірки деякої статистики statistic_function, побудованої за вибіркою з розподілу sample_distr.
        Повертає вибірку розміру number_of_experiments для statistic_function.

        Параметри:
            - number_of_experiments: кількість експериментів, у кожному з яких ми порахуємо statistic_function
            - statistic_function: статистика, яка приймає на вхід вибірку з розподілу sample_distr
            - sample_size: розмір вибірки, яка подається на вхід statistic_function
            - sample_distr: розподіл початкової вибірки, за якою рахується статистика
    """

    statistic_sample = []
    for _ in range(number_of_experiments):
        # генеруємо number_of_experiments раз вибірку
        sample = sample_distr.rvs(sample_size)

        # рахуємо статистику
        statistic = statistic_function(sample)

        # зберігаємо
        statistic_sample.append(statistic)
    return statistic_sample

🐍 Python та 📊 Розподіл статистики

Код
numpy.random.seed(8)

sample_size = 7
M = 100000
sample_distr = norm(loc=5, scale=3) # Нехай вибірка з цього розподілу

T_X = lambda sample: numpy.sqrt(sample_size) * (numpy.mean(sample) - sample_distr.mean()) / numpy.sqrt(numpy.var(sample, ddof=1)) # або numpy.std
Z_X = lambda sample: numpy.sqrt(sample_size) * (numpy.mean(sample) - sample_distr.mean()) / sample_distr.std()
samples = {
    "T(X)": sample_statistics(
    number_of_experiments=M, statistic_function=T_X,
    sample_size=sample_size, sample_distr=sample_distr),

    "Z(X)": sample_statistics(
    number_of_experiments=M, statistic_function=Z_X,
    sample_size=sample_size, sample_distr=sample_distr)
}


pyplot.figure(figsize=(22, 5))

for i, name in enumerate(["T(X)", "Z(X)"]):
    pyplot.subplot(1, 2, i + 1)
    current_sample = samples[name]
    l_bound, r_bound = numpy.quantile(current_sample, [0.001, 0.999])

    x = numpy.linspace(l_bound, r_bound, 1000)
    pyplot.title(f'Розподіл статистики {name}, sample size={sample_size}', fontsize=12)
    distplot(current_sample, label='Емпіричний розподіл')
    pyplot.plot(x, norm(0, 1).pdf(x), label='$\mathcal{N}(0, 1)$')
    pyplot.legend(fontsize=12)
    pyplot.xlabel(f'{name}', fontsize=12)
    pyplot.xlim((l_bound, r_bound))
    pyplot.ylabel('Щільність', fontsize=12)
    pyplot.grid(linewidth=0.2)

pyplot.show()

Важливо

  • \(Z\)-test тут працює: \(\sqrt{n}\dfrac{\overline X - \mu_0}{\sqrt{\sigma^2}} \sim \mathcal{N}(0, 1)\).
  • Але ось для \(T(X)\) це не так! Вони відрізняються! А значить \(t'\)-критерій не підходить для початкової задачі!

📝 Чому так вийшло?

\(S^2\) — це випадкова величина!

Давайте подивимося на розподіл \(\sqrt{S^2}\) на все тому ж нормальному розподілі.

Код
numpy.random.seed(42)

S2 = lambda sample: numpy.std(sample, ddof=1)
S2_sample = sample_statistics(
    number_of_experiments=M, statistic_function=S2,
    sample_size=sample_size, sample_distr=sample_distr
)

pyplot.figure(figsize=(10, 5))
pyplot.title('Розподіл $\sqrt{S^2}$', fontsize=12)
distplot(S2_sample, label='Емпіричний розподіл')
pyplot.legend(fontsize=12)
pyplot.xlabel('$\sqrt{S^2}$', fontsize=12)
pyplot.ylabel('Щільність розподілу', fontsize=12)
pyplot.grid(linewidth=0.2)
pyplot.show()

Висновок

\(T(X) \overset{H_0}{\nsim} \mathcal{N}(0, 1)\) 😭

Який розподіл має \(T(X)\)?

  1. Нехай \(X_1 \ldots X_n \sim \mathcal{N}(\mu, \sigma^2)\)

  2. Нехай \(\xi_1 \ldots \xi_n \sim \mathcal{N}(0, 1)\). Тоді \(\eta=\xi_1^2 +\ ... +\xi_n^2 \sim \chi^2_n\), — \(\chi^2\) розподіл з \(n\) ступенями свободи.

    • Тоді \(\underset{i=1}{\overset{n}{\sum}}\left(\xi_i - \overline \xi \right)^2 \sim \chi^2_{n-1}\). Дов-ня 🫠

    • \(S^2_X = \dfrac{1}{n - 1}\underset{i=1}{\overset{n}{\sum}}(X_i - \overline X)^2\)

    • \(\xi_i := \dfrac{X_i - \mu}{\sigma} \sim \mathcal{N}(0, 1)\). Тоді \(S^2_{\xi} = \dfrac{1}{\sigma^2}S^2_X\).

    • А отже \(\dfrac{(n - 1)\cdot S^2_X}{\sigma^2} = \underset{i=1}{\overset{n}{\sum}}\left(\xi_i - \overline \xi \right)^2 \sim \chi^2_{n-1}\)

  3. Нехай \(\xi \sim \mathcal{N}(0, 1), \eta \sim \chi^2_k\) і \(\xi\) з \(\eta\) незалежні. Тоді статистика \(\zeta = \dfrac{\xi}{\sqrt{\eta/k}} \sim t_{k}\) — з розподілу Ст’юдента з \(k\) ступенями свободи.

    • \(\xi := \sqrt{n}\dfrac{\overline X - \mu_0}{\sigma} \sim \mathcal{N}(0, 1)\)
    • \(\eta := \dfrac{(n - 1)\cdot S^2_X}{\sigma^2} \sim \chi^2_{n-1}\)
    • \(\xi\) і \(\eta\) незалежні
    • Тоді \[ \begin{align} T = \sqrt{n}\dfrac{\overline X - \mu_0}{\sqrt{S^2}} = \frac{\sqrt{n}\dfrac{\overline X - \mu_0}{\sigma}}{\sqrt{\dfrac{(n - 1)\cdot S^2_X}{(n - 1)\sigma^2}}} = \dfrac{\xi}{\sqrt{\dfrac{\eta}{n-1}}} \sim t_{n - 1} \end{align} \]

📈 Задача та 📐 \(t\)-тест

\(T = \sqrt{n}\dfrac{\overline X - \mu_0}{\sqrt{S^2}} \sim t_{n - 1}\) — взята з розподілу Ст’юдента з \(n - 1\) ступенем свободи.


X = numpy.array([6.9, 6.45, 6.32, 6.88, 6.09, 7.13, 6.76])
t_stat = numpy.sqrt(sample_size) * (numpy.mean(X) - 7) / numpy.sqrt(numpy.var(X, ddof=1))
print(f"t-статистика: {round(t_stat, 2)}")
t-статистика: -2.52


p_value = t.cdf(t_stat, df=sample_size - 1)
print(f"p-значення: {round(p_value, 4)}")
p-значення: 0.0225


ttest_1samp(X, 7, alternative='less')
TtestResult(statistic=-2.524793468045073, pvalue=0.02249742917295711, df=6)

Довірчі інтервали

📊 Довірчий інтервал для середнього: варіант 1

Довірчий інтервал — множина \(m\): тест не відхиляє \(H_0: \mu = m\) на рівні значущості \(\alpha\).

  • Нехай \(\mu\) — істинне середнє вибірки. Ми також знаємо, що за \(H_0: \sqrt{n}\dfrac{\overline X - m}{\sqrt{S^2}} \sim t_{n - 1}\).

  • Нас цікавлять такі \(m\), що: \(\left\{-t_{n-1, 1 - \frac{\alpha}{2}} < \sqrt{n}\dfrac{\overline X - m}{\sqrt{S^2}} < t_{n-1, 1 - \frac{\alpha}{2}} \right\}\), у цьому випадку критерій не відкинеться.

  • Розпишемо, щоб у центрі залишилося тільки \(m\):

\[\left\{\overline X - \dfrac{t_{n - 1, 1 - \alpha/2} \sqrt{S^2}}{\sqrt{n}} < m < \overline X + \dfrac{t_{n - 1, 1 - \alpha/2} \sqrt{S^2}}{\sqrt{n}}\right\}\]

  • Тоді довірчий інтервал:

\[CI_{\mu} = \left(\overline X \pm \dfrac{t_{n - 1, 1 - \alpha/2} \sqrt{S^2}}{\sqrt{n}} \right),\]

де \(S^2 = \dfrac{1}{n - 1}\underset{i=1}{\overset{n}{\sum}}(X_i - \overline X)^2\)

📊 Довірчий інтервал для середнього: варіант 2

Класичне визначення довірчого інтервалу:

Довірчим інтервалом для параметра \(\theta\) рівня довіри \(1 - \alpha\) є пара статистик \(L(X), R(X)\), таких, що \(P(L(X) < \theta < R(X)) = 1 - \alpha\).

\[ \begin{align} &T(X) = \sqrt{n}\dfrac{\overline X - \mu}{\sqrt{S^2}} \sim t_{n - 1} \Rightarrow \\ &P\left(-t_{n - 1, 1-\alpha/2} < \sqrt{n}\dfrac{\overline X - \mu}{\sqrt{S^2}} < t_{n - 1, 1-\alpha/2} \right) = 1 - \alpha \Leftrightarrow \\ &P\left(\overline X - \dfrac{t_{n - 1, 1 - \alpha/2} \sqrt{S^2}}{\sqrt{n}} < \mu < \overline X + \dfrac{t_{n - 1, 1 - \alpha/2} \sqrt{S^2}}{\sqrt{n}} \right) = 1 - \alpha \end{align} \]

  • Тоді довірчий інтервал:

\[CI_{\mu} = \left(\overline X \pm \dfrac{t_{n - 1, 1 - \alpha/2} \sqrt{S^2}}{\sqrt{n}} \right)\]

🐍 Python та 📊 довірчий інтервал

sample = norm(loc=10, scale=2).rvs(100)

# sem -- стандартна помилка середнього, sem = sqrt(S^2)/sqrt(n)
left_bound, right_bound = t.interval(confidence=0.95, loc=numpy.mean(sample), df=len(sample)-1, scale=sem(sample))
print(f"CI = [{round(left_bound, 2)}, {round(right_bound, 2)}]")
CI = [9.93, 10.73]


📈 Задача

X = numpy.array([6.9, 6.45, 6.32, 6.88, 6.09, 7.13, 6.76])
left_bound, right_bound = t.interval(confidence=0.95, loc=numpy.mean(X), df=len(X)-1, scale=sem(X))
print(f"CI = [{round(left_bound, 2)}, {round(right_bound, 2)}]")
CI = [6.31, 6.99]

\(t\)-тест та невідомий розподіл

📊 Задача

Ви придумали ідею для стартапу, де кур’єри збирають замовлення для клієнтів і відвозять їм додому. Маржа від замовлення у вашому стартапі — X ₴, а вартість роботи кур’єра — 1k ₴. Специфіка вашого стартапу така, що є великий ризик повернення без оплати. З урахуванням вартостей, інвестори готові проспонсорувати вам інфраструктуру і залучення клієнтів, якщо ви покажете, що у вас буде прибуток.

З даних у вас є принесені гроші від кожного користувача. Іноді позитивна величина, іноді негативна.

  • \(X_1, X_2, \dots X_N\) — вибірка прибутку від користувача.
  • \(H_0\): \(E \overline{X} \leq 0\) vs. \(H_1: E \overline{X} > 0\)

Дані:

profits = numpy.loadtxt('data/profit_from_user.out', delimiter=',')
print(profits[:100])
[-718.  657.  693.  391. -644.  421.  265. -108. 1956. -684. -753. -725.
 -341. -796. -662.  257. -719. 5184. -739. -291. -427.  283.   10.  500.
 -713. -458.   60. -756.  333. -537. -744.  254. -555. -780. -329. -560.
  936. -742. -784.  213.  299. -678. -736.   24.  264.  293. -490. 2667.
 -605. -799. -797. -743.  347. -718. -508. -766. 1395.  392.  -62. -510.
  237. -785. -745. -781. 3232. -727.  204. 2987.  244. -757.  -78.   10.
  364.   -7. -440.  520.  203.  282.  685.  589. -724.  -48.  263. -457.
 -796. -708. -798.  488. -677. -690.  786. -770.  659. -679. -309. -731.
  288. 1047. -796. -721.]

📊 Задача

print(f"average profit = {profits.mean()}")
print(f"n = {profits.shape[0]}")
average profit = 58.4868
n = 5000


seaborn.distplot(profits)
pyplot.grid(linewidth=0.2)
  • Початкова вибірка не з нормального розподілу
  • Вибірка досить велика: не 7 елементів, а вже 5000.

Чи можемо ми використати нормальний розподіл для наближення?

\(t\)-тест та наближення

  1. Будемо розглядати ту саму статистику \(T = \sqrt{n}\dfrac{\overline X - \mu_0}{\sqrt{S^2}}\)
  2. \(\xi := \sqrt{n}\dfrac{\overline X - \mu_0}{\sqrt{\sigma^2}} \stackrel{d}{\rightarrow} \mathcal{N}(0, 1)\). За ЦГТ збіжність є тільки за розподілом.
  3. тоді \(T = \sqrt{n}\dfrac{\overline X - \mu_0}{\sqrt{S^2}} = \xi \cdot \sqrt{\dfrac{\sigma^2}{S^2}}\). Позначимо \(\phi := \sqrt{\dfrac{\sigma^2}{S^2}}\)
    • Пам’ятаєте, на початку лекції було сказано, що \(S^2\) — найкраща оцінка для дисперсії? По-іншому це можна записати так: \(S^2\) збігається за ймовірністю до \(\sigma^2\). Тобто \(S^2 \stackrel{p}{\rightarrow} \sigma^2\)
    • А в цьому випадку існує теорема, яка стверджує, що \(\phi = \dfrac{\sigma^2}{S^2} \stackrel{p}{\rightarrow} 1\).
  4. \(T = \xi \cdot \phi\).
    • \(\xi \stackrel{d}{\rightarrow} \mathcal{N}(0, 1)\)
    • \(\phi \stackrel{p}{\rightarrow} 1\)
    • І тут набуває чинності ще одна теорема: \(T = \xi \cdot \phi \stackrel{d}{\rightarrow} 1\cdot \mathcal{N}(0, 1)\). Та сама збіжність, що й у ЦГТ!
    • Тобто статистика \(T\) так само нормально розподілена!

Отже, якщо вибірка велика, то ми можемо вважати, що \(T(X) \overset{H_0}{\sim} \mathcal{N}(0, 1)\).

🐍 Python та перевірка

Перевіримо наш критерій на великих вибірках:

Код
numpy.random.seed(8)

sample_size=2000
M = 10000
sample_distr = expon(loc=5, scale=300)

T_X = lambda sample: numpy.sqrt(sample_size) * (numpy.mean(sample) - sample_distr.mean()) / numpy.std(sample, ddof=1)
T_sample = sample_statistics(
    number_of_experiments=M, statistic_function=T_X,
    sample_size=sample_size, sample_distr=sample_distr)

pyplot.figure(figsize=(10, 5))
l_bound, r_bound = numpy.quantile(T_sample, [0.001, 0.999])


x = numpy.linspace(l_bound, r_bound, 1000)
pyplot.title(f'Розподіл статистики T(X), sample size={sample_size}', fontsize=12)
distplot(T_sample, label='Емпіричний розподіл')
pyplot.plot(x, norm(0, 1).pdf(x), label='Експоненціальний розподіл')
pyplot.legend(fontsize=12)
pyplot.xlabel(f'{name}', fontsize=12)
pyplot.xlim((l_bound, r_bound))
pyplot.ylabel('Щільність', fontsize=12)
pyplot.grid(linewidth=0.2)
pyplot.show()

Ми бачимо, що розподіли збіглися!

🐍 Python та перевірка

А на нормальному розподілі, де вперше були відмінності на маленькій вибірці?

Код
numpy.random.seed(8)

sample_size=2000
M = 30000
sample_distr = norm(loc=5, scale=300)

T_X = lambda sample: numpy.sqrt(sample_size) * (numpy.mean(sample) - sample_distr.mean()) / numpy.std(sample, ddof=1)
T_sample = sample_statistics(
    number_of_experiments=M, statistic_function=T_X,
    sample_size=sample_size, sample_distr=sample_distr)

pyplot.figure(figsize=(10, 5))
l_bound, r_bound = numpy.quantile(T_sample, [0.001, 0.999])


x = numpy.linspace(l_bound, r_bound, 1000)
pyplot.title(f'Розподіл статистики T(X), sample size={sample_size}', fontsize=12)
distplot(T_sample, label='Емпіричний розподіл')
pyplot.plot(x, norm(0, 1).pdf(x), label='$\mathcal{N}(0, 1)$')
pyplot.legend(fontsize=12)
pyplot.xlabel(f'{name}', fontsize=12)
pyplot.xlim((l_bound, r_bound))
pyplot.ylabel('Щільність', fontsize=12)
pyplot.grid(linewidth=0.2)
pyplot.show()

Тобто першого разу нам не пощастило, що розмір вибірки був маленьким

Довірчий інтервал

\[CI_{\mu} = \left(\overline X \pm \dfrac{z_{1 - \alpha/2} \sqrt{S^2}}{\sqrt{n}} \right)\]

sample = expon(scale=300).rvs(2000) # E sample = scale = 300
left_bound, right_bound = norm.interval(confidence=0.95, loc=numpy.mean(sample), scale=sem(sample))
print(f"CI = [{round(left_bound, 2)}, {round(right_bound, 2)}]")
CI = [289.96, 316.15]


  • 📊 Задача
left_bound, right_bound = norm.interval(confidence=0.95, loc=numpy.mean(profits), scale=sem(profits))
print(f"CI = [{round(left_bound, 2)}, {round(right_bound, 2)}]")
CI = [14.04, 102.94]

Так, виручка позитивна! Значить ми знайшли інвесторів для нашого стартапу 🫡

\(t'\)-тест vs. \(t\)-тест

Для початку визначимося, коли який критерій краще використовувати?

  1. Якщо вибірка розміру 60, то вже \(t_{59} \approx \mathcal{N}(0, 1)\).
    • Подивимося на розподіли Стьюдента і нормального:
df = 59
t_dist = t(df=df)
z_dist = norm(loc=0, scale=1)

x = numpy.linspace(-3, 3, 100)
pyplot.figure(figsize=(10, 5))
pyplot.title(f'Щільність розподілу статистики T і N', fontsize=12)
pyplot.plot(x, z_dist.pdf(x), label='N(0, 1)')
pyplot.plot(x, t_dist.pdf(x), label=f't(df={df})')
pyplot.legend(fontsize=12)
pyplot.grid(linewidth=0.2)
pyplot.show()

  • Ми бачимо, що ці 2 розподіли візуально повністю збігаються, тому неважливо, як порахувати: статистика \(T\sim \mathcal{N}(0, 1)\) або \(T\sim t_n\).
  • Але це не означає, що з N=60 T-test/T’-test працюють коректно! Якщо вибірка не з нормального розподілу, вони обидва можуть все ще помилятися.

\(t'\)-тест vs. \(t\)-тест

  1. Якщо вибірка менше 60, то безпечніше використовувати t-test, ніж t’-test.
    • У T-test FPR завжди буде меншим, ніж у T’-test.
      • На FPR впливає відсоток випадків pvalue < alpha. У t-test pvalue \(\geq\) t’-test pvalue.
      • pvalue = t_distr.cdf(x) або pvalue = norm_dist.cdf(x). Тож чим важчий хвіст у розподілу, тим більше p-value. А тепер подивимося на прикладі:
df_array = [2, 5, 10, 20]
x = numpy.linspace(-3, 3, 100)

pyplot.figure(figsize=(10, 5))
pyplot.title(f'CDF розподілів T та N', fontsize=12)
for df in df_array:
    t_dist = t(df=df)
    pyplot.plot(x, t_dist.cdf(x), label=f't(df={df})')

z_dist = norm(loc=0, scale=1)
pyplot.plot(x, z_dist.cdf(x), c=red_pink, label='N(0, 1)')
pyplot.legend(fontsize=12)
pyplot.xlabel('X', fontsize=12)
pyplot.grid(linewidth=0.2)
pyplot.show()

Розподіл Стьюдента з нескінченністю ступенів свободи — це нормальний розподіл: \(t_{\infty} = \mathcal{N}(0, 1)\). Тому norm(0, 1).cdf(x) = t_distr(df=infinity).cdf(x) < t_distr(df=N).cdf(x).

Висновки

Ми бачимо, що ми скрізь можемо використовувати \(t\)-test (а \(t'\)-test не завжди), і в разі маленьких вибірок він безпечніший. Тому \(t\)-test і став набагато популярнішим, ніж \(t'\)-test. Але \(t'\)-test на практиці може бути теж корисний:

  • Не треба думати під час реалізації про ступені свободи.
  • Написати такий критерій на SQL буде набагато простіше: ви можете використовувати табличні значення в коді, щоб зрозуміти, чи відхиляється критерій.
  • Робити різні теоретичні обчислення простіше.
  • У ньому складніше помилитися під час реалізації.

Двовибірковий \(t\)-тест. Задача AB-тестування

📊 Задача

У нас на сайті є послуги просування. Ми хочемо почати давати на них знижки, щоб залучити більше людей і почати більше заробляти. Для цього було вирішено провести AB тест: Одній половині користувачів ми не видали знижок, а в другій половині ми видали знижки всім новим користувачам. Треба зрозуміти, чи стали ми більше заробляти?

Цього разу у нас 2 вибірки \(A\) — контроль, і \(B\) — тест.

📊 Задача

\[H_0: \mathbb{E} A = \mathbb{E} B \; \text{vs.} \; H_1: \mathbb{E}A < \mathbb{E} B\]

  1. Обидві вибірки нормальні.

Тоді є 2 критерії залежно від наших знань про дисперсію:

  • \(\sigma^2_A = \sigma^2_B\).

    Тоді:

    • \(S^2_{full} = \dfrac{(N - 1)S^2_A + (M - 1)S^2_B}{N + M - 2}\), де N, M - розмір контролю і тесту відповідно. А критерій має такий вигляд:
    • \(T(A, B) = \dfrac{\overline A - \overline B}{S^2_{full}\sqrt{1/N + 1/M}} \overset{H_0}{\sim} T_{n + m - 2}\)
  • \(\sigma^2_A \neq \sigma^2_B\).

    Тоді:

    • \(T(A, B) = \dfrac{\overline A - \overline B}{\sqrt{S^2_{A}/N + S^2_{B}/M}} \overset{H_0}{\sim} T_{v}\)
    • де \(v = \dfrac{\left(\dfrac{S^2_{A}}{N} + \dfrac{S^2_{B}}{M} \right)^2}{\left(\dfrac{(S^2_{A})^2}{N^2(N - 1)} + \dfrac{(S^2_{B})^2}{M^2(M-1)} \right)}\)
  1. Хоч би 1 вибірка ненормальна.

Тоді в бій вступає нормальна апроксимація при великому розмірі вибірок, критерій T’-test:

  • \(T(A, B) = \dfrac{\overline A - \overline B}{\sqrt{S^2_{A}/N + S^2_{B}/M}} \overset{H_0}{\sim} \mathcal{N}(0, 1)\)

🐍 Python та Двовибірковий \(t\)-тест

  • scipy.stats.ttest_ind — 2-вибірковий t-test критерій
  • CompareMeansклас для побудови довірчих інтервалів у t-test.
numpy.random.seed(42)
X = expon(scale=1100).rvs(1000)
Y = norm(loc=980, scale=30).rvs(1000)

ttest_ind(X, Y, equal_var=False, alternative='greater')
TtestResult(statistic=2.5645688722251325, pvalue=0.005237676356845092, df=1000.5367318148768)


ttest_ind(X, Y, equal_var=False, alternative='greater').pvalue
0.005237676356845092


# Довірчий інтервал

cm = CompareMeans(DescrStatsW(X), DescrStatsW(Y))
print(cm.tconfint_diff(usevar='unequal'))
(20.38059303711826, 153.1987434094657)

MDE для \(t\)-тесту.

Згадуємо MDE

MDE — це таке істинне значення ефекту, що наш шанс його виявити дорівнює \(1-\beta\) при використанні нашого критерію.

Від чого залежить MDE?

  • Помилка 1 роду, або \(\alpha\).
    • Наприклад, за \(\alpha = 1\) ми знайдемо ефект і за розміру вибірки, що дорівнює 1 (ми просто завжди відкидатимемо 0 гіпотезу). А за \(\alpha = 0\) ми ніколи не задетектуємо ефект.
  • Потужність, або \(1 - \beta\).
    • Випливає із самого визначення
  • Від шуму в даних, або від дисперсії.
    • Що більш галасливі дані, як ми знаємо, то ширший довірчий інтервал. А отже, складніше точно передбачити рамки для істинного ефекту, тому і MDE буде більшим.
  • Від розміру вибірки.
    • Нас цікавить не просто дисперсія в даних, а дисперсія середнього значення: вона за тією самою логікою має бути якомога меншою. А що таке дисперсія середнього? Це \(\dfrac{\sigma^2}{N}\), тому MDE також залежить від розміру вибірки.

📊 Задача

Для початку визначимося з гіпотезою, що перевіряється:

  • \(H_0: \mu_0 = 0\ vs. \ H_1: \mu_0 > 0\)

Позначимо

  • \(S^2_{\mu} := \dfrac{S^2}{N}\) — оцінка дисперсії середнього значення.
  • \(S_{\mu} = \sqrt{\dfrac{S^2}{N}}\) — оцінка стандартного відхилення середнього значення, або SEM.

Тепер ми знаємо, що

  • \(\overline X \sim \mathcal{N}(\mu, S^2_{\mu})\)

Нам треба знайти \(MDE=m\), таке, що:

  • якщо \(\overline X \sim \mathcal{N}(m, S^2_{\mu})\), то в \(1-\beta\) відсотку випадків для нього відкинеться критерій. Перевіряємо потужність (зелена площа на графіку).
  • якщо \(\overline X \sim \mathcal{N}(0, S^2_{\mu})\), то критерій відкинеться для нього в \(\alpha\) відсотків випадків. Перевіряємо FPR (червона площа на графіку).

\[\text{MDE} = (z_{1 - \alpha} + z_{1 - \beta}) \cdot \sqrt{\dfrac{S^2}{N}}\]

де \(z_{1 - \alpha}\) та \(z_{1 - \beta}\) — квантилі розподілу \(\mathcal{N}(0, 1)\).

🐍 Python та MDE

\(N = 1000\), \(\alpha=5\)%, \(1-\beta=80\)%, а як дізнатися \(S^2\)?

На практиці є 3 способи:

  • Оцінити на історичних даних. У цьому випадку це не підходить, тому що раніше стартапу не було.
  • Оцінити за схожими даними.
  • Якось теоретично оцінити. Найгірший спосіб, який працює, якщо перші 2 не допомагають.
N = 1000
S2 = numpy.var(profits)
alpha = 0.05
beta = 1 - 0.8

MDE = (norm().ppf(1-alpha) + norm().ppf(1 - beta)) * numpy.sqrt(S2/N)
print(f"MDE при N={N}: {MDE}")
MDE при N=1000: 126.08268390090282

MDE та розмір вибірки

Для нас це занадто великий MDE: хочеться, щоб він був \(\leq\) 100, ми припускаємо, що це ймовірніший істинний ефект, виходячи з досвіду.

Давайте тепер розв’яжемо зворотну задачу: Ми знаємо MDE = 100, \(\alpha=5\)%, \(1-\beta=80\)%, \(S^2\), чому дорівнює \(N\)? Виведемо його з формули MDE:

\(N = \left(\dfrac{z_{1 - \alpha} + z_{1 - \beta}}{\text{MDE}}\right)^2 S^2\)

S2 = numpy.var(profits)
alpha = 0.05
beta = 1 - 0.8
mde = 100

N = ((norm().ppf(1-alpha) + norm().ppf(1 - beta)) / mde)**2 * S2
N = int(N) + 1
print(f'Мінімальний розмір вибірки: {N}')
Мінімальний розмір вибірки: 1590

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



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

ihor.miroshnychenko@knu.ua

Data Mirosh

@ihormiroshnychenko

@aranaur

aranaur.rbind.io