Как ускорить pandas: память, типы и обработка больших файлов
Что делать, если pandas медленно работает или не помещает файл в память: категории, downcast, chunksize, Parquet и контроль размера данных.
Содержание статьи
Большой CSV начинает медленно открываться, ноутбук съедает всю память, а простая группировка занимает минуты. Первая реакция — переписать всё на другой инструмент. Но часто проблема проще: pandas читает лишние колонки, хранит категории как длинные строки и создаёт несколько копий DataFrame.
Коротко
Оптимизация начинается с измерения: сколько строк, колонок и памяти занимает таблица. Затем уменьши вход — выбери поля и период, задай типы, переведи повторяющиеся категории в category, а большой файл обрабатывай кусками или в Parquet.
- Не загружай в память поля, которые не участвуют в расчёте.
- Сначала проверь
memory_usage, потом меняй dtype. - При chunking агрегируй каждый кусок и объединяй маленькие результаты.
- Не делай
.copy()и.merge()для огромной таблицы без необходимости.
Узнай, что занимает память
Размер файла на диске не равен размеру DataFrame в памяти. Строки и object-колонки часто занимают больше, чем ожидается. Профиль по колонкам помогает найти главный источник расхода, а не оптимизировать случайное поле.
memory = (
orders.memory_usage(deep=True)
.sort_values(ascending=False)
)
print(memory)
print('total MB:', memory.sum() / 1024**2)Выбери колонки и типы до чтения
Если отчёт использует пять полей, не загружай пятьдесят. Для целых чисел укажи подходящий тип, а повторяющиеся строки переведи в category. Это не универсальное правило: сначала убедись, что диапазон значений помещается в выбранный dtype и что категория не содержит почти уникальную строку на каждую запись.
orders = pd.read_csv(
'data/orders.csv',
usecols=['order_id', 'created_at', 'channel', 'revenue'],
dtype={'order_id': 'int32', 'channel': 'category'},
parse_dates=['created_at'],
)
orders['revenue'] = pd.to_numeric(orders['revenue'], downcast='float')Большой CSV обрабатывай частями
chunksize возвращает итератор DataFrame. Не нужно складывать все куски в список: посчитай агрегат внутри цикла и объедини небольшие результаты. Важно помнить, что mean по средним кускам может быть неверным; для среднего сохраняй сумму и количество.
parts = []
for chunk in pd.read_csv('data/orders.csv', chunksize=100_000):
part = (
chunk.groupby('channel', as_index=False)
.agg(revenue=('revenue', 'sum'), orders=('order_id', 'nunique'))
)
parts.append(part)
report = (
pd.concat(parts)
.groupby('channel', as_index=False)
.sum()
)Суммы и количества можно складывать по кускам. Уникальных пользователей нельзя просто суммировать: один user_id может попасть в несколько chunks. Для точного результата используй другое хранилище или сохраняй множество ключей, если оно помещается в память.
Parquet для повторных чтений
Если один и тот же CSV читается много раз, преобразуй его в Parquet после первичной очистки. Колонночный формат хранит типы, читает только нужные поля и обычно быстрее подходит для аналитических выборок. Но Parquet не исправляет неверное зерно и не заменяет контроль источника.
orders.to_parquet('data/clean/orders.parquet', index=False)
report = pd.read_parquet(
'data/clean/orders.parquet',
columns=['created_at', 'channel', 'revenue'],
)Сначала найди узкое место
Медленная строка кода не всегда является причиной медленного анализа. Чтение CSV может занимать больше времени, чем сама агрегация, а merge может создать временную копию, которая съест остаток памяти. Поэтому замерь этапы отдельно: чтение, очистку, соединение и группировку. В ноутбуке достаточно time.perf_counter, а для устойчивого сравнения — нескольких прогонов на одном и том же файле.
Сравнивай не только секунды, но и результат. После оптимизации должны совпадать число уникальных заказов, сумма выручки, диапазон дат и количество unknown. Если новая версия быстрее потому, что потеряла строки, это не оптимизация. Для больших наборов сохрани небольшой контрольный файл, на котором старая и новая реализации дают одинаковый ответ.
Отдельно следи за пиковым потреблением памяти. Три DataFrame по 400 МБ могут появиться в одной цепочке из-за временных результатов, хотя каждый объект по отдельности выглядит допустимым. Иногда достаточно разбить выражение на этапы и удалить промежуточный объект, иногда лучше изменить порядок операций и отфильтровать данные до merge.
from time import perf_counter
started = perf_counter()
orders = pd.read_csv('data/orders.csv', usecols=USECOLS)
read_seconds = perf_counter() - started
started = perf_counter()
report = orders.groupby('channel', as_index=False).agg(revenue=('revenue', 'sum'))
group_seconds = perf_counter() - started
print({'read': read_seconds, 'groupby': group_seconds})dtype — это договор о диапазоне и точности
Уменьшать тип числа можно только после проверки его диапазона. int16 не подходит для идентификатора, если тот может выйти за пределы 32 767, а float32 способен дать заметную погрешность в денежных расчётах. Для денег чаще храни исходное значение в целых копейках или используйте decimal-логику в слое, где нужна точность. Экономия памяти не должна менять финансовый итог.
Категория хорошо работает для канала, страны или тарифа, когда набор значений повторяется. Для почти уникального transaction_id она может не дать выигрыша и усложнить merge. После изменения типа сравни размер памяти, уникальные значения и пропуски. Проверка orders.dtypes сама по себе не гарантирует, что данные стали корректными.
С датами есть отдельная ловушка: parse_dates ускоряет дальнейшую работу, но не исправляет смешанные форматы. Если часть строк превратилась в NaT, посчитай их долю и вынеси записи в quarantine. Иначе фильтр периода молча исключит часть заказов и создаст красивую, но неполную линию на графике.
| Поле | Кандидат | Что проверить |
|---|---|---|
| канал или тариф | category | число уникальных значений и unknown |
| счётчик событий | int32 или int64 | максимум за период и риск переполнения |
| день или флаг | int8 / boolean | нет ли специальных кодов кроме 0 и 1 |
| выручка | float64 или целые копейки | допустимая точность и возвраты |
| timestamp | datetime64[ns, UTC] | ошибки парсинга и часовой пояс |
Chunking меняет не только память, но и алгоритм
Обработка кусками подходит для ассоциативных агрегатов: суммы, количества, минимумы и максимумы можно посчитать частями и затем объединить. Среднее нужно собирать через сумму и количество. Медиана, точные уникальные пользователи и некоторые квантили не складываются так просто. Для них заранее выбери: хранить необходимое состояние, использовать приближённый алгоритм или перенести расчёт в аналитическую базу.
Порядок операций тоже важен. Если в каждой части оставить только нужные колонки и отфильтровать период, память снизится. Если сначала сделать дорогой merge со справочником, выигрыш от chunksize может исчезнуть. А если справочник маленький, его можно загрузить один раз и присоединять к очередному куску, не создавая копии большого лога.
Никогда не складывай nunique по chunks без проверки. Один пользователь, который был активен в двух кусках, будет посчитан дважды. Для точного DAU по всему файлу можно сохранять множества user_id по дню, но это снова требует памяти. Для регулярного отчёта лучше выполнять дедупликацию в хранилище или использовать движок, который умеет распределённые distinct-агрегаты.
Сначала выпиши, какое состояние нужно сохранить между кусками. Если итог нельзя получить из суммы маленьких итогов, обычный concat и groupby дадут неверный ответ.
Когда pandas уже не лучший инструмент
Pandas хорош для исследования, подготовки небольших и средних наборов и понятных batch-расчётов. Если файл стабильно не помещается в память, нужен повторяемый параллельный запуск или таблица живёт в десятках миллиардов строк, проблема уже не в одной оптимизации. Стоит рассмотреть SQL в warehouse, DuckDB для локальных файлов, Polars или Spark — в зависимости от среды и требований команды.
Переход не должен быть реакцией на один медленный ноутбук. Сначала зафиксируй объём, SLA, частоту обновления, необходимость точных distinct и стоимость поддержки. Иногда перенос одного тяжёлого JOIN в SQL решает проблему, а весь pipeline на новую библиотеку только увеличивает число мест, которые нужно сопровождать.
Практичный критерий — воспроизводимый бенчмарк. Возьми одинаковый вход, одинаковые фильтры и сравни время, пик памяти, совпадение контрольных чисел и сложность запуска. Выбирай инструмент, который команда сможет объяснить и поддерживать через полгода, а не только тот, у которого лучший результат в одном эксперименте.
- Оставаться в pandas: исследование, отчёт, файл помещается в память.
- DuckDB или SQL: повторяемые запросы по локальным или складским таблицам.
- Polars: локальный tabular pipeline с большим объёмом и строгими типами.
- Spark: распределённая обработка, когда её действительно требует объём.
Материалы по теме

Как загружать Excel, JSON и Parquet в pandas
Практический разбор источников в pandas: прочитать Excel и JSON, выбрать лист, сохранить типы и перейти с CSV на Parquet без сюрпризов.

Как тестировать аналитические расчёты на Python и pandas
Практический гайд по тестам для аналитика: проверить метрики на маленьком датасете, поймать регрессию и защитить расчёт от тихих изменений.

Pandas apply и векторизация: как писать быстрее и понятнее
Когда использовать apply, почему векторные операции быстрее и как переписать медленный построчный расчёт в pandas.