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

Многие из нас начинают выполнять операции на Numpy, что значительно ускоряет работу, но Numpy, в отличие от Pandas, не имеет всех классных простых в использовании функций.

Если вы погуглите «Оптимизация управления памятью Pandas», вы найдете множество статей о том, как вы можете преобразовать «float64» в «float32», «object» в «category» и т. д. Хотя все это допустимые шаги, есть определенные основные вещи, которые можно было бы сделать, прежде чем начать играть с типами данных.

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

  1. Удалите столбцы, которые вам не нужны
  2. Удалить дубликаты
  3. Сделка с АН
  4. Совокупные данные, если они подходят для вашего варианта использования
  5. Наконец, мы можем поиграть с разными типами данных.

Данные:

Мы будем использовать известный набор данных онлайн-торговли из репозитория машинного обучения UCI. Набор данных основан на британском ритейлере и содержит транснациональные транзакции в период с 12.01.2010 по 12.09.2011. Включены 541 909 экземпляров и 8 столбцов: InvoiceNo, StockCode, Description, Quantity, InvoiceDate, UnitPrice, CustomerID и Country.

df = pd.read_excel(‘Online Retail.xlsx’)
df[std_col.value] = df[‘UnitPrice’] * df[‘Quantity’]
df[std_col.id] = df[std_col.id].astype(str)

Мы добавляем еще один столбец, умножая два из существующих столбцов «Количество» и «Цена за единицу». Мы также сделали столбец идентификатора клиента строкой, просто для иллюстрации :) Размер DataFrame составляет ~ 175 МБ. Хотя это не особенно много в коммерческом контексте, для наших целей этого вполне достаточно.

Я сохранил некоторые константы как класс для простоты использования,

Цель:

Цель состоит в том, чтобы провести анализ LTV клиента, как это сделано в этом медиа пост.

Шаг 1. Отбросьте столбцы

Это наиболее очевидный и тот, который может иметь наибольшее влияние. В этом случае, кроме «InvoiceDate», «CustomerID», «Country», «InvoiceNo» и «original_amount», мне не нужны никакие другие столбцы.

df_select = df[[std_col.date, std_col.id,std_col.country,std_col.invoice, std_col.value]]

Это приводит к экономии памяти на 43%! Размер нового DataFrame составляет 100 МБ.

Небольшое замечание: я использую следующую функцию для сравнения размера DataFrame:

def compare_pandas_size(_df_pre:pd.DataFrame, _df_post:pd.DataFrame)-> None:
‘’’
This function is used to compare memory usage of two dataframes
Parameters:
_df_pre (pd.DataFrame): The reference df
_df_post (pd.DataFrame): The df whose memory usage needs to be evaluated
Returns:
None
‘’’
df_pre = _df_pre.copy()
df_post = _df_post.copy()

# Get memory usage
new_space = sys.getsizeof(df_post)*std_val.b_to_mb
old_space = sys.getsizeof(df_pre)*std_val.b_to_mb
space_savings = 1 — (new_space/old_space)

# Print output
print(f’Current size: {new_space}’)
print(f’{space_savings: .0%} space savings’)

return None

Шаг 2. Удалите дубликаты

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

В этом наборе данных перед «InvoiceNo» стоит «c» в случаях отмененных/возвращенных заказов. Немного регулярного выражения нам на помощь,

df_select_rem_dup[std_col.return_order] = df_select_rem_dup[std_col.invoice].map(type) == ‘str’
df_select_rem_dup[std_col.invoice] = df_select_rem_dup[std_col.invoice].replace(regex=True,inplace=False,to_replace=r’\D’,value=r’’)

Теперь удаляем дубликаты!

df_select_rem_dup = df_select_rem_dup[~(df_select_rem_dup.duplicated(subset=[std_col.date,std_col.id,std_col.invoice,std_col.value,std_col.return_order]))]

Кроме того, мы сэкономили 42% используемой памяти и теперь используем 58 МБ. И это несмотря на добавление одного дополнительного столбца.

