Лечим проблемы с памятью у Pandas

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

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

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

Чтобы не менять инструмент, но при этом реализовать его использование с большими объёмами информации, мы использовали некоторые приёмы.

На первом шаге нам следует определить текущий объём памяти, который занимает датасет.

Если с числовыми значениями всё более или менее очевидно, то с объектами и строками всё запутанней. Pandas загружает в строковые столбцы как object type по умолчанию.

Чтобы заставить Pandas проверять память для каждого связанного строкового значения и возвращать истинный объем памяти, нам нужно установить параметр memory_usage в значение «deep» при вызове DataFrame.info().

df.info(memory_usage='deep')

Также мы можем использовать метод memory_usage ()

print(df.memory_usage(deep=True)) obj_cols = df.select_dtypes(include=['object']) obj_cols_mem = obj_cols.memory_usage(deep=True) print(obj_cols_mem) obj_cols_sum = obj_cols_mem.sum()/1048576 print(obj_cols_sum)

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

Используем numpy.класс iinfo для проверки минимальных и максимальных значений для каждого целочисленного подтипа:

import numpy as np int_types = ["int8", "int16", "int32", "int64"] for it in int_types: print(np.iinfo(it))

Например, определим максимальное и минимальное значение типа:

print(np.iinfo("int8").min) print(np.iinfo("int8").max)

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

col_max = df['value'].max() col_min = df['value'].min() if col_max < np.iinfo("int8").max and col_min > np.iinfo("int8").min: df['value'] = df['value'].astype("int8") elif col_max < np.iinfo("int16").max and col_min > np.iinfo("int16").min: df['value'] df['value'].astype("int16") elif col_max < np.iinfo("int32").max and col_min > np.iinfo("int32").min: df['value'] = df['value'].astype("int32") elif col_max < np.iinfo("int64").max and col_min > np.iinfo("int64").min: df['value'] = df['value'].astype("int64") print(df['value'].dtype) print( df['value']dd.memory_usage(deep=True))

Но данный алгоритм не эффективен для типа float.

Чтобы помочь найти наиболее эффективный тип пространства для столбца, мы можем использовать функцию pandas.to_numeric () как для float так и для integer, не забывая указать downcast=.

float_cols = df.select_dtypes(include=['float']) for col in float_cols.columns: df[col] = pd.to_numeric(df[col], downcast='float') print(moma.select_dtypes(include=['float']).dtypes)

Некоторые столбцы object type могут являться датами, при этом тип datetime занимает меньше памяти. Мы используем функцию pandas.to_datetime() для преобразования столбца в тип datetime.

df["settledate"] = pd.to_datetime(df["settledate"]) print(df["settledate"].memory_usage(deep=True))

Начиная с версии 0,15 в Pandas ввели категориальный тип. Тип категории использует целочисленные значения для представления значений в столбце. Pandas использует отдельный словарь, который сопоставляет целочисленные значения с необработанными.

Если в категориальном столбце менее 50% уникальных значений (в противном случае выигрыша в памяти не будет), то его смело можно перевести в данный тип. Но следует учесть, что с данным типом нельзя выполнять вычисления или использовать такие методы, как min () и max ().

obj_cols = df.select_dtypes(include=['object']) for col in obj_cols.columns: num_unique_values = len(df[col].unique()) num_total_values = len(df[col]) if num_unique_values / num_total_values < 0.5: df[col] = df[col].astype('category') print(df.info(memory_usage='deep'))

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

Pandas.read_csv() имеет несколько различных параметров, которые позволяют это сделать. Параметр dtype принимает словарь, содержащий имена столбцов (string) в качестве ключей и объекты типа NumPy в качестве значений.

import numpy as np col_types = {"id": np.int32} df = pd.read_csv('data.csv', dtypes=col_types)

Параметр parse_dates принимает список строк, содержащих имена столбцов, которые мы хотим разобрать как значения datetime.

df = pd.read_csv('data.csv', parse_dates=["StartDate", "EndDate"])

Используя параметр usecols, можно указать, какие столбцы мы хотим включить в исследуемые данные.

df = pd.read_csv('data.csv', usecols=["Price", "Value", “Clientcode”])
keep_cols = [ 'Price ', ' Value ', ' Clientcode ', 'Datetradetime', 'Profit'] df = pd.read_csv("data.csv", parse

Резюмируем сказанное выше.

Для оптимизации размера, исследуемого датасета, вы можете использовать следующий алгоритм, который уже применяли мы.

  • Для всех столбцов, имеющих object type, попробуйте присвоить этим столбцам нужный тип. Как уже говорилось, по умолчанию Pandas считывает числовые столбцы как float64. Используйте pd.to_numeric чтобы сменить c float64 на 32 или 16, если это возможно.
  • Устраните nan и используйте dtypes.
  • Устраните или замените недостающие данные перед использованием pd.read_csv. Сделайте пропущенные значения -1 или 0 или что-то, что Pandas не интерпретирует как «nan». Почему? Потому что, если столбец содержит nan, Pandas автоматически делает переменную типом данных, который занимает больше памяти.
  • pd.read_csv часто назначает типы данных, которые занимают больше памяти, чем обычно, например, используя float64, когда float16 достаточно. Чтобы устранить эту проблему, можно явно объявить тип данных (и импортировать только необходимые переменные):
dtypes = {"id" : np.int32 , "operation_year" : np.int16 , "amount" : np.int8 , "contract_year" : np.int16 , "value" : np.int8} vars = dtypes.keys() dfp = pd.read_csv(path + "operations.csv", usecols = vars, dtype = dtypes, sep = ";", index_col="id")
  • Наконец, об этом мы ещё не говорили, но будет хорошим тоном сохранить и позже импортировать файл с использованием формата pickle (или hd5), так как pd.read_csv часто не хватает памяти даже тогда, когда сам файл не очень большой.
import numpy as np import pandas as pd import pickle df = pickle.load(open('data.pkl', 'rb')) df.head(2)

Надеемся, наш опыт поможет и вам в работе.

0
2 комментария
Sergei Zotov

Если совсем беда с памятью и даже метод из статьи не помогает, то можно поюзать Vaex или Dask, которые имеют Pandas-like API с привычными датафреймами, но не грузят их полностью в память, а работают по кускам.

Ответить
Развернуть ветку
NTA
Автор

Спасибо! 👍

Ответить
Развернуть ветку
-1 комментариев
Раскрывать всегда