【问题标题】:Combine Date Ranges in Pandas Dataframe在 Pandas Dataframe 中组合日期范围
【发布时间】:2017-05-30 16:22:14
【问题描述】:

我在 Python 中有一组记录,其中包含一个 id、至少一个属性和一组日期范围。我想要获取每个 id 的代码,并结合属性匹配的所有记录并且在日期范围内没有间隙。

日期范围没有间隔,我的意思是一条记录的结束日期大于或等于该 id 的下一条记录。

例如,ID 为“10”、开始日期“2016-01-01”和结束日期“2017-01-01”的记录可以与另一个具有该 ID、开始日期“2017- 01-01”,结束日期为“2018-01-01”,但它不能与开始于“2017-01-10”的记录合并,因为与 2017-01- 之间存在间隔01 至 2017-01-09。

这里有一些例子--

有:

FruitID,FruitType,StartDate,EndDate
1,Apple,2015-01-01,2016-01-01
1,Apple,2016-01-01,2017-01-01
1,Apple,2017-01-01,2018-01-01
2,Orange,2015-01-01,2016-01-01
2,Orange,2016-05-31,2017-01-01
2,Orange,2017-01-01,2018-01-01
3,Banana,2015-01-01,2016-01-01
3,Banana,2016-01-01,2017-01-01
3,Blueberry,2017-01-01,2018-01-01
4,Mango,2015-01-01,2016-01-01
4,Kiwi,2016-09-15,2017-01-01
4,Mango,2017-01-01,2018-01-01

想要:

FruitID,FruitType,NewStartDate,NewEndDate
1,Apple,2015-01-01,2018-01-01
2,Orange,2015-01-01,2016-01-01
2,Orange,2016-05-31,2018-01-01
3,Banana,2015-01-01,2017-01-01
3,Blueberry,2017-01-01,2018-01-01
4,Mango,2015-01-01,2016-01-01
4,Kiwi,2016-09-15,2017-01-01
4,Mango,2017-01-01,2018-01-01

我目前的解决方案如下。它提供了我正在寻找的结果,但对于大型数据集来说性能似乎并不好。此外,我的印象是,您通常希望尽可能避免迭代数据帧的各个行。非常感谢您提供的任何帮助!

import pandas as pd
from dateutil.parser import parse

have = pd.DataFrame.from_items([('FruitID', [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]),
                                ('FruitType', ['Apple', 'Apple', 'Apple', 'Orange', 'Orange', 'Orange', 'Banana', 'Banana', 'Blueberry', 'Mango', 'Kiwi', 'Mango']),
                                ('StartDate', [parse(x) for x in ['2015-01-01', '2016-01-01', '2017-01-01', '2015-01-01', '2016-05-31',
                                                                  '2017-01-01', '2015-01-01', '2016-01-01', '2017-01-01', '2015-01-01', '2016-09-15', '2017-01-01']]),
                                ('EndDate', [parse(x) for x in ['2016-01-01', '2017-01-01', '2018-01-01', '2016-01-01', '2017-01-01',
                                                                '2018-01-01', '2016-01-01', '2017-01-01', '2018-01-01', '2016-01-01', '2017-01-01', '2018-01-01']])
                                ])

have.sort_values(['FruitID', 'StartDate'])

rowlist = []
fruit_cur_row = None

for row in have.itertuples():
    if fruit_cur_row is None:
        fruit_cur_row = row._asdict()
        fruit_cur_row.update(NewStartDate=row.StartDate, NewEndDate=row.EndDate)

    elif not(fruit_cur_row.get('FruitType') == row.FruitType):
        rowlist.append(fruit_cur_row)

        fruit_cur_row = row._asdict()
        fruit_cur_row.update(NewStartDate=row.StartDate, NewEndDate=row.EndDate)

    elif (row.StartDate <= fruit_cur_row.get('NewEndDate')):
        fruit_cur_row['NewEndDate'] = max(fruit_cur_row['NewEndDate'], row.EndDate)
    else:
        rowlist.append(fruit_cur_row)
        fruit_cur_row = row._asdict()
        fruit_cur_row.update(NewStartDate=row.StartDate, NewEndDate=row.EndDate)

rowlist.append(fruit_cur_row)
have_mrg = pd.DataFrame.from_dict(rowlist)
print(have_mrg[['FruitID', 'FruitType', 'NewStartDate', 'NewEndDate']])

【问题讨论】:

  • 您能解释一下“日期范围内没有间隔”是什么意思吗?我无法理解这个问题。谢谢。
  • 我已经更新了我的帖子,以包含更多关于“没有差距”的细节,以尝试澄清这一点。

标签: python pandas


【解决方案1】:

使用嵌套的groupby 方法:

def merge_dates(grp):
    # Find contiguous date groups, and get the first/last start/end date for each group.
    dt_groups = (grp['StartDate'] != grp['EndDate'].shift()).cumsum()
    return grp.groupby(dt_groups).agg({'StartDate': 'first', 'EndDate': 'last'})

# Perform a groupby and apply the merge_dates function, followed by formatting.
df = df.groupby(['FruitID', 'FruitType']).apply(merge_dates)
df = df.reset_index().drop('level_2', axis=1)

请注意,此方法假定您的日期已经排序。如果没有,您需要先在 DataFrame 上使用sort_values。如果您有嵌套的日期跨度,此方法可能不起作用。

结果输出:

   FruitID  FruitType   StartDate     EndDate
