Ящик с усами (boxplot) показывает, как распределены числа: где середина, насколько плотно лежит основная масса значений и какие точки выбиваются из общей картины. В pandas такой график строится одной строкой: df.boxplot() для таблицы или s.plot.box() для одного столбца. Ящик тянется от первого квартиля до третьего, линия внутри показывает медиану, усы доходят до крайних значений в пределах полутора межквартильных размахов, а всё, что дальше, рисуется отдельными точками-выбросами.
Ниже построим диаграмму на одном наборе данных, посчитаем каждый её элемент вручную, сравним несколько групп и найдём выбросы кодом. Если pandas вы ещё не ставили, начните с обзора библиотеки Pandas для новичков, а код удобнее запускать в Jupyter Notebook: график появится прямо под ячейкой.
SkillboxПрофессия «Data Scientist PRO»224 595 ₽12 месяцев
НетологияПрофессия «Аналитик данных с нуля до middle»145 600 ₽12 месяцев
ЭдюсонData Scientist109 900 ₽9 месяцев
SkillFactoryАналитик данных131 814 ₽6 месяцев
GeekBrainsДО Профессия Data Scientist с нуля до Junior115 771 ₽2 месяца
SF EducationАналитик данных + ИИ77 000 ₽4 месяца
Что показывает ящик с усами

У диаграммы пять элементов, и каждый отвечает на свой вопрос о данных. В документации pandas это сказано так: ящик идёт от Q1 до Q3, линия внутри стоит на медиане, а усы по умолчанию отходят от краёв ящика не дальше чем на 1,5 межквартильного размаха и заканчиваются на самой дальней точке данных внутри этого интервала.
| Элемент | Что это | Как читать |
|---|---|---|
| Нижний край ящика | первый квартиль Q1 | четверть значений меньше этой отметки |
| Линия внутри ящика | медиана | половина значений меньше, половина больше |
| Верхний край ящика | третий квартиль Q3 | три четверти значений меньше этой отметки |
| Высота ящика | межквартильный размах IQR = Q3 − Q1 | разброс средних 50 % данных |
| Усы | крайние реальные значения в пределах Q1 − 1,5 × IQR и Q3 + 1,5 × IQR | где заканчиваются «обычные» значения |
| Точки за усами | выбросы | значения, которые стоит проверить отдельно |
Обратите внимание на строку про усы. Ус заканчивается на настоящем значении из данных, а не на расчётной границе Q3 + 1,5 × IQR. Если перепутать границу с концом уса, цифры с графика потом не сойдутся с расчётом.
Медиану и квартили не путают со средним: среднее тянется за выбросами, а медиана почти не реагирует на одно большое значение. Разницу на примере таблицы мы показывали в инструкции по среднему значению в Excel.
Как построить ящик с усами в pandas
Возьмём время доставки 15 заказов в минутах. Один заказ ехал 75 минут, остальные укладывались в 18–41 минуту. Для одного столбца хватает метода plot.box(), а describe() сразу выдаст квартили, по которым строится ящик.
import pandas as pd
import matplotlib.pyplot as plt
minutes = [18, 22, 23, 25, 26, 27, 28, 30, 31, 33, 35, 36, 38, 41, 75]
s = pd.Series(minutes, name="Доставка, мин")
print(s.describe())
s.plot.box()
plt.title("Время доставки 15 заказов")
plt.show()
count 15.000000
mean 32.533333
std 13.351654
min 18.000000
25% 25.500000
50% 30.000000
75% 35.500000
max 75.000000
Name: Доставка, мин, dtype: float64
Строки 25%, 50% и 75% соответствуют Q1, медиане и Q3. Среднее mean равно 32,5 и стоит выше медианы 30: его подтянул вверх один долгий заказ.

