Представьте: у вас есть файл с данными, которые вы хотите обработать в Pandas. Хочется быть уверенным, что память не закончится. Как оценить использование памяти с учетом размера файла?
Все эти оценки могут как занижать, так и завышать использование памяти. На самом деле оценивать использование памяти просто не стоит. А если конкретнее, в этой статье я:
- покажу широкий диапазон использования памяти ещё до обработки, только во время загрузки данных;
- расскажу о других подходах — измерении и передаче файла по частям.
"Обработка" — слишком широкое понятие
Есть ди хорошая эвристика, которая исходит из размера файла набора данных? Трудно сказать, ведь обработка данных может означать многое. Вот две крайности, когда вы:
- вычисляете максимальное значение столбца, и в этом случае память потрачена только на то, чтобы загрузить данные;
- выполняете перекрестное соединение между двумя столбцами длиной M и N, — объем памяти будет M×N; удачи вам, если это большие числа.
Итак, начнем с того, что оценка зависит от конкретной ситуации. Нужно понять код и детали реализации Pandas. Обобщенные умножения вряд ли будут полезны.
Но полезна была бы некоторая основная информация: объем памяти для данного DataFrame
или Series
. Для примеров выше это ключевой фактор при оценке использования памяти.
Какова же хорошая эвристика оценки использования памяти для загрузки набора данных по размеру файла? Ответ: она зависит от ситуации.
Использование памяти может оказаться намного меньше размера файла
Иногда использование памяти будет намного размера входного файла. Давайте сгенерируем CSV-файл в 1 000 000 строк с тремя числовыми столбцами; первый столбец — в диапазоне от 0 до 100, второй — от 0 до 10 000, третий — от 0 до 1 000 000.
import csv
from random import randint
with open("3columns.csv", "w") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["A", "B", "C"])
for _ in range(1_000_000):
writer.writerow(
[
randint(0, 100),
randint(0, 10_000),
randint(0, 1_000_000_000),
]
)
Файл весит 18 МБ. Теперь загрузим данные в dtype
соответствующего размера и измерим использование памяти:
import pandas as pd
import numpy as np
df = pd.read_csv(
"3columns.csv",
dtype={"A": np.uint8, "B": np.uint16, "C": np.uint32},
)
print(
"{:.1f}".format(df.memory_usage(deep=True).sum() / (1024 * 1024))
)
Итоговое использование памяти при запуске кода выше — 6.7MB, а это 0,4 от размера исходного файла.
Может Parquest поможет?
При представлении чисел в виде удобочитаемого текста (CSV) данные могут занимать больше байтов, чем представление в памяти. Например, \~90% случайных значений от 0 до 1 000 000 будут состоять из 6 цифр, поэтому в CSV потребуется 6 байтов. Однако в памяти для каждого np.int32 требуется всего 4 байта. Кроме того, в CSV есть запятые и новые строки.
Так что, возможно, другой, лучший формат файла может дать нам способ оценить использование памяти с учетом размера файла? Файл Parquet, например, хранит данные так, чтобы они точнее соответствовали представлению в памяти.
Эквивалент Parquet для примера выше составляет 7 МБ. По сути, столько же, сколько использование памяти. Так является ли размер файла Parquet хорошим индикатором в смысле оценки использования памяти?
Использование памяти может быть намного больше размера файла
Размер файла Parquest может вводить в заблуждение
Загрузим еще один файл Parquet из предыдущей статьи с размером в 20 МБ. Использование памяти при загрузке составляет… 300 МБ.
Одна из причин в том, что Parquet может сжимать данные на диске. В нашем исходном примере использовались случайные данные, а это означает, что сжатие помогает не сильно. Данные этого конкретного файла гораздо более структурированы, а значит, сжимаются гораздо лучше. Делать обобщения о том, насколько хорошо будут сжиматься данные, трудно; но, конено, можно представить себе случаи, когда данные сжимаются сильнее.
Другая причина, по которой данные могут быть меньше размера в памяти — строковое представление.
Строки — это весело
Может ли несжатый файл Parquet помочь нам оценить использование памяти? Для числовых данных разумно ожидать соотношения 1 к 1. Для строк… не обязательно.
Давайте создадим несжатый Parquet со строковым столбцом; строки в нём вроде таких — "A B A D E"
:
from itertools import product
import pandas as pd
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
data = {
"strings": [
" ".join(values) for values in product(
*([alphabet] * 5)
)
]
}
df = pd.DataFrame(data)
print(
"{:.1f}".format(df.memory_usage(deep=True).sum() / (1024 * 1024))
)
df.to_parquet("strings.parquet", compression=None)
Размер файла составляет 148 МБ, сжатия нет. Использование памяти — 748 МБ, в 5 раз больше. Разница в том, что Pandas и Parquet по-разному представляют строки; Parquest использует UTF-8, как и Arrow.
Использование строковой памяти Python также зависит от того, происходит ли [интернирование] (https://docs.python.org/3/library/sys.html#sys.intern) (деталь реализации CPython, которая может меняться в разных версиях Python), а также в том, записаны ли ваши строки в ASCII, содержат ли китайские символы или эмоджи.
>>> import sys
>>> sys.getsizeof("0123456789")
59
>>> sys.getsizeof("012345678惑")
94
>>> sys.getsizeof("012345678????")
116
Неэффективная загрузка
Иногда загрузка данных в DataFrame
может занять гораздо больше памяти:
-
Загрузка Parquest с помощью PyArrow может удвоить использование памяти, если сравнивать с
fastparquet
. - Загрузка из базы данных SQL без должного внимания может оказаться неэффективной.
- Если мы преобразуем столбец в категориальный только после загрузки, нам все равно придется загружать этот столбец в его первоначальной, неэффективной для памяти форме.
Размер файла не расскажет вам об использовании памяти
Подведем итоги:
Файл | Размер в памяти (МБ) | Размер на диске (МБ) | Соотношение |
---|---|---|---|
3columns.csv | 7 | 18 | 0.4× |
3columns.parquet | 7 | 7 | 1.0× |
strings.parquet | 748 | 148 | 5.0× |
MBTA.parquet | 300 | 20 | 15.0× |
Использование памяти может быть:
- меньше, чем размер файла;
- таким же;
- намного больше.
Общей эвристики, которая подскажет вам, сколько памяти Pandas будет использовать только для загрузки файла, нет, не говоря уже о том, сколько памяти потребуется для обработки данных.
Не удивлюсь, если несжатые файлы Parquet действительно дадут вам достойную оценку использования памяти в Polars (не Pandas!) в активном режиме, но я не проводил исследования, чтобы проверить предположение.
Альтернативы: измерение и передача по частям
Что же можно сделать?
Измерение использования памяти
Вместо того чтобы гадать, сколько памяти потребуется для обработки данных, ее можно измерить с помощью профайлера.
- Memray или Fil помогут найти часть кода, которая использует больше всего памяти.
- Sciagraph выполнит профилирование памяти и производительности, стремясь к малому оверхеду, чтобы с ним можно было рабоать в продакшене.
Эти инструменты могут помочь найти фактические узкие места памяти в вашем коде. Если данные не помещаются в памяти, это измерение нужно будет выполнить на подмножестве данных. Может помочь обнаружение утечек памяти в Fil.
Структурирование кода для обработки по частям
А еще вы можете просто не использовать столько памяти. Реструктурируя свой код для передачи данных по частям, вы можете обрабатывать файлы любого размера с постоянным объемом памяти. Сделать это вы сможете несколькими способами:
- Прямо в Pandas
- С помощью обертки Pandas от Dask.
- Переключившись в ленивый режим Polars, комбинированный с опцией передачи по частям.
На сегодня все. А на наших курсах — много практики и полезная теория:
Data Science и Machine Learning
- Профессия Data Scientist
- Профессия Data Analyst
- Курс «Математика для Data Science»
- Курс «Математика и Machine Learning для Data Science»
- Курс по Data Engineering
- Курс «Machine Learning и Deep Learning»
- Курс по Machine Learning
Python, веб-разработка
- Профессия Fullstack-разработчик на Python
- Курс «Python для веб-разработки»
- Профессия Frontend-разработчик
- Профессия Веб-разработчик
Мобильная разработка
Java и C#
- Профессия Java-разработчик
- Профессия QA-инженер на JAVA
- Профессия C#-разработчик
- Профессия Разработчик игр на Unity
От основ — в глубину
А также