Pandas:从日期列生成多个日期范围

6

当前的数据框:

ID  Date
11  3/19/2018
22  1/5/2018
33  2/12/2018
..  ..

我有一个包含ID和日期的df。在原始df中,每个ID都是唯一的。 我想基于日期创建一个新的df。每个ID都有一个最大日期,我想使用该日期并向后推4天(每个ID的5行)。 有成千上万个ID。

期望得到:

ID  Date
11  3/15/2018
11  3/16/2018
11  3/17/2018
11  3/18/2018
11  3/19/2018
22  1/1/2018
22  1/2/2018
22  1/3/2018
22  1/4/2018
22  1/5/2018
33  2/8/2018
33  2/9/2018
33  2/10/2018
33  2/11/2018
33  2/12/2018
…   …

我尝试了以下方法,我认为使用date_range可能是正确的方向,但我一直得到错误。

pd.date_range

def date_list(row):
    list = pd.date_range(row["Date"], periods=5)
    return list

df["Date_list"] = df.apply(date_list, axis = "columns")
4个回答

4

pd.date_range重建索引

让我们尝试创建一个日期范围的平坦列表,并重新索引这个数据帧。

from itertools import chain

v = df.assign(Date=pd.to_datetime(df.Date)).set_index('Date')
# assuming ID is a string column
v.reindex(chain.from_iterable(
    pd.date_range(end=i, periods=5) for i in v.index)
).bfill().reset_index()  

         Date  ID
0  2018-03-14  11
1  2018-03-15  11
2  2018-03-16  11
3  2018-03-17  11
4  2018-03-18  11
5  2018-03-19  11
6  2017-12-31  22
7  2018-01-01  22
8  2018-01-02  22
9  2018-01-03  22
10 2018-01-04  22
11 2018-01-05  22
12 2018-02-07  33
13 2018-02-08  33
14 2018-02-09  33
15 2018-02-10  33
16 2018-02-11  33
17 2018-02-12  33

基于 keysconcat 解决方案

仅供娱乐。我的 reindex 解决方案明显更高效且易于阅读,因此如果你只选一个,请使用它。

v = df.assign(Date=pd.to_datetime(df.Date))
v_dict = {
    j : pd.DataFrame(
            pd.date_range(end=i, periods=5), columns=['Date']
        ) 
    for j, i in zip(v.ID, v.Date)
}

(pd.concat(v_dict, axis=0)
  .reset_index(level=1, drop=True)
  .rename_axis('ID')
  .reset_index()
)

    ID       Date
0   11 2018-03-14
1   11 2018-03-15
2   11 2018-03-16
3   11 2018-03-17
4   11 2018-03-18
5   11 2018-03-19
6   22 2017-12-31
7   22 2018-01-01
8   22 2018-01-02
9   22 2018-01-03
10  22 2018-01-04
11  22 2018-01-05
12  33 2018-02-07
13  33 2018-02-08
14  33 2018-02-09
15  33 2018-02-10
16  33 2018-02-11
17  33 2018-02-12

@coldspeed,是的。如果不使用循环,可能有些难以解决,因为这不是传统的日期重新索引问题。 - harvpan
1
如果您愿意通过减去4天来创建第二个日期列,那么jezrael在这个问题的答案允许您通过一些堆叠和分组来实现。 - ALollz
1
@AntonvBR 我很惊讶它可以不用!谢谢!顺便说一下,你的解决方案也很好,+1 :) - cs95
有多种选择是很好的,但你的第二个解决方案似乎对于一个已经有工作解决方案的问题来说代码太多了(而且没有任何速度提升!)。不过还是很令人印象深刻的。 - Anton vBR
1
@AntonvBR 嗯,只是在尝试不同的东西...看看哪个有效。第二种解决方案很糟糕,但它是一种解决方案。无论如何。 - cs95
显示剩余6条评论

4

