Ящик с усами в pandas: как построить boxplot и найти выбросы

Ящик с усами в pandas строится одной строкой: s.plot.box() или df.boxplot().

Показываем код с реальным выводом, считаем квартили и концы усов вручную, сравниваем три службы доставки, строим разбивку в seaborn и находим выбросы по правилу 1,5 × IQR.

В конце разобраны ошибки старого кода с параметрами labels и vert.

Статью написал:
Ваня Буявец, продюсер, основатель Checkroi
Ваня Буявец
Основатель Checkroi, продюсер, эксперт в выборе онлайн-курсов
Все 3233 статьи автора Подписаться на Телеграм-канал
Одобрено экспертом:
Наташа Буявец, основатель Checkroi, эксперт по онлайн-курсам
Наташа Буявец
Основательница Checkroi, продюсер Youtube-каналов, эксперт по онлайн-курсам
Все 3887 экспертных мнений Подписаться на Телеграм-канал
Обложка: Ящик с усами в pandas: как построить boxplot и найти выбросы

Ящик с усами (boxplot) показывает, как распределены числа: где середина, насколько плотно лежит основная масса значений и какие точки выбиваются из общей картины. В pandas такой график строится одной строкой: df.boxplot() для таблицы или s.plot.box() для одного столбца. Ящик тянется от первого квартиля до третьего, линия внутри показывает медиану, усы доходят до крайних значений в пределах полутора межквартильных размахов, а всё, что дальше, рисуется отдельными точками-выбросами.

Ниже построим диаграмму на одном наборе данных, посчитаем каждый её элемент вручную, сравним несколько групп и найдём выбросы кодом. Если pandas вы ещё не ставили, начните с обзора библиотеки Pandas для новичков, а код удобнее запускать в Jupyter Notebook: график появится прямо под ячейкой.

Что показывает ящик с усами

Рой показывает на доске диаграмму ящик с усами

У диаграммы пять элементов, и каждый отвечает на свой вопрос о данных. В документации 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: его подтянул вверх один долгий заказ.

Ящик с усами в pandas: время доставки 15 заказов, медиана 30 минут и выброс 75 минут
Верхний ус кончается на 41 минуте, а заказ на 75 минут стоит отдельной точкой. График построен кодом выше в matplotlib

Для таблицы с несколькими числовыми столбцами пишут df.boxplot() или df.plot.box(): pandas нарисует по ящику на каждый столбец. Выбрать нужные можно параметром column=["A", "B"].

Канал основателя Checkroi Вани Буявца3 800 человек читают мой Телеграм-канал про нейросетиСобрал промпты для Claude Code и ChatGPT, разборы ИИ-инструментов и лайфхаки по продвижению бизнеса в одном местеПрисоединиться

Как читать ящик с усами: считаем вручную

Чтобы доверять графику, полезно один раз пересчитать его элементы кодом. Квартили берём методом 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.

Диаграмма ящик с усами в pandas по трём службам доставки: Север, Центр и Юг
У «Центра» самый высокий ящик и выбросов нет, у «Севера» и «Юга» по одной точке над усом

Что видно на графике. У «Центра» самый высокий ящик и длинные усы: доставка там непредсказуема, разброс средних 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()
Боксплот seaborn с разбивкой hue: доставка днём и вечером по трём службам
В подгруппах границы уже, поэтому у дневных заказов «Севера» появился выброс 20 минут

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

CheckroiCheckroiПодборка курсов по Pandas104 курса • 24 школыСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Как найти выбросы в 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 онлайн прямо в браузере.

CheckroiCheckroiПодборка курсов по Python570 курсов • 53 школыСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Выброс сначала проверяют и только потом решают, удалять ли его. Опечатка в данных и настоящий долгий заказ выглядят на графике одинаково. Множитель 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 с отметкой среднего значения
Треугольник среднего стоит правее линии медианы: один долгий заказ тянет среднее вверх

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

Ваня БуявецКанал основателя Checkroi Вани БуявцаЗабирайте промпты и обучение по нейросетям в моём Телеграм-каналеБольше 3 800 человек уже применяют Claude Code, ChatGPT и другие нейросети в работе, учёбе, бизнесе и жизниПерейти в канал

Частые ошибки

Рабочий стол аналитика с распечатками диаграмм размаха

Первые две ошибки появляются, когда запускаешь старый код из статей и ответов на форумах: 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»
Перейти на сайт курса
SkillboxSkillbox224 595 ₽7245 ₽/мес.12 месяцевОбзор курса
Аналитик данных с нуля
Перейти на сайт курса
SkillboxSkillbox126 936 ₽4994 ₽/мес.4 месяцаОбзор курса
Профессия «Аналитик данных с нуля до middle»
Перейти на сайт курса
НетологияНетология145 600 ₽6066 ₽/мес.12 месяцевОбзор курса
Data Scientist
Перейти на сайт курса
Академия ЭдюсонЭдюсон109 900 ₽4579 ₽/мес.9 месяцевОбзор курса
Аналитик данных
Перейти на сайт курса
SkillFactorySkillFactory131 814 ₽3662 ₽/мес.6 месяцевОбзор курса

Больше программ — в полном каталоге курсов по Pandas

Часто задаваемые вопросы

Что показывает ящик с усами?

Медиану, первый и третий квартили, разброс средних 50 % значений (межквартильный размах), крайние обычные значения на концах усов и выбросы отдельными точками. По одному графику видно, где середина данных и насколько они разбросаны.

Как построить ящик с усами в pandas?

Для одного столбца вызовите s.plot.box(), для таблицы df.boxplot(). Чтобы сравнить группы, передайте столбец группы параметром by: df.boxplot(column="минуты", by="служба"). Рисует график библиотека matplotlib, поэтому в скрипте после построения нужен plt.show().

Почему ус не доходит до Q3 + 1,5 × IQR?

Граница Q3 + 1,5 × IQR только отсекает выбросы, а сам ус стоит на последнем реальном значении внутри неё. В примере из статьи граница равна 50,5 минуты, а верхний ус заканчивается на 41.

Как найти выбросы в pandas?

Посчитайте квартили через quantile(0.25) и quantile(0.75), разницу между ними (IQR) и отфильтруйте значения меньше Q1 − 1,5 × IQR или больше Q3 + 1,5 × IQR. Для нескольких групп ту же функцию применяют через groupby и apply.

Чем ящик с усами отличается от гистограммы?

Гистограмма показывает форму распределения: сколько значений попало в каждый интервал. Ящик с усами сжимает данные до медианы, квартилей и усов, зато несколько групп удобно сравнить на одном графике. Как строить гистограмму, разобрано в инструкции по гистограмме в Excel.

Почему plt.boxplot выдаёт ошибку про labels?

В matplotlib 3.9 параметр labels переименовали в tick_labels, а в версии 3.11 старое имя удалили. Замените labels= на tick_labels=, и подписи под ящиками вернутся.

Читайте Checkroi первым в Google
Добавьте Checkroi в избранные источники — и наши разборы курсов и обзоры школ будут показываться выше в вашей выдаче Google.
Оставить комментарий
0 комментариев
Форма комментария

Оставьте комментарий

Напишите, что думаете. Нам важно ваше мнение!