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

#python #pandas #loops #refactoring #filtering

#python #pandas #циклы #рефакторинг #фильтрация

Вопрос:

Я работаю над некоторым кодом python, который генерирует фрейм данных, подходящий для дальнейшего извлечения EDA, BI и функций.

У меня есть один фрейм данных со следующими столбцами:

   party_id client_id    date_st
0     pid1     clid1 2019-07-01
1     pid2     clid3 2019-06-15
2     pid3     clid3 2019-06-14
3     pid4     clid2 2019-07-01
4     pid5     clid2 2019-04-03
5     pid6     clid3 2019-04-03
6     pid7     clid1 2019-05-20
  

где party_id является уникальным, другие cols — нет. Это означает, что один клиент может представлять несколько разных сторон (даже за одну дату). Вечеринка может рассматриваться как уникальная сделка для конкретного клиента.

И есть еще один фрейм данных:

    fact_id client_id  fact_date  fact_sum
0     fid1     clid1 2018-06-02     24.37
1     fid2     clid1 2020-10-10      2.62
2     fid3     clid2 2016-01-04     47.52
3     fid4     clid3 2019-06-14     60.42
4     fid5     clid1 2019-04-03     32.77
5     fid6     clid2 2019-04-03     28.95
6     fid7     clid1 2019-05-20     46.49
7     fid8     clid2 2019-07-01     76.10
8     fid9     clid3 2018-12-15     85.27
9    fid10     clid1 2019-02-05     53.00
10   fid11     clid2 2017-03-18     19.25
11   fid12     clid3 2019-04-03     51.14
12   fid13     clid1 2019-02-08     56.89
13   fid14     clid2 2018-11-09     80.51
14   fid15     clid2 2019-08-15     68.08
  

где fact_id уникален, другие столбцы нет.
В этой таблице представлены предыдущие покупки клиентов, напрямую не связанные со сделками.

Мне нужен новый фрейм данных, построенный следующим образом: для каждого party_id из приложения мне нужно подмножество строк из фактов, которые имели место до date_st, но не ранее полугода (могут быть изменены) назад. Другими словами, мне нужны все покупки в окне перед конкретной сделкой.

Мне все равно, если для двух разных сторон я получу два идентичных идентификатора client_id за одну дату — это нормально. У клиента может быть две разные сделки в день. И мне не нужны никакие агрегации, поскольку этот фрейм данных будет проанализирован в таких фреймворках, как tsfresh.

Все, что мне удалось до сих пор, если перебирать app [‘party_id’] и объединять отфильтрованные фреймы данных:

 def parse_facts(app, facts, party_id, window):
    clid = app[app['party_id']==party_id]['client_id'].values[0]
    date_st = pd.to_datetime(app[app['party_id']==party_id]['date_st'].values[0])
    temp_df = facts[
        (facts['client_id']==clid)amp; 
        (facts['fact_date']<date_st)amp; 
        (facts['fact_date']>=date_st datetime.timedelta(days=-window))].copy()
    temp_df['party_id'] = party_id
    return temp_df


new_facts = pd.concat([parse_facts(app, facts, i, 180) for i in app['party_id'].values], ignore_index=True)
  

Результирующий фрейм данных должен выглядеть так:

 new_facts[['party_id', 'client_id', 'fact_date', 'fact_sum']]

   party_id client_id  fact_date  fact_sum
0      pid1     clid1 2019-04-03     32.77
1      pid1     clid1 2019-05-20     46.49
2      pid1     clid1 2019-02-05     53.00
3      pid1     clid1 2019-02-08     56.89
4      pid2     clid3 2019-06-14     60.42
5      pid2     clid3 2019-04-03     51.14
6      pid3     clid3 2019-04-03     51.14
7      pid4     clid2 2019-04-03     28.95
8      pid5     clid2 2018-11-09     80.51
9      pid6     clid3 2018-12-15     85.27
10     pid7     clid1 2019-04-03     32.77
11     pid7     clid1 2019-02-05     53.00
12     pid7     clid1 2019-02-08     56.89
  

Мне удалось решить задачу, но она имеет очень низкую производительность для всего набора данных: 50 тыс. уникальных участников и 11 млн уникальных фактов. Это приводит к многодневным вычислениям на моей машине (96 ядер, 512 ГБ ОЗУ), поскольку она выполняется в одном потоке.

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

Ответ №1:

Как насчет client_id сначала объединения фреймов данных, а затем фильтрации плохих строк?

 import pandas as pd
from datetime import timedelta


app['date_bg'] = app['date_st'] - timedelta(days=180)
df = pd.merge(facts, app)
df_c = (df['fact_date'] > df['date_bg']) amp; (df['fact_date'] < df['date_st'])
out = df[df_c][['party_id', 'client_id', 'fact_date', 'fact_sum']]
print(out.sort_values('party_id'))
  

Редактировать:
Более эффективное решение для памяти, которое пришло мне в голову:

 start_date = datetime(2019, 1, 1)
outs = []
for i in range(12):
    start = start_date   timedelta(days=i*31)
    start_f = start - timedelta(days=180)
    end = start_date   timedelta(days=(i 1)*31)
    
    app_sub = app[(app['date_st'] > start) amp; (app['date_st'] <= end)]
    facts_sub = facts[(facts['fact_date'] > start_f) amp; (facts['fact_date'] <= end)]
    df = pd.merge(facts_sub, app_sub)
    df_c = (df['fact_date'] > df['date_bg']) amp; (df['fact_date'] < df['date_st'])
    out = df[df_c][['party_id', 'client_id', 'fact_date', 'fact_sum']]
    outs.append(out)

out = pd.concat(outs)
print(out.sort_values('party_id'))
  

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

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

1. Хорошее решение, попробовал его на slice и получил то, что мне было нужно. Но это около 35 тыс. клиентов (50 тыс. сторон) и 11 млн. фактов. Слияние приложения и фактов приведет к созданию ОЧЕНЬ большого набора данных. Есть еще предложения по экономии памяти?