这里是另一种方法,使用df.assign来覆盖date,并使用pd.concat将范围粘合在一起。cᴏʟᴅsᴘᴇᴇᴅ的解决方案在性能方面胜出,但我认为这可能是一个不错的补充,因为它很容易阅读和理解。

df = pd.concat([df.assign(Date=df.Date - pd.Timedelta(days=i)) for i in range(5)])

替代方案:

dates = (pd.date_range(*x) for x in zip(df['Date']-pd.Timedelta(days=4), df['Date']))

df = (pd.DataFrame(dict(zip(df['ID'],dates)))
        .T
        .stack()
        .reset_index(0)
        .rename(columns={'level_0': 'ID', 0: 'Date'}))

完整示例:

完整示例:

import pandas as pd

data = '''\
ID  Date
11  3/19/2018
22  1/5/2018
33  2/12/2018'''

# Recreate dataframe
df = pd.read_csv(pd.compat.StringIO(data), sep='\s+')
df['Date']= pd.to_datetime(df.Date)

df = pd.concat([df.assign(Date=df.Date - pd.Timedelta(days=i)) for i in range(5)])
df.sort_values(by=['ID','Date'], ascending = [True,True], inplace=True)
print(df)

返回:

   ID       Date
0  11 2018-03-15
0  11 2018-03-16
0  11 2018-03-17
0  11 2018-03-18
0  11 2018-03-19
1  22 2018-01-01
1  22 2018-01-02
1  22 2018-01-03
1  22 2018-01-04
1  22 2018-01-05
2  33 2018-02-08
2  33 2018-02-09
2  33 2018-02-10
2  33 2018-02-11
2  33 2018-02-12

3

按照ID进行分组,选择列Date,对于每个组生成一系列往前五天的日期。

与其编写冗长的lambda表达式,我编写了一个辅助函数。

def drange(x): 
    e = x.max()
    s = e-pd.Timedelta(days=4)
    return pd.Series(pd.date_range(s,e))

res = df.groupby('ID').Date.apply(drange)

然后从结果的多级索引中删除不必要的级别,即可获得所需的输出。

res.reset_index(level=0).reset_index(drop=True)
# outputs:

    ID       Date
0   11 2018-03-15
1   11 2018-03-16
2   11 2018-03-17
3   11 2018-03-18
4   11 2018-03-19
5   22 2018-01-01
6   22 2018-01-02
7   22 2018-01-03
8   22 2018-01-04
9   22 2018-01-05
10  33 2018-02-08
11  33 2018-02-09
12  33 2018-02-10
13  33 2018-02-11
14  33 2018-02-12

紧凑型替代方案
# Help function to return Serie with daterange
func = lambda x: pd.date_range(x.iloc[0]-pd.Timedelta(days=4), x.iloc[0]).to_series()

res = df.groupby('ID').Date.apply(func).reset_index().drop('level_1',1)

不错的解决方案。在这种情况下,您甚至可以使用iloc [0]来检索第一个值(因为它们在此情况下是唯一的)。我已经添加了它...但说实话,我认为您的解决方案可能已经很好了! - Anton vBR

2
你可以尝试使用date_range进行groupby
df.groupby('ID').Date.apply(lambda x : pd.Series(pd.date_range(end=x.iloc[0],periods=5))).reset_index(level=0)
Out[793]: 
   ID       Date
0  11 2018-03-15
1  11 2018-03-16
2  11 2018-03-17
3  11 2018-03-18
4  11 2018-03-19
0  22 2018-01-01
1  22 2018-01-02
2  22 2018-01-03
3  22 2018-01-04
4  22 2018-01-05
0  33 2018-02-08
1  33 2018-02-09
2  33 2018-02-10
3  33 2018-02-11
4  33 2018-02-12

这本质上是我采取的相同方法,但我喜欢你指定结束和句号的方式。 - Haleemur Ali
@HaleemurAli 是的,我刚刚注意到几乎一样 :-) - BENY

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接