Перейти к содержанию
Educora
Университет25 мин34 / 42

Статистика на Python

Описательная статистика, выбросы, нормальное распределение и `scipy.stats`, центральная предельная теорема, доверительный интервал, t-тест и корреляция — с формулами, разобранными примерами и работающим кодом.

Проверь себя
В этом уроке ты узнаешь
  • Вычислять среднее, медиану, стандартное отклонение и IQR, находить выбросы
  • Находить вероятности и квантили нормального распределения с помощью scipy.stats.norm
  • Строить доверительный интервал для среднего и правильно толковать p-значение t-теста
  • Вычислять корреляцию Пирсона и Спирмена и отличать корреляцию от причинности

Служба доставки еды в Баку обещает привезти заказ «в среднем за 15 минут». Мурад засёк время 10 заказов. Достаточно ли этого, чтобы проверить обещание? И как повлияет на вывод один заказ, который ехал 45 минут? Статистика отвечает в три шага: описать данные, смоделировать их распределением и сделать вывод от выборки ко всей генеральной совокупности. В Python для этого хватает NumPy и scipy.stats.

Описательная статистика и выбросы

Меры центра дают «типичное» значение: среднее (среднее арифметическое), медиана (середина отсортированных данных) и мода (самое частое значение). Меры разброса показывают, насколько рассеяны значения: дисперсия, стандартное отклонение и межквартильный размах IQR = Q3 − Q1. Для выборки дисперсию делят на n − 1 (поправка Бесселя) — так оценка получается несмещённой.

x̄ = (1/n) · ∑ xᵢ s² = ∑ (xᵢ − x̄)² / (n − 1) s = √s²x̄ = (1/n) · ∑ xᵢ s² = ∑ (xᵢ − x̄)² / (n − 1) s = √s²
где:
  • nобъём выборки
  • s²выборочная дисперсия (единица — квадрат единицы данных)
  • sвыборочное стандартное отклонение (в единицах данных); в NumPy std(ddof=1)
Python
import numpy as np
from scipy import stats

minutes = np.array([12, 15, 11, 14, 13, 45, 13, 16, 14, 13])
print('mean  :', minutes.mean())
print('median:', np.median(minutes))
print('mode  :', stats.mode(minutes, keepdims=False).mode)
print('var   :', round(minutes.var(ddof=1), 2))
print('std   :', round(minutes.std(ddof=1), 2))
q1, q3 = np.percentile(minutes, [25, 75])
iqr = q3 - q1
print('Q1, Q3, IQR:', q1, q3, iqr)
low, high = q1 - 1.5 * iqr, q3 + 1.5 * iqr
print('outliers:', minutes[(minutes < low) | (minutes > high)])
▸ Ожидаемый результат
mean  : 16.6
median: 13.5
mode  : 13
var   : 101.6
std   : 10.08
Q1, Q3, IQR: 13.0 14.75 1.75
outliers: [45]
Один заказ на 45 минут поднимает среднее до 16,6 минуты, а стандартное отклонение — до 10 минут, тогда как медиана остаётся 13,5 минуты. Без этого заказа среднее было бы 13,44 минуты.
[Q1 − 1,5 · IQR; Q3 + 1,5 · IQR]
где:
  • Q1, Q3первый и третий квартиль (25% и 75%)
  • IQRQ3 − Q1, ширина средней половины данных

Правило Тьюки: значения вне этого интервала считаются выбросами. Здесь: 14,75 + 1,5 · 1,75 = 17,375, значит, 45 — выброс.

Нормальное распределение

Величины, складывающиеся из множества мелких независимых влияний (ошибки измерений, баллы за тест, рост), часто близки к нормальному распределению: колоколообразная кривая, симметричная относительно среднего. Её задают два параметра: среднее μ и стандартное отклонение σ. Любое значение можно стандартизировать z-оценкой — она показывает, на сколько σ значение удалено от среднего.

