Как создать pivot table в pandas

Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.

Зачем это нужно

Pivot table — наследие Excel, которое в pandas стало ещё мощнее. Строки → колонки: из длинного long-формата (transaction per row) в wide (month × category). Это нужно для читаемых отчётов, heatmap retention-таблиц и feature engineering в ML.

В аналитике pivot используется постоянно: выручка по месяцам × категориям, conversion по устройствам × каналам, retention по cohort × period. Без pivot эти данные остаются таблицей длиннющих записей, которую сложно читать и визуализировать.

В статье — три функции pandas и когда что выбрать:

  • pivot_table — с агрегацией (самое частое)
  • pivot — без агрегации (если уже уникальные)
  • crosstab — упрощение для frequency tables
  • Несколько агрегатов и несколько values
  • Итоги (margins) и fill_value для NaN
  • Unpivot через melt — обратная операция
  • Построение heatmap на основе pivot
  • Связь с SQL PIVOT

Три функции

  • pd.pivot_table — с агрегацией (mean, sum и т.д.)
  • pd.pivot — без агрегации (чистая перекладка)
  • pd.crosstab — упрощённый pivot для частот

1. pivot_table — самый частый

import pandas as pd

df = pd.DataFrame({
    'month':    ['Jan', 'Jan', 'Feb', 'Feb', 'Jan'],
    'category': ['A',   'B',   'A',   'B',   'A'],
    'amount':   [100,   200,   150,   250,   120]
})

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

Результат:

category   A    B
month
Jan      220  200
Feb      150  250

2. Параметры pivot_table

Несколько values

df.pivot_table(
    values=['amount', 'qty'],
    index='month',
    columns='category',
    aggfunc='sum'
)

Несколько агрегатов

df.pivot_table(
    values='amount',
    index='month',
    aggfunc=['sum', 'mean', 'count']
)

Разные агрегаты на разные колонки

df.pivot_table(
    values=['amount', 'qty'],
    index='month',
    aggfunc={'amount': 'sum', 'qty': 'mean'}
)

Заполнить NaN

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

Итоги

df.pivot_table(values='amount', index='month', columns='category',
               aggfunc='sum', margins=True, margins_name='Total')

3. pivot без агрегации

Если данные уже уникальны (без дублей по (index, columns)):

df = pd.DataFrame({
    'month':    ['Jan', 'Feb', 'Jan', 'Feb'],
    'category': ['A',   'A',   'B',   'B'],
    'amount':   [100,   150,   200,   250]
})

df.pivot(index='month', columns='category', values='amount')

pivot быстрее, но падает при дублях. Если дубли есть — pivot_table с агрегацией.

4. crosstab — упрощение

# подсчёт частот
pd.crosstab(df['country'], df['category'])

# с агрегацией
pd.crosstab(
    df['country'],
    df['category'],
    values=df['amount'],
    aggfunc='sum'
)

# нормализация (проценты)
pd.crosstab(df['country'], df['category'], normalize='index')  # по строкам
pd.crosstab(df['country'], df['category'], normalize='columns')  # по столбцам
pd.crosstab(df['country'], df['category'], normalize='all')    # глобально

5. Unpivot (обратная операция) — melt

# было:
# month | A   | B
# Jan   | 220 | 200
# Feb   | 150 | 250

pivot.reset_index().melt(
    id_vars='month',
    var_name='category',
    value_name='amount'
)

# стало:
# month | category | amount
# Jan   | A        | 220
# Jan   | B        | 200
# Feb   | A        | 150
# Feb   | B        | 250

6. MultiIndex в pivot

# несколько колонок в index
df.pivot_table(
    values='amount',
    index=['country', 'month'],
    columns='category',
    aggfunc='sum'
)

Получается hierarchical MultiIndex.

7. Stacked bars / heatmaps

Результат pivot легко визуализируется:

import matplotlib.pyplot as plt
import seaborn as sns

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

# stacked bar
pivot.plot(kind='bar', stacked=True)
plt.show()

# heatmap
sns.heatmap(pivot, annot=True, cmap='YlOrRd')
plt.show()

8. pivot_table vs groupby

# через pivot_table
df.pivot_table(values='amount', index='month', columns='category', aggfunc='sum')

# через groupby + unstack
df.groupby(['month', 'category'])['amount'].sum().unstack('category')

Результаты одинаковые. Pivot_table — синтаксический сахар.

9. В SQL эквивалент

-- pivot через CASE в SQL
SELECT
    month,
    SUM(CASE WHEN category = 'A' THEN amount ELSE 0 END) AS cat_a,
    SUM(CASE WHEN category = 'B' THEN amount ELSE 0 END) AS cat_b
FROM df
GROUP BY month;

Подробнее: PIVOT UNPIVOT SQL.

10. Типовые use cases

Продажи по месяцам × категориям

df.pivot_table(values='revenue', index='month', columns='category', aggfunc='sum')

Retention table

df.pivot_table(values='user_id', index='cohort_month', columns='period', aggfunc='nunique')

Conversion по каналам

df.pivot_table(values='converted', index='day', columns='channel', aggfunc='mean')

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

1. Дубликаты в pivot

# если есть дубли по (index, columns) — ошибка
df.pivot(index='month', columns='category', values='amount')
# ValueError: Index contains duplicate entries

# правильно
df.pivot_table(values='amount', index='month', columns='category', aggfunc='sum')

2. Неправильный aggfunc

По умолчанию aggfunc='mean'. Часто хотят sum или count.

3. NaN в результате

fill_value=0 для замены пропусков.

4. MultiIndex путает

После pivot с несколькими уровнями — reset_index() для плоского DataFrame.

5. Медленно на больших данных

Для огромных DataFrame — делайте pivot в базе (SQL) или через ClickHouse.

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

FAQ

pivot или pivot_table?

pivot_table — с агрегацией. pivot — без (если данные уже уникальны).

crosstab зачем?

Упрощённый синтаксис для частот и частот с агрегацией. Меньше кода.

melt — это unpivot?

Да. Превращает wide формат в long.

Можно ли несколько колонок в values?

Да: values=['amount', 'qty'].


Тренируйте pandas — откройте тренажёр с 1500+ вопросами для собесов.