Шаг 3. Разберитесь с АН

Этот шаг снова требует определенного уровня понимания набора данных и не имеет универсального решения. Сначала мы оцениваем, где у нас может быть NA,

df_select_rem_dup.isnull().sum(axis=0)

Вау, никаких нулевых значений! Однако это не всегда приятные новости, когда у вас есть столбцы «объект». NA может подкрасться к вам,

df_select_rem_dup_na[std_col.id] = df_select_rem_dup_na[std_col.id].replace(‘nan’,np.nan)
df_select_rem_dup_na[std_col.country] = df_select_rem_dup_na[std_col.country].replace(‘nan’,np.nan)
df_select_rem_dup_na[std_col.invoice] = df_select_rem_dup_na[std_col.invoice].replace(‘nan’,np.nan)

Теперь мы говорим! Теперь мы можем удалить строки, в которых нет «CustomerID», они абсолютно бесполезны для данного конкретного анализа.

df_select_rem_dup_na = df_select_rem_dup.copy()
# remove NA where the row might not be that useful for the purpose of analysis
df_select_rem_dup_na = df_select_rem_dup_na[(df_select_rem_dup_na[std_col.id].notnull())]

Кроме того, мы сэкономили 16% используемой памяти и теперь используем 48 МБ.

Шаг 4. Обобщите данные, если это вам подходит

Этот шаг также требует определенного уровня понимания набора данных и уникален для того типа анализа, который вы собираетесь выполнять с набором данных. В этой конкретной ситуации меня не очень интересует информация об отдельных продуктах, меня интересует только информация об уровне заказов. Мы там Groupby по «InvoiceDate» и «CustoemrID»,

df_pre_final = df_select_rem_dup_na.copy()
df_pre_final = df_pre_final.groupby([std_col.invoice,std_col.id]).agg({std_col.value:’sum’,std_col.country:’unique’,std_col.date:’min’}).reset_index()
df_pre_final[std_col.country] = df_pre_final[std_col.country].apply(lambda x: str(x[0]))
compare_pandas_size(_df_pre = df_select_rem_dup_na,_df_post = df_pre_final)

Этот шаг является одним из самых полезных шагов! Мы сэкономили 93% с точки зрения использования памяти и теперь используем всего 4 МБ.

Шаг 5: Наконец, игра с типами данных

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

Эта статья проделывает потрясающую работу по объяснению мельчайших подробностей, связанных с работой с типом данных категория. Я не думаю, что смогу сделать это лучше. Что я могу вам сказать, так это то, что категория не всегда помогает сэкономить место. Это будет иметь место, если в вашем столбце слишком много уникальных значений. Если есть меньшее количество уникальных значений, то тип данных категории не может быть лучше! Вот пример этого,

Мы знаем, что существует много «CustomerID», поэтому, если вы попытаетесь преобразовать его в «категорию», посмотрите, что произойдет,

df_temp = df_pre_final[[std_col.id,std_col.date,std_col.value]]
get_memory_savings(_df = df_temp[[std_col.id,std_col.date,std_col.value]], dtypes_to_tackle = [‘object’])

В этом случае нам было бы лучше иметь столбец в качестве «объекта». Существует заблуждение, что лучше использовать «str» вместо «object», но, как показано выше, это не всегда так. Существует секретный лучший вариант, который вы видите здесь как «int32». Что мы по существу делаем, так это то, что нас на самом деле не волнует фактический «CustomerID», все, о чем мы заботимся, это то, что каждый из них должен быть однозначно различим. Поэтому вместо фактического «CustomerID» мы используем фиктивное целое число, которое помогает нам сэкономить больше места. Для этой цели можно использовать следующую функцию:

def convert_str_to_uid_int(_df_input:pd.DataFrame, col_to_convert_to_int: str, int_type: str = ‘int32’) -> tuple:
‘’’
This function is used to map unique values in a column to integer with objective of saving memory
Parameters
 — — — — — — — — — — — — — — — — — — — — 