f(x) = 1 / (σ · √(2π)) · e^(−(x − μ)² / (2σ²)) z = (x − μ) / σf(x) = 1 / (σ · √(2π)) · e^(−(x − μ)² / (2σ²)) z = (x − μ) / σ
где:
  • μсреднее распределения
  • σстандартное отклонение (σ > 0)
  • f(x)плотность вероятности; P(a < X < b) — площадь под кривой между a и b
  • zz-оценка: положение в стандартном нормальном распределении (μ = 0, σ = 1)

scipy.stats.norm создаёт объект распределения: cdf(x) = P(X ≤ x) (функция распределения), sf(x) = P(X > x) = 1 − cdf, а ppf(q) — обратная операция, квантиль для заданной вероятности. Если баллы за тест распределены нормально с μ = 70 и σ = 10:

Python
from scipy import stats

scores = stats.norm(loc=70, scale=10)
print(round(scores.cdf(85), 4))
print(round(scores.sf(85), 4))
print(round(scores.cdf(80) - scores.cdf(60), 4))
print(round(scores.ppf(0.90), 2))
z = (85 - 70) / 10
print(z, round(stats.norm.cdf(z), 4))
▸ Ожидаемый результат
0.9332
0.0668
0.6827
82.82
1.5 0.9332
Пример 1: z-оценка и вероятность

Баллы за тест распределены как N(70; 10²). а) Какова вероятность, что случайный студент наберёт больше 85? б) Сколько баллов нужно, чтобы попасть в лучшие 10%? Φ(1,5) = 0,9332, z₀,₉₀ = 1,2816.

Показать решение
а) z = (85 − 70) / 10 = 1,5.
P(X > 85) = 1 − Φ(1,5) = 1 − 0,9332 = 0,0668, то есть около 6,7%.
б) x = μ + z₀,₉₀ · σ = 70 + 1,2816 · 10 ≈ 82,8 балла.
Оба ответа совпадают с кодом (sf(85) и ppf(0.90)).

Выборки, центральная предельная теорема и стандартная ошибка

Обычно мы видим не всю генеральную совокупность (все заказы), а лишь выборку. У каждой новой выборки среднее немного другое. Центральная предельная теорема гласит: при достаточно большом n распределение выборочных средних приближается к нормальному, даже если исходное распределение скошено, а его стандартное отклонение — стандартная ошибка — равно σ/√n. Ниже из скошенного экспоненциального распределения берутся 5000 выборок по 30:

SE = σ / √n ≈ s / √nSE = σ / √n ≈ s / √n
где:
  • SEстандартная ошибка среднего
  • σ, sстандартное отклонение генеральной совокупности и выборки
Python
import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(1)
population = rng.exponential(scale=10, size=100_000)
means = rng.choice(population, size=(5000, 30)).mean(axis=1)
print(f'population: mean {population.mean():.2f}, std {population.std():.2f}')
print(f'sample means: mean {means.mean():.2f}, std {means.std():.2f}')
print(f'sigma / sqrt(n) = {population.std() / np.sqrt(30):.2f}')

fig, (a, b) = plt.subplots(1, 2, figsize=(8, 3), layout='constrained')
a.hist(population, bins=50)
a.set_title('Population (skewed)')
b.hist(means, bins=40)
b.set_title('Means of samples, n = 30')
plt.show()
▸ Ожидаемый результат
population: mean 9.96, std 9.92
sample means: mean 9.94, std 1.81
sigma / sqrt(n) = 1.81
Гистограмма справа — почти симметричный колокол, а её разброс (1,81) ровно равен σ/√30.

Доверительный интервал и t-тест

Когда σ неизвестна, её заменяют на s и вместо нормального используют t-распределение Стьюдента с n − 1 степенями свободы — при малых выборках у него более «тяжёлые хвосты». С фабричной линии взвесили 10 пачек чая по 100 граммов:

x̄ ± t* · s / √n, t* = t₁₋α/₂(n − 1)x̄ ± t* · s / √n, t* = t₁₋α/₂(n − 1)
где:
  • t*критическое значение t-распределения; для 95% α = 0,05
  • n − 1число степеней свободы

95%-й доверительный интервал для среднего. stats.t.ppf(0.975, df=n - 1) возвращает t*, а stats.t.interval — весь интервал.

