在大型数据集中查找两个日期之间是否有节假日?

时间:2019-07-07 23:26:08

标签: python pandas numpy data-science

我正在处理一个包含约2600万行和13列的数据集,其中包括两个datetime列arr_date和dep_date。我正在尝试创建一个新的布尔值列,以检查这些日期之间是否有美国假期。 我正在对整个数据框使用apply函数,但是执行时间太慢。该代码现已在Goolge Cloud Platform(24GB ram,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)

2 个答案:

答案 0 :(得分:1)

您是否已经考虑过为此使用pandas.merge_asof

我可以想象带有lambda函数的mapapply不能高效地执行。

更新:抱歉,我刚刚读到,如果之间有任何假期,则只需要一个布尔值,这使操作变得容易得多。如果足够,您只需要执行第1-5步,然后按开始/结束日期对作为第5步的结果的DataFrame进行分组,并使用count作为汇总函数即可使范围内的节假日数成为可能。您可以将这个结果加入到原始数据集,类似于下面描述的步骤8。然后用fillna(0)填充其余的值。做类似joined_df['includes_holiday']= joined_df['joined_count_column']>0的事情。之后,您可以根据需要再次从数据框中删除joined_count_column

如果您使用pandas_merge_asof,则可以按照以下步骤进行操作(仅当您需要同时在结果DataFrame的开始和结束之间(不仅是布尔值)具有所有假期时,才需要执行步骤6和7):

  1. 将您的假期记录加载到DataFrame中,并在日期将其编入索引。假期应该是每行一个日期(存储范围如圣诞节从24日到26日连续一行,这会使它变得更加复杂)。
  2. 仅使用开始日期和结束日期列创建数据框的副本。更新:每个开始,结束日期都应该只出现一次。例如。通过使用groupby。
  3. 使用具有合理公差值的merge_asof(如果在期间开始时加入,请使用direction='forward',如果使用结束日期,请使用direction='backward'和{{1} }。
  4. 因此,您有一个合并的DataFrame,其中包含您的假日数据框中的开始,结束列和日期列。您只会得到记录,该记录的假期被发现具有给定的公差,但是稍后您可以将此数据与原始DataFrame合并。您现在可能会拥有原始记录的副本。
  5. 然后通过将索引器与开始和结束列进行比较来检查索引中与索引一起的假期,并删除假期之间的假期。
  6. 对从步骤5获得的数据帧进行排序(使用类似how='inner'的内容。现在,您应该插入一个数字列,为从1到2的各个期间(在步骤5之后获得的假日)之间的假日编号。 (对于从1开始的每个期间)。在下一步中使用unstack来获取假期在列中是必要的。
  7. 根据期间开始日期,期间结束日期和在步骤6中插入的计数列,在数据框架上添加索引。在步骤1-7中准备的数据框架上使用df.sort_values(['start', 'end', 'holiday'], inplace=True)。您现在拥有的是一个精简的DataFrame,其中包含您的原始时间段,假期按列排列。
  8. 现在您只需要使用df.unstack(level=-1)
  9. 将此DataFrame合并回原始数据

此操作的结果是一个文件,该文件的原始数据包含日期范围,对于每个日期范围,期间之间的假日存储在数据后面的单独列中。粗略地说,第6步中的编号将假日分配给各列,并且具有以下效果:假日总是从右到左分配给各列(如果第1列为空,则不会在第3列有假日)。 / p>

步骤6可能也有些棘手,但是您可以这样做,例如,添加一个填充有范围的序列,然后对其进行修复,因此可以使用{{1}在每个组中从0或1开始编号。 }或按开头分组,以original_df.merge(df_from_step7, left_on=['start', 'end'], right_index=True, how='left')结尾,然后将结果结合起来,以从范围序列分配的值中减去。

总的来说,我认为它听起来要复杂得多,应该高效执行。特别是如果您的周期不那么大,因为在步骤5之后,您的结果集应该比原始数据帧小得多,但是即使不是这种情况,它也应该非常有效,因为它可以使用编译后的代码。

答案 1 :(得分:0)

这花了我大约两分钟的时间来运行一个包含两列start_dateend_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进行比较,如果holiday_in_range大于或等于该假期值,则Trueend_date,否则假期必须在此之后end_date