pivot_table в Pandas — сводные таблицы с примерами

Проверь себя · 1/3разбор после ответа
Считается число «успешных» статусов. Код: cnt = 0; цикл for st in ['ok', 'cancelled', 'ok']:; если st == 'cancelled', выполняется continue, иначе cnt += 1. Чему равно cnt после цикла?

Что такое pivot_table

Представьте, что у вас выгрузка платежей: строка на каждую транзакцию, в столбцах — месяц, категория и сумма. Аналитика в таком «длинном» виде неудобно читать: чтобы понять, сколько принесла каждая категория по месяцам, придётся листать тысячи строк. Хочется компактную таблицу, где строки — это месяцы, столбцы — категории, а в ячейках — суммы. Ровно это делает сводная таблица в Excel, и ровно это делает pivot_table в pandas.

pd.pivot_table берёт «длинный» датафрейм и разворачивает его в «широкий»: вы говорите, что класть в строки, что в столбцы и какую величину агрегировать. На выходе — двумерная таблица, готовая для отчёта, дашборда или быстрой проверки гипотезы глазами. На собеседованиях аналитиков она встречается в задачах на трансформацию данных и почти всегда обсуждается в паре с groupby — интервьюеры любят спросить, чем они отличаются и когда что брать.

Ключевое слово здесь — «агрегирует». В отличие от простой перестановки, pivot_table умеет схлопывать несколько значений в одной ячейке через функцию агрегации. Поэтому она не падает на дубликатах, и поэтому же её чаще всего и берут в реальной работе.

Синтаксис и параметры

Базовый вызов держится на четырёх параметрах. values — столбец с числами, которые агрегируем. index — по какому полю раскладывать строки. columns — по какому полю раскладывать столбцы. aggfunc — как именно схлопывать значения.

import pandas as pd

df = pd.DataFrame({
    'month': ['янв', 'янв', 'фев', 'фев', 'янв', 'фев'],
    'category': ['еда', 'транспорт', 'еда', 'транспорт', 'еда', 'транспорт'],
    'amount': [500, 150, 600, 200, 450, 180]
})

pd.pivot_table(
    df,
    values='amount',       # что агрегируем
    index='month',         # строки
    columns='category',    # столбцы
    aggfunc='sum'          # функция агрегации
)
# category   еда  транспорт
# month
# фев        600        380
# янв        950        150

Обратите внимание: по строке «янв» у нас было два значения по категории «еда» (500 и 450), и pivot_table сложила их в 950. Это и есть та самая агрегация, которая отличает её от обычного разворота. Параметры index и columns можно менять местами или передавать списком — об этом ниже в разделе про multi-index.

aggfunc: одна и несколько функций

Главный подвох: по умолчанию aggfunc='mean', а не сумма. Очень многие пишут вызов без явного aggfunc, видят непонятные числа и долго ищут «баг», которого нет, — просто посчиталось среднее. Правило простое: всегда указывайте aggfunc явно, даже если вам нужно среднее.

# Сумма
pd.pivot_table(df, values='amount', index='month', columns='category', aggfunc='sum')

# Количество
pd.pivot_table(df, values='amount', index='month', columns='category', aggfunc='count')

# Несколько функций сразу — столбцы станут MultiIndex
pd.pivot_table(df, values='amount', index='month', columns='category',
               aggfunc=['sum', 'mean', 'count'])

Когда передаёте список функций, в столбцах появляется второй уровень: верхний — имя функции, нижний — категория. Это удобно для отчёта «сумма и среднее рядом», но потом такой MultiIndex часто приходится «сплющивать» — об этом в FAQ. Если разным столбцам нужны разные агрегаты, передавайте словарь:

pd.pivot_table(df, index='month',
               aggfunc={'amount': 'sum', 'category': 'count'})

aggfunc принимает не только строки-имена, но и любую вызываемую функцию: aggfunc='median', aggfunc=lambda x: x.quantile(0.9) для 90-го перцентиля, любую свою функцию агрегации. Это превращает сводную таблицу в гибкий инструмент: например, можно за один вызов получить и медиану чека, и его разброс.

margins и fill_value

Два параметра, которые экономят кучу ручной работы. margins=True добавляет строку и столбец с общими итогами — ровно как «Итого» в Excel. Имя по умолчанию — «All», но его можно переопределить через margins_name.

pd.pivot_table(df, values='amount', index='month', columns='category',
               aggfunc='sum', margins=True, margins_name='Итого')
# category   еда  транспорт  Итого
# month
# фев        600        380    980
# янв        950        150   1100
# Итого     1550        530   2080

Важная деталь: итог в углу (2080) считается по той же aggfunc, что и остальная таблица. С суммой это интуитивно, а вот с aggfunc='mean' итоговая ячейка будет средним по всем исходным строкам, а не средним из средних — об этом легко споткнуться, если не помнить.

Второй параметр — fill_value. Если для какой-то пары строка×столбец данных нет, в ячейке окажется NaN. Это ломает дальнейшие вычисления (любая арифметика с NaN даёт NaN) и портит вид отчёта. fill_value сразу заменяет пропуски на нужное значение:

