2025, Dec 24 18:02
Словарь на каждую строку в pandas: assign, apply и рабочие приёмы
Разбираем, почему DataFrame.assign в pandas даёт NaN при создании столбца словарей, и показываем 3 надёжных способа: списковое включение, распаковка, apply.
Добавить для каждой строки словарь в pandas DataFrame кажется простым — пока не пытаешься собрать этот словарь из нескольких столбцов и вместо объектов получаешь NaN. Ниже — короткий пример из финансов (построчная серия денежных потоков), объяснение, почему первая попытка не срабатывает, и надёжные приёмы, которые возвращают желаемый столбец словарей.
Воспроизводим проблему
Возьмём таблицу акций с несколькими признаками. Цель — для каждой строки сформировать словарь, представляющий серию денежных потоков, полученную из выбранных столбцов.
import pandas as pd
stocks = pd.DataFrame(
{
"price": [100, 103, 240],
"Feat1": [1, 3, 3],
"Feat2": [5, 7, 1],
"Feat3": [1, 4, 6],
},
index=["Company A", "Company B", "Company C"],
)
# price Feat1 Feat2 Feat3
# Company A 100 1 5 1
# Company B 103 3 7 4
# Company C 240 3 1 6
def make_flows(a=1, b=2):
return {0: a, 0.5: b, 1: 7, 2: 8, 3: 9}
# Первая попытка: в новом столбце получается NaN
broken = stocks.assign(
flows=lambda frame: make_flows(
a=frame["Feat1"], b=frame["Feat3"]
)
)
В результате новый столбец заполняется NaN вместо словарей по строкам.
Почему так происходит
При использовании DataFrame.assign с вызываемым объектом pandas передаёт в него весь DataFrame. В проблемном выражении frame["Feat1"] и frame["Feat3"] — это Series, а не скаляры. Передавая Series в функцию, которая должна возвращать по одному словарю на строку, вы получаете один-единственный словарь, собранный из объектов Series; его нельзя разложить в колонку «одно значение на строку». Pandas ожидает либо скаляр для растяжения (broadcast), либо массивоподобный объект/Series той же длины, что и DataFrame. Один словарь не соответствует ни тому, ни другому, поэтому присваивание даёт NaN.
Рабочие варианты решения
Нужно формировать по одному словарю на каждую строку. Простой способ — списковое включение по индексу с извлечением скалярных значений из строки.
def make_flows(a=1, b=2):
return {0: a, 0.5: b, 1: 7, 2: 8, 3: 9}
fixed = stocks.assign(
flows=lambda frame: [
make_flows(a=frame.loc[idx, "Feat1"], b=frame.loc[idx, "Feat3"])
for idx in frame.index
]
)
Более лаконичный подход — распаковка параметров из двухколоночного массива NumPy. Значения берутся по строкам и распаковываются в функцию:
fixed_unpack = stocks.assign(
flows=lambda frame: [
make_flows(*vals) for vals in stocks[["Feat1", "Feat2"]].values
]
)
Есть и вариант, полностью остающийся в рамках API pandas и изначально рассчитанный на построчную обработку:
stocks["flows"] = stocks.apply(
lambda row: make_flows(a=row["Feat1"], b=row["Feat3"]), axis=1
)
Результат
При построчном построении в каждой ячейке оказывается собственный словарь денежных потоков, рассчитанный из признаков этой строки.
# Пример вида результата для первого подхода (Feat1 + Feat3)
# price Feat1 Feat2 Feat3 flows
# Company A 100 1 5 1 {0: 1, 0.5: 1, 1: 7, 2: 8, 3: 9}
# Company B 103 3 7 4 {0: 3, 0.5: 4, 1: 7, 2: 8, 3: 9}
# Company C 240 3 1 6 {0: 3, 0.5: 6, 1: 7, 2: 8, 3: 9}
Зачем это важно
Во многих реальных пайплайнах обработки данных и квант-процессах на каждую строку создают структурированные объекты Python, особенно когда дальнейшая логика требует полноценные объекты, а не скаляры. Понимание того, как ведут себя assign, apply и векторизованные выборки, помогает избежать тонких ошибок, когда pandas тихо нарушает согласованность форм или возвращает NaN, потому что не может распространить один объект на все строки.
Выводы
Формируя столбец из словарей, убедитесь, что выражение возвращает ровно один словарь на строку. Используйте списковое включение с .loc для получения скалярных значений, распаковку параметров из массива с двумя столбцами, если это совпадает с сигнатурой вашей функции, или построчный apply с axis=1. Все эти методы дают аккуратный столбец типа object, который затем можно итерировать или преобразовывать для дальнейших вычислений.