2025, Sep 29 13:17
Как получать точные выборки из MultiIndex после groupby в pandas
Пошагово объясняем, как работать с MultiIndex после groupby в pandas: выборка по .loc и .xs, доступ к zone1 при Setpoint 80, типичные ошибки и лучший синтаксис
Когда вы группируете данные по нескольким ключам в pandas, на выходе получается MultiIndex. Это мощный инструмент, но он меняет способ выборки строк. Частая проблема как раз в следующем: после группировки по zone и Setpoint как получить одно значение для zone1 при Setpoint 80? Ниже — краткое руководство, которое поможет делать такую выборку корректно и предсказуемо.
Воспроизводим настройку
Окружение: Python 3.9.2, pandas 2.3.2, JupyterLab. Представьте, что у вас уже есть DataFrame со столбцами zone, data и Setpoint, и вы посчитали среднее по паре (zone, Setpoint). Пример кода агрегации может выглядеть так:
# начинаем с существующего DataFrame под названием sensor_df
# столбцы: 'zone', 'Setpoint', 'data'
agg_mean = sensor_df[["zone", "Setpoint", "data"]] \
    .groupby(["zone", "Setpoint"]) \
    .mean()
В результате получается DataFrame с индексом строк в виде MultiIndex из двух уровней — zone и Setpoint — и единственным столбцом data.
Если вы выделите одну зону:
zone1_slice = agg_mean.loc["zone1"]
Вы увидите, что оставшийся индекс — это значения Setpoint (40, 50, …, 110), а столбец по‑прежнему один — data.
Что здесь происходит
После groupby(...).mean() в строках формируется MultiIndex: первый уровень — zone, второй — Setpoint. Подпись data, которую вы видите «над» таблицей, — это имя столбца, а не метка индекса. При вызове agg_mean.loc["zone1"] вы фиксируете один уровень MultiIndex и получаете DataFrame, где индексом выступают значения Setpoint. Следовательно, 80 — это метка индекса, а не имя столбца, и обращаться к ней нужно соответствующим образом.
Отсюда понятно, почему такие попытки приводят к ошибкам:
# 80 здесь не имя столбца
zone1_slice[80]  # KeyError: 80
# .at — это индексатор; его применяют через [] или [, ] к Series/DataFrame, а не вызывают как функцию
zone1_slice.at(80)  # TypeError
# .loc/.iloc — индексаторы; используйте квадратные скобки, не круглые
zone1_slice.loc(80)   # KeyError / ValueError
zone1_slice.iloc(80)  # та же проблема
# 'Setpoint' после groupby — не столбец, а уровень индекса
zone1_slice.loc(zone1_slice["Setpoint"] == 80)  # KeyError: Setpoint
Практическое правило: для меток индекса используйте .loc[...]; для MultiIndex передавайте кортеж, соответствующий уровням индекса; чтобы взять срез по одному уровню сквозь все остальные, используйте .xs(..., level=...).
Решение: выборка по уровню MultiIndex
Если нужен средний показатель для Setpoint 80 по всем зонам, возьмите срез по уровню Setpoint:
agg_mean.xs(80, level="Setpoint")
# выдаёт DataFrame с индексом 'zone' и столбцом 'data', например:
#             data
# zone            
# zone1  80.045247
# zone3  80.043304
# zone4  80.034280
Если нужно одно значение для zone1 при Setpoint 80, проиндексируйте оба уровня сразу через .loc и кортеж:
agg_mean.loc(("zone1", 80))
# возвращает Series из одной строки:
# data    80.045247
agg_mean.loc(("zone1", 80), "data")
# возвращает скаляр:
# 80.045247
Можно и «цепочечно» выбирать данные, но помните: это создаёт промежуточные объекты. Например, эти варианты дают одинаковый результат:
agg_mean["data"]["zone1"][80]
agg_mean.loc["zone1"].loc[80]["data"]
Прямой вариант с кортежем — agg_mean.loc[("zone1", 80), "data"] — понятнее и, что важнее, не порождает лишние промежуточные объекты.
Почему это важно
MultiIndex — ключевой элемент многих типичных сценариев в pandas, особенно после groupby. Понимание того, что ключи группировки превращаются в уровни индекса, а не в столбцы, позволяет выбирать данные явно и предсказуемо. Это сокращает попытки «наугад» с индексаторами, избавляет от неприятных KeyError и помогает писать более понятный код без лишних промежуточных выделений.
Итоги
После groupby по нескольким ключам мыслите уровнями индекса. Если известны точные метки на всех уровнях, используйте .loc[(level1, level2)]. Чтобы срезать по одному уровню сквозь остальные, применяйте .xs(label, level="LevelName"). И помните: подпись сверху — это имя столбца, а метки слева — индекс. С такой картиной мира получить одиночное значение — например среднее data для zone1 при Setpoint 80 — это одна строка кода.
Статья основана на вопросе на StackOverflow от Brian A. Henning и ответе от mozway.