Условие
DataFrame занимает 8 ГБ. Менеджер выдал машину на 16 ГБ, но нужно ещё join со справочником на 4 ГБ. Как уменьшить df, не теряя данные?
Решение
Шаг 1: измерить
df.info(memory_usage='deep')
df.memory_usage(deep=True).sort_values(ascending=False)deep=True обязателен для object-колонок — иначе показывает только размер указателей.
Шаг 2: downcast чисел
import pandas as pd
for c in df.select_dtypes('integer').columns:
df[c] = pd.to_numeric(df[c], downcast='integer') # int64 → int8/16/32
for c in df.select_dtypes('float').columns:
df[c] = pd.to_numeric(df[c], downcast='float') # float64 → float32Шаг 3: category для строк с низкой кардинальностью
for c in df.select_dtypes('object').columns:
if df[c].nunique() / len(df) < 0.5:
df[c] = df[c].astype('category')category хранит уникальные значения один раз + int-коды → строка «active»/«inactive» в 100M строк = ~100 МБ вместо ~6 ГБ.
Шаг 4: pyarrow backend (pandas 2.0+)
df = df.convert_dtypes(dtype_backend='pyarrow')PyArrow строки компактнее object на 30-70%, и read_parquet быстрее.
Шаг 5: сбросить ненужные колонки рано
df = pd.read_parquet('events.parquet', columns=['user_id', 'amount', 'country'])Чтение columns= — самая дешёвая оптимизация.
Подводные камни
float32теряет точность — для денег, координат, ML-фичей оставлятьfloat64.category + intпосле merge может неожиданно стать object. Проверять dtype после операций.Int8(nullable) хорошо, но в groupby медленнее обычного int — баланс памяти и скорости.convert_dtypes(dtype_backend='pyarrow')не всё поддерживает (некоторые операции падают) — тестировать на куске.- Если df всё равно не лезет — переходить на чанки/polars/Dask, downcast не панацея.
Эталонный ответ
memory_usage(deep=True) → downcast чисел pd.to_numeric(downcast=) → строки в category → columns= при чтении → опционально dtype_backend='pyarrow'. Это даёт обычно 3-10× экономии.