2025, Dec 14 06:10

Построчный максимум дат в pandas: почему apply по умолчанию ломается и как сделать правильно

Разбираем, почему pandas apply с axis=0 даёт NaN и KeyError при поиске построчного максимума дат, и показываем верные решения: apply(axis=1) и DataFrame.max.

Построчная обработка в pandas кажется обманчиво простой, пока не сталкиваешься с поведением apply() по умолчанию. Типичный сценарий — посчитать максимум по строке среди нескольких столбцов с датами. Если лямбда получает не тот объект или вы обращаетесь к нему неверным индексом, вы ловите KeyError, IndexingError или получаете столбец из одних NaN, который маскирует истинную причину.

Воспроизведение проблемы

Ниже — минимальный пример с датафреймом, где столбцы содержат даты. Он демонстрирует несколько вариантов с apply(), которые по разным причинам падают, хотя вычисление максимума для одной конкретной строки работает.

import pandas as pds
import datetime as dtime

tbl = pds.DataFrame(
   [ [
      dtime.date(2025, 6, 5), dtime.date(2025, 6, 6) ],[
      dtime.date(2025, 6, 7), dtime.date(2025, 6, 8) ]
   ],
   columns=["A", "B"], index=["Row1", "Row2"]
)

# Явно находим максимум для строки 0 (РАБОТАЕТ)
max(tbl.loc[tbl.index[0], ["A", "B"]])

# Ни один из следующих трёх вариантов с "apply" не работает

if False:

   tbl["MaxDate"] = tbl.apply(lambda rec: max(
      rec.loc[rec.index[0], ["A", "B"]]
   ))

   # IndexingError: "Слишком много индексаторов"

elif False:

   tbl["MaxDate"] = tbl.apply(lambda rec: max(
      rec["A", "B"]
   ))

   # KeyError:
   # "ключ типа tuple не найден и индекс не является MultiIndex"

elif False:

   tbl["MaxDate"] = tbl.apply(lambda rec: max(
      rec["A"], rec["B"]
   ))

   # KeyError: 'A'

# Запрос типа объекта, передаваемого в лямбду, даёт столбец из NaN
if False:
   tbl["MaxDate"] = tbl.apply(lambda rec: type(rec))

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

Суть проблемы — в том, как apply() формирует входы и выравнивает выходы.

По умолчанию axis=0, поэтому apply() итерируется по столбцам. В таком режиме в лямбду передаётся объект Series, представляющий целый столбец, а его индекс — это метки строк DataFrame. Отсюда и KeyError при попытке rec["A"] в режиме по умолчанию: такого ключа нет в Series-столбце, индексированном метками строк. Аналогично, rec.loc[rec.index[0], ["A", "B"]] недопустимо, потому что rec — это Series, а вы пытаетесь индексировать его как двумерную структуру, что и вызывает IndexingError о «слишком большом числе индексаторов». А rec["A", "B"] использует кортеж-ключ, которого не существует, так как индекс Series — не MultiIndex.

NaN при присваивании type(rec) — следствие выравнивания. При axis=0 лямбда возвращает Series с индексом из имён столбцов (здесь A и B). Когда вы пытаетесь поместить этот Series в один новый столбец исходного DataFrame, pandas выравнивает по меткам индекса. Возвращённый Series имеет индекс ["A", "B"], а строки DataFrame — ["Row1", "Row2"]. Метки не совпадают — новый столбец заполняется NaN.

Чтобы работать построчно, нужно сменить ось. При axis=1 apply() передаёт в лямбду каждую строку как Series, где индекс — это названия столбцов. В таком контексте выбор нескольких полей следует делать списком меток, а не кортежем. Поэтому rec[["A", "B"]] — корректно, а rec["A", "B"] — нет. Внешние квадратные скобки выполняют индексирование Series, а внутренние задают список меток, которые нужно извлечь вместе. Если список уже лежит в переменной, просто передайте его: rec[my_cols].

Исправляем код

Когда ось указана верно, и общий построчный максимум, и прямое свёртывание по DataFrame работают без сюрпризов.

import pandas as pds
import datetime as dtime

tbl = pds.DataFrame(
   [ [
      dtime.date(2025, 6, 5), dtime.date(2025, 6, 6) ],[
      dtime.date(2025, 6, 7), dtime.date(2025, 6, 8) ]
   ],
   columns=["A", "B"], index=["Row1", "Row2"]
)

# Корректный способ использовать apply построчно
tbl["MaxDate"] = tbl.apply(lambda rec: max(rec), axis=1)

# Или проще: сразу свести выбранные столбцы
tbl["MaxDate"] = tbl[["A", "B"]].max(axis=1)

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

Понимание работы apply() помогает избежать тонких багов, которые выглядят как непонятные ошибки или «тихие» столбцы из NaN. На вход лямбда получает Series, чей индекс зависит от выбранной оси. Возвращаемый Series выравнивается по меткам индекса с тем объектом, в который вы присваиваете. Если метки не совпадают, pandas заполняет NaN. Это проявляется далеко не только в примере с максимумом по датам: любая построчная логика, где вы объединяете поля, вычисляете типы или агрегируете частичные выборки, чувствительна к выбору оси и к тому, как вы индексируете Series внутри лямбды.

Выводы

Явно указывайте axis в apply(), когда нужны построчные операции. Работая внутри лямбды, помните: индекс Series для строки — это имена столбцов. Чтобы выбрать несколько полей, используйте двойные скобки и передавайте список меток. А когда требуется простое свёртывание по столбцам, предпочитайте векторизированную форму на DataFrame, например tbl[["A", "B"]].max(axis=1), — это лаконично и прямо соответствует нужной операции.