pivot_table в Pandas — сводные таблицы с примерами
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 встречается на порядок чаще.
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. Больше задач — в примерах вопросов.