Для таблицы с несколькими числовыми столбцами пишут df.boxplot() или df.plot.box(): pandas нарисует по ящику на каждый столбец. Выбрать нужные можно параметром column=["A", "B"].
Как читать ящик с усами: считаем вручную
Чтобы доверять графику, полезно один раз пересчитать его элементы кодом. Квартили берём методом quantile(), границы усов считаем по формуле Тьюки, а концы усов находим среди реальных значений.
import pandas as pd
minutes = [18, 22, 23, 25, 26, 27, 28, 30, 31, 33, 35, 36, 38, 41, 75]
s = pd.Series(minutes)
q1 = s.quantile(0.25)
q3 = s.quantile(0.75)
iqr = q3 - q1
low = q1 - 1.5 * iqr
high = q3 + 1.5 * iqr
print("Q1 =", q1, "Q3 =", q3, "IQR =", iqr)
print("границы:", low, "и", high)
print("нижний ус:", s[s >= low].min())
print("верхний ус:", s[s <= high].max())
print("выбросы:", s[(s < low) | (s > high)].tolist())
Q1 = 25.5 Q3 = 35.5 IQR = 10.0
границы: 10.5 и 50.5
нижний ус: 18
верхний ус: 41
выбросы: [75]
Расчётная верхняя граница равна 50,5, но верхний ус на графике стоит на 41: это самое большое значение, которое в границу укладывается. Нижняя граница 10,5 лежит ниже всех данных, поэтому нижний ус доходит до минимума, до 18. Заказ на 75 минут вышел за границу и стал отдельной точкой.
Откуда 25,5, если такого числа в списке нет? По умолчанию quantile() в pandas считает квантиль линейной интерполяцией (interpolation='linear'): первый квартиль попал между четвёртым и пятым значением, между 25 и 26.
Порядок чтения любого боксплота: сначала медиана (где середина), потом высота ящика (насколько разбросана основная масса), потом длина усов и их симметрия (есть ли перекос), в самом конце точки-выбросы.
Если медиана сдвинута к нижнему краю ящика, а верхний ус длиннее нижнего, у распределения длинный правый хвост: много обычных значений и редкие большие. Форму самого распределения подробнее показывает гистограмма, о ней у нас есть отдельная инструкция.
Несколько групп на одном графике
Ящик с усами полезнее всего, когда групп несколько: три службы доставки, четыре магазина, два варианта A/B-теста. Данные удобнее держать в «длинном» виде: один столбец с группой, другой со значением. Группу передают параметром by.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
rng = np.random.default_rng(42)
df = pd.DataFrame({
"служба": ["Север"] * 60 + ["Центр"] * 60 + ["Юг"] * 60,
"минуты": np.concatenate([
rng.normal(30, 5, 60),
rng.normal(38, 12, 60),
rng.normal(27, 4, 60),
]).round(),
})
df.boxplot(column="минуты", by="служба", grid=False)
plt.suptitle("")
plt.title("Время доставки по службам, мин")
plt.xlabel("")
plt.show()
print(df.groupby("служба")["минуты"].describe().round(1))
count mean std min 25% 50% 75% max
служба
Север 60.0 30.3 3.9 20.0 27.8 31.0 33.0 41.0
Центр 60.0 35.8 9.2 17.0 30.8 35.5 43.0 57.0
Юг 60.0 26.8 4.2 18.0 23.8 26.5 29.0 39.0
Строка plt.suptitle("") нужна, потому что с параметром by pandas добавляет над графиком служебный заголовок «Boxplot grouped by служба». Без этой строки он наложится на ваш. Сама картинка рисуется библиотекой matplotlib: pandas только передаёт ей данные, поэтому подписи осей и заголовки задают её командами. Если хочется разобраться в ней глубже, в каталоге есть курсы по Matplotlib.

Что видно на графике. У «Центра» самый высокий ящик и длинные усы: доставка там непредсказуема, разброс средних 50 % заказов больше 12 минут. У «Юга» самая низкая медиана, 26,5 минуты. У «Севера» и «Юга» по одному выбросу сверху, а у «Центра» выбросов нет, хотя его самый долгий заказ ехал 57 минут. Так работает правило 1,5 × IQR: при широком ящике границы уходят дальше, и 57 минут для «Центра» считаются обычным значением.
Разбивка по второму признаку в seaborn
Когда внутри групп есть ещё одно деление (день и вечер, новые и постоянные клиенты), проще взять библиотеку seaborn. Параметр hue рисует рядом ящики разного цвета и сам добавляет легенду.
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
rng = np.random.default_rng(42)
df = pd.DataFrame({
"служба": ["Север"] * 60 + ["Центр"] * 60 + ["Юг"] * 60,
"минуты": np.concatenate([
rng.normal(30, 5, 60),
rng.normal(38, 12, 60),
rng.normal(27, 4, 60),
]).round(),
})
df["время"] = np.tile(["день", "вечер"], 90)
sns.boxplot(data=df, x="служба", y="минуты", hue="время")
plt.title("Доставка днём и вечером")
plt.show()

