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". Благодаря этим приёмам преобразование получается и надёжным, и наглядным.