Перейти к содержимому
TelegramGitHub

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

Нормальное распределение — колокол. Значения сгруппированы вокруг среднего и симметрично спадают в обе стороны. Оно встречается, когда на результат влияет много мелких независимых причин: рост людей, время доставки, ошибки измерений.

У него два параметра: среднее μ и стандартное отклонение σ.

Для нормального распределения доли данных вокруг среднего известны заранее:

ДиапазонДоля данных
μ ± 1σоколо 68%
μ ± 2σоколо 95%
μ ± 3σоколо 99,7%

Колокол нормального распределения для времени доставки: 68% данных в пределах одного σ, 95% в пределах двух

Для времени доставки μ = 48,3 часа, σ = 8,1. Значит, 95% заказов должны приходить за 48,3 ± 16,2, то есть за 32–65 часов.

Подготовка: вы в папке stats-practice.

import csv
import statistics as st
with open("data/orders.csv") as f:
orders = list(csv.DictReader(f))
hours = [float(r["delivery_hours"]) for r in orders]
m = st.mean(hours)
s = st.stdev(hours)
for k in (1, 2, 3):
share = sum(abs(h - m) <= k * s for h in hours) / len(hours)
print(k, share)

Ответ:

1 0.675
2 0.961
3 0.999

Реальные доли: 67,5%, 96,1% и 99,9%. Правило сработало: время доставки близко к нормальному.

z-оценка (z-score) говорит, на сколько стандартных отклонений значение отстоит от среднего:

z = (x − среднее) / σ
  • z = 0 — ровно среднее.
  • z = 2 — на два σ выше среднего. Такое значение редкое: выше него лишь около 2,5% данных.
  • z = −3 — очень низкое значение.

Правило: |z| > 3 — повод проверить значение. Это выброс для нормальных данных.

z-оценка нужна, чтобы сравнивать разные шкалы. Время доставки 60 часов и сумма 150 — несравнимы. Но их z-оценки сравнить можно.

nd = st.NormalDist(m, s) # нормальное распределение с нашими μ и σ
print(round((60 - m) / s, 2)) # z-оценка для 60 часов
print(round(1 - nd.cdf(60), 4)) # доля заказов дольше 60 часов по теории
print(sum(h > 60 for h in hours) / len(hours)) # доля по факту

Ответ:

1.44
0.0749
0.079

cdf(x) — доля значений не больше x. Поэтому «дольше 60» — это 1 − cdf(60).

Теория говорит: дольше 60 часов будут 7,5% заказов. По факту — 7,9%. Близко. Модель нормального распределения подходит.

Найдите z-оценку самого дорогого заказа (4493,42) по сумме. Что она говорит?

Проверка: z ≈ 15,95. Заказ в 16 стандартных отклонений от среднего. При нормальном распределении это невозможно.

Решение
amount = [float(r["amount"]) for r in orders]
ma, sa = st.mean(amount), st.stdev(amount)
print(round((max(amount) - ma) / sa, 2))

SQL:

SELECT round((max(amount) - avg(amount)) / stddev_samp(amount), 2) FROM orders;

Сколько заказов имеют |z| > 3 по времени доставки? Какой это заказ?

Проверка: один заказ. Время 21,8 часа, z = −3,28.

Решение
for r in orders:
z = (float(r["delivery_hours"]) - m) / s
if abs(z) > 3:
print(r["order_id"], r["delivery_hours"], round(z, 2))

SQL:

WITH stats AS (
SELECT avg(delivery_hours) AS m, stddev_samp(delivery_hours) AS s FROM orders
)
SELECT order_id, delivery_hours, round((delivery_hours - m) / s, 2) AS z
FROM orders, stats
WHERE abs((delivery_hours - m) / s) > 3;

По теории |z| > 3 бывает у 0,3% значений, то есть у 3 из 1000. У нас один. Это нормально.

Найдите по модели нормального распределения время, за которое приходят 95% заказов (то есть 95-й перцентиль). Сравните с реальным.

Проверка: по модели 61,66 часа. По факту (P95) — 61,8 часа. Почти одно и то же.

Решение
print(round(nd.inv_cdf(0.95), 2))

inv_cdf(p) работает наоборот: даёт значение, ниже которого лежит доля p.

Реальный P95 мы считали в уроке про разброс: quantile_cont(delivery_hours, 0.95).

Дальше: Выборка и доверительный интервал.