Новые столбцы в Pandas: apply, map и векторные операции над колонками
Добавляем в DataFrame выручку, скидки и категории: векторные операции, np.where, map и apply — и разбираемся, почему apply по строкам почти всегда лишний.
Редакция Питоники
Директор ставит задачу на пятницу: «Добавь в отчёт выручку по каждому заказу, подели заказы на крупные и мелкие, назначь промо-скидки — и заодно покажи, какие товары дают основной оборот». В Excel это колонки с формулами, протянутые на тысячу строк. В pandas новый столбец — одно присваивание, а вся логика считается сразу для всех строк. Именно этим мы и займёмся.
Умея добавлять столбцы, вы превращаете сырые данные в метрики: выручку, средний чек, долю заказа в обороте. Это последний вычислительный навык перед группировкой через groupby и финальным проектом. Заодно разберёмся, где у новичков проседает производительность: подсказываю, почти всегда виноват apply по строкам.
Как добавить новый столбец в dataframe
Возьмём знакомую по прошлым урокам таблицу заказов и сразу посчитаем в ней выручку:
import pandas as pd
data = {
"order_id": [1001, 1002, 1003, 1004, 1005],
"city": ["Москва", "Казань", "Москва", "Сочи", "Казань"],
"product": ["Ноутбук", "Смартфон", "Планшет", "Наушники", "Ноутбук"],
"price": [54990, 24990, 31990, 8990, 57990],
"qty": [1, 2, 1, 3, 1],
}
df = pd.DataFrame(data)
df["revenue"] = df["price"] * df["qty"]
print(df)
print("Общая выручка:", df["revenue"].sum())
order_id city product price qty revenue 0 1001 Москва Ноутбук 54990 1 54990 1 1002 Казань Смартфон 24990 2 49980 2 1003 Москва Планшет 31990 1 31990 3 1004 Сочи Наушники 8990 3 26970 4 1005 Казань Ноутбук 57990 1 57990 Общая выручка: 221920
Ответ на вопрос «как добавить новый столбец в dataframe» звучит почти обидно просто: df["имя"] = выражение. Если столбца с таким именем ещё нет, pandas создаст его в конце таблицы; если есть — молча перезапишет. А само выражение df["price"] * df["qty"] — это векторная операция: pandas умножил два столбца поэлементно сразу целиком, на уровне быстрых массивов NumPy, а не по одной строке в Python. Здесь же ответ и на вопрос «как посчитать сумму по столбцу pandas»: df["revenue"].sum() — общая выручка 221920 рублей. Рядом живут mean(), max(), min() и count(), и все они работают мгновенно даже на миллионе строк.
Векторные операции: столбец на столбец и столбец на число
Со столбцами работает вся школьная арифметика: +, -, *, /, ** и %, а ещё сравнения, которые дают булевы маски из урока про фильтрацию. Число тоже участвует в операции — оно применяется к каждой ячейке. Посчитаем цену со скидкой 10 процентов и долю заказа в общей выручке:
# Скидка 10 процентов, округляем до целого
df["price_10"] = (df["price"] * 0.9).round(0).astype("int64")
# Доля заказа в общей выручке, в процентах
df["share"] = (df["revenue"] / df["revenue"].sum() * 100).round(1)
print(df[["order_id", "price", "price_10", "revenue", "share"]])
order_id price price_10 revenue share 0 1001 54990 49491 54990 24.8 1 1002 24990 22491 49980 22.5 2 1003 31990 28791 31990 14.4 3 1004 8990 8091 26970 12.2 4 1005 57990 52191 57990 26.1
Разберём цепочку по шагам. df["price"] * 0.9 — столбец на число, результат дробный: 49491.0, 22491.0 и так далее. round(0) убирает дробную часть, astype("int64") возвращает целочисленный тип — так отчёт выглядит опрятно. Второй столбец хитрее: df["revenue"].sum() — это одно число, 221920, и pandas честно поделит каждый элемент столбца на него. Получились проценты: ноутбук из Казани дал 26,1 процента оборота. Обратите внимание: если повторить присваивание df["share"] = ... ещё раз, старые значения просто заменятся — столбцы свободно перезаписываются.
np.where: категория по условию
Арифметика считает, но не решает. Директор же хочет: «бесплатная доставка для заказов от 40 000 рублей, остальным — платная». Для «если-то-иначе» над целым столбцом есть функция numpy np.where(условие, значение_если_True, значение_если_False). Условие — та самая булева маска из урока про loc и фильтрацию:
import numpy as np
df["delivery"] = np.where(df["price"] >= 40000, "бесплатно", "платно")
print(df[["order_id", "price", "delivery"]])
order_id price delivery 0 1001 54990 бесплатно 1 1002 24990 платно 2 1003 31990 платно 3 1004 8990 платно 4 1005 57990 бесплатно
Читается как тернарный оператор Python, только сразу для всех строк: где price >= 40000 — «бесплатно», где нет — «платно». Это ответ на типичный запрос «pandas добавить столбец по условию». Значения могут быть любыми: строки, числа, даже другие столбцы — np.where(df["qty"] > 1, df["price"], 0) тоже сработает.
np.select: когда вариантов больше двух
np.where умеет только две ветки, а промо-акций у магазина три: скидка 15 процентов для товаров от 50 000, скидка 5 процентов для товаров от 20 000 и «без скидки» остальным. Для этого есть np.select: первым аргументом идёт список условий-масок, вторым — список значений, третьим default для всего остального:
conditions = [
df["price"] >= 50000, # первый уровень
df["price"] >= 20000, # второй уровень
]
rates = ["скидка 15%", "скидка 5%"]
df["promo"] = np.select(conditions, rates, default="без скидки")
print(df[["order_id", "price", "promo"]])
order_id price promo 0 1001 54990 скидка 15% 1 1002 24990 скидка 5% 2 1003 31990 скидка 5% 3 1004 8990 без скидки 4 1005 57990 скидка 15%
Условия проверяются по порядку, и срабатывает первое истинное — поэтому более строгое условие ставьте выше. Если поменять уровни местами, всё утонет в «скидке 5 процентов», и никто этого не заметит, пока финансовый отдел не сверит цифры. Наушники за 8990 не подошли ни под одно условие и получили default.
map: значения по словарю
Частая бытовая задача: сопоставить значения столбца со справочником. Города — это менеджеры, категории — это ставки НДС, статусы заказов — человеческие подписи. Метод map принимает словарь и для каждого значения столбца подставляет значение по ключу:
manager = {"Москва": "Анна", "Казань": "Игорь", "Сочи": "Мария"}
df["manager"] = df["city"].map(manager)
print(df[["order_id", "city", "manager"]])
order_id city manager 0 1001 Москва Анна 1 1002 Казань Игорь 2 1003 Москва Анна 3 1004 Сочи Мария 4 1005 Казань Игорь
Кроме словаря, map принимает функцию (тогда работает как apply для Series) или другую Series, выровненную по индексу. Это из тех инструментов, что вы будете применять еженедельно. Но у map есть характер — сейчас покажу подводный камень.
small_dict = {"Москва": "Анна", "Казань": "Игорь"}
df["manager2"] = df["city"].map(small_dict)
print(df[["city", "manager2"]])
print("Строк без менеджера:", df["manager2"].isna().sum())
city manager2 0 Москва Анна 1 Казань Игорь 2 Москва Анна 3 Сочи NaN 4 Казань Игорь Строк без менеджера: 1
apply: своя функция для столбца
Когда логику не выразить ни арифметикой, ни словарём, в дело идёт apply: он вызывает вашу функцию для каждого значения столбца и собирает результаты в новый Series. Запрос «apply map lambda pandas примеры использования» — из тех, что гуглят годами, поэтому разберём связку целиком. Для короткой логики берут lambda:
df["customer"] = [
"Анна Петрова", "Игорь Смирнов", "Ольга Кузнецова",
"Пётр Иванов", "Мария Соколова",
]
df["surname"] = df["customer"].apply(lambda name: name.split()[0])
print(df[["customer", "surname"]])
customer surname 0 Анна Петрова Анна 1 Игорь Смирнов Игорь 2 Ольга Кузнецова Ольга 3 Пётр Иванов Пётр 4 Мария Соколова Мария
lambda name: name.split()[0] — это «возьми первое слово». apply прогнал её по всем пяти строкам и собрал столбец. Для длинной функции пишут обычный def с именем — читабельнее для коллег. На одном столбце apply и map с функцией взаимозаменяемы, но по традиции map оставляют для словарей-замен, а apply — для вычислений.
apply по строкам axis=1: когда он действительно нужен
У apply есть второй режим — для DataFrame с axis=1: функция получает не одно значение, а целую строку, и может смотреть сразу несколько столбцов. Магазин вводит логистический тариф: для московских заказов от 40 000 доставка бесплатная, всем остальным — 350 рублей:
# Вариант 1: функция смотрит на всю строку
def order_fee(row):
if row["city"] == "Москва" and row["price"] >= 40000:
return 0
return 350
df["fee"] = df.apply(order_fee, axis=1)
# Вариант 2: тот же результат векторно
df["fee_fast"] = np.where(
(df["city"] == "Москва") & (df["price"] >= 40000), 0, 350
)
print(df[["order_id", "city", "price", "fee", "fee_fast"]])
order_id city price fee fee_fast 0 1001 Москва 54990 0 0 1 1002 Казань 24990 350 350 2 1003 Москва 31990 350 350 3 1004 Сочи 8990 350 350 4 1005 Казань 57990 350 350
Результаты идентичны. Разница — в механизме. np.where пускает в работу скомпилированные массивы NumPy и отвечает за миллисекунды. apply с axis=1 достаёт каждую строку как отдельный Python-объект, вызывает вашу функцию, упаковывает результат обратно — это обычный цикл for, просто написанный красиво. На пяти строках незаметно. На пятнадцати тысячах — перерыв на кофе.
import numpy as np
import pandas as pd
import time
np.random.seed(42)
big = pd.DataFrame({
"city": np.random.choice(["Москва", "Казань", "Сочи", "Уфа"], size=100_000),
"price": np.random.randint(1000, 80000, size=100_000),
})
def order_fee(row):
if row["city"] == "Москва" and row["price"] >= 40000:
return 0
return 350
t0 = time.perf_counter()
big["fee_apply"] = big.apply(order_fee, axis=1)
t1 = time.perf_counter()
big["fee_where"] = np.where(
(big["city"] == "Москва") & (big["price"] >= 40000), 0, 350
)
t2 = time.perf_counter()
print("apply(axis=1):", round(t1 - t0, 2), "сек")
print("np.where: ", round(t2 - t1, 4), "сек")
print("Результаты совпадают:", bool((big["fee_apply"] == big["fee_where"]).all()))
apply(axis=1): 0.4 сек np.where: 0.0061 сек Результаты совпадают: True
astype("category"): сжимаем повторяющиеся строки
Города, менеджеры, статусы — столбцы, где одни и те же строки повторяются тысячи раз. Хранить каждую строку отдельным Python-объектом расточительно. Тип category хранит уникальные значения один раз, а в столбце раскладывает только ссылки на них — целые коды:
cities = pd.DataFrame({"city": ["Москва", "Казань", "Москва", "Сочи", "Казань"]})
cities["city"] = cities["city"].astype("category")
print(cities.dtypes)
print(cities["city"])
print(cities["city"].value_counts())
city category dtype: object 0 Москва 1 Казань 2 Москва 3 Сочи 4 Казань Name: city, dtype: category Categories (3, object): ['Казань', 'Москва', 'Сочи'] city Казань 2 Москва 2 Сочи 1 Name: count, dtype: int64
Строка Categories (3, object) показывает словарь категорий — всего три уникальных значения на весь столбец. На учебных пяти строках выигрыша нет, зато на выгрузке в миллион строк переход object -> category превращает сотни мегабайт памяти в единицы и ускоряет groupby по этому столбцу. Бонус: категории сортируются по своему алфавиту, а не по частоте, и их список можно задать фиксированным — удобно для отчётов.
Кейс: выручка, уровни заказов и средний чек
Соберём рабочий конвейер: читаем исходные данные заново и за четыре строки строим метрики, которые директор просил на пятницу. Выручка — векторной операцией, уровень заказа — через np.select, средний чек (AOV, average order value) — делением суммы на число заказов:
df = pd.DataFrame(data)
df["revenue"] = df["price"] * df["qty"]
df["tier"] = np.select(
[df["revenue"] >= 50000, df["revenue"] >= 30000],
["крупный", "средний"],
default="малый",
)
aov = df["revenue"].mean()
print(df[["order_id", "revenue", "tier"]])
print("Средний чек (AOV):", round(aov, 2))
order_id revenue tier 0 1001 54990 крупный 1 1002 49980 средний 2 1003 31990 средний 3 1004 26970 малый 4 1005 57990 крупный Средний чек (AOV): 44384.0
Средний чек магазина — 44384 рубля. Два крупных заказа дают почти половину оборота, и это уже наблюдение, а не просто таблица. Заметьте: ни одного apply, ни одного цикла — только выражения над столбцами. Такой конвейер на миллионе строк отработает за доли секунды.
ABC-анализ: кто приносит выручку
Классика розницы — ABC-анализ в pandas пошагово: сортируем заказы по выручке, считаем накопленную долю и режем на классы. Класс A — заказы, дающие первые 70 процентов оборота, B — до 90, C — хвост. Всё это уже умеет наш набор инструментов:
abc = df.sort_values("revenue", ascending=False).copy()
abc["share"] = (abc["revenue"].cumsum() / abc["revenue"].sum() * 100).round(1)
abc["abc_class"] = np.where(
abc["share"] <= 70, "A", np.where(abc["share"] <= 90, "B", "C")
)
print(abc[["order_id", "revenue", "share", "abc_class"]])
order_id revenue share abc_class 4 1005 57990 26.1 A 0 1001 54990 50.9 A 1 1002 49980 73.4 B 2 1003 31990 87.8 B 3 1004 26970 100.0 C
cumsum() считает накопленную сумму — после сортировки это накопленная доля оборота. Два заказа класса A дают уже 50,9 процента выручки: именно им хочется персональных условий и контроля остатков. Вложенный np.where читается изнутри: сначала класс C для хвоста, потом B для середины, остальное — A.
Что дальше: даты и временные ряды
Все наши метрики пока статичны: посчитали — и всё. Но у каждого заказа есть дата, и главный вопрос бизнеса звучит так: растём или падаем? Для этого pandas умеет превращать строки в настоящие даты, агрегировать выручку по месяцам и сглаживать шум скользящим средним. В уроке про даты и временные ряды мы построим тренд продаж магазина через to_datetime, resample и rolling — и заодно поймаем пару коварных ловушек с форматами дат. А перед этим загляните в урок про очистку данных: новые столбцы на грязных данных считают кривые цифры. Полный маршрут из десяти уроков — самоучитель Pandas с нуля, и новые столбцы здесь восьмая ступень.
Пройдите тест и решите задачу ниже: добавьте столбцы выручки и доставки руками. После этого apply по строкам вы будете писать только с мыслью «а точно ли нельзя векторно?» — и это правильная мысль.
Сначала предскажи ответ в голове — это главный навык программиста.
import pandas as pd
df = pd.DataFrame({"price": [100, 200, 300], "qty": [1, 2, 3]})
df["revenue"] = df["price"] * df["qty"]
print(df.loc[df["qty"] > 1, "revenue"].sum())
import pandas as pd
df = pd.DataFrame({"city": ["Москва", "Сочи", "Казань"]})
m = {"Москва": "Анна", "Казань": "Игорь"}
print(df["city"].map(m).isna().sum())
1. Как добавить в df новый столбец revenue, равный цене умножить на количество?
2. Что вернёт np.where(df["price"] >= 40000, "бесплатно", "платно")?
3. В словаре для map нет ключа «Сочи», а в столбце city Сочи есть. Что произойдёт?
4. Почему apply с axis=1 медленный и что используют вместо него?
5. Зачем столбец с повторяющимися городами приводят к типу category?
В таблице шесть заказов. Добавьте столбец revenue (price умножить на qty) и столбец delivery через np.where: выручка от 50 000 рублей — «бесплатно», иначе — «платно». Выведите столбцы order_id, revenue и delivery.
Как добавить новый столбец в DataFrame?
Присваиванием: df["revenue"] = df["price"] * df["qty"]. Справа может быть любое выражение над столбцами, число или массив той же длины. Для столбца по условию удобно np.where, для замены значений по справочнику — map, для сложной логики — apply.
Чем apply отличается от map в pandas?
map работает только с Series: подставляет значения из словаря или применяет функцию к каждому элементу. apply умеет больше: работает и с Series, и с DataFrame, а с axis=1 получает целую строку сразу. Для замены значений по справочнику берите map, для вычислений — векторные операции, и только потом apply.
Почему apply по строкам (axis=1) медленный и чем его заменить?
apply с axis=1 превращает каждую строку в Python-объект и вызывает вашу функцию в цикле — это на порядки медленнее компилированных операций NumPy. В большинстве случаев заменой служат арифметика над столбцами, np.where и np.select для условий и map для справочников: на 100 000 строк разница доходит до сотен раз.
После map в столбце появились NaN. Что это значит?
Значит, в словаре не нашлось ключа для каких-то значений столбца — map не сообщает об этом, а молча ставит NaN. Найдите пропуски через df["col"].isna().sum() и решите: расширить словарь, заполнить через fillna("не назначен") или отфильтровать строки.
Как добавить столбец по условию в pandas?
Для двух веток — np.where(df["price"] >= 40000, "бесплатно", "платно"), для нескольких уровней — np.select со списком условий-масок и default для остальных. Дозаполнить уже существующий столбец у выбранных строк можно через df.loc[df["price"] > 30000, "bonus"] = 500 — присваивание через цепочку срезов вместо этого даст SettingWithCopyWarning.
Понравился урок? Сошлитесь на него
«apply по строкам — это скрытый цикл по Python-объектам, самый медленный способ считать в pandas.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
Pandas · Урок 7
Объединение таблиц: merge, concat и join в Pandas
merge с параметром how как SQL JOIN, concat для склейки выгрузок и join по индексу — соединяем товары, заказы и клиентов в одну таблицу и не теряем строки по дороге.
Pandas · Урок 9
Даты и временные ряды в Pandas: resample и rolling
Строки «01.07.2024» превращаются в настоящие даты, выручка агрегируется по неделям и месяцам через resample, а шум сглаживает rolling — строим тренд продаж магазина.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
NumPy · Урок 8
Булева индексация и маски: фильтрация данных в NumPy
Достаём из массива только нужное: булевы маски, операторы & | ~ вместо and/or, np.where в двух ролях, argmax и честная чистка выбросов в данных датчика.
np.where примерымаски numpy с несколькими условиями
sqlite3 · Урок 4
WHERE и фильтрация в SQL: условия, LIKE, BETWEEN и IN
Главный фильтр SQL: сравнения и логика с AND, OR, NOT, шаблоны LIKE с % и _, диапазоны BETWEEN, списки IN — и классическая ловушка = NULL.
sql wherewhere sql
Pandas · Урок 6
Сортировка, топы и подсчёты: sort_values, value_counts, rank
sort_values по нескольким столбцам, nlargest вместо сортировки всей таблицы, частоты value_counts и рейтинги rank — собираем отчёты «топ-10 товаров» и «доли городов».
pandas sort_valuesсортировка dataframe