Реструктуризация: 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— инструменты работы с ним. - Длинный формат — для анализа, широкий — для отчётов.