2025, Oct 02 11:17

Как в pandas pivot_table исключить неиспользуемые категории с observed=True

Почему pivot_table в pandas добавляет лишние столбцы из категориального столбца и как это исправить флагом observed=True. Пример, объяснение и готовое решение.

Когда вы отфильтровываете DataFrame в pandas по подмножеству категориальных значений, а затем строите сводную таблицу, легко неожиданно увидеть столбцы для категорий, которые, казалось бы, уже исключены. Это часто проявляется на данных по игровым платформам: вы оставляете всего несколько платформ, но pivot_table всё равно создаёт столбцы для каждой платформы, встречавшейся в исходных данных.

Как воспроизвести проблему

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

# Исходный DataFrame
df_games_src['platform'].unique()
# Новый DataFrame только с шестью целевыми платформами
df_games_filtered = df_games_src[df_games_src['platform'].isin(
    ['3DS', 'PSV', 'WiiU', 'PS4', 'XOne', 'PC'])].sort_values(by=['year', 'platform']).reset_index(drop=True)
df_games_filtered['platform'].unique()
# Создание сводной таблицы (появляются неожиданные лишние столбцы)
subset_sales = df_games_filtered[['name', 'platform', 'total_sales']]
sales_wide = subset_sales.pivot_table(
    values='total_sales', index='name', columns='platform', aggfunc='sum')
sales_wide

В результате появляется таблица со столбцами для платформ далеко за пределами выбранной шестёрки, хотя в списке уникальных значений отфильтрованного DataFrame — только они.

Что происходит

Такое поведение связано с типом данных category в pandas. По умолчанию разные методы, включая pivot_table, добавляют в результат неиспользуемые категории. То есть категории, прописанные в dtype, но отсутствующие в строках отфильтрованных данных, всё равно попадают в итог. Об этом говорится в руководстве пользователя pandas в разделе про операции с категориями: https://pandas.pydata.org/pandas-docs/stable/user_guide/categorical.html#operations.

Как исправить

Самый прямой способ заставить pivot_table игнорировать неиспользуемые категории — указать observed=True. С этим флагом при формировании результата учитываются только те категории, которые действительно присутствуют в отфильтрованных данных.

subset_sales = df_games_filtered[['name', 'platform', 'total_sales']]
sales_wide = subset_sales.pivot_table(
    values='total_sales', index='name', columns='platform', aggfunc='sum', observed=True)
sales_wide

В итоге сводная таблица содержит только шесть платформ из вашего фильтра.

Минимальная иллюстрация

Короткий пример, показывающий разницу между поведением по умолчанию и observed=True.

import pandas as pd
# Базовая таблица с четырьмя категориями
base_tbl = pd.DataFrame({
    'name': ['a', 'b', 'c', 'd'],
    'platform': ['w', 'x', 'y', 'z'],
    'total_sales': [1, 2, 3, 4]
})
# Делаем 'platform' категориальным типом
base_tbl = base_tbl.astype({'platform': 'category'})
# Оставляем строки, где платформа — x или z
narrow_tbl = base_tbl[base_tbl['platform'].isin(['x', 'z'])]
# По умолчанию: в сводной таблице появляются неиспользуемые категории
narrow_tbl.pivot_table(index='name', columns='platform', values='total_sales', aggfunc='sum')
# платформа  w  x  y  z
# имя
# b         0  2  0  0
# d         0  0  0  4
# С observed=True: включаются только используемые категории
narrow_tbl.pivot_table(index='name', columns='platform', values='total_sales', aggfunc='sum', observed=True)
# платформа    x    z
# имя
# b         2.0  NaN
# d         NaN  4.0

Почему это важно

Помимо того что вывод становится чище и понятнее, впереди — изменение поведения. Ваш код должен на самом деле выдавать предупреждение (как минимум в v2.3.2), так как значение по умолчанию изменится. Сообщение говорит прямо:

FutureWarning: Значение по умолчанию для observed=False устарело и в одной из будущих версий pandas будет изменено на observed=True. Укажите observed=False, чтобы скрыть это предупреждение и сохранить текущее поведение

Явно указывая observed=True, вы заранее согласуете код с будущим значением по умолчанию и избежите сюрпризов по мере развития pandas.

Итоги

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

Статья основана на вопросе на StackOverflow от RicardoDLM и ответе jqurious.