Обработка строк Pandas с помощью apply () выполняется очень медленно

#python #pandas #lambda #cython

#python #pandas #лямбда #cython

Вопрос:

У меня есть два фрейма данных, df1 (5 миллионов строк) и df2 (всего около 150 строк). Я хотел выполнить поиск df2 и обновить df1 на основе данных в df2. В принципе, у меня есть несколько условий, которые необходимо применить к df1 в виде фрейма данных df1. Установите флаг, если условия в df2 соответствуют условиям в df1.

 data1 = {'type':['1','2','3'],
         'code':['A','B','C'],
         'type_2':['A1', 'B1', 'C1'],
         'num1': ['101','102', '103'],
         'num2': ['','',''],
         'p_cd':['AA', 'BB', 'CC']
        }

df1 = pd.DataFrame(data1)

data2 = {'type':['1','2'],
         'code':['A','B'],
         'type_2':['', 'B1'],
         'num1': ['','102'],
         'num2': ['',''],
         'custom_expression':["p_cd=='AA'", ''],
         'delete_flag':['Y', 'Y']
        }

df2 = pd.DataFrame(data2)

df_cols = df1.columns.tolist()

def delete_flag(row_from_outlier):
    for index, row_from_delete in  df2.iterrows():
        if row_from_delete['custom_expression'].strip():
            for col in df_cols:
                if row_from_delete['custom_expression'].find(col) != -1:
                    print(row_from_delete['custom_expression'])
                    cust_exp = row_from_delete['custom_expression'].replace(col, "row_from_outlier['" col "']" )
        else:
            cust_exp = '1==1'
            print(cust_exp)

        if (
                ( (not row_from_delete['type'].strip()  ) or (row_from_delete['type'] ==row_from_outlier['type'] )  )
            and ( (not row_from_delete['code'].strip()  ) or (row_from_delete['code'] == row_from_outlier['code'] )               )
            and ( (not row_from_delete['type_2'].strip()) or (row_from_delete['type_2'] == row_from_outlier['type_2'] ) )
            and ( (not row_from_delete['num1'].strip()  ) or (row_from_delete['num1'] == row_from_outlier['num1'] ) )
            and ( (not row_from_delete['num2'].strip()  ) or (row_from_delete['num2'] == row_from_outlier['num2'] ) )
            and eval(cust_exp)
        ) :
            del_flg = row_from_delete['delete_flag']
    if not 'del_flg' in locals():
        del_flg = 'N/A'
    return del_flg

df1['delete_flag'] = df1.apply(lambda row:delete_flag(row), axis=1)

 

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

Нашел эту рекомендацию Cython, Numba и pandas.eval(), которая может сработать, но я был новичком в Python и не знал, как преобразовать приведенный выше код для использования CYTHON

Комментарии:

1. пожалуйста, опубликуйте ожидаемый результат. iterrows и apply combo будут медленнее, чем векторизованные альтернативы

Ответ №1:

Пара наблюдений, которые могут помочь вам ускорить работу.

Вы делаете много вещей повторно:

Удаление внутри delete_flag :

Просто сделайте это только один раз, вне функции, в начале:

 for col in df1.columns:
    df1[col] = df1[col].str.strip()
for col in df2.columns:
    df2[col] = df2[col].str.strip()
 

Построение cust_exp :

Эта часть функции

     ...
    for index, row_from_delete in df2.iterrows():
        if row_from_delete['custom_expression'].strip():
            for col in df_cols:
                if row_from_delete['custom_expression'].find(col) != -1:
                    cust_exp = row_from_delete['custom_expression'].replace(col, "row_from_outlier['" col "']" )
        else:
            cust_exp = '1==1'
    ...
 

фактически не зависит от аргумента функции! row_from_outlier Таким образом, вы cust_exp снова и снова создаете одинаковые s. Это означает, что вы должны сделать это только один раз, вне функции. Для этого вы можете попробовать

 df2['cust_exp'] = df2.custom_expression.str.replace(
                        '|'.join(str(col) for col in df1.columns),
                        lambda m: 'row_from_outlier["'   m.group(0)   '"]',
                        regex=True
                    )
df2.cust_exp[df2.cust_exp == ''] = 'True'
 

который добавляет результат в df2 ( print(df2.cust_exp) ):

 0    row_from_outlier["p_cd"]=='AA'
1                              True
 

Для дальнейшего «дециклирования» функции вы можете выполнить следующее:

 from functools import reduce
from operator import and_

def delete_flag(row_from_outlier):
    scope = locals()
    ser = reduce(and_, [df2[col].isin(['', row_from_outlier[col]])
                        for col in df2.columns[:5]]
                         [df2.cust_exp.apply(lambda s: eval(s, scope))])
    if ser.any():
        return df2.delete_flag[ser].to_list()[-1]
    return 'N/A'
 

Здесь я использовал это

 ( (not row_from_delete['type'].strip()  )
  or (row_from_delete['type'] == row_from_outlier['type'] ) )
 

эквивалентно

 row_from_delete['type'].isin(['', row_from_outlier['type']])
 

и что операция может выполняться по столбцам

 df2['type'].isin(['', row_from_outlier['type']])
 

И and/amp; -aggreation столбцов выполняется reduce(and_, ...) .

И последний, но не менее важный вопрос: мне кажется, что в вашем коде есть потенциал, который del_flg назначается более одного раза, и используется только последний? Это кажется странным. Предназначено ли это?

Комментарии:

1. Спасибо за ваш ответ @Timus. Я обновил код, как было предложено, и выполнил тестовый запуск на выборке данных, но все равно не увидел улучшения производительности. Потребовалось 15,5 секунд, чтобы обработать 5 тыс. записей в df1 с 2 записями в df2.

2. @SuryaKiran Спасибо за отзыв. Я бы не ожидал большой разницы только с 2 строками в df2.