Производительность, best practices и что дальше
Итоговый урок: как сделать pandas-код быстрым и чистым, какие антипаттерны убивают производительность и где у pandas заканчиваются возможности.
Производительность pandas определяется тремя вещами: правильными типами данных, векторизацией вместо циклов и разумным объёмом данных для одной машины.
Рычаг 1: правильные dtypes и память
Память — частое узкое место. Грамотный выбор типов сокращает её в разы и заодно ускоряет операции:
- category для строковых столбцов с низкой кардинальностью (город, статус) — экономия в разы.
- Уменьшение разрядности:
int8/int16вместоint64,float32вместоfloat64, где диапазон позволяет (pd.to_numeric(..., downcast=...)). - Чтение только нужных столбцов (
usecols) — не грузите лишнее в память.
Проверять расход помогает df.memory_usage(deep=True) и df.info(memory_usage="deep"). Часто простой перевод нескольких столбцов в category снижает память кратно.
Рычаг 2: векторизация вместо циклов
Это главный закон pandas, который мы выводили в разделе про apply: операция над всем массивом на C в десятки-сотни раз быстрее, чем Python-цикл по строкам. Иерархия скорости от быстрого к медленному:
- Векторные операции (
df["a"] * df["b"], маски,.str,.dt) — почти всегда выбор №1. np.where/np.selectдля условной логики «если-то».- Встроенные
groupby.agg/transformс именованными функциями. apply— гибко, но медленно; только когда иначе нельзя.- Явный цикл
for/iterrows()— почти всегда антипаттерн.
Сравним «условную категоризацию» циклом и векторно (через идею np.where) по числу шагов:
prices = [990, 2500, 18000, 300, 7500]
# антипаттерн: Python-цикл, ветвление на каждой строке
labels_loop = []
for p in prices:
labels_loop.append("дорого" if p >= 5000 else "дёшево")
# векторная идея (np.where): одно выражение над всем массивом
labels_vec = ["дорого" if p >= 5000 else "дёшево" for p in prices]
print(labels_loop)
print("совпадают:", labels_loop == labels_vec)
Вывод:
['дёшево', 'дёшево', 'дорого', 'дёшево', 'дорого'] совпадают: True
В pandas это np.where(df["цена"] >= 5000, "дорого", "дёшево") — одно векторное выражение вместо цикла. На больших данных разница в скорости огромна.
Рычаг 3: eval и query для тяжёлых выражений
Для сложных арифметических выражений над большими DataFrame df.eval("...") и df.query("...") используют движок numexpr, который вычисляет выражение за один проход без создания множества промежуточных временных массивов:
df["итог"] = df.eval("цена * шт - скидка") # без промежуточных Series
big = df.query("цена > 1000 and город == 'Москва'")
Выигрыш заметен именно на больших данных и длинных выражениях; на маленьких разницы почти нет.
Антипаттерны, которых надо избегать
| Антипаттерн | Как правильно |
iterrows() / цикл по строкам | векторная операция или apply в крайнем случае |
Рост DataFrame через concat в цикле | собрать список и один concat в конце |
Цепочечное присваивание df[m]["c"]=… | df.loc[m, "c"] = … |
| Многократное чтение одного файла | прочитать раз, сохранить в parquet |
apply там, где есть векторный путь | арифметика столбцов, маски, np.where |
«Рост в цикле» особенно коварен: каждый concat копирует всю накопленную таблицу, и сложность становится квадратичной. Соберите куски в список и склейте одним вызовом.
Почему именно iterrows() так плох? Он не просто медленно итерирует — на каждой строке он создаёт новую Series со смешанным типом (ведь у разных столбцов разные dtypes), упаковывая и распаковывая значения. Это сотни тактов на строку там, где векторная операция тратит единицы. На миллионе строк разница между iterrows-циклом и векторной операцией легко достигает сотен раз — минуты против долей секунды. Если уж совсем нельзя без построчного прохода (редкий случай), itertuples() заметно быстрее iterrows(), потому что отдаёт лёгкие именованные кортежи без накладных расходов на Series. Но и это — последнее средство.
Профилирование: сначала измерь, потом оптимизируй
Прежде чем что-то ускорять, узнайте, что именно медленно. В интерактивном анализе для этого есть простой инструмент — измерение времени ячейки. Оптимизировать «на глазок» — частая ошибка: люди вылизывают код, который занимает 1% времени, и не замечают одного медленного merge или apply, съедающего остальные 99%. Типичный порядок действий: измерить общее время пайплайна, найти самый дорогой шаг, проверить, нет ли там скрытого цикла (apply, iterrows) или неоптимальных типов (строки вместо category), и точечно переписать именно его. Память профилируйте через df.info(memory_usage="deep") — часто оказывается, что половину занимают два-три строковых столбца, которые просятся в category.
Визуализация: быстрый взгляд через .plot
Для быстрой проверки данных у pandas есть встроенный .plot поверх matplotlib — не для финальных графиков, но идеально, чтобы «глазами» увидеть тренд или распределение:
df["продажи"].plot() # линия по индексу
df["цена"].plot(kind="hist", bins=30) # гистограмма распределения
df.groupby("город")["чек"].sum().plot(kind="bar") # столбцы по группам
Это часть рабочего цикла исследования: посчитал агрегат — тут же построил, увидел аномалию — пошёл разбираться.
Когда pandas не тянет: что дальше
pandas рассчитан на данные, помещающиеся в память одной машины (ориентир — до нескольких гигабайт). Когда данных больше или нужна параллельность, есть наследники и альтернативы:
| Инструмент | Когда |
| Polars | данные всё ещё на одной машине, но pandas медленный; ленивые вычисления, многопоточность, схожий API |
| Dask | данные больше памяти, но хочется pandas-подобный API; разбивает на части и считает параллельно |
| Spark (PySpark) | по-настоящему большие данные на кластере из многих машин |
| DuckDB | аналитический SQL прямо по файлам parquet/csv, не загружая всё в память |
Хорошая новость: навыки переносятся. Polars и Spark используют те же идеи — столбцы, выравнивание, group by, join, ленивость, — поэтому, освоив pandas, вы освоите и их быстрее.
Лучшие практики (сводка курса)
- Задавайте типы рано и точно; используйте
categoryи nullable-типы осознанно. - Векторизуйте: маски, арифметика столбцов,
.str/.dt,np.where— раньше, чемapply. - Присваивайте через
locодной операцией; включайте Copy-on-Write. - Защищайте merge через
validateи проверкуshape. - Храните промежуточные данные в parquet; читайте только нужные столбцы.
- Профилируйте память (
info,memory_usage) и не растите DataFrame в цикле.
Итог
- Три рычага скорости: правильные dtypes, векторизация, разумный объём данных.
eval/queryускоряют тяжёлые выражения на больших данных.- Главные антипаттерны — циклы по строкам и рост DataFrame в цикле.
- Когда pandas не тянет — Polars, Dask, Spark, DuckDB; навыки переносятся.