pd.pivot_table(df, values='amount', index='month', columns='category',
               aggfunc='sum', fill_value=0)

Multi-index: несколько уровней

В index и columns можно передавать списки столбцов — получится многоуровневая сводная. Это удобно, когда нужно разложить данные по двум измерениям сразу, например по региону и каналу в строках и по устройству в столбцах.

orders = pd.DataFrame({
    'region': ['МСК', 'МСК', 'СПБ', 'СПБ', 'МСК', 'СПБ'],
    'channel': ['web', 'app', 'web', 'app', 'web', 'app'],
    'device': ['desktop', 'mobile', 'desktop', 'mobile', 'mobile', 'mobile'],
    'revenue': [1000, 800, 600, 500, 900, 700]
})

pd.pivot_table(orders, values='revenue', index=['region', 'channel'],
               columns='device', aggfunc='sum', fill_value=0)
# device              desktop  mobile
# region channel
# МСК    app                0     800
#        web             1000     900
# СПБ    app                0    1200
#        web              600       0

Результат с MultiIndex выглядит наглядно, но для дальнейшей работы (джойнов, экспорта в CSV, фильтрации) почти всегда придётся вызвать reset_index(), чтобы вернуть уровни индекса в обычные столбцы.

pivot vs pivot_table

Это первый вопрос, который любят на собеседовании. pivot — простая перестановка без агрегации. Он работает только если для каждой пары index×columns ровно одно значение. Как только встречается дубликат, pivot падает с ValueError, потому что не знает, какое из значений положить в ячейку.

# pivot — упадёт, если есть дубликаты
df.pivot(index='month', columns='category', values='amount')  # ValueError!

# pivot_table — агрегирует дубликаты
pd.pivot_table(df, index='month', columns='category', values='amount', aggfunc='sum')  # OK

В нашем df по паре (янв, еда) два значения — поэтому pivot сломается, а pivot_table спокойно сложит их в одно. Правило для практики: если данные гарантированно уникальны по ключу (например, уже агрегированы) — берите pivot, он быстрее и прозрачнее. Во всех остальных случаях — pivot_table. На реальных выгрузках уникальность по ключу скорее исключение, поэтому в работе pivot_table встречается на порядок чаще.

Прокачай SQL для собеса
500+ задач по SQL: оконные функции, JOIN, CTE — с разбором каждой
Тренировать SQL в Telegram

pivot_table vs groupby

По сути pivot_table — это groupby плюс unstack. Числа в результате одинаковые, разница в форме: groupby отдаёт «длинный» результат с MultiIndex по строкам, а pivot_table разворачивает один из ключей в столбцы и даёт «широкую» таблицу.

# groupby — длинный формат
df.groupby(['month', 'category'])['amount'].sum()
# month  category
# фев    еда            600
#        транспорт      380
# янв    еда            950
#        транспорт      150

# pivot_table — широкий формат
pd.pivot_table(df, values='amount', index='month', columns='category', aggfunc='sum')
# category   еда  транспорт
# фев        600        380
# янв        950        150

Когда что брать: groupby удобнее, когда результат идёт дальше в цепочку вычислений, джойнов или агрегаций — длинный формат проще обрабатывать программно. pivot_table берут, когда нужна читаемая двумерная таблица для человека: отчёт, выгрузка для менеджера, заготовка под heatmap. Хороший ответ на собесе звучит так: «результат тот же, отличается форма; pivot_table — это синтаксический сахар над groupby + unstack для случая, когда нужна сводка вид строки×столбцы».

pivot_table vs crosstab

crosstab — близкий родственник, но с двумя отличиями. Во-первых, он принимает не датафрейм и имена столбцов, а сами Series или массивы. Во-вторых, по умолчанию он считает частоты (count), а не среднее, — это его основной сценарий «таблица сопряжённости».

# crosstab по умолчанию считает количество строк
pd.crosstab(df['month'], df['category'])
# category   еда  транспорт
# month
# фев          1          2
# янв          2          1

Если передать values и aggfunc, crosstab начинает вести себя как pivot_table:

pd.crosstab(df['month'], df['category'],
            values=df['amount'], aggfunc='sum')

Главный козырь crosstab — параметр normalize: normalize='index' даёт доли по строкам, normalize='columns' — по столбцам, normalize=True — от общей суммы. Это удобно для процентных таблиц без ручного деления. Практическое правило: для подсчёта частот и долей берите crosstab, для агрегации числовых метрик из датафрейма — pivot_table. Подробнее — в гайде по crosstab в Pandas.

Практические примеры

Самый частый кейс — выручка по месяцам и категориям. Сырые транзакции группируем в месяц через dt.to_period('M'), затем разворачиваем в сводную с итогами:

sales = pd.DataFrame({
    'date': pd.date_range('2025-01-01', periods=120, freq='D'),
    'category': ['подписка', 'разовая', 'подписка', 'разовая'] * 30,
    'revenue': [500, 200, 550, 180] * 30
})
sales['month'] = sales['date'].dt.to_period('M')

