Python实现按日期拆分数据框首尾n%并合并排名至原数据
问题
我有一个包含PERMNO(ID)、date和RET(预测股票收益率)的数据框,需要按日期分组,筛选出RET排名前n%和后n%的观测值,对这部分数据进行排名后,将排名结果合并回原数据框(非首尾n%的观测值排名为0)。
以1994-05-23的子数据集为例,原数据如下:
print(EQ_port.loc[EQ_port['date'] == '1994-05-23'])
输出:
PERMNO date RET 1360 10001.0 1994-05-23 0.000000 8557 10002.0 1994-05-23 0.120000 14628 10003.0 1994-05-23 -0.037050 17002 10009.0 1994-05-23 -0.038180 19983 10010.0 1994-05-23 0.000000 21049 10011.0 1994-05-23 0.011360 23332 10016.0 1994-05-23 0.000000 26431 10018.0 1994-05-23 -0.166600 28149 10019.0 1994-05-23 -0.040530 33427 10025.0 1994-05-23 -0.044770 40472 10026.0 1994-05-23 0.000000 49501 10032.0 1994-05-23 -0.051730 58517 10035.0 1994-05-23 -0.060000 62790 10037.0 1994-05-23 0.013885 66649 10039.0 1994-05-23 0.088260 69775 10042.0 1994-05-23 0.000000 74588 10043.0 1994-05-23 0.000000 77202 10046.0 1994-05-23 -0.081050 78740 10047.0 1994-05-23 -0.166600
当前我通过以下代码分组筛选并排名:
a = 0.2 # 拆分首尾20%数据 EQ_top20 = (EQ_port.groupby('date',group_keys=False).apply(lambda x: x.nlargest(int(len(x) * a), 'RET'))) EQ_bottom20 = (EQ_port.groupby('date',group_keys=False).apply(lambda x: x.nsmallest(int(len(x) * a), 'RET'))) # 计算排名 EQ_top20['rank'] = EQ_top20.groupby("date", group_keys=True)['RET'].rank(method='average', ascending=True) EQ_bottom20['rank'] = -EQ_bottom20.groupby("date", group_keys=True)['RET'].rank(method='average', ascending=False)
得到的排名结果如下:
print(EQ_top20.loc[EQ_top20['date'] == '1994-05-23'].sort_values('rank'))
输出:
PERMNO date RET rank 62790 10037.0 1994-05-23 0.013885 1.0 66649 10039.0 1994-05-23 0.088260 2.0 8557 10002.0 1994-05-23 0.120000 3.0
print(EQ_bottom20.loc[EQ_bottom20['date'] == '1994-05-23'].sort_values('rank'))
输出:
PERMNO date RET rank 26431 10018.0 1994-05-23 -0.16660 -2.5 78740 10047.0 1994-05-23 -0.16660 -2.5 77202 10046.0 1994-05-23 -0.08105 -1.0
现在无法将这些排名合并回原数据框,期望得到的目标结果如下:
PERMNO date RET rank 1360 10001.0 1994-05-23 0.000000 0 8557 10002.0 1994-05-23 0.120000 3 14628 10003.0 1994-05-23 -0.037050 0 17002 10009.0 1994-05-23 -0.038180 0 19983 10010.0 1994-05-23 0.000000 0 21049 10011.0 1994-05-23 0.011360 0 23332 10016.0 1994-05-23 0.000000 0 26431 10018.0 1994-05-23 -0.166600 -2.5 28149 10019.0 1994-05-23 -0.040530 0 33427 10025.0 1994-05-23 -0.044770 0 40472 10026.0 1994-05-23 0.000000 0 49501 10032.0 1994-05-23 -0.051730 0 58517 10035.0 1994-05-23 -0.060000 0 62790 10037.0 1994-05-23 0.013885 1 66649 10039.0 1994-05-23 0.088260 2 69775 10042.0 1994-05-23 0.000000 0 74588 10043.0 1994-05-23 0.000000 0 77202 10046.0 1994-05-23 -0.081050 -1.0 78740 10047.0 1994-05-23 -0.166600 -2.5
另外,当前用lambda函数在大数据集上运行较慢,希望获得更高效的分组筛选首尾n%数据的方法。
解决方案
1. 将排名合并至原数据框
可以先合并EQ_top20和EQ_bottom20的排名数据,再通过左连接关联到原数据框,最后用0填充缺失的排名值:
# 合并top和bottom的排名数据,只保留需要的列 rank_data = pd.concat([ EQ_top20[['PERMNO', 'date', 'rank']], EQ_bottom20[['PERMNO', 'date', 'rank']] ]) # 左连接原数据框,填充未入选首尾n%的观测值的排名为0 EQ_port = EQ_port.merge(rank_data, on=['PERMNO', 'date'], how='left').fillna(0)
执行后即可得到目标结果,非首尾n%的观测值rank列自动填充为0。
2. 更高效的分组筛选首尾n%方法
避免使用apply+lambda的循环式操作,改用groupby.transform计算分位数阈值,再通过向量化筛选和排名,大幅提升大数据集下的运行效率:
a = 0.2 # 按日期计算RET的分位数阈值:前n%的下限、后n%的上限 EQ_port['top_cutoff'] = EQ_port.groupby('date')['RET'].transform(lambda x: x.quantile(1 - a)) EQ_port['bottom_cutoff'] = EQ_port.groupby('date')['RET'].transform(lambda x: x.quantile(a)) # 标记是否属于top或bottom组 EQ_port['in_top'] = EQ_port['RET'] >= EQ_port['top_cutoff'] EQ_port['in_bottom'] = EQ_port['RET'] <= EQ_port['bottom_cutoff'] # 初始化rank列为0 EQ_port['rank'] = 0 # 计算top组的排名:按日期升序排名(和原逻辑一致) EQ_port.loc[EQ_port['in_top'], 'rank'] = EQ_port[EQ_port['in_top']].groupby('date')['RET'].rank(method='average', ascending=True) # 计算bottom组的排名:按日期降序排名后取负(和原逻辑一致) EQ_port.loc[EQ_port['in_bottom'], 'rank'] = -EQ_port[EQ_port['in_bottom']].groupby('date')['RET'].rank(method='average', ascending=False) # 清理临时列 EQ_port = EQ_port.drop(['top_cutoff', 'bottom_cutoff', 'in_top', 'in_bottom'], axis=1)
效率说明
transform是pandas的向量化操作,会将计算结果广播到原数据框的对应行,比apply+lambda的逐组循环快数倍,尤其适合百万级以上的大数据集。
内容的提问来源于stack exchange,提问作者Sebastian
相关产品推荐
相关产品推荐

