Проверка на хипотеза
Накратко: Проверката на хипотеза пита дали разликата може да е случайност: нулевата хипотеза казва „няма разлика“, а p-стойността е колко често случайността дава поне такава разлика.
Проблем
„Петъкът наистина ли е по-силен?“
Управителят иска да сложи още един човек на опаковането в петък и да махне един в неделя. Преди това пита: петъкът наистина ли е по-силен, или така се е паднало тази година? Ето колко поръчки идват средно на ден, по ден от седмицата (0 е понеделник, 6 е неделя):
Броим всички поръчки, и отказаните: въпросът е колко пристигат, а и отказаната се приема и обработва. В петък идват средно 36,35 поръчки, а в неделя 24,62. Петъците в годината са 52 и неделите също. Всеки ден е малко различен: в един петък идват 22 поръчки, в друг 68. Дали 11,73 поръчки разлика е истинска, или е шум като в урока за извадките?
Идеята
Ако денят нямаше значение
Въпросът „петъкът по-силен ли е от неделята?“ е хипотеза. За да я провериш, започваш от скучното обяснение: денят няма значение и разликата е случайна. Това е нулевата хипотеза (null hypothesis).
Ако нулевата хипотеза е вярна, етикетите „петък“ и „неделя“ не значат нищо. Можеш да ги разбъркаш: слагаш 104-те дни на една купчина, разбъркваш ги и казваш, че първите 52 са „петъци“, а другите 52 „недели“. Разликата между двете половини е колко разлика дава самата случайност:
Истинската разлика е 11,73 поръчки. Три разбърквания дават 0,50, 1,23 и 1,46. np.concatenate слепва двата масива в един, а rng.permutation връща разбъркано копие.
Идеята
10 000 разбърквания и p-стойността
Три разбърквания са малко. Правиш 10 000 наведнъж: np.tile повтаря 104-те дни в 10 000 реда, а rng.permuted(..., axis=1) разбърква всеки ред поотделно. После една сметка дава разликата за всеки ред:
Най-голямата разлика от 10 000 разбърквания е 7,50 поръчки. Истинската е 11,73. Делът на разбъркванията с разлика поне толкова голяма е p-стойността (p-value): тук е 0, значи под 1 на 10 000. Ако денят нямаше значение, такава разлика почти никога нямаше да се получи. Заключението: петъкът наистина е по-силен.
Този начин се казва пермутационен тест (permutation test). np.abs е, защото въпросът е „има ли разлика“, без да казваш предварително в коя посока. Обичайният праг е 0,05: под него казваш, че разликата е статистически значима.
Пробвай
Понеделник и събота
Разликата между понеделник и събота е по-малка. Случайна ли е? Запиши в gap разликата в средния брой поръчки на ден (понеделник минус събота), в gaps разликите от 10 000 разбърквания, а в p p-стойността.
Решение
import numpy as np
import pandas as pd
orders = pd.read_csv('year_orders.csv')
orders['ordered_at'] = pd.to_datetime(orders['ordered_at'])
daily = orders.set_index('ordered_at').resample('D').size()
weekday = daily.index.dayofweek.rename('weekday')
mon = daily[weekday == 0].to_numpy()
sat = daily[weekday == 5].to_numpy()
gap = mon.mean() - sat.mean()
both = np.concatenate([mon, sat])
rng = np.random.default_rng(42)
shuffled = rng.permuted(np.tile(both, (10_000, 1)), axis=1)
gaps = shuffled[:, :len(mon)].mean(axis=1) - shuffled[:, len(mon):].mean(axis=1)
p = (np.abs(gaps) >= abs(gap)).mean()
print(round(gap, 2), p)Понеделникът има средно с 3,46 поръчки повече от съботата. От 10 000 разбърквания около 1% (p = 0,0104) дават поне толкова голяма разлика. Под 0,05 е: има доказателство, че съботата е по-слаба. Понеделниците са 52, а съботите 53, затова срязваш при len(mon), не при 52. Без rng.permuted всички редове са едни и същи и p е 1, а без np.abs броиш само едната посока.
Идеята
Когато разлика няма
Неделята има по-малко поръчки. По-малки ли са и самите поръчки? Тук се сравняват 10 459 поръчки без отказаните, а не дни, затова таблица 2 000 × 10 459 с числа би заела около 170 MB. Цикълът с 2 000 разбърквания на маската е под секунда:
Неделните поръчки са 1 204, със средна 10,83 €, а другите са средно 10,60 €. Разликата е 0,23 € и p = 0,425: в 42,5% от разбъркванията разликата е поне толкова голяма. Това е обикновен шум.
Честният извод е „няма доказателство, че неделната поръчка е различна“. Не е „доказахме, че са равни“: малка разлика може да има, само че тези данни не могат да я покажат. В неделя хората поръчват по-рядко, а за по-малки поръчки доказателство няма.
Идеята
Какво p не казва
p-стойността отговаря на един тесен въпрос: колко изненадващи са данните, ако нулевата хипотеза е вярна. Тя не е две неща, с които често се бърка:
- не е вероятността хипотезата да е вярна: p = 0,425 не значи „42,5% шанс да няма разлика“;
- не е големината на разликата: много малко p значи „не е случайно“, а не „голяма разлика“.
Колко е голяма разликата казва самата разлика с интервал. С bootstrap от предишния урок, отделно за петъците и за неделите:
Петъкът има средно 11,73 поръчки повече, с 95% интервал от 9,00 до 14,56. Това е 1,48 пъти повече от неделята. С това число управителят решава колко хора да сложи, а p само казва, че не гони шум.
По-точно, за любопитните
Прагът 0,05 е договорка, не закон на природата. При p = 0,06 данните не са „почти доказателство“, а при p = 0,04 не са „сигурно вярно“. С много данни и нищожна разлика също дава малко p: при милион поръчки във всяка група и 10 цента разлика тестът ще е „значим“, но решение от това не следва. Затова винаги пиши и разликата, и интервала.
Идеята
Същото със scipy
scipy.stats има тестовете готови. ttest_ind (t-тест) сравнява средните на две групи по формула. permutation_test прави разбъркванията вместо теб, на парчета (batch), за да не свърши паметта:
За неделната поръчка t-тестът дава 0,426, permutation_test 0,419, а нашият цикъл 0,425. Отговорът е същият: няма доказателство. За петък срещу неделя t-тестът дава 1,04 × 10⁻¹²: нула за всички практически цели, като нашето 0 от 10 000.
По-точно, за любопитните
t-тестът приема, че средната на всяка група е разпределена почти нормално. При стотици поръчки в група това е вярно (централната гранична теорема). При малко данни с дълга опашка пермутационният тест е по-сигурен, защото не приема нищо за разпределението. permutation_test дава p малко по-различно от нашето: разбъркванията са други, а и брои самите данни като едно от тях, (брой + 1) / (разбърквания + 1).
Упражнение
Декември и петъкът
През декември се продават подаръчни карти. По-голяма ли е декемврийската поръчка от поръчките в другите месеци, или е случайно? Запиши в gap разликата в средната поръчка (декември минус другите месеци), а в p p-стойността. Можеш да ползваш пермутационен тест или stats.ttest_ind.
Решение
from scipy import stats
import numpy as np
import pandas as pd
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
orders = orders[orders['status'] != 'cancelled']
items['revenue'] = items['quantity'] * items['unit_price']
totals = items.groupby('order_id')['revenue'].sum()
orders['value'] = orders['id'].map(totals)
orders['ordered_at'] = pd.to_datetime(orders['ordered_at'])
value = orders['value'].to_numpy()
is_dec = (orders['ordered_at'].dt.month == 12).to_numpy()
gap = value[is_dec].mean() - value[~is_dec].mean()
p = stats.ttest_ind(value[is_dec], value[~is_dec]).pvalue
print(is_dec.sum(), round(gap, 2), p)Декемврийските поръчки са 1 211, а поръчката е средно с 2,84 € по-голяма от поръчката в другите месеци. t-тестът дава p = 5,1 × 10⁻²³: случайност не обяснява тази разлика. Сравнението е декември срещу другите 11 месеца. Срещу цялата година сравняваш декември и със самия себе си и разликата се свива.
Петъчната поръчка е малко по-малка от поръчката в другите дни. Има ли доказателство за това? Направи пермутационен тест с 2 000 разбърквания (seed 42): gap е разликата в средната поръчка (петък минус другите дни), p е p-стойността, а evidence е дали p е под прага 0,05.
Решение
import numpy as np
import pandas as pd
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
orders = orders[orders['status'] != 'cancelled']
items['revenue'] = items['quantity'] * items['unit_price']
totals = items.groupby('order_id')['revenue'].sum()
orders['value'] = orders['id'].map(totals)
orders['ordered_at'] = pd.to_datetime(orders['ordered_at'])
value = orders['value'].to_numpy()
is_fri = (orders['ordered_at'].dt.dayofweek == 4).to_numpy()
gap = value[is_fri].mean() - value[~is_fri].mean()
rng = np.random.default_rng(42)
gaps = np.empty(2_000)
for i in range(2_000):
s = rng.permutation(is_fri)
gaps[i] = value[s].mean() - value[~s].mean()
p = (np.abs(gaps) >= abs(gap)).mean()
evidence = p < 0.05
print(round(gap, 2), p, evidence)Петъчната поръчка е с 0,45 € по-малка, а p = 0,0645. Над 0,05 е: няма доказателство при обичайния праг, и evidence е False. Изкушаващо е да кажеш „почти значимо“ или да смениш прага на 0,1, след като видиш p. Не го прави: прагът се избира преди теста. Честният отговор е „може да има малка разлика, но тези данни не я доказват“.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Дати и времеви редове · Описание на данните · Извадка и популация · Доверителен интервал
- Продължава в: A/B тест · Корелация и причина · Защо оборотът падна?
- Задачи по темата: Статистика за решения