2025, Nov 20 00:03

Как выровнять строки в pandas при повторяющихся ключах MultiIndex

Как получить diff‑выравнивание в pandas при дубликатах MultiIndex: нумеруем вхождения, делаем outer concat и compare. Подходит для аудита данных и регрессий.

При подготовке данных часто нужен вид «как в diff»: две таблицы с в целом похожими строками, но с пропусками, повторами и частичными пересечениями. Цель — выстроить их построчно, как это делает инструмент сравнения текстов, чтобы совпадающие записи шли в одной линии, а лишние строки были явно смещены и заполнены NaN. Стандартные объединения не дают такого выравнивания при дублирующихся индексах: вместо этого появляются декартовы расширения или неверно расставленные пропуски.

Минимальный пример: подготовка и почему наивные объединения не подходят

Ниже мы создаём два DataFrame в pandas с тройным индексом year, pos и score. Части ключей совпадают, части различаются, при этом в обоих есть повторяющиеся комбинации индексов.

import pandas as pd
payload_a = {"year": [2023] * 7,
             "pos": [1, 2, 4, 4, 4, 8, 8],
             "score": [15, 20, 30, 30, 30, 60, 60],
             "value": ["a", "b", "c", "c", "c", "d", "d"]}
left_frame = pd.DataFrame(payload_a).set_index(["year", "pos", "score"])
payload_b = {"year": [2023] * 9,
             "pos": [1, 1, 1, 3, 3, 4, 4, 8, 10],
             "score": [15, 15, 15, 25, 25, 30, 30, 60, 80],
             "value": ["v", "v", "v", "w", "w", "x", "x", "y", "z"]}
right_frame = pd.DataFrame(payload_b).set_index(["year", "pos", "score"])
joined = left_frame.join(right_frame, how="outer", lsuffix="_l", rsuffix="_r")
print(joined)
l_aligned, r_aligned = left_frame.align(right_frame)
print(l_aligned)
print(r_aligned)

Обычное внешнее объединение дублирует записи при повторяющихся ключах и не формирует нужное «diff»-выравнивание с пустыми местами там, где они ожидаются. align тоже не про это: он выравнивает только по ключам, не расширяя записи, чтобы сопоставить количество дублей с обеих сторон.

Что на самом деле происходит

В pandas предполагается, что индекс однозначно определяет строку. Когда ключи повторяются, join и align действуют корректно по своим определениям, но не имитируют текстовый diff. Нужно сделать каждый повтор ключа различимым, чтобы N‑е вхождение слева выровнялось с N‑м вхождением справа.

Решение 1: добавить счётчик по ключу, чтобы строки стали уникальными, затем склеить

Подход прост. Для каждого кортежа ключей (year, pos, score) пронумеруйте вхождения с нуля. Добавьте эту нумерацию новым уровнем индекса. Теперь ключи индекса уникальны с обеих сторон, и простой внешний concat даст нужное выравнивание. В конце отсортируйте для наглядности.

left_unique = left_frame.set_index(
    left_frame.groupby(level=[0, 1, 2]).cumcount(),
    append=True
)
right_unique = right_frame.set_index(
    right_frame.groupby(level=[0, 1, 2]).cumcount(),
    append=True
)
aligned_view = pd.concat([left_unique, right_unique], axis=1, join='outer').sort_index()
print(aligned_view)

Результат:

                 value value
year pos score              
2023 1   15    0     a     v
               1   NaN     v
               2   NaN     v
     2   20    0     b   NaN
     3   25    0   NaN     w
               1   NaN     w
     4   30    0     c     x
               1     c     x
               2     c   NaN
     8   60    0     d     y
               1     d   NaN
     10  80    0   NaN     z

Это именно то «diff»-выравнивание, которое нам нужно: повторы ключей разворачиваются по порядку, совпадающие позиции стоят напротив друг друга, а лишние элементы проявляются как NaN на противоположной стороне.

Решение 2: «diff»-представление через DataFrame.compare с единым индексом

Если нужен наглядный вид «сравнение бок о бок», переиндексируйте оба датафрейма с уникальными ключами на объединение их мультииндексов и вызовите compare.

full_index = left_unique.index.union(right_unique.index)
result = left_unique.reindex(full_index).compare(right_unique.reindex(full_index))
print(result)

Результат:

                 value      
                  self other
year pos score              
2023 1   15    0     a     v
               1   NaN     v
               2   NaN     v
     2   20    0     b   NaN
     3   25    0   NaN     w
               1   NaN     w
     4   30    0     c     x
               1     c     x
               2     c   NaN
     8   60    0     d     y
               1     d   NaN
     10  80    0   NaN     z

Так сохраняется выравнивание и подсвечиваются расхождения и пропуски между двумя входами.

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

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

Итоги

Чтобы получить «diff»-выравнивание при дублирующихся ключах мультииндекса, добавьте счётчик вхождений как дополнительный уровень индекса и объедините фреймы внешним concat. Для удобного визуального сравнения переиндексируйте обе стороны на общий полный мультииндекс и примените compare. В быстрых проверках первый способ обычно быстрее, хотя это зависит от размера данных. Идея проста: сделать каждое повторяющееся вхождение ключа уникальным по порядку, а дальше позволить pandas делать своё — выполнять выровненные операции по чётким индексам.