Перцентиль и квантиль — что это простыми словами
GROUP BY на оконную SUM(amount) OVER (PARTITION BY user_id). Почему результат содержит столько же строк, сколько и исходный набор?Содержание:
Что такое квантиль и перцентиль
Среднее почти всегда врёт. Стоит одному миллионеру зайти в кафе, и «средний чек гостя» взлетает до небес, хотя реально столько никто не платит. Чтобы описывать данные честно, аналитику нужны не одно число, а пороги, которые показывают, как значения распределены от меньших к большим. Эти пороги называются квантилями и перцентилями, и без них не обходится ни один разбор распределений, SLA-метрик и выбросов.
Квантиль — это значение, которое делит упорядоченные данные в заданной пропорции. Если отсортировать наблюдения по возрастанию и взять точку, левее которой лежит, скажем, 0.9 всех данных, это и есть квантиль уровня 0.9. Перцентиль — ровно то же самое, только доля выражается в процентах, а не в долях: квантиль 0.9 и 90-й перцентиль (P90) — это одно и то же число. Грубо говоря, перцентиль — это квантиль в процентах.
Из этой пары растёт всё семейство связанных понятий. Квартили делят данные на четыре части (по 25%), децили — на десять, перцентили — на сто. Поэтому квартиль, дециль и перцентиль — это частные случаи квантиля, отличающиеся лишь тем, на сколько кусков мы режем распределение. Дальше разберём интуицию, посчитаем всё в Python и SQL и закроем типичные ошибки, из-за которых перцентиль путают с процентом.
Интуиция: что значит P90
Самая надёжная картинка в голове такая: P90 — это значение, ниже которого лежит 90% данных. Если P90 времени ответа сервиса равен 800 мс, значит 90% запросов обрабатываются быстрее 800 мс, а оставшиеся 10% — медленнее. P90 не говорит «среднее время 800 мс» и не говорит «худший случай 800 мс»; он говорит ровно про границу, отсекающую верхние 10% хвоста.
Точно так же читаются любые перцентили. P50 — порог, ниже которого половина данных, то есть медиана. P25 — порог для нижней четверти, P75 — для нижних трёх четвертей. P99 отсекает верхний 1%: ниже него 99% наблюдений, а выше — самые экстремальные. Чем ближе перцентиль к 100, тем глубже в хвост распределения мы заглядываем, и тем интереснее он для задач про надёжность и выбросы.
Важно держать в голове, что перцентиль — это значение из той же шкалы, что и данные (рубли, миллисекунды, баллы), а не «сколько процентов». Когда говорят «зарплата на уровне 90-го перцентиля», имеют в виду конкретную сумму, выше которой получают только 10% людей, а не то, что человек зарабатывает 90% от чего-то. Эта подмена — источник большинства ошибок, к ней вернёмся отдельно.
Медиана, квартили и IQR
Медиана — это 50-й перцентиль, значение, делящее упорядоченный ряд ровно пополам. Первый квартиль Q1 — это P25 (25% данных ниже), третий квартиль Q3 — это P75 (75% данных ниже). Вместе Q1, медиана и Q3 разбивают распределение на четыре равные по количеству наблюдений части и дают компактный портрет данных: где сосредоточена основная масса и насколько широко она размазана.
Q1 Медиана Q3
│ │ │
────┼────────┼────────┼────
25% 50% 75%
│←── IQR ──→│Расстояние между третьим и первым квартилями называют межквартильным размахом — IQR (InterQuartile Range), и считают как Q3 − Q1. Это мера разброса «середины» данных, которая по построению игнорирует крайние 25% сверху и снизу, а потому устойчива к выбросам куда лучше, чем размах от минимума до максимума. Именно на IQR опирается правило Тьюки: значения ниже Q1 − 1.5×IQR или выше Q3 + 1.5×IQR помечают как кандидаты в выбросы. Этот же интервал рисует «усы» на box plot — ящике с усами, где коробка тянется от Q1 до Q3, линия внутри неё показывает медиану, а точки за усами — подозрительные наблюдения.
Квартили удобны и для сегментации: разбив пользователей на четыре группы по выручке, легко отделить верхнюю четверть от нижней и работать с каждой по-своему. Когда четырёх корзин мало, берут децили (десять групп) или произвольные перцентили — механика та же, меняется только число частей.
Перцентиль против среднего
Главная причина любить перцентили — их устойчивость к выбросам. Среднее суммирует все значения и делит на количество, поэтому один экстремум тянет его на себя: достаточно одного клиента с гигантским чеком, чтобы среднее перестало описывать типичного клиента. Медиана и другие перцентили основаны на порядке, а не на сумме, поэтому единичные аномалии почти не сдвигают их.
| Метрика | Среднее | Медиана (P50) | P90 |
|---|---|---|---|
| Устойчивость к выбросам | Нет | Да | Да |
| Что описывает | «Среднее по больнице» | Типичное значение | Верхний хвост |
| Когда использовать | Симметричное распределение | Скошенное распределение | SLA, worst-case |
Разница особенно заметна на скошенных данных, а в продуктовой аналитике скошено почти всё: выручка, длительность сессий, время до покупки. Если 80% пользователей платят 0–500 ₽, а 5% — больше 50 000 ₽, среднее может показать 7000 ₽, тогда как медиана останется около 200 ₽. Менеджеру, который хочет понять «сколько платит обычный пользователь», нужна именно медиана, а среднее уведёт его в иллюзию богатой аудитории. Подробнее этот контраст разобран в сравнении медианы и среднего.
Это не значит, что среднее не нужно. Для симметричных распределений среднее и медиана почти совпадают, а среднее ещё и удобно складывать и масштабировать (суммарная выручка = среднее × число клиентов). Правило простое: симметрия — можно среднее, скошенность или важные хвосты — берите перцентили.
P95 и P99 в мониторинге latency
Нагляднее всего польза хвостовых перцентилей видна в мониторинге времени ответа. Среднее время ответа маскирует проблему: пользователь, у которого страница грузилась 5 секунд, ничего не знает про «среднее по сервису 200 мс» — он видит свои 5 секунд. Поэтому SLA формулируют в перцентилях: «P99 < 500 мс» означает, что 99% запросов укладываются в полсекунды, и лишь 1% медленнее. Это требование сразу про хвост, а не про середину.
import numpy as np
response_times = [50, 80, 100, 120, 150, 200, 250, 500, 800, 2000]
print(round(np.mean(response_times), 1)) # 425.0 — среднее
print(round(np.percentile(response_times, 50), 1)) # 175.0 — P50 (медиана)
print(round(np.percentile(response_times, 95), 1)) # 1460.0 — P95
print(round(np.percentile(response_times, 99), 1)) # 1892.0 — P99Смотрите на разрыв: среднее — 425 мс, медиана — 175 мс, а P99 — почти 1900 мс. Половина запросов летает быстрее 175 мс, но самый медленный процент тянет почти две секунды, и именно его чувствуют реальные пользователи. Среднее здесь обманчиво вдвойне: оно и выше медианы (из-за хвоста), и сильно ниже того, что переживает несчастливый процент аудитории. P95 показывает «плохой, но не катастрофический» опыт, P99 — границу, за которой начинаются настоящие инциденты. Поэтому в дашбордах latency почти всегда выводят P50/P95/P99 рядом, а не одно среднее.
Как считать перцентили в Python
В Python перцентили считают через NumPy и Pandas. numpy.percentile принимает значение от 0 до 100, а numpy.quantile — долю от 0 до 1; это одна и та же функция в двух шкалах.
import numpy as np
data = [15, 20, 35, 40, 50, 60, 70, 80, 85, 95]
np.percentile(data, 50) # 55.0 — медиана
np.percentile(data, 25) # 36.25 — Q1
np.percentile(data, 75) # 77.5 — Q3
np.percentile(data, 90) # 86.0 — P90
# Несколько перцентилей сразу
np.percentile(data, [25, 50, 75, 90, 99]) # [36.25, 55.0, 77.5, 86.0, 94.1]
# Квантиль в долях (0..1) — тот же результат
np.quantile(data, 0.5) # 55.0
np.quantile(data, 0.25) # 36.25В Pandas перцентили доступны прямо на колонке через quantile, а describe сразу печатает квартили. Это самый быстрый способ оценить распределение новой метрики.
import pandas as pd
df = pd.DataFrame({
'user_id': range(1, 11),
'revenue': [100, 200, 500, 800, 1200, 2000, 3500, 5000, 8000, 50000],
})
df['revenue'].quantile(0.5) # 1600.0 — медиана
df['revenue'].quantile([0.25, 0.5, 0.75, 0.9])
df['revenue'].describe()
# count 10.0
# mean 7130.0
# std 15272.6
# min 100.0
# 25% 575.0
# 50% 1600.0
# 75% 4625.0
# max 50000.0Перцентили легко считаются и по группам — например, P90 выручки в каждом сегменте:
df.groupby('segment')['revenue'].quantile(0.9)
df.groupby('segment')['revenue'].describe(percentiles=[.25, .5, .75, .9, .95])А вот определение выбросов через IQR в одну формулу — тот самый метод Тьюки из раздела про квартили:
q1 = df['revenue'].quantile(0.25)
q3 = df['revenue'].quantile(0.75)
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
outliers = df[(df['revenue'] < lower) | (df['revenue'] > upper)]Как считать перцентили в SQL
В PostgreSQL перцентили считают упорядоченно-множественным агрегатом PERCENTILE_CONT (или PERCENTILE_DISC) с обязательной конструкцией WITHIN GROUP (ORDER BY ...). Аргумент — доля от 0 до 1.
-- Медиана
SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY revenue) AS median
FROM orders;
-- Несколько перцентилей в одном проходе
SELECT
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY revenue) AS q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY revenue) AS median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY revenue) AS q3,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY revenue) AS p95,
PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY revenue) AS p99
FROM orders;
-- Перцентили по группам
SELECT
segment,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY revenue) AS median_revenue
FROM users
GROUP BY segment;Две версии функции различаются тем, как они ведут себя между соседними значениями. PERCENTILE_CONT (continuous) интерполирует и может вернуть число, которого нет в данных, — это правильный выбор для непрерывных величин вроде выручки и времени. PERCENTILE_DISC (discrete) возвращает ближайшее реальное значение из выборки и подходит для дискретных шкал (оценки, количество товаров).
-- CONT интерполирует: между 10 и 20 вернёт 15.0
SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY val)
FROM (VALUES (10), (20)) t(val); -- 15.0
-- DISC берёт ближайшее существующее значение
SELECT PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY val)
FROM (VALUES (10), (20)) t(val); -- 10Когда перцентили нужны не агрегатом, а в привязке к каждой строке, помогают оконные функции. NTILE(n) раскладывает строки по n равным корзинам, а PERCENT_RANK() выдаёт перцентильный ранг каждой строки от 0 до 1.
SELECT
user_id,
revenue,
NTILE(10) OVER (ORDER BY revenue DESC) AS decile,
ROUND(PERCENT_RANK() OVER (ORDER BY revenue) * 100, 1) AS percentile
FROM users;Частая задача — навесить сегмент на каждого пользователя по порогам перцентилей. Важно: PERCENTILE_CONT — это упорядоченно-множественный агрегат, его нельзя использовать как оконную функцию с OVER (). Поэтому пороги считают один раз в CTE, а потом сравнивают через CROSS JOIN:
WITH bounds AS (
SELECT
PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY total_revenue) AS p90,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total_revenue) AS p50
FROM user_revenue
)
SELECT
u.user_id,
u.total_revenue,
CASE
WHEN u.total_revenue >= b.p90 THEN 'top_10%'
WHEN u.total_revenue >= b.p50 THEN 'top_50%'
ELSE 'bottom_50%'
END AS segment
FROM user_revenue u
CROSS JOIN bounds b;Частые ошибки
Путают перцентиль и процент. Это самая частая подмена. Перцентиль — значение из шкалы данных (рубли, миллисекунды), а процент — доля. «P90 выручки = 12 000 ₽» означает, что 90% пользователей платят меньше 12 000 ₽, а не что кто-то заплатил «90 процентов». Фраза «90-й перцентиль» отвечает на вопрос «какое значение», а не «сколько процентов».
Путают «P90» и «топ-10%». P90 — это граница, а не группа. Сам P90 — одно число, ниже которого лежит 90% данных; «топ-10%» — это множество строк выше этой границы. Если в задаче просят «выделить топ-10% клиентов», сначала считают порог P90, а затем фильтруют строки, которые его превышают.
Берут среднее там, где данные скошены. На распределении с длинным хвостом среднее завышено и не описывает типичный объект. Для выручки, времени ответа, длительности сессий по умолчанию смотрят медиану и хвостовые перцентили, а среднее держат рядом только как дополнительный, осторожно читаемый показатель.
Игнорируют метод расчёта. Существует несколько способов интерполяции перцентилей (линейный, «исключающий» Тьюки и другие), и на маленьких выборках они дают разные числа. NumPy и Pandas по умолчанию используют линейный метод, поэтому квартили из describe() могут не совпасть с тем, что вы посчитаете «по учебнику» вручную. На больших данных разница исчезает, но в задачах на собеседовании про неё стоит помнить. Набить руку на таких задачах удобно в тренажёре по статистике — там перцентили, квартили и IQR разобраны на конкретных вопросах.
Связанные темы
- Медиана vs среднее: в чём разница
- Дисперсия и стандартное отклонение
- Выбросы в данных и как их искать
- Как посчитать latency-перцентили в SQL
- Задачи на медиану и перцентили с собеседований
FAQ
Перцентиль и процент — это одно и то же?
Нет. Перцентиль — это значение из шкалы данных, ниже которого лежит заданная доля наблюдений (P90 выручки = конкретная сумма в рублях). Процент — это доля сама по себе. «90-й перцентиль» и «90-й процентиль» — действительно одно и то же слово (percentile), а вот «перцентиль» и «процент» путать нельзя.
Чем квартиль отличается от перцентиля?
Это всё квантили, разница — в количестве частей. Квартили делят данные на 4 части (Q1, Q2, Q3), децили — на 10, перцентили — на 100. Поэтому Q1 = P25, Q2 = P50 = медиана, Q3 = P75. Квартиль — просто «крупная» нарезка, перцентиль — самая подробная.
Как посчитать медиану в SQL?
В PostgreSQL — PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY col). В MySQL до версии 8 встроенной функции нет, медиану считают через оконные функции или подзапрос с упорядочиванием. Медиана — это просто 50-й перцентиль, поэтому отдельной функции под неё обычно и не заводят.
Когда использовать PERCENTILE_CONT, а когда PERCENTILE_DISC?
PERCENTILE_CONT интерполирует между соседними значениями и может вернуть число, которого нет в данных, — это подходит для непрерывных величин (выручка, время ответа). PERCENTILE_DISC возвращает ближайшее реальное значение из выборки и уместен для дискретных шкал (баллы, количество). Для медианы выручки берите CONT, для «типичной оценки из списка» — DISC.
Почему для latency смотрят P99, а не среднее?
Потому что пользователь чувствует свой запрос, а не среднее по сервису. Среднее размывает редкие, но болезненные задержки: при среднем 425 мс P99 может быть почти 1900 мс, и именно эти секунды видит самый невезучий процент аудитории. P95 и P99 прямо отвечают на вопрос «насколько плохо в худших случаях», а среднее этот хвост прячет.
Как считать перцентили в ClickHouse?
В ClickHouse используют функции семейства quantile: quantile(0.5)(revenue) вернёт медиану, quantiles(0.25, 0.5, 0.75)(revenue) — сразу несколько квантилей. По умолчанию quantile считает приближённо для скорости; когда нужна точность, берут quantileExact, который работает медленнее, но даёт точный результат.