report = pd.pivot_table(sales, values='revenue', index='month',
                         columns='category', aggfunc='sum', margins=True)

Второй типовой кейс — конверсия по каналу и устройству. Агрегируем сессии и покупки в одну сводную, а коэффициент конверсии считаем уже на готовой широкой таблице:

events = pd.DataFrame({
    'channel': ['organic', 'paid', 'organic', 'paid', 'organic', 'paid'],
    'device': ['desktop', 'desktop', 'mobile', 'mobile', 'desktop', 'mobile'],
    'sessions': [1000, 800, 1200, 600, 950, 700],
    'purchases': [50, 60, 30, 25, 45, 20]
})

conv = pd.pivot_table(events, values=['sessions', 'purchases'],
                       index='channel', columns='device', aggfunc='sum')
cr = conv['purchases'] / conv['sessions']   # CR по каждому устройству

Здесь values — список из двух метрик, поэтому столбцы получаются с MultiIndex: верхний уровень — метрика, нижний — устройство. Деление блока purchases на блок sessions идёт поэлементно и сразу даёт таблицу конверсии. Отрабатывать такие трансформации удобно на готовых задачах — например, в Python-тренажёре Карьерника собраны pandas-задачи из реальных собеседований аналитиков.

Вопросы с собеседований

Чем отличается pivot от pivot_table? pivot — простая перестановка без агрегации, требует уникальных комбинаций index×columns и падает с ValueError на дубликатах. pivot_table агрегирует дубликаты через aggfunc, поэтому работает всегда. По сути pivot_table — это groupby плюс unstack.

Какой aggfunc по умолчанию в pivot_table? mean. Это классический подвох: если ждёте сумму, а не указали aggfunc='sum', получите среднее и непонятные числа. Указывайте функцию явно.

Как добавить итоги в сводную таблицу? margins=True добавляет строку и столбец с общими итогами; имя по умолчанию «All», переопределяется через margins_name. Итоги считаются той же aggfunc, что и таблица.

Когда лучше pivot_table, а когда groupby? pivot_table — когда нужна читаемая двумерная таблица строки×столбцы для отчёта или визуализации. groupby — когда длинный формат идёт дальше в вычисления, джойны или цепочки. Результат один, разница в форме.

Как применить несколько агрегирующих функций? Передать список в aggfunc: aggfunc=['sum', 'mean', 'count'] — столбцы станут MultiIndex. Либо словарь для разных столбцов: aggfunc={'revenue': 'sum', 'orders': 'count'}.

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

Забыть, что aggfunc по умолчанию — mean. Самая частая ошибка: вызвать pivot_table без aggfunc и удивляться, что суммы не сходятся. На самом деле посчиталось среднее. Всегда указывайте aggfunc явно — это и страховка от ошибки, и подсказка читателю кода.

Путать pivot и pivot_table на дубликатах. pivot не агрегирует и бросает ValueError, как только в паре index×columns окажется больше одного значения. Если не уверены в уникальности ключа — берите pivot_table, она схлопнет дубликаты вместо падения.

Игнорировать NaN в результате. Комбинации без данных дают NaN, и любая последующая арифметика тоже превращается в NaN. Если дальше идут вычисления — ставьте fill_value=0 прямо в вызове или вызывайте fillna().

Забыть reset_index после сводной. Результат несёт значения в индексе строк и в MultiIndex столбцов. Для джойнов, экспорта в CSV или фильтрации по столбцам почти всегда нужен reset_index(), иначе нужные поля «спрятаны» в индексе.

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

FAQ

Как убрать MultiIndex из столбцов после pivot_table?

После сводной с несколькими функциями или метриками столбцы часто имеют два уровня. Чтобы «сплющить» их в одну строку: df.columns = ['_'.join(col).strip() for col in df.columns.values]. Если лишний уровень просто не нужен — уберите его через droplevel().

Можно ли развернуть pivot_table обратно в длинный формат?

Да, обратная операция — melt(). Например: pd.melt(pivot_df, ignore_index=False).reset_index(). Это вернёт «широкую» таблицу обратно в «длинную», где каждая строка — одна пара ключ-значение.

Как отсортировать результат pivot_table?

sort_values() по нужному столбцу или sort_index() по индексу. При MultiIndex в столбцах указывайте кортеж: result.sort_values(('revenue', 'sum'), ascending=False).

Чем pivot_table отличается от crosstab?

crosstab принимает Series или массивы напрямую и по умолчанию считает частоты (count), плюс умеет нормировать в доли через normalize. pivot_table работает с датафреймом и именами столбцов, по умолчанию считает среднее. Для частот и долей берите crosstab, для агрегации метрик — pivot_table.

Почему pivot падает с ошибкой, а pivot_table нет?

pivot не агрегирует: при дубликатах в паре index×columns он не знает, какое значение положить в ячейку, и бросает ValueError. pivot_table схлопывает дубликаты выбранной aggfunc, поэтому работает на любых данных. Если ловите ошибку на pivot — почти всегда нужен pivot_table. Больше задач — в примерах вопросов.