Перцентиль и квантиль — что это простыми словами

Проверь себя · 1/3разбор после ответа
Вы хотите получить одну строку на пользователя с итоговой выручкой и заменили 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 рядом, а не одно среднее.

Подготовься к собесу по A/B и статистике
300+ вопросов с разбором: дизайн, размер выборки, p-value, ловушки
Тренировать A/B в Telegram

Как считать перцентили в 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 разобраны на конкретных вопросах.

Связанные темы

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, который работает медленнее, но даёт точный результат.