Python
import numpy as np
from scipy import stats

packs = np.array([99.2, 100.4, 98.7, 100.9, 99.5, 100.1, 99.8, 98.9, 100.6, 99.4])
n = packs.size
mean = packs.mean()
s = packs.std(ddof=1)
se = s / np.sqrt(n)
t_crit = stats.t.ppf(0.975, df=n - 1)
print(f'mean = {mean:.2f} g, s = {s:.3f} g, SE = {se:.3f} g')
print(f't* = {t_crit:.3f}')
print(f'95% CI: [{mean - t_crit * se:.2f}; {mean + t_crit * se:.2f}] g')
low, high = stats.t.interval(0.95, df=n - 1, loc=mean, scale=se)
print(round(low, 2), round(high, 2))
▸ Ожидаемый результат
mean = 99.75 g, s = 0.738 g, SE = 0.233 g
t* = 2.262
95% CI: [99.22; 100.28] g
99.22 100.28
Пример 2: доверительный интервал вручную

Время подготовки к экзамену у 16 студентов: x̄ = 52 часа, s = 8 часов. Найди 95%-й доверительный интервал для среднего. t₀,₉₇₅(15) = 2,131.

Показать решение
SE = s / √n = 8 / √16 = 8 / 4 = 2 часа.
Предельная ошибка = t* · SE = 2,131 · 2 = 4,262.
Интервал: 52 ± 4,26 → [47,74; 56,26] часа.
Если увеличить выборку до 64 студентов, SE упадёт до 1, и интервал станет примерно вдвое уже (закон √n).

Проверка гипотезы ставит вопрос так: если бы нулевая гипотеза H₀ («различий нет») была верна, насколько вероятно было бы получить различие такое же или ещё больше, чем наблюдаемое? Эта вероятность — p-значение. Если p < α (обычно 0,05), H₀ отвергают. Одновыборочный t-тест сравнивает среднее с заданным числом, двухвыборочный — средние двух групп; поскольку дисперсии групп могут различаться, выбираем вариант Уэлча (equal_var=False).

t = (x̄₁ − x̄₂) / √(s₁²/n₁ + s₂²/n₂)t = (x̄₁ − x̄₂) / √(s₁²/n₁ + s₂²/n₂)
где:
  • x̄₁, x̄₂средние групп
  • s₁², s₂²выборочные дисперсии групп
  • n₁, n₂объёмы групп

t-статистика Уэлча: разность средних, делённая на её стандартную ошибку. Чем больше |t|, тем меньше p.

Python
import numpy as np
from scipy import stats

packs = np.array([99.2, 100.4, 98.7, 100.9, 99.5, 100.1, 99.8, 98.9, 100.6, 99.4])
res1 = stats.ttest_1samp(packs, popmean=100)
print(f'one-sample: t = {res1.statistic:.3f}, p = {res1.pvalue:.3f}')

group_a = np.array([72, 85, 78, 90, 66, 81, 77, 88, 74, 83])
group_b = np.array([80, 91, 86, 94, 79, 88, 85, 97, 82, 90])
print(group_a.mean(), group_b.mean())
res2 = stats.ttest_ind(group_b, group_a, equal_var=False)
print(f'Welch: t = {res2.statistic:.3f}, p = {res2.pvalue:.4f}')
▸ Ожидаемый результат
one-sample: t = -1.071, p = 0.312
79.4 87.2
Welch: t = 2.581, p = 0.0194
Пачки: p = 0,312 > 0,05 — нет оснований считать, что средний вес отличается от 100 г. Группы (A — старая, B — новая методика обучения): p ≈ 0,019 < 0,05 — различие статистически значимо.

Корреляция

Коэффициент корреляции Пирсона r измеряет силу и направление линейной связи между двумя переменными, от −1 до 1; r² показывает, какую долю вариации y линейно объясняет x. Коэффициент Спирмена ρ делает то же вычисление на рангах, поэтому устойчив к выбросам и улавливает любую монотонную связь.

