CSV на миллионы строк: pandas против чистого Python
Файл на 8 ГБ и 40 миллионов строк, а на машине 16 ГБ памяти. pandas падает с MemoryError, чистый Python читает без проблем, но медленно. Разбираем, где ломается каждый подход и что мерить.

Файл выгрузки транзакций: 8,2 ГБ, около 40 миллионов строк, 12 колонок. Задача простая — посчитать сумму по одной колонке в разрезе категории. pandas.read_csv(path) на машине с 16 ГБ падает с MemoryError примерно на середине чтения. Наивный цикл на чистом Python с csv.reader отрабатывает целиком, но за 90 секунд против 20 у pandas на файле, который в память влезает. Разберём, почему так, и когда какой инструмент выигрывает на объёмах в миллионы строк.
Всё ниже — про CPython 3.11 и pandas 2.x (важно: в pandas 2.x появился Arrow-бэкенд, он меняет расклад по памяти). Замеры условные, привожу порядок величин и, главное, что и как мерить у себя.
Почему pandas ест память кратно размеру файла
Ключевое заблуждение новичка: «файл 8 ГБ — значит нужно чуть больше 8 ГБ памяти». Это неверно. read_csv без указания типов проходит эвристикой по данным и подбирает dtype на колонку. Строковые колонки в pandas по умолчанию хранятся как Python-объекты (object): каждая ячейка — отдельный PyObject со своим оверхедом. Целые с пропусками уходят в float64, потому что NaN — это float.
Практический ориентир: DataFrame в памяти занимает от 2 до 10 раз больше исходного CSV в зависимости от доли строковых колонок. 8 ГБ файла легко превращаются в 30–50 ГБ объектов. Отсюда MemoryError.
Размер CSV на диске почти не говорит о том, сколько памяти займёт DataFrame. Решает не размер файла, а типы колонок и доля текста в них.
Что даёт чистый Python
Построчное чтение через csv.reader держит в памяти одну строку за раз. Память — константа, не зависящая от размера файла. Платите за это скоростью: каждая строка проходит через интерпретатор, парсинг чисел идёт в Python, а не в C.
Три рабочих подхода
Подход 1. pandas целиком, но с ужатыми типами
Если после ужатия типов данные влезают в память — это самый быстрый вариант для последующей аналитики. Указывайте dtype и категориальные колонки явно, не полагайтесь на эвристику.
# pandas 2.x, Python 3.11
import pandas as pd
# Явные типы: категории вместо object, int32 вместо int64 где хватает диапазона
dtypes = {
"category": "category", # повторяющиеся строки — резкая экономия
"amount": "float32",
"user_id": "int32",
}
df = pd.read_csv(
"transactions.csv",
dtype=dtypes,
usecols=["category", "amount", "user_id"], # читаем только нужные колонки
)
result = df.groupby("category")["amount"].sum()
print(result)
usecols здесь не мелочь: если из 12 колонок вам нужны 3, вы сразу режете память в четыре раза ещё до всякой оптимизации типов. category для колонки с несколькими сотнями уникальных значений на 40 миллионах строк экономит гигабайты.
Подход 2. pandas чанками
Когда даже ужатый DataFrame не влезает, читаем кусками фиксированного размера и агрегируем на лету. В память попадает только один чанк.
import pandas as pd
chunk_iter = pd.read_csv(
"transactions.csv",
usecols=["category", "amount"],
dtype={"category": "category", "amount": "float32"},
chunksize=1_000_000, # строк на чанк
)
partial_sums = []
for chunk in chunk_iter:
partial_sums.append(chunk.groupby("category")["amount"].sum())
# Складываем частичные суммы по совпадающим индексам-категориям
total = pd.concat(partial_sums).groupby(level=0).sum()
print(total)
Важная ловушка: category внутри чанка кодируется по значениям, встреченным в этом чанке. Сами суммы корректны, потому что при concat и groupby(level=0) группировка идёт по значению индекса, а не по внутреннему коду категории. Но не пытайтесь сравнивать коды категорий между чанками напрямую — они не совпадают.
Подход 3. Чистый Python потоком
Никаких зависимостей, память константна, поведение предсказуемо. Подходит, когда операция — простая агрегация или фильтрация, а не сложная аналитика.
import csv
from collections import defaultdict
sums = defaultdict(float)
with open("transactions.csv", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
# amount приходит строкой — парсим вручную
sums[row["category"]] += float(row["amount"])
for category, total in sums.items():
print(category, total)
DictReader удобен, но заметно медленнее обычного csv.reader: он создаёт словарь на каждую строку. На 40 миллионах строк это ощутимо. Если важна скорость — работайте по индексам колонок через csv.reader, а заголовок читайте отдельно первой строкой.
Сравнение подходов
| Критерий | pandas целиком | pandas чанками | Чистый Python |
|---|---|---|---|
| Пик памяти | 2–10× размера файла | размер одного чанка | константа, единицы МБ |
| Скорость простой агрегации | самая высокая | близко к целиком | в 3–5 раз медленнее |
| Сложная аналитика (join, pivot) | из коробки | руками, сложно | писать всё самому |
| Зависимости | pandas, numpy | pandas, numpy | только stdlib |
| Порог входа | низкий | средний | низкий по коду |
Когда что выбирать
- Данные влезают в память после ужатия типов — pandas целиком. Проверьте оценку памяти до чтения: перемножьте число строк на предполагаемый размер строки в байтах.
- Не влезают, но операция агрегирующая (сумма, счётчик, среднее по группам) — чанки. Средние считайте через сумму и количество раздельно, потом делите: наивное «среднее средних» по чанкам даёт неверный результат.
- Разовая простая обработка без аналитики, или окружение без права ставить пакеты — чистый Python. Одна колонка, один проход, минимум логики.
- Регулярная тяжёлая аналитика на файлах, что не влезают — присмотритесь к движкам с ленивым выполнением (Polars, DuckDB). Это отдельная тема, здесь их не разбираем, но на десятках гигабайт они часто снимают вопрос выбора между чанками и памятью.
Что ломается на практике
Граничные случаи, на которых теряется время:
- Кодировка. Файл из выгрузки 1С или Excel часто в
cp1251, а не UTF-8.openиread_csvбез явногоencodingупадут сUnicodeDecodeErrorгде-то на середине — там, где встретится первый нестандартный символ. Указывайте кодировку явно. - Разделитель. Русскоязычные выгрузки нередко идут через точку с запятой, а запятая внутри чисел — десятичный разделитель. Для pandas это
sep=";"иdecimal=",". - Переносы строк внутри поля. Многострочные значения в кавычках ломают наивный построчный разбор через
for line in file. Модульcsvи pandas это учитывают, ручнойsplit(",")— нет. Не парсите CSV черезstr.split. - float32 и потеря точности. Для денежных сумм
float32держит около 7 значащих цифр. На больших суммах после агрегации накапливается ошибка. Если считаете деньги — беритеfloat64илиDecimal, экономия памяти тут не стоит неверных цифр. - MemoryError без сообщения. На Linux процесс может убить OOM-killer молча — вы увидите
Killedв консоли, а не исключение Python. Если скрипт «просто пропал» — смотритеdmesg.
Что мерить и что делать дальше
Прежде чем оптимизировать, замерьте фактический пик памяти и время. Не гадайте по размеру файла.
- Пик памяти процесса — модулем
tracemallocиз стандартной библиотеки или внешним/usr/bin/time -v(строка Maximum resident set size). - Реальный размер DataFrame после чтения —
df.info(memory_usage="deep"):deepучитывает вес строковых объектов, обычный вывод его занижает. - Время —
time.perf_counter()вокруг чтения и вокруг агрегации отдельно, чтобы понять, где узкое место: в парсинге или в вычислении.
Следующий шаг, если файлы стабильно не влезают в память и приходят регулярно: посмотрите в сторону колоночных форматов. Один раз конвертировать CSV в Parquet — и последующее чтение нужных колонок станет в разы быстрее и легче по памяти. Это тоже отдельный разговор, но именно с него стоит начать, если обработка миллионов строк превратилась в рутину.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: pandas: Scaling to large datasets (официальная документация), Python csv — CSV File Reading and Writing
Частые вопросы
У меня pandas 1.x, а не 2.x — примеры сработают?
Скрипт просто пишет Killed и завершается, без ошибки Python — что это?
Чанки съедают меньше памяти, но результат отличается от чтения целиком — почему?
Насколько чистый Python медленнее pandas на самом деле?
Можно ли распараллелить обработку CSV, чтобы ускорить?
df.info() показывает один объём памяти, а процесс ест больше — где правда?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.