0        1      Apple  2015-01-01  2018-01-01
1        2     Orange  2015-01-01  2016-01-01
2        2     Orange  2016-05-31  2018-01-01
3        3     Banana  2015-01-01  2017-01-01
4        3  Blueberry  2017-01-01  2018-01-01
5        4       Kiwi  2016-09-15  2017-01-01
6        4      Mango  2015-01-01  2016-01-01
7        4      Mango  2017-01-01  2018-01-01

【讨论】:

  • 这种方法似乎是解决问题的最干净的方法。非常感谢!
【解决方案2】:

很好的答案root。我已经修改了你的函数,所以现在它在日期范围相交时也可以工作。也许它会帮助某人。

def merge_dates(grp):
    dt_groups = (grp['StartDate'] > grp['EndDate'].shift()).cumsum()
    grouped = grp.groupby(dt_groups).agg({'StartDate': 'min', 'EndDate': 'max'})
    if len(grp) == len(grouped):
        return grouped
    else:
        return merge_dates(grouped)

【讨论】:

  • 对于大型数据集(>4M 行)有没有办法加速这个函数?
【解决方案3】:

这是我想出的......

df = pd.melt(data, id_vars=['FruitID', 'FruitType'], var_name='WhichDate', value_name='Date')
df['Date'] = pd.to_datetime(df['Date'])
df = df.sort_values(['FruitType', 'Date']).drop_duplicates(['FruitType', 'Date'])
df = df.assign(Counter = np.nan)
StartDf = df[df['WhichDate']=='StartDate']
StartDf = StartDf.assign(Counter=np.arange(len(StartDf)))
df[df['WhichDate']=='StartDate'] = StartDf
df.fillna(method='ffill', inplace=True)
s = df.groupby(['Counter', 'FruitID', 'FruitType']).agg({'Date': [min, max]}).rename(columns={'min': 'NewStartDate', 'max': 'NewEndDate'})
s.columns = s.columns.droplevel()
s = s.reset_index()
del s['Counter']
s = s.sort_values(['FruitID', 'FruitType']).reset_index(drop=True)

哪些输出...

   FruitID  FruitType NewStartDate NewEndDate
0        1      Apple   2015-01-01 2018-01-01
1        2     Orange   2015-01-01 2016-01-01
2        2     Orange   2016-05-31 2018-01-01
3        3     Banana   2015-01-01 2017-01-01
4        3  Blueberry   2017-01-01 2018-01-01
5        4       Kiwi   2016-09-15 2017-01-01
6        4      Mango   2015-01-01 2016-01-01
7        4      Mango   2017-01-01 2018-01-01

说明

首先,我重新创建了您的数据框。

data = pd.DataFrame({'FruitID' : [1,1,1,2,2,2,3,3,3,4,4,4],
                     'FruitType': ['Apple', 'Apple', 'Apple', 'Orange', 'Orange', 'Orange', 'Banana', 'Banana',
                                   'Blueberry', 'Mango', 'Kiwi',
                                   'Mango'],
            'StartDate': ['2015-01-01', '2016-01-01', '2017-01-01', '2015-01-01', '2016-05-31',
                          '2017-01-01', '2015-01-01', '2016-01-01', '2017-01-01', '2015-01-01',
                          '2016-09-15', '2017-01-01'],
            'EndDate' : ['2016-01-01', '2017-01-01', '2018-01-01', '2016-01-01', '2017-01-01',
                         '2018-01-01', '2016-01-01', '2017-01-01', '2018-01-01', '2016-01-01', '2017-01-01',
                         '2018-01-01']})

接下来,我使用 pandas melt 函数将数据重塑为长格式。

df = pd.melt(data, id_vars=['FruitID', 'FruitType'], var_name='WhichDate', value_name='Date')

然后,我按每个水果类型的日期排序,并删除所有重复日期的行

df['Date'] = pd.to_datetime(df['Date'])
df = df.sort_values(['FruitType', 'Date']).drop_duplicates(['FruitType', 'Date'])

我创建了一个辅助列,用于用 StartDate 标记每一行。在执行groupby 之前,我们需要执行此操作。然后使用fillna 帮助划分组。

df = df.assign(Counter = np.nan)
StartDf = df[df['WhichDate']=='StartDate']
StartDf = StartDf.assign(Counter=np.arange(len(StartDf)))
df[df['WhichDate']=='StartDate'] = StartDf
df.fillna(method='ffill', inplace=True)

最后,我们使用groupbyagg 来获取每个分区的minmax 日期。

s = df.groupby(['Counter', 'FruitID', 'FruitType']).agg({'Date': [min, max]}).rename(columns={'min': 'NewStartDate', 'max': 'NewEndDate'})
s.columns = s.columns.droplevel()
s = s.reset_index()
del s['Counter']
s = s.sort_values(['FruitID', 'FruitType']).reset_index(drop=True)

【讨论】:

  • 这似乎非常接近,但输出的结构似乎与我习惯的不同。当我运行 s = s.sort_values(['FruitID', 'FruitType']) print(s.info()) ) 时,NewStartDate 和 NewEndDate 字段似乎处于不同的级别(我不熟悉)作为 ids?
  • 请查看我的代码更新。我添加了一行将从多级列索引中删除“日期”
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-12-13
  • 1970-01-01
  • 2012-11-06
  • 2018-02-23
  • 2018-08-10
  • 1970-01-01
  • 2017-09-26
相关资源
最近更新 更多