Все материалы
Продуктовая аналитикапрактикумсредний

Как ускорить pandas: память, типы и обработка больших файлов

Что делать, если pandas медленно работает или не помещает файл в память: категории, downcast, chunksize, Parquet и контроль размера данных.

КПКейсПрактика8 августа 2026 г.18 мин

Большой CSV начинает медленно открываться, ноутбук съедает всю память, а простая группировка занимает минуты. Первая реакция — переписать всё на другой инструмент. Но часто проблема проще: pandas читает лишние колонки, хранит категории как длинные строки и создаёт несколько копий DataFrame.

Коротко

Оптимизация начинается с измерения: сколько строк, колонок и памяти занимает таблица. Затем уменьши вход — выбери поля и период, задай типы, переведи повторяющиеся категории в category, а большой файл обрабатывай кусками или в Parquet.

  • Не загружай в память поля, которые не участвуют в расчёте.
  • Сначала проверь memory_usage, потом меняй dtype.
  • При chunking агрегируй каждый кусок и объединяй маленькие результаты.
  • Не делай .copy() и .merge() для огромной таблицы без необходимости.

Узнай, что занимает память

Размер файла на диске не равен размеру DataFrame в памяти. Строки и object-колонки часто занимают больше, чем ожидается. Профиль по колонкам помогает найти главный источник расхода, а не оптимизировать случайное поле.

pythonПрофиль памяти по колонкам
memory = (
    orders.memory_usage(deep=True)
    .sort_values(ascending=False)
)
print(memory)
print('total MB:', memory.sum() / 1024**2)

Выбери колонки и типы до чтения

Если отчёт использует пять полей, не загружай пятьдесят. Для целых чисел укажи подходящий тип, а повторяющиеся строки переведи в category. Это не универсальное правило: сначала убедись, что диапазон значений помещается в выбранный dtype и что категория не содержит почти уникальную строку на каждую запись.

pythonЧитать только нужное и уменьшить повторяющиеся категории
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 по средним кускам может быть неверным; для среднего сохраняй сумму и количество.

pythonСумма и количество по каналам без загрузки всего файла
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 не исправляет неверное зерно и не заменяет контроль источника.

pythonСохранить подготовленный слой в 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.

pythonИзмерить этапы без гадания
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 или целые копейкидопустимая точность и возвраты
timestampdatetime64[ns, UTC]ошибки парсинга и часовой пояс

Chunking меняет не только память, но и алгоритм

Обработка кусками подходит для ассоциативных агрегатов: суммы, количества, минимумы и максимумы можно посчитать частями и затем объединить. Среднее нужно собирать через сумму и количество. Медиана, точные уникальные пользователи и некоторые квантили не складываются так просто. Для них заранее выбери: хранить необходимое состояние, использовать приближённый алгоритм или перенести расчёт в аналитическую базу.

Порядок операций тоже важен. Если в каждой части оставить только нужные колонки и отфильтровать период, память снизится. Если сначала сделать дорогой merge со справочником, выигрыш от chunksize может исчезнуть. А если справочник маленький, его можно загрузить один раз и присоединять к очередному куску, не создавая копии большого лога.

Никогда не складывай nunique по chunks без проверки. Один пользователь, который был активен в двух кусках, будет посчитан дважды. Для точного DAU по всему файлу можно сохранять множества user_id по дню, но это снова требует памяти. Для регулярного отчёта лучше выполнять дедупликацию в хранилище или использовать движок, который умеет распределённые distinct-агрегаты.

Правило chunking

Сначала выпиши, какое состояние нужно сохранить между кусками. Если итог нельзя получить из суммы маленьких итогов, обычный concat и groupby дадут неверный ответ.

Когда pandas уже не лучший инструмент

Pandas хорош для исследования, подготовки небольших и средних наборов и понятных batch-расчётов. Если файл стабильно не помещается в память, нужен повторяемый параллельный запуск или таблица живёт в десятках миллиардов строк, проблема уже не в одной оптимизации. Стоит рассмотреть SQL в warehouse, DuckDB для локальных файлов, Polars или Spark — в зависимости от среды и требований команды.

Переход не должен быть реакцией на один медленный ноутбук. Сначала зафиксируй объём, SLA, частоту обновления, необходимость точных distinct и стоимость поддержки. Иногда перенос одного тяжёлого JOIN в SQL решает проблему, а весь pipeline на новую библиотеку только увеличивает число мест, которые нужно сопровождать.

Практичный критерий — воспроизводимый бенчмарк. Возьми одинаковый вход, одинаковые фильтры и сравни время, пик памяти, совпадение контрольных чисел и сложность запуска. Выбирай инструмент, который команда сможет объяснить и поддерживать через полгода, а не только тот, у которого лучший результат в одном эксперименте.

  • Оставаться в pandas: исследование, отчёт, файл помещается в память.
  • DuckDB или SQL: повторяемые запросы по локальным или складским таблицам.
  • Polars: локальный tabular pipeline с большим объёмом и строгими типами.
  • Spark: распределённая обработка, когда её действительно требует объём.
Продолжить чтение
Вся библиотека