_df_input (pd.DataFrame): Input dataframe
col_to_convert_to_int (str): column name in _df_input that needs to coded up using random numbers
int_type (str): Deafult: “int32” or altenatively use “int64”
Returns
 — — — — — — — — — — — — — — — — — — — -
(pd.DataFrame, pd.DataFrame)
‘’’
df_input = _df_input.copy()
# Input validation
if not isinstance(df_input, pd.DataFrame):
raise ValueError(f’df_input needs to be a dataframe but got {type(df_input)}’)
if col_to_convert_to_int not in _df_input.columns:
raise ValueError(f’The input dataframe does not have the column by the name {col_to_convert_to_int}’)
if int_type not in [‘int32’,’int64']:
raise ValueError(‘The function supports int32 and int64 alone’)
# Get a dictionary that maps the column value to an unique int value
df_mapping = pd.DataFrame()
df_mapping[col_to_convert_to_int] = df_input[col_to_convert_to_int].unique()
df_mapping = df_mapping.reset_index()
df_mapping = df_mapping.rename(columns={‘index’:col_to_convert_to_int+’_int’})
df_mapping = df_mapping.set_index([col_to_convert_to_int])
dict_mapping = df_mapping.to_dict()[col_to_convert_to_int+’_int’]
# Replace the col value with the int value
df_input[col_to_convert_to_int] = df_input[col_to_convert_to_int].map(dict_mapping)
df_input[col_to_convert_to_int] = df_input[col_to_convert_to_int].astype(int_type)
return df_input, df_mapping

Если бы мы анализировали влияние различных типов данных на столбец «Страна», в нем, естественно, было бы гораздо меньше уникальных значений. В этом случае, как упоминалось ранее, тип данных «категория» не может быть лучше!

get_memory_savings(_df = df_pre_final[[std_col.id,std_col.country,std_col.value]], dtypes_to_tackle = [‘object’])

Обычно я очень скептически отношусь к использованию типа данных категория, поскольку нам нужно делать что-то по-другому, как указано в этой статье. Поскольку у нас в любом случае есть лучший вариант, мы используем стратегию сопоставления уникальных строковых значений с уникальными значениями int. Мы сэкономили 31% и теперь осталось 2,9 МБ.

Еще одно место, где мы потенциально могли бы сэкономить немного памяти, — это преобразование «float64» в «float32» или «float16».

Недостатки float16:

  1. Делает что-то большее, чем ~99999, как inf, поэтому, если ваш столбец может принимать значения >99999, то это не будет хорошим выбором.
  2. Он может отлично удерживать до 3 цифр, после чего начинает приближаться

Недостатки float32:

  1. У этого нет ограничений, таких как «float16», с точки зрения значений, которые он может принимать, поэтому в этом случае вы не получите кучу информации.
  2. Он отлично держит информацию до 7-го знака (с/без десятичной точки), после чего начинает аппроксимацию. Например, 12 540 000 099 будет сохранено как 12 540 000 000.

«float32» будет работать в большинстве случаев, и он примерно на 50% меньше по сравнению с float64, но большинство операций pandas не ускоряются с «float32», возможно, потому, что тип данных float, как правило, не занимает много памяти. Вдобавок ко всему, если бы мы сгруппировали столбец «float32», Pandas автоматически преобразовал бы его в «float64», поэтому усилия были бы потрачены впустую. Поэтому я собираюсь придерживаться «float62».

Хорошо, мы закончили. Давайте оценим, где мы находимся. Мы начали с набора данных размером 175 МБ и, наконец, получили DataFrame размером всего 3 МБ. Нам удалось успешно сократить объем используемой памяти на 98 %, не заморачиваясь с типами данных. Это может привести к значительному повышению производительности, например, простая операция groupby будет выполняться в 28 раз быстрее из-за меньшего использования памяти.

Это от меня, спасибо за прочтение!