
pivot и pivot_table в pandas: что делать с повторяющимися парами ключей


Если pivot() в pandas не разворачивает длинную таблицу в широкую, причина часто не в самом развороте, а в том, что одна будущая ячейка получается из нескольких строк. Для pandas это не мелкая помеха, а логическая неоднозначность: библиотека не знает, какое значение положить в пересечение строки и столбца. Поэтому рабочий порядок такой: сначала определить, какой должна быть одна ячейка результата, потом найти все повторы пары ключей, затем задать явное правило обработки через pivot_table() или подготовить данные до разворота, и в конце сверить итог.
Ожидаемая сетка результата
У широкого отчета всегда есть скрытый вопрос: что означает одна ячейка. В задаче из учебного отчета удобно считать, что каждая ячейка — это итоговые баллы одного студента по одному модулю. Тогда строки — студенты, столбцы — модули, а значение — число баллов. Если для пары студент + модуль существует ровно одна строка, pivot() подходит без дополнительных настроек.
По документации pandas, pivot() работает только там, где комбинации ключей уникальны. Это полезное ограничение: оно заставляет заранее проверить, действительно ли вы переводите «длинную» запись в «широкую», а не пытаетесь незаметно спрятать несколько разных событий в одну ячейку.
Учебный пример
Учебный пример. Ниже условный отчет в среде Python с pandas. В нем три студента и три модуля, а повторения возникают там, где баллы пришли несколькими строками: как исправление, ручная корректировка или второй тест.
import pandas as pd
report = pd.DataFrame({
"student": ["Анна", "Анна", "Анна", "Борис", "Борис", "Борис", "Вика", "Вика", "Вика"],
"module": ["Python", "Python", "SQL", "Python", "BI", "BI", "SQL", "Python", "Python"],
"points": [8, 2, 7, 5, 6, 1, 9, 4, 3],
"reason": [
"основная попытка",
"добавлены баллы за исправление",
"одна запись",
"одна запись",
"основная оценка",
"ручная корректировка",
"одна запись",
"первый тест",
"второй тест",
],
})
Ожидаемая сетка здесь такая:
| student | BI | Python | SQL |
|---|---|---|---|
| Анна | — | ? | ? |
| Борис | ? | ? | — |
| Вика | — | ? | ? |
Проблема в том, что знаки вопроса для Анна + Python, Борис + BI и Вика + Python должны быть вычислены по нескольким строкам, а не прочитаны из одной записи. Именно на этом месте pivot() и останавливается.
Повтор пары ключей
Когда вы запускаете report.pivot(index="student", columns="module", values="points"), pandas не может выбрать одно значение для дублирующихся сочетаний ключей. Технически это выглядит как ошибка reshape, но по сути это предупреждение о несогласованной структуре данных. Если проигнорировать его и сразу выбрать первую попавшуюся агрегацию, можно получить аккуратную, но смыслово неверную таблицу.
Поэтому следующий шаг — не менять метод, а объяснить каждый повтор. Для этого полезно собрать отдельную диагностическую таблицу: сколько строк попало в каждую будущую ячейку, какова их сумма и почему они возникли.
duplicates = (
report.groupby(["student", "module"], as_index=False)
.agg(
rows=("points", "size"),
points_total=("points", "sum"),
reasons=("reason", lambda s: "; ".join(s))
)
)
duplicate_pairs = duplicates.query("rows > 1")
| student | module | rows | points_total | reasons |
|---|---|---|---|---|
| Анна | Python | 2 | 10 | основная попытка; добавлены баллы за исправление |
| Борис | BI | 2 | 7 | основная оценка; ручная корректировка |
| Вика | Python | 2 | 7 | первый тест; второй тест |
Такая проверка важнее самого разворота. Она показывает, одинаковы ли причины повторов. Если в одной ячейке строки складываются, а в другой поздняя запись должна заменять раннюю, единого правила для всей таблицы уже нет. Значит, данные сначала нужно привести к одному смыслу, и только потом разворачивать.
Явное правило агрегации
В нашем учебном кейсе будем считать, что каждая строка добавляет баллы к итогу модуля. Тогда повторы не конфликтуют между собой, а суммируются. Это уже не задача для pivot(), а задача для pivot_table(), потому что здесь нужна явная агрегация.
По документации pandas, pivot_table() строит сводную таблицу и агрегирует значения. Для этого нужно не только назвать ключи, но и честно прописать правило: sum, mean, count и так далее. Важно именно слово «явно»: не «как-нибудь собрать», а зафиксировать, что повтор означает в вашем отчете.
wide = report.pivot_table(
index="student",
columns="module",
values="points",
aggfunc="sum"
)
| student | BI | Python | SQL |
|---|---|---|---|
| Анна | — | 10 | 7 |
| Борис | 7 | 5 | — |
| Вика | — | 7 | 9 |
Здесь арифметика проверяется вручную: у Анны по Python 8 + 2 = 10, у Бориса по BI 6 + 1 = 7, у Вики по Python 4 + 3 = 7. Обратите внимание на пустые ячейки: после pivot_table() отсутствующие сочетания ключей остаются пропусками, и для учебного отчета это честнее, чем автоматически подставлять 0. Пустая ячейка означает «записи для такой пары нет», а ноль означал бы уже другой смысл — «запись есть, но результат равен нулю».
Если в вашем отчете отсутствие модуля по правилам действительно нужно показывать как ноль, подставляйте его отдельным шагом и только после того, как согласовали предметный смысл. Например, это может быть уместно в таблице обязательных модулей, где отсутствие записи трактуется как неполученные баллы. Но это уже отдельное решение, не часть борьбы с дублями.
Как сохранить объяснение каждого повторения
Широкая таблица удобна для анализа, но она почти всегда убирает контекст. После суммирования видно итог 10, но не видно, что он собран из основной попытки и добавленных баллов за исправление. Поэтому хороший практический прием — хранить рядом не только итоговую матрицу, но и короткий аудит повторов.
Если вам нужно видеть именно число исходных строк в каждой будущей ячейке, не стоит полагаться на count по столбцу points: такой расчет считает только непустые значения и совпадает с числом строк лишь там, где в points нет пропусков. Надежнее добавить явный счетчик строк и агрегировать его суммой. Документация pandas отдельно показывает, что у сводных таблиц могут появляться иерархические заголовки, если в результате участвуют несколько наборов значений. Это удобно, когда нужно рядом видеть итог баллов и точное число исходных записей.
report["row_n"] = 1
audit = report.pivot_table(
index="student",
columns="module",
values=["points", "row_n"],
aggfunc="sum"
)
В таком результате появятся два блока столбцов: points и row_n. Для пары Анна + Python вы увидите points = 10 и row_n = 2, а для Анна + SQL — points = 7 и row_n = 1. Для отсутствующих сочетаний снова останутся пропуски. Если вам нужен именно визуальный отчет по числу строк, можно уже в отдельной копии этой таблицы подставить 0 только в блок row_n, не меняя смысл столбцов с баллами.
Редакционная рекомендация на случай, когда повторы нельзя агрегировать: добавьте еще один ключ, например номер попытки. Тогда вы не «лечите» дубль агрегацией, а честно признаете, что в данных есть третье измерение.
report["attempt"] = report.groupby(["student", "module"]).cumcount() + 1
После этого можно строить широкую таблицу уже по student и паре module + attempt. Такой ход нужен не всегда, но он спасает там, где каждая повторная строка должна остаться самостоятельным наблюдением.
Сверка итогов
После разворота важно проверить не только форму, но и сумму. В нашем примере выбранное правило — суммирование баллов внутри пары ключей. Значит, общий итог в длинной таблице и общий итог в широкой должны совпасть. Если они расходятся, проблема не в оформлении, а в логике подготовки данных.
long_total = report["points"].sum()
wide_total = wide.sum().sum()
unique_pairs = report[["student", "module"]].drop_duplicates().shape[0]
assert long_total == 45
assert wide_total == 45
assert unique_pairs == 6
Здесь результат прозрачен: в исходном отчете 9 строк и 45 баллов, после агрегации остается 6 уникальных пар student + module, но те же 45 баллов. Это и есть проверяемый эффект корректного pivot_table(): таблица стала шире и короче, а смысл выбранного показателя сохранился.
Практический вывод такой: если pivot() ругается на повторяющиеся пары ключей, не заменяйте его на pivot_table() автоматически. Сначала сформулируйте, что должна означать одна ячейка, затем соберите список повторов и только после этого задайте правило агрегации или добавьте третий ключ. В нашем условном кейсе это дало широкую матрицу баллов без подмены пустых сочетаний нулями, таблицу причин повторов и контрольную сверку суммы 45 = 45.
Если хотите отдельно отработать похожие задачи, у курса «Аналитика данных на Python: pandas, numpy, визуализация» на внутренней карточке каталога перечислены темы по pandas, первичной обработке, повторам в данных, переформированию, группировке и агрегации; официальная страница школы также подтверждает блоки по pandas, первичной обработке и преобразованию данных.





















