pandas быстр, пока вы работаете целыми колонками. Как только код идёт по строкам, он замедляется в десятки и сотни раз. На собесе middle и senior обязательно спросят, как ускорить медленный ноутбук и как уместить данные в память.
Векторизация
Векторизованная операция применяется ко всей колонке сразу, цикл выполняется внутри NumPy на C.
# медленно: Python-вызов на каждую строку
df['total'] = df.apply(lambda r: r['qty'] * r['price'], axis=1)
# быстро: одна операция над массивами
df['total'] = df['qty'] * df['price']
На миллионе строк разница обычно в сотни раз. Иерархия скорости, от лучшего к худшему:
- встроенные векторные операции pandas и NumPy;
- методы
.str,.dt,mapпо словарю; applyпо колонке (Series.apply);apply(axis=1),iterrowsи обычный цикл.
itertuples заметно быстрее iterrows, но это всё равно цикл в Python.
Условия без apply
import numpy as np
df['size'] = np.where(df['amount'] > 1000, 'big', 'small')
df['tier'] = np.select(
[df['amount'] < 100, df['amount'] < 1000],
['low', 'mid'],
default='high',
)
np.select проверяет условия по порядку, как цепочка if/elif/else. Для интервалов есть pd.cut, для маппинга кодов map со словарём.
Память: типы данных
Проверка: df.memory_usage(deep=True) или df.info(memory_usage='deep'). deep=True важен для строк: без него pandas показывает только размер указателей.
Способы ужать таблицу:
- category для строк с небольшим числом уникальных значений (город, статус, платформа). Хранится словарь значений и маленькие целые коды. Экономия бывает в десятки раз, а
groupbyпо категориям быстрее. - downcast чисел:
pd.to_numeric(s, downcast='integer')выберет минимальный подходящий тип (int8,int16...). Вместоfloat64часто хватаетfloat32. - читать только нужное:
usecols,dtypeпрямо вread_csv.
Ловушка category: при конкатенации или merge таблиц с разными наборами категорий колонка может молча стать обычной строкой. И category бесполезна, если почти все значения уникальны.
Copy-on-Write в pandas 3.0
В pandas 3.0 по умолчанию включён режим Copy-on-Write. Главное следствие: любой объект, полученный из DataFrame (колонка, срез, результат фильтра), ведёт себя как независимая копия. Изменить исходную таблицу через него нельзя.
df = pd.DataFrame({'a': [1, 2]})
col = df['a']
col.iloc[0] = 100
df['a'].tolist() # [1, 2] в pandas 3.0 (в 2.x было [100, 2])
Цепочное присваивание df[df['a'] > 1]['b'] = 0 в pandas 3.0 никогда не меняет df. Правильно одной операцией: df.loc[df['a'] > 1, 'b'] = 0. Знаменитое предупреждение SettingWithCopyWarning ушло вместе со старым поведением.
Бонус CoW: копии создаются лениво, только при записи. Цепочки методов вроде df.rename(...).fillna(...).query(...) больше не копируют данные на каждом шаге.
Когда pandas мало
- Polars: колоночный движок на Rust, многопоточный, с ленивыми запросами. На группировках и join по десяткам миллионов строк обычно в разы быстрее pandas.
- DuckDB: SQL прямо по DataFrame, CSV и Parquet, тоже многопоточный, удобен для тяжёлых агрегатов.
- Spark: когда данные не помещаются на одну машину.
На собесе хороший ответ звучит так: сначала профилирую и убираю циклы, ужимаю типы, и только потом меняю инструмент.
Как искать узкое место
%timeitв Jupyter для одной строки,%%timeдля ячейки.%prunилиline_profiler, чтобы найти медленную функцию.- Проверьте, не вызывается ли
applyилиiterrowsвнутри цикла, и не растёт ли DataFrame черезconcatв цикле.
Частые ошибки
apply(axis=1)для арифметики, которую можно записать колонками.iterrowsс присвоением вdf.loc[i, ...]внутри цикла: медленно вдвойне.- Строки вместо
categoryна сотнях миллионов строк. memory_usage()безdeep=True, и строки «ничего не весят».- Цепочное присваивание, которое в pandas 3.0 ничего не меняет.
Что спросят на собесе
- Почему
apply(axis=1)медленный и чем его заменить? - Как уменьшить память, которую занимает DataFrame?
- Когда
categoryпомогает, а когда вредит? - Что такое Copy-on-Write и что поменялось в pandas 3.0?
- Когда стоит переходить на Polars, DuckDB или Spark?