Коротко: .apply() и особенно iterrows() медленные, потому что гоняют питоновский цикл по строкам и на каждой итерации создают Python-объекты вместо работы с C-массивами. Замена на векторные операции (арифметика по колонкам, np.where, .map, .str-методы) убирает цикл из интерпретатора и ускоряет обработку в 10-100 раз на тех же данных. Правило простое: если операцию можно выразить через операции над целыми колонками — векторизуй; .apply оставляй только для сложной построчной логики, которую иначе не записать.
Разберём, откуда берётся тормоз, как переписать типовые случаи и когда .apply всё-таки оправдан — с замерами до и после.
Почему apply и iterrows так тормозят в pandas?
DataFrame внутри хранит данные не как список строк, а как набор колонок — каждая колонка это непрерывный C-массив (numpy ndarray). Быстрые операции pandas работают сразу над всем массивом в скомпилированном коде на C, минуя интерпретатор Python.
iterrows() делает ровно противоположное: он проходит по строкам питоновским циклом и на каждой итерации собирает из строки объект Series. Создание Series — это аллокация объекта, копирование значений, разбор типов. Для таблицы на миллион строк вы получаете миллион созданных и выброшенных объектов. Отсюда и катастрофическая медленность.
.apply(func, axis=1) немного умнее, но принципиально то же самое: он вызывает вашу питоновскую функцию func для каждой строки. Даже если тело функции — одно сложение, накладные расходы на вызов Python-функции миллион раз перевешивают саму работу.
Иерархия скорости для построчной обработки, от худшего к лучшему:
iterrows()— самый медленный, каждый раз лепитSeries.itertuples()— быстрееiterrowsв разы, отдаёт namedtuple без оверхедаSeries..apply(axis=1)— сравним сitertuples, чуть удобнее синтаксически..map/.applyпо одной колонке (Series.apply) — быстрее, работает над одним массивом.- Векторные операции — быстрее всего, цикла в интерпретаторе нет вообще.
Если хотите с нуля разобраться, как устроены Series и DataFrame и почему колонки — это массивы, начните с pandas с нуля: Series и DataFrame. А проверить себя на построчных преобразованиях удобно в Python-тренажёре.
Что такое векторизация и почему она в разы быстрее?
Векторизация — это выполнение операции сразу над всем массивом одной командой, а не поэлементно в цикле Python. Когда вы пишете df['a'] + df['b'], pandas не проходит по строкам на Python — он передаёт два C-массива в numpy, где сложение выполняется скомпилированным циклом на уровне процессора, часто с SIMD-инструкциями (одна инструкция обрабатывает несколько чисел за такт).
Разница в накладных расходах. В питоновском цикле на каждый элемент тратится: разбор байткода, проверка типов, разыменование объектов, boxing/unboxing чисел. В векторной операции этого нет — цикл выполняется в C, типы известны заранее, числа лежат подряд в памяти и хорошо кэшируются.
Грубая интуиция по стоимости одной операции:
| Подход | Где крутится цикл | Относительная скорость |
|---|---|---|
iterrows + Python | интерпретатор Python | 1x (база) |
itertuples / apply(axis=1) | интерпретатор Python | 3-10x |
Series.apply / .map | интерпретатор Python | 5-20x |
| Векторная операция (numpy/pandas) | скомпилированный C | 50-300x |
Числа в таблице — порядковые ориентиры, конкретика зависит от данных и операции. Но вывод устойчив: перенос цикла из Python в C даёт выигрыш на один-два порядка. Механику ufunc и векторизации в numpy подробно разбирали в numpy с нуля: векторизация и ufunc, а базовый курс — numpy с нуля.
Как переписать apply на векторные операции?
Самый частый антипаттерн — арифметика через .apply(axis=1). Возьмём расчёт выручки строки и наценки:
import pandas as pd
import numpy as np
df = pd.DataFrame({
'price': np.random.randint(100, 1000, size=1_000_000),
'qty': np.random.randint(1, 10, size=1_000_000),
'discount': np.random.rand(1_000_000),
})
# Медленно: питоновская функция на каждую строку
def revenue_row(row):
return row['price'] * row['qty'] * (1 - row['discount'])
df['revenue_slow'] = df.apply(revenue_row, axis=1)
Здесь apply(axis=1) вызывает revenue_row миллион раз. Векторная версия делает то же самое одной строкой над колонками:
# Быстро: операции над целыми колонками
df['revenue_fast'] = df['price'] * df['qty'] * (1 - df['discount'])
Результат идентичен, но вторая версия работает в десятки раз быстрее, потому что умножение и вычитание идут по всему массиву в numpy. Проверить равенство результатов:
assert np.allclose(df['revenue_slow'], df['revenue_fast'])
Такая построчная выручка потом сворачивается в продуктовые агрегаты — AOV (средний чек) или ARPU на пользователя. Если базовый расчёт по строкам делать через apply, то агрегация по десяткам миллионов строк встанет колом ещё до groupby, поэтому именно этот нижний слой обязан быть векторным.
Тот же принцип работает для условий, сравнений, логических операций, округления. Почти любую формулу «взять несколько колонок и что-то с ними сделать» можно записать без apply. Если внутри был if/else, его заменяет np.where (следующий раздел). Больше практики по преобразованиям — в разборе pandas с нуля: apply, str, dt, векторизация.
Когда логика содержит несколько столбцов и вложенные условия, часто помогает сначала посчитать промежуточные булевы маски по колонкам, а потом собрать результат — это остаётся полностью векторным.
Как использовать np.where и .map вместо apply?
Условная логика — второй по частоте повод для apply. Классика:
# Медленно: apply с if/else построчно
def segment(row):
if row['revenue_fast'] > 5000:
return 'high'
elif row['revenue_fast'] > 2000:
return 'mid'
else:
return 'low'
df['seg_slow'] = df.apply(segment, axis=1)
Одно условие переписывается через np.where(условие, значение_если_да, значение_если_нет):
# Быстро: одно условие
df['is_big'] = np.where(df['revenue_fast'] > 5000, 'big', 'small')
Несколько условий — через np.select, который принимает список масок и список значений:
# Быстро: несколько условий без apply
conditions = [
df['revenue_fast'] > 5000,
df['revenue_fast'] > 2000,
]
choices = ['high', 'mid']
df['seg_fast'] = np.select(conditions, choices, default='low')
np.select проверяет условия по порядку, как if/elif/else — первое сработавшее выигрывает. Это остаётся векторным: маски считаются над целыми колонками.
Отдельный случай — маппинг значений по словарю. Если у вас есть справочник «код → название», не пишите apply(lambda x: mapping.get(x)), используйте .map:
region_map = {1: 'Москва', 2: 'СПб', 3: 'Казань'}
df['region_code'] = np.random.randint(1, 4, size=len(df))
# Медленно
df['region_slow'] = df['region_code'].apply(lambda x: region_map.get(x, 'other'))
# Быстро: векторный маппинг по словарю
df['region_fast'] = df['region_code'].map(region_map).fillna('other')
.map со словарём реализован эффективно и обгоняет apply заметно, а читается чище. Такие приёмы постоянно всплывают на собеседованиях — типовые задачи собраны в разделе вопросы для собеседования, а живой прогон — в AI-интервьюере.
Как ускорить обработку текста через .str-методы?
Строки — та область, где apply кажется неизбежным, но чаще всего нет. У колонки-строки в pandas есть аксессор .str с векторизованными версиями почти всех строковых операций: .lower(), .strip(), .contains(), .replace(), .split(), .extract() с регулярками.
s = pd.Series([' Ivan@MAIL.ru ', 'PETR@yandex.ru', ' Olga@gmail.com '] * 300_000)
# Медленно: питоновская обработка строки на каждую ячейку
clean_slow = s.apply(lambda x: x.strip().lower())
# Быстро: цепочка .str-методов
clean_fast = s.str.strip().str.lower()
.str-методы всё ещё выполняют часть работы на уровне Python (обработка строк в CPython не так тривиально векторизуется, как числа), поэтому выигрыш тут скромнее, чем на арифметике — но он есть, а код заметно чище и меньше подвержен ошибкам с NaN (.str корректно пропускает пропуски вместо падения).
Извлечение по регулярке — тоже без apply:
# Достать домен из email одной операцией
domains = s.str.strip().str.extract(r'@(.+)$')
Для тяжёлой текстовой логики, которую .str не покрывает (сложный парсинг с состоянием), apply остаётся разумным выбором — об этом ниже. Строковые функции SQL и их аналоги в pandas удобно держать под рукой; см. шпаргалку по pandas.
Насколько быстрее векторизация — замеры %timeit?
Замерять надо самому: цифры зависят от размера данных, типов и версии pandas. Инструмент — %timeit в Jupyter (или timeit в скрипте). Возьмём миллион строк и сравним три версии расчёта выручки.
import pandas as pd
import numpy as np
n = 1_000_000
df = pd.DataFrame({
'price': np.random.randint(100, 1000, size=n),
'qty': np.random.randint(1, 10, size=n),
'discount': np.random.rand(n),
})
# 1. iterrows — самый медленный
def with_iterrows(df):
out = []
for _, row in df.iterrows():
out.append(row['price'] * row['qty'] * (1 - row['discount']))
return out
# 2. apply по строкам
def with_apply(df):
return df.apply(lambda r: r['price'] * r['qty'] * (1 - r['discount']), axis=1)
# 3. векторизация
def vectorized(df):
return df['price'] * df['qty'] * (1 - df['discount'])
В ячейках Jupyter:
%timeit with_iterrows(df) # порядка десятков секунд
%timeit with_apply(df) # порядка нескольких секунд
%timeit vectorized(df) # порядка единиц-десятков миллисекунд
Типичная картина по порядкам величины на такой задаче:
| Метод | Время на 1 млн строк | Во сколько раз медленнее векторной |
|---|---|---|
iterrows | ~30-60 с | ~1000-3000x |
apply(axis=1) | ~5-12 с | ~300-800x |
.map / Series.apply | ~0.3-0.8 с | ~20-60x |
| Векторизация | ~10-20 мс | 1x |
Конкретные числа у вас будут другими — важен порядок. На чистой арифметике векторизация легко даёт ×100 и больше против apply(axis=1) и ×1000 против iterrows. На строках разрыв меньше (×2-×10), потому что и .str частично работает в Python.
Практический совет по замерам:
- Гоняйте
%timeitна репрезентативном объёме, а не на 100 строках — на мелочи накладные расходы pandas смазывают картину. - Проверяйте, что результаты совпадают (
np.allcloseдля чисел,.equals()для Series), иначе ускорять нечего. - Смотрите не только время, но и память:
iterrowsс накоплением в список раздувает RAM.
Если данные перестали влезать в память или векторизация в pandas упирается в потолок на десятках миллионов строк, стоит посмотреть в сторону колоночных движков — сравнение есть в Polars против pandas.
В каких случаях apply всё-таки оправдан?
apply — не абсолютное зло. Он оправдан, когда векторной альтернативы нет или она сложнее и хуже читается. Реальные случаи:
- Сложная построчная логика с состоянием, вложенными условиями и обращением к внешним данным, которую не выразить через маски и
np.select. - Вызов внешней функции/библиотеки на каждую строку (парсер, геокодер, сложный форматтер), у которой нет векторного API.
- Разовый скрипт на маленьком датафрейме (тысячи строк), где скорость не важна, а читаемость важнее — микрооптимизация не окупает время разработчика.
groupby().apply()для агрегаций по группам, которые не сводятся к встроеннымagg-функциям (хотя и тут сначала стоит попробоватьtransform/agg).
Правило принятия решения:
- Можно выразить через арифметику/сравнения над колонками? — векторизуй.
- Это условие/выбор значения? —
np.whereилиnp.select. - Это замена значений по справочнику? —
.mapсо словарём. - Это строковая операция? —
.str-методы. - Ничего из этого не подходит и логика реально сложная? —
apply, но по одной колонке (Series.apply), а неaxis=1, если возможно. - Совсем тяжело и данных много? — вынеси в
numpy/numbaили смени движок.
Ещё нюанс: даже когда apply нужен, apply по одной колонке (df['x'].apply(f)) почти всегда быстрее, чем apply(axis=1) по всей строке, потому что не собирает Series из строки. Старайтесь сузить область до конкретных колонок.
Хотите закрепить это руками — в Python-тренажёре первые 5 задач открыты бесплатно: там есть кейсы на переписывание apply в векторную форму с проверкой результата прямо в браузере через Pyodide. Дальше, если зайдёт, открывается Pro с полным банком из 402 задач. Никакого давления — просто решаете и видите, где ваш код тормозит. Параллельно можно подтянуть базу в SQL-тренажёре и разложить теорию по полочкам в курсе pandas с нуля.
Короткий итог: тормозит не pandas, а питоновский цикл поверх него. Уберите цикл из интерпретатора — и те же данные обрабатываются на один-два порядка быстрее без единой новой строчки инфраструктуры. А apply держите как аккуратный инструмент для случаев, где векторизация правда не помогает.
Смежные материалы: pandas с нуля: groupby, agg, transform, pivot, шпаргалка по SQL для аналитика, Python на собеседовании: вопросы и задачи.