Почему pandas.DataFrame ест так много памяти?

Есть файл Excel размером 37 Мб. Я хочу обработать его с помощью python, и выбрал для этого pandas как очень мощный инструмент для обработки больших объёмов данных. Возникла такая проблема: когда я открываю этот файл в pandas:

from pandas import read_excel
table = read_excel("my_table.xlsx")

я в диспетчере задач вижу что мой скрипт кушает пол-гига оперативной памяти, и начинают происходить очень странные вещи:

sys.getsizeof(table) == 1238122071  # это больше гигабайта
sys.getsizeof(table.values) == 112  # 112 байт
sys.getsizeof(table.values.tolist()) == 5045384  # ~5 мегабайт

Почему такой большой разброс размеров? Плюс ко всему когда я делаю

del table

или

table = table.values

потребление памяти скриптом в диспетчере задач не снижается - почему? Я буду очень благодарен если кто-нибудь объяснит мне все эти вещи. Основной вопрос: Как мне снизить потребление памяти программой? Просто я делаю на заказ прогу, которая работает в фоновом режиме, и очень не хочется чтобы заказчик случайно заглянув в диспетчер задач увидел там такое. Всем заранее спасибо за ответы!


Ответы (1 шт):

Автор решения: CrazyElf

Посмотрите, что выдаёт такая команда:

table.info(memory_usage='deep')

Скорее всего у вас там текстовые данные, причём сильно повторяющиеся. xlsx - это упакованный формат данных, он может эффективно сжимать текстовые данные. А вот в Pandas по умолчанию строки будут кушать очень много памяти.

Если у вас там действительно окажутся сильно повторяющиеся текстовые данные, то можно их перевести в категориальный формат и тогда dataframe сильно сократит свой размер, потому что вместо строк будут повторяться ссылки на строки.

Теперь насчёт del table, я не помню подробностей, но вполне может быть, что Garbage Collector в Python срабатывает не сразу и освобождённая память не сразу отдаётся в систему. Попробуйте вызвать GC вручную:

import gc
gc.collect()
gc.collect() # два раза подряд, для надёжности

Но бывает и такое, что некоторые виды объектов не освобождают память до конца работы программы, всякое бывает. Тогда нужно думать, как сделать так, чтобы данные сразу считывались в правильном формате.

P.S. Но первым делом нужно проверить, правильно ли распарсился файл, возможно, у вас числовые поля склеились в одну большую строку из-за неправильного разделителя полей. Может быть и такое.

P.P.S. Код для проверки ситуации и в подтверждение моих слов:

import pandas as pd
import sys 

df = pd.DataFrame({'text': ['some very very long string or not very long but close to it' * 40 for _ in range(500_000)]})

print('--- ваш способ определения размера ---')
print(sys.getsizeof(df))
print(sys.getsizeof(df.values))
print(sys.getsizeof(df.values.tolist()))
print('--- правильный способ определения размера ---')
df.info(memory_usage='deep')
print('--- попробуем сжать ---')
df['text'] = df['text'].astype('category')
df.info(memory_usage='deep')

Вывод:

--- ваш способ определения размера ---
1208500160
112
4000072

--- правильный способ определения размера ---
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 500000 entries, 0 to 499999
Data columns (total 1 columns):
 #   Column  Non-Null Count   Dtype 
---  ------  --------------   ----- 
 0   text    500000 non-null  object
dtypes: object(1)
memory usage: 1.1 GB

--- попробуем сжать ---
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 500000 entries, 0 to 499999
Data columns (total 1 columns):
 #   Column  Non-Null Count   Dtype   
---  ------  --------------   -----   
 0   text    500000 non-null  category
dtypes: category(1)
memory usage: 490.8 KB

У меня одна строка повторяется, поэтому так хорошо сжалось в категориальный тип. У вас будет похуже, но тоже гораздо луше всё-равно, чем если ничего не делать.

В целом я постарался подогнать цифры под ваши, получилось очень похоже. Почему цифры именно такие - это нужно лезть в глубь устройства Pandas, просто запомните, что проверять размеры объектов Pandas нужно именно средствами Pandas, а так поверхностно вы смотрите размеры каких-то хитрых ссылок на объекты, а не самих объектов внутри Pandas.DataFrame. Хотя sys.getsizeof(df) в принципе правильно отработал, даже удивительно.

→ Ссылка