Pandas 中基于组内日期的高效转移?
Efficient shifting based on date within groups in Pandas?
我有一个数据框df
:
df = pd.DataFrame({'id1':[1,1,1,1,1,4,4,4,6,6],
'id2':[45,45,33,33,33,1,1,1,34,34],
'vals':[0.1,0.2,0.6,0.1,0.15,0.34,0.12,0.5,0.4,0.45],
'date':pd.to_datetime(['2017-01-01','2017-01-02','2017-01-01',
'2017-04-01','2017-04-02','2017-01-01',
'2017-01-02','2017-01-03','2017-01-04',
'2017-01-05'])})
我想为 每组 id1
和 id2
创建基于时间的滞后项。例如,t_1
将是前一天的值。 t_2
将是两天前的值。如果两天前没有值,我希望它是 nan
。这将是上述数据帧的输出:
date id1 id2 vals t_1 t_2
0 2017-01-01 1 33 0.60 NaN NaN
1 2017-04-01 1 33 0.10 NaN NaN
2 2017-04-02 1 33 0.15 0.10 NaN
0 2017-01-01 1 45 0.10 NaN NaN
1 2017-01-02 1 45 0.20 0.10 NaN
0 2017-01-01 4 1 0.34 NaN NaN
1 2017-01-02 4 1 0.12 0.34 NaN
2 2017-01-03 4 1 0.50 0.12 0.34
0 2017-01-04 6 34 0.40 NaN NaN
1 2017-01-05 6 34 0.45 0.40 NaN
我可以使用下面的代码来做到这一点,但它对于大量的组来说效率极低——也就是说,如果我有 10000 x 500 个 id1
和 id2
的独特组合,几个每个天的数据,我想要 2 个滞后项,需要 long 时间。
num_of_lags = 2
for i in range(1, num_of_lags+1):
final = pd.DataFrame()
for name, group in df.groupby(['id1', 'id2']):
temp = group.set_index('date', verify_integrity=False)
temp = temp.shift(i, 'D').rename(columns={'vals':'t_' + str(i)}).reset_index()
group = pd.merge(group, temp[['id1', 'id2', 'date', 't_' + str(i)]],
on=['id1', 'id2', 'date'], how='left')
final = pd.concat([final, group], axis=0)
df = final.copy()
有没有更有效的方法?
通过使用apply
...
班次 1
df.groupby(['id1','id2'],sort=False).apply(lambda x : x['vals'].shift()*((x['date'] - pd.to_timedelta(1, unit='d')).isin(x['date'].tolist())).replace(False,np.nan))
Out[775]:
id1 id2
1 45 0 NaN
1 0.10
33 2 NaN
3 NaN
4 0.10
4 1 5 NaN
6 0.34
7 0.12
6 34 8 NaN
9 0.40
dtype: float64
班次 2
df.groupby(['id1','id2'],sort=False).apply(lambda x : x['vals'].shift(2)*((x['date'] - pd.to_timedelta(2, unit='d')).isin(x['date'].tolist())).replace(False,np.nan))
Out[776]:
id1 id2
1 45 0 NaN
1 NaN
33 2 NaN
3 NaN
4 NaN
4 1 5 NaN
6 NaN
7 0.34
6 34 8 NaN
9 NaN
dtype: float64
您可以尝试使用 set_index
、resample
和 shift
:
df1 = df.set_index('date')
df1.groupby(['id1','id2'], as_index=False)\
.apply(lambda x: x.assign(t_1=x.vals.resample('D').asfreq().shift(1),
t_2=x.vals.resample('D').asfreq().shift(2)))
输出:
id1 id2 vals t_1 t_2
date
0 2017-01-01 1 33 0.60 NaN NaN
2017-04-01 1 33 0.10 NaN NaN
2017-04-02 1 33 0.15 0.10 NaN
1 2017-01-01 1 45 0.10 NaN NaN
2017-01-02 1 45 0.20 0.10 NaN
2 2017-01-01 4 1 0.34 NaN NaN
2017-01-02 4 1 0.12 0.34 NaN
2017-01-03 4 1 0.50 0.12 0.34
3 2017-01-04 6 34 0.40 NaN NaN
2017-01-05 6 34 0.45 0.40 NaN
通过结合使用分配组和 unstack 和 shift 可以避免使用 apply,从而大大加快速度。
def compute_shift(df):
df['group_no'] = df.groupby(['id1','id2']).ngroup()
tmp = df[['date','vals','group_no']].set_index(['group_no','date'])\
.unstack('group_no')\
.resample('D').asfreq()
tmp1 = tmp.shift(1).stack('group_no')['vals'].rename('t_1')
tmp2 = tmp.shift(2).stack('group_no')['vals'].rename('t_2')
df = df.join(tmp1, on=['date','group_no'])
df = df.join(tmp2, on=['date','group_no'])
return df
compute_shift(df)
date id1 id2 vals group_no t_1 t_2
0 2017-01-01 1 45 0.10 1 NaN NaN
1 2017-01-02 1 45 0.20 1 0.10 NaN
2 2017-01-01 1 33 0.60 0 NaN NaN
3 2017-04-01 1 33 0.10 0 NaN NaN
4 2017-04-02 1 33 0.15 0 0.10 NaN
5 2017-01-01 4 1 0.34 2 NaN NaN
6 2017-01-02 4 1 0.12 2 0.34 NaN
7 2017-01-03 4 1 0.50 2 0.12 0.34
8 2017-01-04 6 34 0.40 3 NaN NaN
9 2017-01-05 6 34 0.45 3 0.40 NaN
为了比较性能,我创建了一个大小合理的假数据集:
df = pd.DataFrame({'date':np.random.randint(1, 1000, 10**6),
'id1':np.random.randint(1, 100, 10**6),
'id2':np.random.randint(1, 100, 10**6),
'vals':np.random.random(10**6)})
df = df.drop_duplicates(subset=['date','id1','id2'], keep='last')
df = df.sort_values('date')
dates = pd.date_range('20150101','20180101').to_series().reset_index(drop=True)
df['date'] = df['date'].map(dates)
如果我们将性能与 Wen 和 Scott 的解决方案进行比较:
%timeit df.groupby(['id1','id2'],sort=False).apply(lambda x : x['vals'].shift()*((x['date'] - pd.to_timedelta(1, unit='d')).isin(x['date'].tolist())).replace(False,np.nan))
824 ms ± 19.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit df.groupby(['id1','id2'], as_index=False)\
.apply(lambda x: x.assign(t_1=x.vals.resample('D').asfreq().shift(1),\
t_2=x.vals.resample('D').asfreq().shift(2)))
1.38 s ± 25.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit compute_shift(df)
96.4 ms ± 2.14 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
如果你的 DataFrame 不是那么大,我可能更喜欢 Scott Boston 的解决方案,因为它感觉更干净,但如果运行时是一个问题,unstack+shift+join 会更快。
编辑:添加了重新抽样以填补缺失的日期。
你可以尝试多索引和合并
df.index = [df['date'],df['id1'],df['id2']]
df1 = df.copy()
df1.index = [df.index.get_level_values(0) - timedelta(2),df['id1'],df['id2']]
df.merge(df1,left_index=True,right_index=True,how='left')
我有一个数据框df
:
df = pd.DataFrame({'id1':[1,1,1,1,1,4,4,4,6,6],
'id2':[45,45,33,33,33,1,1,1,34,34],
'vals':[0.1,0.2,0.6,0.1,0.15,0.34,0.12,0.5,0.4,0.45],
'date':pd.to_datetime(['2017-01-01','2017-01-02','2017-01-01',
'2017-04-01','2017-04-02','2017-01-01',
'2017-01-02','2017-01-03','2017-01-04',
'2017-01-05'])})
我想为 每组 id1
和 id2
创建基于时间的滞后项。例如,t_1
将是前一天的值。 t_2
将是两天前的值。如果两天前没有值,我希望它是 nan
。这将是上述数据帧的输出:
date id1 id2 vals t_1 t_2
0 2017-01-01 1 33 0.60 NaN NaN
1 2017-04-01 1 33 0.10 NaN NaN
2 2017-04-02 1 33 0.15 0.10 NaN
0 2017-01-01 1 45 0.10 NaN NaN
1 2017-01-02 1 45 0.20 0.10 NaN
0 2017-01-01 4 1 0.34 NaN NaN
1 2017-01-02 4 1 0.12 0.34 NaN
2 2017-01-03 4 1 0.50 0.12 0.34
0 2017-01-04 6 34 0.40 NaN NaN
1 2017-01-05 6 34 0.45 0.40 NaN
我可以使用下面的代码来做到这一点,但它对于大量的组来说效率极低——也就是说,如果我有 10000 x 500 个 id1
和 id2
的独特组合,几个每个天的数据,我想要 2 个滞后项,需要 long 时间。
num_of_lags = 2
for i in range(1, num_of_lags+1):
final = pd.DataFrame()
for name, group in df.groupby(['id1', 'id2']):
temp = group.set_index('date', verify_integrity=False)
temp = temp.shift(i, 'D').rename(columns={'vals':'t_' + str(i)}).reset_index()
group = pd.merge(group, temp[['id1', 'id2', 'date', 't_' + str(i)]],
on=['id1', 'id2', 'date'], how='left')
final = pd.concat([final, group], axis=0)
df = final.copy()
有没有更有效的方法?
通过使用apply
...
班次 1
df.groupby(['id1','id2'],sort=False).apply(lambda x : x['vals'].shift()*((x['date'] - pd.to_timedelta(1, unit='d')).isin(x['date'].tolist())).replace(False,np.nan))
Out[775]:
id1 id2
1 45 0 NaN
1 0.10
33 2 NaN
3 NaN
4 0.10
4 1 5 NaN
6 0.34
7 0.12
6 34 8 NaN
9 0.40
dtype: float64
班次 2
df.groupby(['id1','id2'],sort=False).apply(lambda x : x['vals'].shift(2)*((x['date'] - pd.to_timedelta(2, unit='d')).isin(x['date'].tolist())).replace(False,np.nan))
Out[776]:
id1 id2
1 45 0 NaN
1 NaN
33 2 NaN
3 NaN
4 NaN
4 1 5 NaN
6 NaN
7 0.34
6 34 8 NaN
9 NaN
dtype: float64
您可以尝试使用 set_index
、resample
和 shift
:
df1 = df.set_index('date')
df1.groupby(['id1','id2'], as_index=False)\
.apply(lambda x: x.assign(t_1=x.vals.resample('D').asfreq().shift(1),
t_2=x.vals.resample('D').asfreq().shift(2)))
输出:
id1 id2 vals t_1 t_2
date
0 2017-01-01 1 33 0.60 NaN NaN
2017-04-01 1 33 0.10 NaN NaN
2017-04-02 1 33 0.15 0.10 NaN
1 2017-01-01 1 45 0.10 NaN NaN
2017-01-02 1 45 0.20 0.10 NaN
2 2017-01-01 4 1 0.34 NaN NaN
2017-01-02 4 1 0.12 0.34 NaN
2017-01-03 4 1 0.50 0.12 0.34
3 2017-01-04 6 34 0.40 NaN NaN
2017-01-05 6 34 0.45 0.40 NaN
通过结合使用分配组和 unstack 和 shift 可以避免使用 apply,从而大大加快速度。
def compute_shift(df):
df['group_no'] = df.groupby(['id1','id2']).ngroup()
tmp = df[['date','vals','group_no']].set_index(['group_no','date'])\
.unstack('group_no')\
.resample('D').asfreq()
tmp1 = tmp.shift(1).stack('group_no')['vals'].rename('t_1')
tmp2 = tmp.shift(2).stack('group_no')['vals'].rename('t_2')
df = df.join(tmp1, on=['date','group_no'])
df = df.join(tmp2, on=['date','group_no'])
return df
compute_shift(df)
date id1 id2 vals group_no t_1 t_2
0 2017-01-01 1 45 0.10 1 NaN NaN
1 2017-01-02 1 45 0.20 1 0.10 NaN
2 2017-01-01 1 33 0.60 0 NaN NaN
3 2017-04-01 1 33 0.10 0 NaN NaN
4 2017-04-02 1 33 0.15 0 0.10 NaN
5 2017-01-01 4 1 0.34 2 NaN NaN
6 2017-01-02 4 1 0.12 2 0.34 NaN
7 2017-01-03 4 1 0.50 2 0.12 0.34
8 2017-01-04 6 34 0.40 3 NaN NaN
9 2017-01-05 6 34 0.45 3 0.40 NaN
为了比较性能,我创建了一个大小合理的假数据集:
df = pd.DataFrame({'date':np.random.randint(1, 1000, 10**6),
'id1':np.random.randint(1, 100, 10**6),
'id2':np.random.randint(1, 100, 10**6),
'vals':np.random.random(10**6)})
df = df.drop_duplicates(subset=['date','id1','id2'], keep='last')
df = df.sort_values('date')
dates = pd.date_range('20150101','20180101').to_series().reset_index(drop=True)
df['date'] = df['date'].map(dates)
如果我们将性能与 Wen 和 Scott 的解决方案进行比较:
%timeit df.groupby(['id1','id2'],sort=False).apply(lambda x : x['vals'].shift()*((x['date'] - pd.to_timedelta(1, unit='d')).isin(x['date'].tolist())).replace(False,np.nan))
824 ms ± 19.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit df.groupby(['id1','id2'], as_index=False)\
.apply(lambda x: x.assign(t_1=x.vals.resample('D').asfreq().shift(1),\
t_2=x.vals.resample('D').asfreq().shift(2)))
1.38 s ± 25.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit compute_shift(df)
96.4 ms ± 2.14 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
如果你的 DataFrame 不是那么大,我可能更喜欢 Scott Boston 的解决方案,因为它感觉更干净,但如果运行时是一个问题,unstack+shift+join 会更快。
编辑:添加了重新抽样以填补缺失的日期。
你可以尝试多索引和合并
df.index = [df['date'],df['id1'],df['id2']]
df1 = df.copy()
df1.index = [df.index.get_level_values(0) - timedelta(2),df['id1'],df['id2']]
df.merge(df1,left_index=True,right_index=True,how='left')