Реструктуризация: melt, pivot, stack и MultiIndex

Одни и те же данные можно хранить «широко» и «длинно» — умение переключаться между формами важнее, чем кажется.

Широкий (wide) формат: значения разнесены по столбцам. Длинный (long) формат: одна строка на наблюдение, признак — в отдельном столбце.

Wide против long: две формы одних данных

Представьте продажи по месяцам. В широком виде каждый месяц — отдельный столбец:

город   янв   фев   мар
Москва  100   120   140
Сочи     80    90   110

В длинном виде месяц — это значение в столбце, а не имя столбца:

город   месяц  продажи
Москва  янв    100
Москва  фев    120
...

Широкий формат нагляден для человека и таблиц; длинный — удобен для группировок, фильтров и большинства библиотек визуализации. Переходы между ними — melt (wide → long) и pivot (long → wide).

Почему длинный формат так любят инструменты анализа? Потому что в нём каждая переменная — это столбец, а каждое наблюдение — строка (принцип «tidy data»). В длинном виде легко добавить новый месяц — это просто новые строки, структура таблицы не меняется. В широком — пришлось бы добавлять новый столбец, и весь код, перечислявший месяцы, поехал бы. Длинный формат масштабируется по данным, широкий — по восприятию. Поэтому правило большого пальца: храните и обрабатывайте в длинном, показывайте в широком.

melt: из широкого в длинный

melt «расплавляет» столбцы в строки: указанные столбцы становятся парами «имя признака → значение».

long = df.melt(
    id_vars=["город"],                  # что оставить как есть
    value_vars=["янв", "фев", "мар"],   # что расплавить в строки
    var_name="месяц",                   # имя для бывших названий столбцов
    value_name="продажи",               # имя для значений
)

Сделаем melt руками, чтобы увидеть превращение «столбцы → строки»:

wide = [
    {"город": "Москва", "янв": 100, "фев": 120},
    {"город": "Сочи",   "янв": 80,  "фев": 90},
]
months = ["янв", "фев"]

# melt: каждую пару (месяц, значение) разворачиваем в отдельную строку
for row in wide:
    for m in months:
        print(row["город"], m, row[m])

Вывод:

Москва янв 100
Москва фев 120
Сочи янв 80
Сочи фев 90

Из двух широких строк получилось четыре длинных — по одной на каждую пару (город, месяц). Это и есть melt.

pivot: из длинного в широкий

pivot — обратная операция: разворачивает значения столбца обратно в столбцы. В отличие от pivot_table, обычный pivot не агрегирует и требует уникальных пар индекс-столбец:

wide = long.pivot(index="город", columns="месяц", values="продажи")

Если на пересечении может быть несколько значений (нужна агрегация) — берите pivot_table из раздела про группировку.

stack и unstack: между столбцами и индексом

Это «младшие братья» melt/pivot, работающие с индексом. stack сдвигает уровень столбцов в индекс (делает уже и длиннее), unstack — наоборот, поднимает уровень индекса в столбцы (делает шире):

df.stack()     # столбцы → внутренний уровень индекса (long-подобно)
df.unstack()   # уровень индекса → столбцы (wide-подобно)

Вспомните прошлый раздел: df.groupby(["город", "месяц"]).sum().unstack() превращает результат группировки по двум ключам в широкую таблицу — это типичное применение unstack.

MultiIndex: иерархический индекс

Группировка по нескольким ключам, stack и pivot часто порождают MultiIndex — индекс из нескольких уровней (например, город + месяц). Это компактный способ хранить многомерные данные в двумерной таблице.

# создание MultiIndex
idx = pd.MultiIndex.from_tuples(
    [("Москва", "янв"), ("Москва", "фев"), ("Сочи", "янв")],
    names=["город", "месяц"],
)
s = pd.Series([100, 120, 80], index=idx)

# доступ по уровням
s.loc["Москва"]              # все месяцы Москвы
s.loc[("Москва", "янв")]     # конкретная ячейка
s.xs("янв", level="месяц")   # срез по внутреннему уровню — все города за янв

# перестановка и сортировка уровней
s.swaplevel()                # поменять уровни местами
s.sort_index()               # отсортировать (нужно для эффективных срезов)
s.reset_index()              # превратить уровни индекса обратно в столбцы

xs делает срез по конкретному уровню, swaplevel меняет уровни местами, reset_index «распускает» MultiIndex в обычные столбцы. Для срезов по MultiIndex его полезно сначала отсортировать (sort_index) — иначе срезы по диапазону могут не работать.

Когда MultiIndex оправдан, а когда — лишняя сложность? Иерархический индекс хорош для компактного хранения многомерных агрегатов (продажи по город×месяц×категория) и для быстрых срезов по уровням. Но в повседневной обработке он часто мешает: к нему непривычно обращаться, и многие операции требуют помнить про уровни. Практичный подход — пусть группировки и сводки порождают MultiIndex, но как только вы переходите к фильтрам, новым столбцам и выгрузке, делайте reset_index() и работайте с плоской таблицей. Иными словами, MultiIndex — хороший промежуточный формат, но необязательный для постоянного хранения.

Подводные камни

  • pivot падает на дубликатах. Если пара (index, columns) встречается дважды, обычный pivot выдаст ошибку — нужен pivot_table с агрегацией.
  • MultiIndex без сортировки. Срезы по диапазону уровней требуют отсортированного индекса; иначе предупреждение или ошибка.
  • Потеря смысла после reshape. После melt легко забыть, что один столбец теперь смешивает разные признаки; давайте осмысленные var_name/value_name.
  • Доступ к MultiIndex кортежами. df.loc[("a", "b")] и df.loc["a", "b"] — разные вещи (вторая трактует «b» как столбец); используйте явные кортежи.

Лучшие практики

  • Для анализа и визуализации чаще удобен длинный формат — приводите к нему через melt.
  • Для отчётов «для людей» разворачивайте в широкий через pivot/pivot_table.
  • Если на пересечении возможны дубли — только pivot_table с aggfunc.
  • MultiIndex сортируйте сразу после создания (sort_index) и не бойтесь reset_index, когда уровни мешают.

Итог

  • wide и long — две формы одних данных; melt и pivot переключают между ними.
  • stack/unstack двигают данные между столбцами и уровнями индекса.
  • MultiIndex хранит многомерность в двумерной таблице; xs, swaplevel, reset_index — инструменты работы с ним.
  • Длинный формат — для анализа, широкий — для отчётов.
Проверьте себя
1. Какая операция переводит данные из широкого формата (месяцы-столбцы) в длинный (месяц-значение в строках)?
Apivot
Bmelt
Cmerge
Dconcat
2. Чем pivot отличается от pivot_table?
Apivot быстрее
Bpivot не агрегирует и требует уникальных пар index-columns, а pivot_table агрегирует дубликаты через aggfunc
Cpivot_table работает только с числами
DОни идентичны
3. Что такое MultiIndex и зачем его сортировать?
AИндекс из нескольких таблиц; сортировка не нужна
BИерархический индекс из нескольких уровней; сортировка (sort_index) нужна для корректных срезов по диапазону уровней
CСписок столбцов; сортировка ускоряет merge
DТип данных для дат
Поддержать проект