Оптимизация памяти в pandas: практические приёмы
import numpy as np вы сравниваете операции над Python list и NumPy ndarray. Что верно для lst * 2 и arr * 2, где lst = [1, 2, 3], а arr = np.array([1, 2, 3])?Почему pandas ест память
Pandas под капотом использует массивы NumPy. Каждый тип данных занимает определённое количество байт:
int64— 8 байт на значение.float64— 8 байт.object(строки, смешанные типы) — 50+ байт на строку из-за Python-объектов.bool— 1 байт, но часто хранится как int8.
При чтении CSV pandas по умолчанию выбирает максимально вместительные типы: int64 для всех чисел, float64, object для строк. Это безопасно, но расточительно.
Для файла на 10 миллионов строк с 20 колонками это может означать 2-3 GB в памяти вместо 500 MB при оптимальных типах. На 100 миллионах строк ноутбук падает, а правильными типами всё бы прошло.
Диагностика
Перед оптимизацией — понять, сколько памяти съедает DataFrame:
df.info(memory_usage='deep')deep=True считает реальный размер, включая строки в object-колонках. Без этого параметра object-колонки показываются как 8 байт на указатель, что обманчиво.
Сколько памяти занимает каждая колонка:
df.memory_usage(deep=True).sort_values(ascending=False)Обычно топ-1-3 колонок съедают 80% памяти. Это object-колонки или большие int64 там, где хватило бы int16.
Сужение числовых типов
Самая простая оптимизация — сузить int- и float-типы.
df['count'] = df['count'].astype('int32') # 4 байта вместо 8
df['small_int'] = df['small_int'].astype('int16') # 2 байта
df['flag'] = df['flag'].astype('int8') # 1 байт
df['amount'] = df['amount'].astype('float32') # 4 байта вместо 8Диапазоны:
int8: -128 до 127.int16: -32768 до 32767.int32: ±2 миллиарда.int64: ±9 квинтильонов.
Если user_id до 10 миллионов — хватает int32. Если age до 150 — int8. Экономия в 4-8 раз.
Автоматическое сужение типа:
df['amount'] = pd.to_numeric(df['amount'], downcast='integer')
df['price'] = pd.to_numeric(df['price'], downcast='float')Pandas сам выберет минимальный тип. Удобно.
Categorical для строк с низкой кардинальностью
Колонки типа country, status, category часто имеют ограниченный набор значений. Хранить «Russia», «USA» как object — 50+ байт на строку. Всё это можно закодировать:
df['country'] = df['country'].astype('category')
df['status'] = df['status'].astype('category')Category хранит уникальные значения в отдельном массиве, а в основной колонке — int-индексы. Для колонки с 10 уникальными значениями и 10 миллионами строк экономия — десятки MB.
Бонус: операции groupby/sort на category-колонках быстрее, потому что сравнение int быстрее сравнения строк.
Когда не использовать category:
- Высокая кардинальность (например, email — уникальный для каждого). Там накладные расходы перевесят выигрыш.
- Текстовые операции (перевод в нижний регистр, split). Они потребуют обратной конвертации.
Правило: если уникальных значений меньше 1% от числа строк — category.
dtype при чтении
Оптимально задавать типы сразу при чтении, а не менять потом:
dtypes = {
'user_id': 'int32',
'amount': 'float32',
'category': 'category',
'is_active': 'bool',
'created_at': 'datetime64[ns]'
}
df = pd.read_csv('file.csv', dtype=dtypes, parse_dates=['created_at'])Это экономит и память (pandas не будет сначала читать как int64, потом конвертировать), и время.
Для object-колонок, которые потом станут category, можно передать так:
df = pd.read_csv('file.csv', dtype={'category': 'category'})Pandas сам распознаёт уникальные значения и кодирует.
Nullable integer
Стандартный int-тип в pandas не поддерживает NULL. Если в int-колонке есть NaN, pandas конвертирует её в float64 (потому что NaN — это float). Это увеличивает память.
В pandas 1.0 появились nullable-типы:
df['user_id'] = df['user_id'].astype('Int64') # заглавная IInt64 — отдельный тип, который поддерживает NA и остаётся целочисленным. Память меньше, чем у float64.
Также есть Float32, Float64, boolean — все nullable-версии.
Чтение по частям (chunksize)
Если файл настолько большой, что даже с оптимальными типами не помещается в память — читайте по частям:
chunks = []
for chunk in pd.read_csv('huge_file.csv', chunksize=100_000):
# Обработать чанк
processed = chunk[chunk['amount'] > 100]
chunks.append(processed)
df = pd.concat(chunks, ignore_index=True)Pandas читает файл по 100k строк за раз. Вы обрабатываете каждый чанк, накапливаете результат. Итоговый DataFrame может быть значительно меньше исходного файла, если обработка фильтрует или агрегирует.
Для агрегаций — ещё эффективнее: не накапливать чанки, а суммировать по мере обработки:
total_revenue = 0
for chunk in pd.read_csv('huge_file.csv', chunksize=100_000):
total_revenue += chunk['amount'].sum()Вся память занята только одним чанком в 100k строк.
Работа с большими данными в pandas — важная часть навыков middle-уровня. В тренажёре Карьерник есть задачи на оптимизацию Python-кода для аналитических задач.
Parquet вместо CSV
CSV — текстовый формат, неэффективный. Parquet — колоночный, сжатый.
Сравнение для одной таблицы на 10 миллионов строк:
- CSV: 5 GB на диске, 2 GB в памяти после чтения.
- Parquet: 1 GB на диске, 800 MB в памяти.
И parquet читается быстрее, и сразу сохраняет типы (не нужно переопределять).
# Сохранение
df.to_parquet('data.parquet', compression='snappy')
# Чтение
df = pd.read_parquet('data.parquet')Для регулярно используемых файлов — всегда parquet. CSV оставьте для обмена с сервисами, которые parquet не понимают.
Parquet также поддерживает чтение только нужных колонок:
df = pd.read_parquet('data.parquet', columns=['user_id', 'amount'])Если в parquet 50 колонок, а вам нужны 2 — чтение в 25 раз быстрее.
Удаление ненужных колонок сразу
Частая ошибка — прочитать все 50 колонок и потом выбрать 5. Лучше читать только 5:
# Плохо
df = pd.read_csv('big.csv')
df = df[['user_id', 'amount', 'date']]
# Хорошо
df = pd.read_csv('big.csv', usecols=['user_id', 'amount', 'date'])Это экономит и память, и время парсинга.
Удаление после использования
После того как DataFrame больше не нужен:
del df
import gc
gc.collect()Python освобождает память сборщиком мусора, но иногда это происходит с задержкой. Для длинных скриптов полезно делать это явно.
В Jupyter-ноутбуках ещё полезно:
df = NoneПеред повторным присваиванием, чтобы не держать две копии в памяти.
copy vs view
Pandas иногда создаёт view, иногда copy при срезах. Это влияет на память:
sub = df[df['amount'] > 100] # часто view (экономит память)
sub = df[df['amount'] > 100].copy() # явно copy (больше памяти, но безопаснее)Для долгоживущих DataFrame делайте copy явно, чтобы избежать SettingWithCopyWarning. Для быстрой фильтрации и агрегации — view подходит.
Когда ничего не помогает
Если даже после всех оптимизаций данные не помещаются:
Dask — pandas-подобный API для данных, которые не помещаются в память. Автоматически разбивает на чанки.
Polars — современная альтернатива, эффективнее pandas по памяти.
DuckDB — читать из файлов напрямую через SQL, без загрузки в Python.
Spark — для действительно больших объёмов на кластере.
Переход на эти инструменты — шаг серьёзный, но для задач на 10+ GB часто оправдан.
Измерение до и после
Итоговый процесс:
import pandas as pd
# Базовый замер
df = pd.read_csv('data.csv')
print(f'Before: {df.memory_usage(deep=True).sum() / 1024**2:.0f} MB')
# Оптимизация
for col in df.select_dtypes(include='int64').columns:
df[col] = pd.to_numeric(df[col], downcast='integer')
for col in df.select_dtypes(include='float64').columns:
df[col] = pd.to_numeric(df[col], downcast='float')
for col in df.select_dtypes(include='object').columns:
if df[col].nunique() < len(df) * 0.01:
df[col] = df[col].astype('category')
print(f'After: {df.memory_usage(deep=True).sum() / 1024**2:.0f} MB')Обычно вижу экономию 50-80% памяти. Для файла в 2 GB — это 400 MB вместо 2 GB. Ноутбук выживает.
Читайте также
FAQ
Сколько памяти экономят эти оптимизации?
Обычно 50-70% для файлов с разнообразными типами. Больше — если много object-колонок с малой кардинальностью.
Работает ли с MultiIndex?
Да, к каждому уровню индекса применимо то же (можно делать categorical).
Есть ли штраф по скорости?
Для category — наоборот, ускоряет groupby и sort. Для суженных числовых типов — небольшое замедление из-за менее оптимизированных операций NumPy, но обычно незаметно.
Когда оптимизация не помогает?
Когда все данные — уникальные строки (логи, URL, event ID). Там ничего не сжать, нужно переходить на чанки или другие инструменты.