Квартили, межквартильный размах и перцентили

Если разбить упорядоченные данные на равные доли, мы получим устойчивую к выбросам карту распределения.

Перцентиль p — значение, ниже которого лежит p% данных. 90-й перцентиль зарплат — порог, который не превышают 90% сотрудников.

Квартили: три точки, четыре части

Квартили делят упорядоченные данные на четыре равные части:

  • Q1 (первый квартиль, 25-й перцентиль) — ниже него 25% данных;
  • Q2 — это медиана, 50-й перцентиль;
  • Q3 (третий квартиль, 75-й перцентиль) — ниже него 75% данных.

В модуле statistics есть функция quantiles: по умолчанию она режет данные на 4 части и возвращает три точки разреза — это и есть Q1, Q2, Q3.

from statistics import quantiles, median

data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
q1, q2, q3 = quantiles(data, n=4)
print("Q1:", q1)
print("Q2 (медиана):", q2, "/ median:", median(data))
print("Q3:", q3)

Вывод:

Q1: 3.25
Q2 (медиана): 6.5 / median: 6.5
Q3: 9.75

Примечание: способов считать квартили несколько, и разные инструменты могут давать чуть разные числа на концах. Модуль statistics по умолчанию использует метод «exclusive»; для аналитики это вполне годится, главное — пользоваться одним методом последовательно.

Межквартильный размах (IQR)

IQR (Interquartile Range) — это разница Q3 − Q1, то есть размах «средних 50%» данных. Он показывает, насколько разбросана центральная масса, и при этом игнорирует верхние и нижние 25% — а значит, устойчив к выбросам, в отличие от обычного размаха.

from statistics import quantiles

# Те же данные, но добавили дикий выброс
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1000]
q1, q2, q3 = quantiles(data, n=4)
iqr = q3 - q1
print("Q1:", q1, "Q3:", q3)
print("IQR:", iqr)
print("Обычный размах:", max(data) - min(data))

Вывод:

Q1: 3.25 Q3: 9.75
IQR: 6.5
Обычный размах: 999

Видите разницу? Выброс 1000 раздул обычный размах до 999, а IQR остался 6.5 — он почти не заметил аномалию. Поэтому IQR — надёжная мера разброса для «грязных» данных.

Произвольные перцентили

Перцентили нужны не только на четвертях. В вебе и нагрузочном тестировании постоянно смотрят на 90-й, 95-й и 99-й перцентили времени ответа: «95% запросов обрабатываются быстрее, чем за X». Чтобы получить, например, 90-й перцентиль, режем данные на 100 частей и берём 90-ю границу.

from statistics import quantiles

# Время ответа сервиса в миллисекундах
times = [50, 52, 55, 60, 61, 63, 70, 75, 80, 500]
cuts = quantiles(times, n=100)   # 99 точек: 1-й..99-й перцентили
p90 = cuts[89]   # индекс 89 -> 90-й перцентиль
print("90-й перцентиль времени ответа:", p90, "мс")

Вывод:

90-й перцентиль времени ответа: 458.0 мс

Здесь 90-й перцентиль высокий из-за одного медленного запроса в 500 мс — это сигнал, что у части пользователей всё плохо, даже если медиана выглядит хорошо. Среднее и медиана такой «хвост» легко прячут, а перцентили его обнажают.

Пятичисловая сводка

Минимум, Q1, медиана, Q3, максимум вместе называют пятичисловой сводкой (five-number summary) — компактный портрет распределения, на котором строится «ящик с усами» (boxplot).

ЧислоСмысл
минимумнаименьшее значение
Q1граница нижних 25%
медианасередина
Q3граница верхних 25%
максимумнаибольшее значение

Итог

  • Квартили Q1, Q2, Q3 делят данные на четыре равные части; Q2 — это медиана.
  • IQR = Q3 − Q1 — разброс центральных 50%, устойчивый к выбросам.
  • Перцентили (90-й, 95-й, 99-й) показывают «хвосты», которые прячут среднее и медиана.
  • statistics.quantiles режет данные на нужное число частей.
Проверьте себя
1. Что показывает межквартильный размах (IQR)?
AРазницу между максимумом и минимумом
BРазброс центральных 50% данных (Q3 − Q1)
CСреднее всех значений
DСамое частое значение
2. Почему IQR устойчивее к выбросам, чем обычный размах?
AПотому что он всегда меньше
BПотому что он считается только по краям
CПотому что он игнорирует верхние и нижние 25% данных, где обычно сидят выбросы
DПотому что он не зависит от данных
3. Что означает «95-й перцентиль времени ответа равен 480 мс»?
AСреднее время ответа — 480 мс
B95% запросов обработаны быстрее или за 480 мс
CСамый медленный запрос — 480 мс
D95 запросов заняли по 480 мс
Поддержать проект