Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 8 из 10 Средний 40 мин 150 XP

Новые столбцы в 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 и фильтрацию:

np.where: две категории доставки
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 для всего остального:

np.select: три уровня промо
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 принимает словарь и для каждого значения столбца подставляет значение по ключу:

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:

apply с 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 рублей:

apply по строкам и его векторная замена
# Вариант 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, просто написанный красиво. На пяти строках незаметно. На пятнадцати тысячах — перерыв на кофе.

Замер на 100 000 строк
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 хранит уникальные значения один раз, а в столбце раскладывает только ссылки на них — целые коды:

Столбец 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-анализ за четыре строки
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())
Проверь себя
0 / 5

1. Как добавить в df новый столбец revenue, равный цене умножить на количество?

2. Что вернёт np.where(df["price"] >= 40000, "бесплатно", "платно")?

3. В словаре для map нет ключа «Сочи», а в столбце city Сочи есть. Что произойдёт?

4. Почему apply с axis=1 медленный и что используют вместо него?

5. Зачем столбец с повторяющимися городами приводят к типу category?

Карточки терминов
Запомнено: 0 / 6
Практика

В таблице шесть заказов. Добавьте столбец revenue (price умножить на qty) и столбец delivery через np.where: выручка от 50 000 рублей — «бесплатно», иначе — «платно». Выведите столбцы order_id, revenue и delivery.

practice.py
Вопросы и ответы по уроку

Как добавить новый столбец в 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.