March 13, 2022

Прожорливость индексов датафрейма, о которой следует знать каждому

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

Создадим два датафрейма и посмотрим, какие индексы для них создает система:

import pandas as pd
import numpy as np
import sys
np.random.seed(0)

df1 = pd.DataFrame([[1, 'first'], [2, 'second'], [3, 'third']])
df2 = pd.DataFrame([[4, 'fourth'], [5, 'fifth'], [6, 'sixth']])
df1.index, df2.index

Конструкция RangeIndex обозначает границы индекса и шаг. Она предназначена для экономии памяти и ускорения работы с датафреймом. Однако, если создавать индексы вручную и проводить с ними операции система создает более тяжеловесную конструкцию Int64Index и др.

Например, вот индекс после конкатенации наших массивов:

df = pd.concat([df1, df2])
df.index

Если мы создадим аналогичные индексы сами, все равно получим Int64Index. При этом, даже если они будут идти последовательно, при конкатенации не будет присвоен RangeIndex:

df1 = pd.DataFrame([[1, 'first'], [2, 'second'], [3, 'third']], index=[1,2,3])
df2 = pd.DataFrame([[4, 'fourth'], [5, 'fifth'], [6, 'sixth']], index=[4,5,6])
df1.index, df2.index
df = pd.concat([df1, df2])
df.index

А вот индекс можно сбросить и так получить конструкцию RangeIndex:

df.reset_index().index

Того же эффекта можно достичь при конкатенации, если в методе указать параметр ignore_index=True:

df = pd.concat([df1, df2], ignore_index=True)
df.index

Теперь сгенерируем большие датафреймы и посмотрим, как влияет тип индекса на память:

rands_ar1 = np.random.randint(10, size=(10000000, 5))
rands_ar2 = np.random.randint(10, size=(10000000, 5))

df1 = pd.DataFrame(rands_ar1, columns= ['1', '2', '3', '4', '5'])
df2 = pd.DataFrame(rands_ar2, columns= ['1', '2', '3', '4', '5'])
df_all1 = pd.concat([df1, df2])
df_all1.info()

Если же конкатенировать с ignore_index=True:

df_all2 = pd.concat([df1, df2], ignore_index=True)
df_all2.info()

Почти вся разница (150-160 МБ) - это вклад размера индекса:

sys.getsizeof(df_all2.index), sys.getsizeof(df_all1.index) 

Особо хочу отметить, что даже сжатие в популярный формат parquet очень слабо влияет на размер индекса:

df_all1.to_parquet('data/df_all1.parquet')
df_all2.to_parquet('data/df_all2.parquet')

Хоть разница сократилась, она достаточно велика - 80 МБ, в то же время сам датафрейм стал весить чуть более 50 МБ. То есть индекс занимает почти в 2 раза больше места, чем сам датафрейм!

Выводы, если значения индекса вам не принципиальны:

не задавайте его вручную;

время от времени делайте reset_index;

при конкатенации используйте флаг ignore_index=True.