r = ∑ (xᵢ − x̄)(yᵢ − ȳ) / √( ∑ (xᵢ − x̄)² · ∑ (yᵢ − ȳ)² )r = ∑ (xᵢ − x̄)(yᵢ − ȳ) / √( ∑ (xᵢ − x̄)² · ∑ (yᵢ − ȳ)² )
где:
  • r−1 ≤ r ≤ 1; знак показывает направление, |r| — силу
  • x̄, ȳсреднее каждой переменной
Python
import numpy as np
from scipy import stats

hours = np.array([1, 2, 2, 3, 4, 5, 5, 6, 7, 8])
score = np.array([52, 55, 61, 60, 68, 70, 75, 74, 82, 88])
r, p = stats.pearsonr(hours, score)
rho, _ = stats.spearmanr(hours, score)
print(f'Pearson r = {r:.3f}, p = {p:.1e}')
print(f'Spearman rho = {rho:.3f}')
print(f'r squared = {r ** 2:.3f}')
print(np.corrcoef(hours, score).round(3))
▸ Ожидаемый результат
Pearson r = 0.980, p = 6.5e-07
Spearman rho = 0.957
r squared = 0.961
[[1.   0.98]
 [0.98 1.  ]]
Между часами подготовки и баллом очень сильная положительная связь: 96% вариации баллов линейно объясняется часами. np.corrcoef возвращает корреляционную матрицу.
Пример 3: считаем r вручную

x = (1, 2, 3, 4, 5), y = (2, 4, 5, 4, 5). Найди коэффициент корреляции Пирсона.

Показать решение
x̄ = 3, ȳ = 4.
xᵢ − x̄: −2, −1, 0, 1, 2; yᵢ − ȳ: −2, 0, 1, 0, 1.
Сумма произведений: 4 + 0 + 0 + 0 + 2 = 6.
∑ (xᵢ − x̄)² = 10, ∑ (yᵢ − ȳ)² = 4 + 0 + 1 + 0 + 1 = 6.
r = 6 / √(10 · 6) = 6 / √60 ≈ 0,775; r² ≈ 0,6.
Проверка: np.corrcoef(x, y)[0, 1] → 0,7746.
Задание

Для массива data выведи в первой строке через пробел среднее (1 знак после запятой), медиану и выборочное стандартное отклонение (ddof=1, 2 знака). Во второй строке выведи список выбросов по правилу Тьюки 1,5 · IQR.

Задание · Python
import numpy as np

data = np.array([48, 52, 50, 47, 53, 51, 49, 95, 50, 52])
# line 1: mean (1 decimal), median, sample std (2 decimals)
# line 2: outliers as a list
▸ Ожидаемый результат
54.7 50.5 14.28
[95]
Задание

В лаборатории сделали 8 измерений. В первой строке выведи выборочное среднее, во второй — нижнюю и верхнюю границы 95%-го доверительного интервала для среднего (через пробел); всё округли до 2 знаков после запятой. t* найди через stats.t.ppf.

Задание · Python
import numpy as np
from scipy import stats

sample = np.array([23.1, 24.5, 22.8, 25.0, 23.9, 24.2, 23.5, 24.8])
# mean, then the 95% confidence interval
▸ Ожидаемый результат
23.98
23.31 24.64

Главное

  • Выборочная дисперсия делится на n − 1 (ddof=1); для скошенных данных надёжнее медиана и IQR.
  • Правило Тьюки: значения вне [Q1 − 1,5 · IQR; Q3 + 1,5 · IQR] — выбросы.
  • z = (x − μ) / σ; norm.cdf, norm.sf, norm.ppf дают вероятности и квантили; правило 68–95–99,7.
  • Стандартная ошибка SE = s / √n; интервал для среднего — x̄ ± t* · SE.
  • При p < α H₀ отвергают, но p — не вероятность H₀ и не величина эффекта.
  • r Пирсона измеряет линейную связь, ρ Спирмена работает с рангами; корреляция не доказывает причинность.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Зарплаты: 800, 900, 950, 1000, 12 000 манатов. Какой показатель лучше описывает «типичную» зарплату?