Подсчет количества вхождений между датами (с учетом значения идентификатора) из другого кадра данных - PullRequest
4 голосов
/ 05 июля 2019

Панды: выберите строки DF на основе другого DF - самый близкий ответ, который я могу найти на мой вопрос, но я не верю, что он вполне решает его.

В любом случае, я работаю с двумя очень большими панелями данных панд (так что скорость учитывается), df_emails и df_trips, которые уже отсортированы по CustID, а затем по дате.

df_emails включает дату, когда мы отправили клиенту электронное письмо, и выглядит это так:

   CustID   DateSent
0       2 2018-01-20
1       2 2018-02-19
2       2 2018-03-31
3       4 2018-01-10
4       4 2018-02-26
5       5 2018-02-01
6       5 2018-02-07

df_trips включает в себя даты, когда покупатель пришел в магазин, и сколько он потратил, и это выглядит так:

   CustID   TripDate  TotalSpend
0       2 2018-02-04          25
1       2 2018-02-16         100
2       2 2018-02-22         250
3       4 2018-01-03          50
4       4 2018-02-28         100
5       4 2018-03-21         100
6       8 2018-01-07         200

По сути, мне нужно найти количество поездок и общие расходы для каждого клиента между каждым отправленным письмом. Если это последний раз, когда электронное письмо отправляется данному клиенту, мне нужно узнать общее количество поездок и общие расходы после получения электронного письма, но до окончания данных (2018-04-01). Таким образом, окончательный кадр данных будет выглядеть так:

   CustID   DateSent NextDateSentOrEndOfData  TripsBetween  TotalSpendBetween
0       2 2018-01-20              2018-02-19           2.0              125.0
1       2 2018-02-19              2018-03-31           1.0              250.0
2       2 2018-03-31              2018-04-01           0.0                0.0
3       4 2018-01-10              2018-02-26           0.0                0.0
4       4 2018-02-26              2018-04-01           2.0              200.0
5       5 2018-02-01              2018-02-07           0.0                0.0
6       5 2018-02-07              2018-04-01           0.0                0.0

Хотя я изо всех сил старался сделать это дружественным для Python / Pandas способом, единственное точное решение, которое я смог реализовать, - это np.where, смещение и циклы. Решение выглядит так:

df_emails["CustNthVisit"] = df_emails.groupby("CustID").cumcount()+1

df_emails["CustTotalVisit"] = df_emails.groupby("CustID")["CustID"].transform('count')

df_emails["NextDateSentOrEndOfData"] = pd.to_datetime(df_emails["DateSent"].shift(-1)).where(df_emails["CustNthVisit"] != df_emails["CustTotalVisit"], pd.to_datetime('04-01-2018'))

for i in df_emails.index:
    df_emails.at[i, "TripsBetween"] = len(df_trips[(df_trips["CustID"] == df_emails.at[i, "CustID"]) & (df_trips["TripDate"] > df_emails.at[i,"DateSent"]) & (df_trips["TripDate"] < df_emails.at[i,"NextDateSentOrEndOfData"])])

for i in df_emails.index:
    df_emails.at[i, "TotalSpendBetween"] = df_trips[(df_trips["CustID"] == df_emails.at[i, "CustID"]) & (df_trips["TripDate"] > df_emails.at[i,"DateSent"]) & (df_trips["TripDate"] < df_emails.at[i,"NextDateSentOrEndOfData"])].TotalSpend.sum()

df_emails.drop(['CustNthVisit',"CustTotalVisit"], axis=1, inplace=True)

Тем не менее, %% timeit обнаружил, что это занимает 10,6 мс только для семи строк, показанных выше, что делает это решение практически неосуществимым для моих реальных наборов данных из примерно 1 000 000 строк. Кто-нибудь знает здесь решение, которое является более быстрым и, следовательно, выполнимым?

Ответы [ 2 ]

2 голосов
/ 05 июля 2019

Добавить следующий столбец даты в электронные письма

df_emails["NextDateSent"] = df_emails.groupby("CustID").shift(-1)

Сортировка по merge_asof, а затем объединение с ближайшим для создания таблицы поиска поездок

df_emails = df_emails.sort_values("DateSent")
df_trips = df_trips.sort_values("TripDate")
df_lookup = pd.merge_asof(df_trips, df_emails, by="CustID", left_on="TripDate",right_on="DateSent", direction="backward")

Агрегируйте таблицу поиска для нужных вам данных.

df_lookup = df_lookup.loc[:, ["CustID", "DateSent", "TotalSpend"]].groupby(["CustID", "DateSent"]).agg(["count","sum"])

Оставьте его присоединенным к таблице электронной почты.

df_merge = df_emails.join(df_lookup, on=["CustID", "DateSent"]).sort_values("CustID")

Я предпочитаю оставлять NaN в качестве NaN, потому что мне не нравится заполнять значения по умолчанию (вы всегда можете сделать это позже, если хотите, но вы не можете легко отличить вещи, которые существовали, от вещей, которые не были, если вы положили по умолчанию в начале)

   CustID   DateSent NextDateSent  (TotalSpend, count)  (TotalSpend, sum)
0       2 2018-01-20   2018-02-19                  2.0              125.0
1       2 2018-02-19   2018-03-31                  1.0              250.0
2       2 2018-03-31          NaT                  NaN                NaN
3       4 2018-01-10   2018-02-26                  NaN                NaN
4       4 2018-02-26          NaT                  2.0              200.0
5       5 2018-02-01   2018-02-07                  NaN                NaN
6       5 2018-02-07          NaT                  NaN                NaN
2 голосов
/ 05 июля 2019

Это был бы простой случай merge_asof, если бы я смог справиться с max_date, поэтому я прошёл долгий путь:

max_date = pd.to_datetime('2018-04-01')

# set_index for easy extraction by id
df_emails.set_index('CustID', inplace=True)

# we want this later in the final output
df_emails['NextDateSentOrEndOfData'] = df_emails.groupby('CustID').shift(-1).fillna(max_date)

# cuts function for groupby
def cuts(df):
    custID = df.CustID.iloc[0]
    bins=list(df_emails.loc[[custID], 'DateSent']) + [max_date]
    return pd.cut(df.TripDate, bins=bins, right=False)

# bin the dates:
s = df_trips.groupby('CustID', as_index=False, group_keys=False).apply(cuts)

# aggregate the info:
new_df = (df_trips.groupby([df_trips.CustID, s])
                  .TotalSpend.agg(['sum', 'size'])
                  .reset_index()
         )

# get the right limit:
new_df['NextDateSentOrEndOfData'] = new_df.TripDate.apply(lambda x: x.right)

# drop the unnecessary info
new_df.drop('TripDate', axis=1, inplace=True)

# merge:
df_emails.reset_index().merge(new_df, 
                on=['CustID','NextDateSentOrEndOfData'],
                              how='left'
                )

Выход:

   CustID   DateSent NextDateSentOrEndOfData    sum  size
0       2 2018-01-20              2018-02-19  125.0   2.0
1       2 2018-02-19              2018-03-31  250.0   1.0
2       2 2018-03-31              2018-04-01    NaN   NaN
3       4 2018-01-10              2018-02-26    NaN   NaN
4       4 2018-02-26              2018-04-01  200.0   2.0
5       5 2018-02-01              2018-02-07    NaN   NaN
6       5 2018-02-07              2018-04-01    NaN   NaN
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...