Эффективная модификация значений датафрейма, чтобы избежать капризов Pandas
В этой статье рассмотрим ключевые походы к изменению значений в ячейках датафрейма Pandas - наиболее популярного формата первичной обработки данных для любителей Python. Оценку способов будем производить исходя из их результативности, скорости и трудозатратности.
Рассмотрим вопрос на примере конкретной задачи. Пусть имеется датафрейм, содержащий предсказания для пары object, item (например, размер поставки в филиал организации/object определенного материала/item) следующего вида:
import pandas as pd
import numpy as np
np.random.seed(0)
# предсказания количества item-ов для объектов
items = ['item1','item2','item3','item4','item5']
all_df = pd.MultiIndex.from_product([np.arange(5), items]).to_frame()\
.reset_index(drop=True).rename(columns={0:'objs', 1:'items'})
preds_df = pd.concat([all_df.sample(10).reset_index(drop=True),
pd.Series(np.random.randint(100, size=10), name='preds')], axis=1)
preds_df.sort_values(by='objs')Для генерации набора сначала с помощью функции from_product объекта MultiIndex получили декартово произведение item-ов и объектов, а потом выбрали из них 10 случайных с помощью метода sample объекта DataFrame. Нашей задачей является замена item-ов в датафрейме предсказаний при условии, что он отсутствует в некотором перечне возможных item-ов для каждого объекта (задаем в obj_items_df, допустим, в филиале организации уже не используются гелевые ручки, а только шариковые). При этом правила замены одного item-а на другой (задаются в словаре maps_d):
obj_items_df = preds_df.copy()[['objs', 'items']]
obj_items_df.drop([4,9], inplace=True)
obj_items_df.sort_values(by='objs')
# словарь замены
maps_d = {'item1':'item2', 'item2':'item1', 'item3':'item5', 'item5':'item3'}Как видно, в демонстрационных целях мы сформировали obj_items_df из значений самих предсказаний за вычетом двух строк (они и будут заменены).
Рассмотрим теперь несколько подходов для решения задачи:
- Задаем цикл по матрице предсказаний и в строках, где пара объект, item не попадает в список пар из obj_items_df осуществляем замену.
- То же, но сравнение присутствия объект, item происходит методом прихотливой индексации на уровне датафреймов.
- Оба датафрейма индексируются парами объект, item и операции наличия значений осуществляются на уровне индексов.
Преимуществом первых двух подходов является простота реализации:
pairs = obj_items_df.apply(lambda x:tuple(x), axis=1).to_list()
match1_df = preds_df.copy()
for i, row in enumerate(match1_df.itertuples()):
if not (row.objs, row.items) in pairs:
match1_df.at[row.Index, 'items_new'] = maps_d[row.items]
else:
match1_df.at[row.Index, 'items_new'] = row.items
match1_dfВидим, что строки, где objs ==2 имеют item_new отличающийся от item в соответствии с нашим правилом. Обратите внимание, что при попытке внесения изменений путем выбора сначала строки i, а потом столбца item_new Pandas вернет предупреждение и не даст внести изменения:
Теперь проверим скорость метода:
%%timeit
pairs = obj_items_df.apply(lambda x:tuple(x), axis=1).to_list()
match1_df = preds_df.copy()
for i, row in enumerate(match1_df.itertuples()):
if not (row.objs, row.items) in pairs:
match1_df.at[row.Index, 'items_new'] = maps_d[row.items]
else:
match1_df.at[row.Index, 'items_new'] = row.items
match1_dfВторой метод также выдает правильный результат:
# %%timeit
match2_df = preds_df.copy()
for i, row in enumerate(match2_df.itertuples()):
if obj_items_df[(obj_items_df['objs']==row.objs)&(obj_items_df['items']==row.items)].shape[0]==0:
match2_df.loc[row.Index, 'items_new'] = maps_d[row.items]
else:
match2_df.loc[row.Index, 'items_new'] = row.items
match2_dfно работает значительно медленнее:
А теперь перейдем к третьему способу. Он немного сложнее предыдущих, так как требует работы на уровне индексов. На основе обоих датафреймов (preds_df, obj_items_df) создаются структуры с индексами из пар объект, item (preds_ser, obj_items_flag), затем вызывается переиндексация obj_items_flag и flag для новых значений становится равным null, что инициирует правило замены. Код представлен ниже:
# %%timeit
cols = obj_items_df.columns
obj_items_df['flag']=0
obj_items_flag = obj_items_df.set_index(['objs','items'])
preds_ser = preds_df.set_index(['objs','items'])
obj_items_flag = obj_items_flag.reindex(preds_ser.index)
match_data = np.where(obj_items_flag['flag'].notnull(), obj_items_flag.index.map(lambda x: (x[0], (x[1], x[1]))),
obj_items_flag.index.map(lambda x: (x[0], (x[1], maps_d.get(x[1], x[1])))))
match_df = pd.MultiIndex.from_tuples(match_data).to_frame().reset_index(drop=True)
match_df[cols[1]] = match_df[1].str[0]
match_df['items_new'] = match_df[1].str[1]
match_df.drop([1], axis=1, inplace=True)
match_df.rename(columns={0:cols[0]})На заданных данных несмотря на трудоемкость время метода не очень впечатляет:
Если же увеличить объем данных, то соотношение скорости сильно поменяется:
# предсказания количества item-ов для объектов
items = ['item1','item2','item3','item4','item5']
all_df = pd.MultiIndex.from_product([np.arange(5000), items]).to_frame()\
.reset_index(drop=True).rename(columns={0:'objs', 1:'items'})
preds_df = pd.concat([all_df.sample(10000).reset_index(drop=True),
pd.Series(np.random.randint(100, size=10000), name='preds')], axis=1)
preds_df.sort_values(by='objs')Вывод - логику многочисленных операций сравнения в датафреймах лучше реализовать на уровне индексов. А вы что бы добавили?
Не пропустите ничего интересного и подписывайтесь на страницы канала в других социальных сетях: