You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 05:35:42