Доверителен интервал
Накратко: Доверителният интервал е диапазон около стойността от извадката, който при 95% хваща истинската стойност в около 95 от 100 извадки; bootstrap го строи с повторни извадки с връщане.
Проблем
„Колко харчи клиентът на поръчка?“
Управителят пита колко е средната поръчка. В живота рядко имаш всичко: да кажем, че имаш 200 поръчки, изтеглени случайно от годината. Тук имаме и цялата година, 10 459 поръчки без отказаните, и можем да видим какво става:
Три извадки по 200 дават три различни средни: 9,77 €, 10,74 € и 11,16 €. От урока за извадките знаеш, че средната шуми. Но ти имаш само една извадка. Какво казваш на управителя: едно число или нещо повече?
По-честно е да кажеш „средната е някъде между … и … €“. Остава да разбереш колко широко да е това „някъде“, само от извадката, която имаш.
Идеята
Извадката вместо годината: bootstrap
Ако можеше да теглиш нови извадки от годината, щеше да видиш колко се люшка средната. Не можеш. Затова използваш извадката като умалена година: теглиш от нея 200 поръчки с връщане и смяташ средната. Това е повторна извадка (resample). Някоя поръчка се пада два пъти, друга никак, и средната е малко по-различна всеки път.
Правиш го 10 000 пъти и гледаш къде са средните. Отрязваш 2,5% от най-малките и 2,5% от най-големите. Средните 95% са доверителният интервал (confidence interval). Методът се казва bootstrap:
Първите 8 поръчки от извадката са от 0,90 до 45,20 €. Първите три повторни извадки дават средни 10,07 €, 11,07 € и 11,13 €. Средната на извадката е 11,41 €, а 95% интервалът е от 10,04 до 12,83 €. Истинската средна на годината е 10,63 €: вътре е.
rng.choice(sample, size=(10_000, 200)) е таблица 10 000 × 200: всеки ред е една повторна извадка. mean(axis=1) дава по една средна на ред. Цикъл не трябва.
Виж го
Как от една извадка се ражда интервал
Същият код бавно: плочки от извадката се копират в повторна извадка, средната ѝ пада на оста, а 10 000 такива средни правят купчина.
import numpy as npimport pandas as pd
orders = pd.read_csv('year_orders.csv')items = pd.read_csv('year_items.csv')cancelled = orders.loc[orders['status'] == 'cancelled', 'id']items = items[~items['order_id'].isin(cancelled)]items['revenue'] = items['quantity'] * items['unit_price']totals = items.groupby('order_id')['revenue'].sum()
rng = np.random.default_rng(42)sample = rng.choice(totals.to_numpy(), size=200, replace=False)print(sample[:8])boots = rng.choice(sample, size=(10_000, 200)).mean(axis=1)print(boots[:3].round(2))low, high = np.quantile(boots, [0.025, 0.975])print(round(sample.mean(), 2), round(low, 2), round(high, 2))print(round(totals.mean(), 2))Стъпка по стъпка, с думи
- totals е стойността на всяка поръчка за годината. В живота цялото не го виждаш: имаш само извадка.
- sample е извадка от 200 поръчки. Горе са първите 8, в евро: 3,6; 0,9; 2,1; 17; 45,2; 13,2; 3,4 и 2.
- Повторна извадка: 200 тегления от sample с връщане. Някоя поръчка се пада два пъти, друга изобщо не.
- Средната на повторната извадка пада като точка на оста. Първите три са 10,07 €, 11,07 € и 11,13 €.
- Така 10 000 пъти. Точките се трупат около средната на извадката, 11,41 €.
- Отрязваш 2,5% отляво и 2,5% отдясно. Средните 95% са интервалът: от 10,04 € до 12,83 €.
- Истинската средна на годината е 10,63 € и е вътре. В живота не я знаеш: интервалът казва докъде може да е.
Купчината в сцената е схема: истински са числата в надписите, от извадката до границите на интервала. Ето истинските 10 000 средни с matplotlib, с двете граници (сини линии) и истинската средна (оранжевата прекъсната):
Пробвай
Без връщане?
Медианата на поръчките е по-устойчива на големите поръчки. Направи 95% интервал за медианата със същата извадка от 200: в meds запиши медианите на 10 000 повторни извадки, а в low и high границите на интервала.
Решение
import numpy as np
import pandas as pd
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
cancelled = orders.loc[orders['status'] == 'cancelled', 'id']
items = items[~items['order_id'].isin(cancelled)]
items['revenue'] = items['quantity'] * items['unit_price']
totals = items.groupby('order_id')['revenue'].sum()
rng = np.random.default_rng(42)
sample = rng.choice(totals.to_numpy(), size=200, replace=False)
meds = np.median(rng.choice(sample, size=(10_000, 200)), axis=1)
low, high = np.quantile(meds, [0.025, 0.975])
print(np.median(sample), low, high)meds = np.median(rng.choice(sample, size=(10_000, 200)), axis=1) дава по една медиана на повторна извадка, а np.quantile(meds, [0.025, 0.975]) дава интервала: от 6,25 до 9,45 €. Медианата на извадката е 7,95 €, а тази на годината е 7,70 €, от урока за описанието: вътре е. Квантилите на самата извадка (1,20 и 36,72 €) отговарят на друг въпрос: между колко са 95% от поръчките. Формула за интервал на медиана не ти трябва: bootstrap смята всяко число, щом смениш mean с median.
Идеята
Какво значи 95%
Истинската средна на годината е едно число: 10,63 €. Интервалът от една извадка или я хваща, или не. 95% е обещание на метода: ако теглиш много извадки и за всяка правиш интервал, около 95 от 100 интервала ще хванат истината. Това се казва ниво на доверие.
Тук годината е цялата у нас и можем да проверим обещанието. 100 извадки по 200, за всяка bootstrap с 1 000 повторни извадки:
93 от 100 интервала хващат 10,63 €. Близо е до 95: самите 100 извадки са случайни и следващите 100 ще дадат малко друго. Около 5 от 100 честни интервала изпускат истината и няма как да знаеш кои са.
По-точно, за любопитните
Изкушаващо е да кажеш „с вероятност 95% истинската средна е между 10,04 и 12,83 €“. Строго погледнато не е това: истинската средна не е случайна, а интервалът от твоята извадка или я съдържа, или не. Случайна е извадката. Затова 95% е за метода, а не за едно конкретно число. В отчет пиши „95% доверителен интервал: 10,04–12,83 €“.
Bootstrap работи, когато извадката е случайна. Ако 200-те поръчки са от един декември, интервалът ще е за декември, колкото и повторни извадки да направиш: повторните извадки не поправят една изкривена извадка.
Идеята
Формулата като кратък път
За средна има кратък път без 10 000 повторни извадки. Шумът на средната е стандартната грешка: std / √n. При нормално разпределение средните 95% са на 1,96 стандартни грешки от средата. Затова интервалът е средната ± 1,96 × std / √n:
Стандартната грешка е 0,72 €, а интервалът е от 10,00 до 12,81 €. Bootstrap даде 10,04–12,83 €: почти същото. Вторият ред проверява обещанието на формулата: от 1 000 извадки по 200, 948 интервала хващат истината. Близо до 950.
std(ddof=1) дели на n − 1 вместо на n, както std() в pandas. При 200 поръчки разликата е в третия знак.
По-точно, за любопитните
Числото 1,96 идва от нормалното разпределение: между −1,96 и 1,96 стандартни отклонения е 95% от площта. Средната на 200 поръчки е близо до нормална, дори когато самите поръчки имат дълга опашка (централната гранична теорема от урока за извадките). При малки извадки, под 30, или при много дълга опашка формулата греши повече, а bootstrap е по-честен.
Идеята
Интервал за дял
Каква част от поръчките са отказани? Делът е средна на нули и единици: True е 1, False е 0, и mean() дава дела. Затова bootstrap и формулата работят и тук. Формулата за дял е p ± 1,96 × √(p(1 − p) / n):
В извадка от 500 поръчки 5,8% са отказани. Bootstrap дава от 3,8 до 8,0%, а формулата от 3,8 до 7,8%. Истинският дял за годината е 5,9%. От 500 поръчки знаеш дела с точност около ± 2 пункта: „5,8%“ без интервал звучи по-точно, отколкото е.
Идеята
Малка извадка, широк интервал
Колкото по-малко поръчки, толкова повече шуми средната и толкова по-широк е интервалът:
50 поръчки дават интервал, широк 4,73 €, 200 поръчки 2,69 €, а 800 поръчки 1,35 €. 4 пъти повече данни дават около два пъти по-тесен интервал, защото стандартната грешка пада с корена от n. Точността е скъпа: още двойно по-тесен интервал иска пак 4 пъти повече поръчки.
Упражнение
Отговорът за управителя
Нова извадка от 400 поръчки (seed 2026). Направи 95% интервал за средната поръчка по два начина: с bootstrap от 10 000 повторни извадки (boot_low, boot_high) и с формулата (se е стандартната грешка, f_low и f_high са границите).
Решение
import numpy as np
import pandas as pd
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
cancelled = orders.loc[orders['status'] == 'cancelled', 'id']
items = items[~items['order_id'].isin(cancelled)]
items['revenue'] = items['quantity'] * items['unit_price']
totals = items.groupby('order_id')['revenue'].sum()
rng = np.random.default_rng(2026)
sample = rng.choice(totals.to_numpy(), size=400, replace=False)
boots = rng.choice(sample, size=(10_000, 400)).mean(axis=1)
boot_low, boot_high = np.quantile(boots, [0.025, 0.975])
se = sample.std(ddof=1) / np.sqrt(len(sample))
f_low = sample.mean() - 1.96 * se
f_high = sample.mean() + 1.96 * se
print(round(sample.mean(), 2))
print(round(boot_low, 2), round(boot_high, 2))
print(round(f_low, 2), round(f_high, 2))Средната на извадката е 10,80 €. Bootstrap дава от 9,79 до 11,90 €, а формулата от 9,74 до 11,85 €. Отговорът за управителя е „средната поръчка е около 10,80 €, с 95% доверителен интервал от 9,79 до 11,90 €“. С se = sample.std(ddof=1) без корена от n интервалът щеше да е ± 21,10 €: това е колко се различават поръчките, а не колко шуми средната им.
Управителят обмисля безплатна доставка над 20 €. Каква част от поръчките са над 20 €? От извадка от 300 поръчки (seed 7) запиши в p дела, а в low и high 95% интервала му.
Решение
import numpy as np
import pandas as pd
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
cancelled = orders.loc[orders['status'] == 'cancelled', 'id']
items = items[~items['order_id'].isin(cancelled)]
items['revenue'] = items['quantity'] * items['unit_price']
totals = items.groupby('order_id')['revenue'].sum()
rng = np.random.default_rng(7)
sample = rng.choice(totals.to_numpy(), size=300, replace=False)
big = sample > 20
p = big.mean()
se = np.sqrt(p * (1 - p) / len(sample))
low, high = p - 1.96 * se, p + 1.96 * se
print(round(p, 3), round(low, 3), round(high, 3))
print(round((totals > 20).mean(), 3))big = sample > 20 е маска, а big.mean() е делът: 12,7%. Стандартната грешка на дял е np.sqrt(p * (1 - p) / 300), а интервалът е от 8,9 до 16,4%. Истинският дял за годината е 13,3%: вътре е. big.sum() щеше да даде броя поръчки, 38, а не дела.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Първата графика · Разпределения · Описание на данните · Извадка и популация
- Продължава в: Проверка на хипотеза · A/B тест · Защо оборотът падна?
- Задачи по темата: Статистика за решения