从另一个数据帧计算日期之间的出现次数(给定 ID 值)
Posted
技术标签:
【中文标题】从另一个数据帧计算日期之间的出现次数(给定 ID 值)【英文标题】:Counting Number of Occurrences Between Dates (Given an ID value) From Another Dataframe 【发布时间】:2019-11-16 06:41:42 【问题描述】: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.6 毫秒,这使得该解决方案在我大约 1,000,000 行的实际数据集上几乎不可行。有谁知道这里的解决方案更快,因此可行?
【问题讨论】:
@QuangHoang 2018-04-01 是 df_trips 数据的最后/最近日期。因此,在 2018 年 4 月 1 日之后不可能有任何旅行或花费,因此在给每位顾客的最后一封电子邮件的行中,NextDateSentOrEndOfData 将为 2018 年 4 月 1 日。 【参考方案1】:在电子邮件中添加下一个日期列
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
【讨论】:
@CJR 非常感谢。这对我有用。一个问题。实施后,我收到“用户警告:不同级别之间的合并可能会产生意想不到的结果(左侧 1 级,右侧 2 级)warnings.warn(msg, UserWarning)”。这可以安全地忽略吗? 为此,我相信是这样 - 因为df_lookup
数据帧由于聚合而具有 MultiIndexes。您可以在将这些索引加入到df_emails
之前显式地将这些索引分离到单独的列中以抑制警告(无论如何这可能是一个好习惯)。
再次感谢您的回复。在最终合并之前使用了df_lookup.columns = df_lookup.columns.get_level_values(0)
,并且 UserWarning 消失了。【参考方案2】:
如果我能够处理max_date
,这将是merge_asof
的简单案例,所以我走了很长一段路:
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
【讨论】:
感谢您花时间回答。不知道我是否在代码的实现上犯了初学者错误,但是当我自己尝试您的解决方案时,我得到了一个“KeyError:”None of [Int64Index([8], dtype='int64', name=' CustID')] 在 [index]"" 中。似乎发生在'bin the dates'步骤中......以上是关于从另一个数据帧计算日期之间的出现次数(给定 ID 值)的主要内容,如果未能解决你的问题,请参考以下文章