计算另一个数据框中日期之间的出现次数(给定ID值)

时间:2019-07-05 16:06:32

标签: python pandas numpy dataframe jupyter-notebook

Pandas: select DF rows based on another DF是我对问题的最接近的答案,但我认为它不能完全解决问题。

无论如何,我正在处理两个非常大的pandas数据帧(因此要考虑速度)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.6ms,这使得该解决方案在大约1,000,000行的实际数据集上几乎不可行。有人知道这里有一个更快,因此可行的解决方案吗?

2 个答案:

答案 0 :(得分:2)

如果我能够处理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

答案 1 :(得分:2)

在电子邮件中添加下一个日期列

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