Наверняка каждый, кто только начинает погружаться в анализ данных, сталкивался с этой классической проблемой. Вы скачиваете гигантский CSV-файл, по привычке пишете pd.read_csv(), запускаете ячейку и... кулеры начинают выть, система жутко тормозит, а в итоге скрипт падает с ошибкой нехватки памяти.
Первая мысль в такой ситуации — нужен компьютер помощнее или облачный сервер. На самом деле, чтобы переваривать огромные файлы, вовсе не обязательно наращивать оперативку. Проблема кроется в том, что по умолчанию мы пытаемся запихнуть весь объем данных в память целиком.
Существует довольно много простых техник, которые позволяют обойти это ограничение. Ниже мы разберем несколько таких приемов, которые спасают, когда ваши данные переросли возможности вашего железа. Пойдем от самых базовых к чуть более продвинутым.
Едим слона по частям (чтение чанками)
Если вам нужно просто посчитать какую-то общую метрику (например, сумму выручки по всем магазинам), нет никакого смысла держать в памяти всю таблицу. Pandas умеет читать файлы порциями, так называемыми чанками.
Вы загружаете, скажем, 100 тысяч строк, вытаскиваете из них нужную цифру, прибавляете к общей сумме и идете за следующей порцией.
import pandas as pd # Задаем размер порции (количество строк за один проход) chunk_size = 100000 total_revenue = 0 # Читаем файл кусками for chunk in pd.read_csv('shop_sales_data.csv', chunksize=chunk_size): # Считаем выручку только для текущего куска и плюсуем к общей total_revenue += chunk['revenue'].sum() print(f"Общая выручка: {total_revenue:,.2f} руб.")
В этом случае, даже если в файле 10 миллионов строк, ваша оперативная память будет одновременно хранить только 100 тысяч. Отличный подход для простых агрегаций или базовых математических операций.
Игнорируем ненужные колонки
Часто бывает так, что в таблице полсотни столбцов, а для конкретного анализа вам нужны только три: ID клиента, его возраст и сумма покупок. Если не сказать об этом явно, библиотека безжалостно потащит в память всё содержимое файла.
Чтобы этого избежать, используйте аргумент usecols.
import pandas as pd # Заранее определяем, что именно нас интересует columns_to_use = ['customer_id', 'age', 'purchase_amount'] # Загружаем только эти столбцы df = pd.read_csv('customers.csv', usecols=columns_to_use) # Теперь у нас в памяти легкий и аккуратный датафрейм average_purchase = df.groupby('age')['purchase_amount'].mean() print(average_purchase)
Вы просто передаете список нужных колонок, и Pandas игнорирует всё остальное еще на этапе чтения. Если изначально столбцов было много, этот трюк может сократить потребление памяти на 90% и больше.
Наводим порядок в типах данных
По умолчанию Pandas очень щедр на выделение памяти. Любое целое число он с большой вероятностью запишет как тяжеловесное 64-битное (int64). Но представьте, что у вас есть колонка с рейтингом товара от 1 до 5. Зачем тратить на нее 8 байт, если такое число отлично влезет в 1 байт (int8)?
import pandas as pd # Смотрим, сколько памяти таблица ест по умолчанию df = pd.read_csv('ratings.csv') print("Память до оптимизации:") print(df.memory_usage(deep=True)) # Жестко урезаем типы данных df['rating'] = df['rating'].astype('int8') # Для чисел от 1 до 5 этого хватит с головой df['user_id'] = df['user_id'].astype('int32') # Допустим, ID пользователей не такие уж огромные print("\nПамять после оптимизации:") print(df.memory_usage(deep=True))
Ручное приведение типов (int64 -> int32 или int8, float64 -> float32) помогает таблице экстремально похудеть без потери данных.
Используем тип category для текстов
Если в текстовой колонке постоянно повторяются одни и те же значения (например, названия городов, статусы заказов, пол клиентов), Pandas будет хранить каждую строку как отдельный текстовый объект. Это жутко неэффективно.
На помощь приходит тип данных category. Он сохраняет каждое уникальное слово в словаре только один раз, а в самой колонке оставляет компактные числовые указатели на эти слова.
import pandas as pd df = pd.read_csv('products.csv') # Замеряем вес колонки до конвертации (в мегабайтах) mem_before = df['category'].memory_usage(deep=True) / 1024**2 print(f"До: {mem_before:.2f} МБ") # Превращаем текст в категории df['category'] = df['category'].astype('category') # Замеряем после mem_after = df['category'].memory_usage(deep=True) / 1024**2 print(f"После: {mem_after:.2f} МБ") # При этом с колонкой всё еще можно работать как с обычным текстом print(df['category'].value_counts())
Внешне ничего не поменяется, вы сможете фильтровать и группировать данные как обычно, но вес колонки кардинально снизится. Главное, применяйте это только к тем столбцам, где уникальных значений мало (низкая кардинальность). Делать айдишники пользователей категориальными бессмысленно и даже вредно.
Отсеиваем лишнее прямо во время загрузки
Допустим, вам нужен полноценный датафрейм, но только с транзакциями за 2024 год. Загрузить всё и потом отфильтровать не выйдет. Зато можно скрестить чтение по кускам из первого пункта с фильтрацией на лету.
import pandas as pd chunk_size = 100000 filtered_chunks = [] for chunk in pd.read_csv('transactions.csv', chunksize=chunk_size): # Оставляем только нужный год в текущем куске filtered = chunk[chunk['year'] == 2024] filtered_chunks.append(filtered) # Склеиваем отфильтрованные огрызки в одну таблицу df_2024 = pd.concat(filtered_chunks, ignore_index=True) print(f"Успешно загружено {len(df_2024)} строк за 2024 год")
Мы читаем кусок, выкидываем из него лишние годы, сохраняем остаток в список и идем дальше. В конце просто собираем всё воедино через pd.concat. В оперативку попадает только то, что действительно необходимо для работы.
Подключаем тяжелую артиллерию в виде Dask
Если файл настолько гигантский, что даже возня с чанками становится невыносимой (или вы хотите задействовать все ядра своего процессора), стоит присмотреться к библиотеке Dask.
Она создана специально для таких случаев. API у Dask почти один в один как у Pandas, но под капотом библиотека сама разбивает данные на блоки и планирует параллельные вычисления.
import dask.dataframe as dd # Dask не грузит данные в память сразу, он только готовит план df = dd.read_csv('huge_dataset.csv') # Синтаксис привычный, такой же как и в Pandas result = df['sales'].mean() # Вычисления стартуют только в этот момент average_sales = result.compute() print(f"Средние продажи: {average_sales:,.2f} руб.")
В отличие от Pandas, который пытается выполнить любую команду «здесь и сейчас», Dask использует принцип ленивых вычислений. Когда вы пишете .read_csv() или .mean(), библиотека не трогает данные, а лишь строит «граф задач», подробную карту того, что нужно будет сделать.
Основная магия происходит в момент вызова .compute(). Сначала Dask виртуально разбивает ваш огромный файл на множество маленьких сегментов. Затем загружает эти сегменты по очереди или параллельно, задействуя все ядра вашего процессора. Пока одно ядро считает сумму в первом сегменте, другое уже подгружает следующий сегмент, а третье очищает память от обработанного кусочка.
Это спасение для тех ситуаций, когда таблица совершенно точно не помещается в RAM.
Работаем с мини-версией датасета на этапе разработки
Когда вы только исследуете данные, пишете код для графиков или собираете пайплайн для машинного обучения, гонять туда-сюда весь гигантский файл — это пустая трата нервов и времени. Куда умнее взять небольшую выборку, отладить логику на ней, и только потом натравливать готовый скрипт на полный объем.
Сделать это можно прямо в функции чтения:
import pandas as pd import random # Вариант 1: просто берем первые 50 тысяч строк df_head = pd.read_csv('huge_dataset.csv', nrows=50000) # Вариант 2: делаем случайную выборку (примерно 1% от всех данных) # x == 0 игнорировать нельзя, чтобы не пропустить строку с названиями колонок skip_rows = lambda x: x > 0 and random.random() > 0.01 df_random_sample = pd.read_csv('huge_dataset.csv', skiprows=skip_rows) print(f"Размер случайной выборки: {len(df_random_sample)} строк")
Первый вариант идеален, если вам просто нужно посмотреть на структуру данных. Второй вариант (со случайным пропуском строк) лучше подходит, если данные в файле как-то отсортированы, и первые строки не отражают общей картины.
Подводя итог
Как видите, укрощение больших данных не требует знаний высшей магии. В заключение приведу краткую шпаргалка, что и когда применять:
Чтение чанками: если нужны простые агрегации и нет возможности держать всё в памяти.
Выбор конкретных колонок: первое, что нужно сделать, если таблица слишком «широкая».
Оптимизация типов данных: делайте всегда, это бесплатное освобождение гигабайтов памяти.
Тип category: отлично сжимает колонки с повторяющимся текстом (статусы, города и т.д.).
Фильтрация на лету: идеально, когда из огромного файла нужно выцепить узкий срез данных.
Dask: когда файл слишком большой даже для ухищрений с Pandas, или когда нужно ускорить работу за счет распараллеливания.
Случайные выборки (сэмплирование): используйте на этапе написания и отладки кода.
Чаще всего комбинации из фильтрации колонок и правильных типов данных уже хватает, чтобы решить 90% проблем с нехваткой памяти. Ну а если вы чувствуете, что CSV-файлы окончательно перестали справляться с вашими аппетитами, нужно смотреть в сторону более современных и сжатых форматов хранения, таких как Parquet или HDF5. Но это уже тема, выходящая за пределы данной статьи.