Деление на подгруппы меняет и выбросы. У дневных заказов службы Север значение 20 минут стало выбросом, хотя на общем графике оно было концом нижнего уса. Подгруппа меньше и плотнее, поэтому её границы уже. Длину усов в seaborn задаёт тот же параметр whis=1.5, это видно в описании функции boxplot.
Как найти выбросы в pandas

График показывает выбросы точками, но список строк удобнее получить кодом: проверить заказы, почистить данные перед моделью, отправить таблицу коллеге. Ниже та же формула Тьюки, только для каждой группы отдельно.
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
df = pd.DataFrame({
"служба": ["Север"] * 60 + ["Центр"] * 60 + ["Юг"] * 60,
"минуты": np.concatenate([
rng.normal(30, 5, 60),
rng.normal(38, 12, 60),
rng.normal(27, 4, 60),
]).round(),
})
def outliers(group):
q1, q3 = group.quantile([0.25, 0.75])
iqr = q3 - q1
return group[(group < q1 - 1.5 * iqr) | (group > q3 + 1.5 * iqr)]
print(df.groupby("служба")["минуты"].apply(outliers))
служба
Север 30 41.0
Юг 139 39.0
Name: минуты, dtype: float64
Во втором столбце вывода стоит номер строки в исходной таблице: df.loc[30] и df.loc[139] вернут эти заказы целиком. Ровно эти две точки и нарисованы над усами «Севера» и «Юга». Если у вас нет локального Python, этот код можно запустить в Jupyter онлайн прямо в браузере.
Выброс сначала проверяют и только потом решают, удалять ли его. Опечатка в данных и настоящий долгий заказ выглядят на графике одинаково. Множитель 1,5 взят из классического определения боксплота, которое дал статистик Джон Тьюки, так сказано и в документации matplotlib. Чем занимается статистик сегодня, мы разбирали отдельно. Если выбросов много, а распределение явно несимметричное (зарплаты, чеки, время ответа), правило 1,5 × IQR будет отмечать слишком много нормальных значений, и границу лучше сдвинуть параметром whis, например до процентилей whis=(5, 95).
Настройка: горизонтальный ящик, среднее, длина усов
Метод df.boxplot() передаёт лишние параметры в функцию matplotlib.pyplot.boxplot, поэтому настройки у pandas и matplotlib общие. Самые нужные собраны в таблице. Если графики нужны без кода, посмотрите сервисы визуализации данных.
| Параметр | Что меняет | Пример |
|---|---|---|
whis |
длину усов: число задаёт множитель IQR, пара чисел задаёт процентили | whis=(5, 95) |
orientation |
ориентацию ящиков | orientation="horizontal" |
showmeans |
добавляет отметку среднего | showmeans=True |
showfliers |
скрывает или показывает выбросы | showfliers=False |
notch |
рисует выемку вокруг медианы: доверительный интервал медианы | notch=True |
patch_artist |
делает ящики заливаемыми фигурами, чтобы их можно было раскрасить | patch_artist=True |
tick_labels |
подписи под ящиками | tick_labels=["Доставка"] |
Пример с горизонтальным ящиком и отметкой среднего:
import matplotlib.pyplot as plt
data = [18, 22, 23, 25, 26, 27, 28, 30, 31, 33, 35, 36, 38, 41, 75]
plt.boxplot(data, orientation="horizontal", showmeans=True,
tick_labels=["Доставка"])
plt.title("Горизонтальный ящик со средним")
plt.show()

Горизонтальный вариант удобен, когда групп много или их названия длинные: подписи помещаются слева и не налезают друг на друга. Зелёный треугольник среднего стоит правее медианы, и разница между ними сразу показывает, насколько сильно выброс тянет среднее. О том, как ещё сравнивают два набора чисел, рассказывает статья про корреляцию.
Частые ошибки

