2025, Sep 23 23:16

Сопоставление 1‑основанной позиции с именем столбца в pandas

Как в pandas сопоставить 1‑основанную позицию year с названием столбца в широком DataFrame без lambda: векторная индексация через columns и reindex для NaN. Без ошибок.

Сопоставить числовой индикатор из одного столбца с именем заголовка — типичная задача в pandas, которая выглядит проще, чем есть на самом деле. В широком DataFrame, где столбцы — это годы, а отдельный столбец хранит 1‑основанную позицию (year), нам нужно записать соответствующую метку столбца в новый столбец (year_name). Часто сначала пытаются пройтись по строкам через lambda, но это приводит к непонятным ошибкам и лишней сложности. Хорошая новость: lambda не нужна вовсе.

Постановка задачи

Предположим, есть DataFrame, где годы — это столбцы, а year — 1‑основанный указатель на нужный столбец. Столбец itemName используется как индекс.

          2020  2021  2022  2023  2024  year
itemName                                      
item1        5    20    10    10    50     3
item2       10    10    50    20    40     2
item3       12    35    73    10    54     4

Ожидаемый результат — добавить столбец year_name с заголовком столбца, на который указывает позиция из year.

          2020  2021  2022  2023  2024  year year_name
itemName                                               
item1        5    20    10    10    50     3      2022
item2       10    10    50    20    40     2      2021
item3       12    35    73    10    54     4      2023

Попытка, которая вызывает ошибки

Проход по строкам через apply с lambda кажется очевидным решением, но на практике приводит к проблемам с типами и индексированием. Примеры ниже сохраняют исходную идею и показывают, почему такой подход ломается.

col_labels = repo[key].columns.tolist()
frame_out[["last_year_name"]] = frame_out[["_last_year"]].apply(
    lambda s: col_labels[s]
)

Этот код падает с TypeError вроде "list indices must be integers or slices, not Series". Причина в том, что lambda получает целую строку (Series), а Series нельзя использовать как индекс списка.

col_labels = repo[key].columns.tolist()
frame_out[["last_year_name"]] = frame_out[["_last_year"]].apply(
    lambda s: col_labels[s.iloc[0].astype(int)]
)

Далее можно нарваться на "IndexError: list index out of range", а если слева указывать двумерную цель через двойные скобки, а справа отдавать одномерную Series, получите ещё и "ValueError: Columns must be same length as key".

Что на самом деле не так

.apply по строкам (axis=0) возвращает Series для каждой строки, а не скаляр, который подошёл бы для индексации списка. Поэтому col_labels[s] и падает: s — это Series. Попытка "подправить" ситуацию через s.iloc[0] лишь маскирует проблему и добавляет риск выхода за границы. Плюс, если присваивать в один новый столбец, указывая слева двойные скобки, как в frame_out[["last_year_name"]], а справа передавать одномерную Series, легко получить ошибку несоответствия размеров.

Векторное решение (без lambda)

Индекс столбцов сам по себе индексируем. Поскольку year — 1‑основанный, вычитаем единицу, чтобы перейти к 0‑основанной позиции, и берём метку напрямую из columns. Так мы избегаем построчных вызовов Python и всех ловушек с формами и типами.

# Прямое сопоставление позиционного индикатора с названием столбца
grid["year_name"] = grid.columns[grid["year"] - 1]  # .astype("Int64")

Это выражение для каждой строки берёт метку столбца на позиции year - 1 и записывает её в year_name. При необходимости можно привести тип к Int64, если вам нужен конкретный целочисленный тип с поддержкой пропусков.

Обработка некорректных или пропущенных значений

Если в year встречаются NaN или позиции вне диапазона, используйте Series с reindex, чтобы безопасно выровнять индексы и получить NaN при некорректной позиции вместо исключения.

grid["year_name"] = pd.Series(grid.columns).reindex(grid["year"] - 1).values

Этот подход корректно обрабатывает случаи с NaN в year или с позициями за пределами последнего столбца, возвращая NaN в year_name, а не выбрасывая ошибку.

Воспроизводимые входные данные

Корректные значения:

grid = pd.DataFrame.from_dict({
    "index": ["item1", "item2", "item3"],
    "columns": [2020, 2021, 2022, 2023, 2024, "year"],
    "data": [
        [5, 20, 10, 10, 50, 3],
        [10, 10, 50, 20, 40, 2],
        [12, 35, 73, 10, 54, 4],
    ],
    "index_names": ["itemName"],
    "column_names": [None],
}, "tight")

Некорректные значения:

grid = pd.DataFrame.from_dict({
    "index": ["item1", "item2", "item3"],
    "columns": [2020, 2021, 2022, 2023, 2024, "year"],
    "data": [
        [5, 20, 10, 10, 50, 3],
        [10, 10, 50, 20, 40, 20],
        [12, 35, 73, 10, 54, None],
    ],
    "index_names": ["itemName"],
    "column_names": [None],
}, "tight")

Почему это важно

Векторная индексация делает операцию краткой, избегает неоднозначных преобразований форм и типов и предотвращает ошибки, возникающие при попытке обращаться с Series как со скаляром. Она также обходит типичные ловушки присваивания, например несоответствие форм из-за двойных скобок при записи в один столбец. На DataFrame с тысячами строк отказ от построчных lambda упрощает код и снижает вероятность ошибок.

Выводы

Когда нужно сопоставить 1‑основанную позицию с названием столбца, используйте индекс столбцов напрямую: grid.columns[grid["year"] - 1]. Если возможны NaN или выход за границы, переходите к приёму с reindex, чтобы безопасно получать NaN вместо исключений. Присваивая значение в один новый столбец, используйте одинарные скобки, чтобы избежать "Columns must be same length as key". Благодаря этим приёмам преобразование получается и надёжным, и наглядным.

Статья основана на вопросе на StackOverflow от DGMS89 и ответе от mozway.