查找大型数据集中的两个日期之间是不是有假期?
Posted
技术标签:
【中文标题】查找大型数据集中的两个日期之间是不是有假期?【英文标题】:Find if there is any holidays between two dates in a large dataset?查找大型数据集中的两个日期之间是否有假期? 【发布时间】:2019-07-07 23:26:08 【问题描述】:我正在处理一个包含大约 2600 万行和 13 列的数据集,其中包括两个日期时间列 arr_date 和 dep_date。我正在尝试创建一个新的布尔列来检查这些日期之间是否有任何美国假期。 我正在对整个数据框使用应用功能,但执行时间太慢。该代码现已在 Goolge 云平台(24GB 内存,4 核)上运行超过 48 小时。有没有更快的方法来做到这一点?
数据集如下所示: Sample data
我使用的代码是 -
import pandas as pd
import numpy as np
from pandas.tseries.holiday import USFederalHolidayCalendar as calendar
df = pd.read_pickle('dataGT70.pkl')
cal = calendar()
def mark_holiday(df):
df.apply(lambda x: True if (len(cal.holidays(start=x['dep_date'], end=x['arr_date']))>0 and x['num_days']<20) else False, axis=1)
return df
df = mark_holiday(df)
【问题讨论】:
也许我们可以做并行计算来加快进程 样本数据会很好。您的申请中还有一个过滤器:x['num_days']<20
。能详细点吗?
@Alexander 这是一个很好的观察结果,但在我的情况下这只会消除 1.5% 的数据。
【参考方案1】:
这花了我大约两分钟的时间在一个包含两列 start_date
和 end_date
的 30m 行示例数据帧上运行。
这个想法是获取在最短开始日期或之后发生的所有假期的排序列表,然后使用来自bisect
模块的bisect_left
来确定在每个开始日期或之后发生的下一个假期。然后将此假期与结束日期进行比较。如果它小于或等于结束日期,则在开始日期和结束日期之间(包括两者)的日期范围内必须至少有一个假期。
from bisect import bisect_left
import pandas as pd
from pandas.tseries.holiday import USFederalHolidayCalendar as calendar
# Create sample dataframe of 10k rows with an interval of 1-19 days.
np.random.seed(0)
n = 10000 # Sample size, e.g. 10k rows.
years = np.random.randint(2010, 2019, n)
months = np.random.randint(1, 13, n)
days = np.random.randint(1, 29, n)
df = pd.DataFrame('start_date': [pd.Timestamp(*x) for x in zip(years, months, days)],
'interval': np.random.randint(1, 20, n))
df['end_date'] = df['start_date'] + pd.TimedeltaIndex(df['interval'], unit='d')
df = df.drop('interval', axis=1)
# Get a sorted list of holidays since the fist start date.
hols = calendar().holidays(df['start_date'].min())
# Determine if there is a holiday between the start and end dates (both inclusive).
df['holiday_in_range'] = df['end_date'].ge(
df['start_date'].apply(lambda x: bisect_left(hols, x)).map(lambda x: hols[x]))
>>> df.head(6)
start_date end_date holiday_in_range
0 2015-07-14 2015-07-31 False
1 2010-12-18 2010-12-30 True # 2010-12-24
2 2013-04-06 2013-04-16 False
3 2013-09-12 2013-09-24 False
4 2017-10-28 2017-10-31 False
5 2013-12-14 2013-12-29 True # 2013-12-25
因此,对于给定的 start_date
时间戳(例如 2013-12-14
),bisect_right(hols, '2013-12-14')
将产生 39,而 hols[39] 将产生 2013-12-25
,即下一个假期在 2013-12-14
开始日期或之后.下一个假期计算为df['start_date'].apply(lambda x: bisect_left(hols, x)).map(lambda x: hols[x])
。然后将此假期与end_date
进行比较,如果end_date
大于或等于此假期值,则holiday_in_range
即为True
,否则假期必须在此end_date
之后。
【讨论】:
谢谢@Alexander。它起作用了,大约需要 16 分钟。快点!接受你的回答。【参考方案2】:您是否已经考虑过为此使用pandas.merge_asof
?
我可以想象带有 lambda 函数的 map
和 apply
无法如此有效地执行。
更新:抱歉,我刚刚读到,如果中间有任何假期,你只需要一个布尔值,这使它更容易。如果这已经足够了,您只需要执行步骤 1-5,然后将作为 step5 结果的 DataFrame 按开始/结束日期分组,并使用 count 作为聚合函数来获得范围内的假期数。您可以将此结果加入到您的原始数据集,类似于下面描述的第 8 步。然后用fillna(0)
填充其余的值。做类似joined_df['includes_holiday']= joined_df['joined_count_column']>0
的事情。之后,您可以根据需要再次从 DataFrame 中删除 joined_count_column
。
如果您使用 pandas_merge_asof
,您可以完成这些步骤(仅当您需要在结果 DataFrame 中包含开始和结束之间的所有假期,而不仅仅是布尔值时,才需要执行第 6 步和第 7 步):
-
在 DataFrame 中加载您的假期记录并在日期上对其进行索引。假期应该是每行一个日期(在一行中存储从 24 日到 26 日的圣诞节范围,这会使其更加复杂)。
创建仅包含开始日期和结束日期列的数据框副本。更新:每个开始,结束日期应该只出现一次。例如。通过使用 groupby。
使用具有合理容差值的
merge_asof
(如果您在期间开始时加入,请使用direction='forward'
,如果您使用结束日期,请使用direction='backward'
和how='inner'
。
因此,您有一个合并的 DataFrame,其中包含您的假期数据框中的开始、结束列和日期列。您只获得记录,其中发现具有给定容差的假期,但稍后您可以将此数据与原始 DataFrame 合并回来。您现在可能会拥有原始记录的副本。
然后通过将它们与开始列和结束列进行比较来使用索引器检查加入的假期以获取记录,并删除不在中间的假期。
对您从第 5 步获得的数据框进行排序(使用类似 df.sort_values(['start', 'end', 'holiday'], inplace=True)
的东西。现在您应该插入一个数字列,用于对您的期间(您在第 5 步之后获得的那些)之间的假期进行编号,从 1 到 ...(对于每个周期从 1) 开始。这是在下一步中使用 unstack 来获取列中的假期所必需的。
根据期间开始日期、期间结束日期和您在步骤 6 中插入的计数列在您的数据帧上添加索引。在您在步骤 1-7 中准备的数据帧上使用df.unstack(level=-1)
。您现在拥有的是一个精简的 DataFrame,其中包含您的原始期间以及按列排列的假期。
现在您只需使用 original_df.merge(df_from_step7, left_on=['start', 'end'], right_index=True, how='left')
将此 DataFrame 合并回您的原始数据
结果是一个文件,其中包含您的原始数据,其中包含日期范围,并且对于每个日期范围,位于期间之间的假期存储在数据后面的单独列中。粗略地说,第 6 步中的编号将假期分配给列,并具有这样的效果,即假期总是从右到左分配给列(如果第 1 列为空,则第 3 列中不会有假期)。
步骤 6. 可能也有点棘手,但您可以通过添加一个填充了一个范围的系列然后修复它来做到这一点,因此使用 shift
或在每个组中编号从 0 或 1 开始按开始分组,以aggregate('idcol':'min')
结束,并将结果连接回以从范围序列分配的值中减去它。
总而言之,我认为这听起来比实际更复杂,而且它的执行效率应该很高。特别是如果您的周期不是那么大,因为在第 5 步之后,您的结果集应该比原始数据帧小得多,但即使不是这种情况,它仍然应该非常有效,因为它可以使用已编译的代码。
【讨论】:
以上是关于查找大型数据集中的两个日期之间是不是有假期?的主要内容,如果未能解决你的问题,请参考以下文章