Первые две ошибки появляются, когда запускаешь старый код из статей и ответов на форумах: matplotlib переименовал несколько параметров.
TypeError: unexpected keyword argument ‘labels’
Подписи раньше передавали через labels. В matplotlib 3.9 параметр переименовали в tick_labels, а в 3.11 старое имя удалили, об этом сказано в документации функции. Код с labels теперь падает:
plt.boxplot(data, labels=["Доставка"])
TypeError: boxplot() got an unexpected keyword argument 'labels'. Did you mean 'label'?
Решение: заменить labels= на tick_labels=. Подсказка «Did you mean ‘label’» ведёт не туда: label задаёт подпись в легенде, а не под ящиком.
Предупреждение про vert
Горизонтальный ящик раньше включали через vert=False, но в matplotlib 3.11 этот параметр объявили устаревшим. Такой код пока работает, но выводит предупреждение:
MatplotlibDeprecationWarning: vert: bool was deprecated in Matplotlib 3.11 and will be removed in 3.13. Use orientation: {'vertical', 'horizontal'} instead.
Решение: писать orientation="horizontal". Через df.boxplot(vert=False) параметр уходит в ту же функцию matplotlib, так что замена нужна и в коде с pandas. То же предупреждение печатает и пример с sns.boxplot выше, хотя vert в нём нет: seaborn 0.13.2 сам передаёт этот параметр в matplotlib 3.11. График при этом строится, в своём коде ничего менять не нужно.
Другие частые промахи
| Симптом | Причина | Что сделать |
|---|---|---|
| Над графиком лишний заголовок «Boxplot grouped by …» | так pandas подписывает график с параметром by |
plt.suptitle("") после построения |
| Конец уса не совпадает с Q3 + 1,5 × IQR | ус стоит на последнем реальном значении внутри границы | сравнивать с s[s <= high].max(), а не с границей |
| Квартили не сходятся с диаграммой Excel | разные методы расчёта квартилей | сверить метод в Excel и interpolation в pandas |
| Ящик из пяти-шести точек «врёт» | на маленькой выборке квартили считаются по двум-трём числам | при малом числе значений рисовать сами точки, например sns.stripplot |
| Пустой график в скрипте | не вызван plt.show() |
в Jupyter график выводится сам, в файле .py нужен plt.show() |
Ящик с усами в Excel
Если данных немного и Python под рукой нет, такой же график строят в Excel: это отдельный тип статистической диаграммы на вкладке «Вставка». Порядок действий для Windows и macOS расписан в справке Microsoft о диаграмме «ящик с усами».
Главное отличие от pandas в методе расчёта квартилей. В параметрах диаграммы Excel есть два метода: «Инклюзивная медиана» и «Исключающая медиана». При нечётном числе значений первый включает медиану в расчёт квартилей, второй исключает, поэтому Q1 и Q3 на маленькой выборке выходят разными. Если цифры в отчёте не совпали с расчётом в pandas, начинайте с этой настройки. Остальные типы графиков в таблицах разобраны в инструкции по диаграммам в Excel.
Где освоить pandas и визуализацию данных системно
Боксплот обычно идёт в паре с группировками, сводными таблицами и проверкой гипотез: так аналитик сравнивает группы в отчётах и A/B-тестах. Если хочется собрать это в навык, а не искать каждый приём отдельно, посмотрите, чем занимается аналитик данных, и сравните курсы по pandas в нашем каталоге по цене и длительности.
| Курс | Школа | Стоимость со скидкой | В рассрочку | Длительность | Обзор курса от Checkroi |
|---|---|---|---|---|---|
| Профессия «Data Scientist PRO» Перейти на сайт курса | 224 595 ₽ | 7245 ₽/мес. | 12 месяцев | Обзор курса | |
| Аналитик данных с нуля Перейти на сайт курса | 126 936 ₽ | 4994 ₽/мес. | 4 месяца | Обзор курса | |
| Профессия «Аналитик данных с нуля до middle» Перейти на сайт курса | 145 600 ₽ | 6066 ₽/мес. | 12 месяцев | Обзор курса | |
| Data Scientist Перейти на сайт курса | 109 900 ₽ | 4579 ₽/мес. | 9 месяцев | Обзор курса | |
| Аналитик данных Перейти на сайт курса | 131 814 ₽ | 3662 ₽/мес. | 6 месяцев | Обзор курса |
Больше программ — в полном каталоге курсов по Pandas




