☰
聚宽量化研究必备:Pandas插入列、排序与筛选实战
2026/10/15 6:01:26 网站建设 项目流程

第一次在聚宽里拉数据的时候,我看着那几十行行情记录,脑子里只有一个念头:接下来怎么做涨跌幅?怎么做排名?怎么把"涨幅超过5%且放量"的日子挑出来?这些问题背后其实都指向同一件事——Pandas里的插入列、排序、筛选。

这篇文章不讲复杂因子,也不讲策略设计,只把这三件基本功掰开揉碎讲清楚。如果你刚开始接触聚宽API,只会看表格还没法加工表格,那这篇内容就是为你准备的;如果你已经会用Excel做同样的事,这篇能帮你把同样的思路迁移到Pandas上。我尽量用聚宽环境里的真实场景来说话,而不是写一堆和量化无关的通用示例。

1. 在聚宽里第一次拉数据:DataFrame长这样

1.1 get_price拉一帧数据

先看聚宽研究环境里最常规的一个操作:用get_price拉单只股票的日线数据。

import pandas as pd # 聚宽研究环境中已内置 get_price,这里直接调用 # 000000.XSHE 仅为演示占位,请替换成你想分析的目标股票代码 demo_symbol = '000000.XSHE' df = get_price( demo_symbol, start_date='2024-01-01', end_date='2024-03-01', frequency='daily', fields=['open', 'close', 'high', 'low', 'volume'] ) df

返回结果大致是这样一个表格:

open close high low volume 2024-01-02 10.20 10.50 10.80 10.10 158000 2024-01-03 10.60 10.30 10.90 10.20 210000 2024-01-04 10.40 10.70 10.75 10.30 175400 ... ... ... ... ... ... 2024-03-01 11.20 11.50 11.60 11.05 246000

这里左起第一列看起来像是"第几行",但它不是什么普通编号,而是日期索引。Pandas里这一列的身份很特殊,它叫Index,不参与普通列运算,却决定了数据对齐、切片、合并时的行为。很多新手第一次接触时最容易犯的错,就是把这个索引当成普通列去操作。

1.2 行、列、索引和dtype

拿到df之后,我建议先养成看三个属性的习惯:

df.shape # 行数和列数 df.columns # 列名 df.dtypes # 每一列的数据类型

比如上面这帧数据,shape可能是(41, 5),代表41个交易日、5个字段;columns是['open', 'close', 'high', 'low', 'volume'];dtypes里open、close这些是float64,volume大概率是int64。

为什么要关心dtype?因为排序和筛选的正确性完全依赖类型。举个典型的坑:如果某个字段被读成了object类型,那它里面存的很可能是字符串而不是真正的数字。对字符串做sort_values,排出来是按字典序,比如"10"会排在"9"前面,结果怎么看怎么奇怪。聚宽返回的数据类型基本已经规范好了,但你在后面自己拼接表、自己构造列的时候,dtype问题一定会找上门。

1.3 为什么这三个操作在聚宽里绕不开

从聚宽API拿到的行情数据,说白了就是"裸数据"。get_price给你的只有OHLCV,但做策略研究时,你想要的通常是涨跌幅、振幅、量比、换手率、区间收益这些衍生指标。这些指标没有一个是get_price直接给的,都需要先拿到基础数据,再用Pandas加工出来。

而"加工"这件事,落到具体动作上就是:插入新的计算列、按某个指标排序、按条件筛选。我用下来最大的感受是:插入列、排序、筛选这三个操作拼起来,基本覆盖了日常策略研究里80%的数据整理工作。把这套基本功练顺了,后面看任何因子、任何选股逻辑,都会轻松很多。

2. 插入列:三种写法和一个必踩的警告

2.1 直接赋值:最自然,也最常用

插入列最直白的方式就是直接赋值,跟字典添加键值对差不多。最常见的场景是算涨跌幅:

df['pct'] = df['close'].pct_change()

这样就在df右边多了一列,每行是当日收盘价相对前一日的涨跌幅,比如0.02代表2%。如果你想展示成百分数,可以再乘100:

df['pct_pct'] = df['close'].pct_change() * 100

直接赋值的本质是在DataFrame末尾追加一列。这里有个硬性要求:赋值的Series长度必须和df行数一致,否则会直接报错。我最早犯过的错就是从另一个筛选后的df里取了一列塞回来,长度对不上,弹出一句ValueError: Length of values does not match length of index。遇到这个报错不要慌,先检查两个表的行数。

除了数值列,也可以直接赋值一个布尔列,为后面筛选做准备:

df['is_big_rise'] = df['pct'] > 0.02

2.2 insert:需要控制列顺序时再用

直接赋值只能追加到末尾,但有些场景下你希望字段按某个顺序排列,比如把股票代码放到最左边当标识列。这时候用insert:

df.insert(0, 'symbol', demo_symbol)

insert的第一个参数是位置,0就代表放在最前面;第二个参数是列名;第三个是值。注意,insert是在原df上原地修改,不像直接赋值那样每次生成新浅拷贝。它还支持allow_duplicates参数,允许你创建重复列名,不过日常使用中我基本不开这个选项,重复列名在后续按列名取值时容易引发混乱。

什么样的场景我会优先用insert?主要是"标识优先、数据在后"的要求。比如最终要输出一张表来观察,我希望第一列是标的代码,第二列是日期,然后才是行情和指标列。这种视觉顺序用insert控制起来最直观。

2.3 assign:适合一口气增加多个列

如果一次要新增好几个字段,逐一赋值代码会显得很啰嗦。Pandas提供的assign方法可以一口气加多列,并且支持链式写法:

df = df.assign( log_close=lambda x: x['close'].apply(np.log), ma5=lambda x: x['close'].rolling(5).mean(), ma20=lambda x: x['close'].rolling(20).mean() )

注意,assign不像insert那样原地修改,它会返回一个新df,所以要用df = df.assign(...)接住返回值。assign里常见的写法是传lambda函数,这样每个新列内部都能引用前面已经存在的列,也能引用本次assign里刚定义的其他列。

我在聚宽研究环境里比较喜欢用assign来构造特征列。一次三五个指标,写在一坨代码里,看起来像一个完整的数据加工步骤,后面回看也更清楚。

2.4 三种写法的取舍:一张表说清楚

方法是否原地修改能否指定列位置适合场景
直接赋值是否,只能追加末尾日常加一两列,最常用
insert是是,指定任意位置需要控制列顺序,比如把标识列放最前
assign否,返回新df否,追加末尾一股脑加多个字段,配合链式操作

我的经验是:日常顺手加列用直接赋值;想控制表结构时用insert;批量构造特征列时用assign。没有绝对标准,看代码可读性来决定。

2.5 触发过的警告:SettingWithCopyWarning

这一节必须单独说,因为我真的在这个警告上栽过跟头。场景是这样的:先用筛选条件取了一个子集,然后想给子集加一列标记:

sub_df = df[df['volume'] > 1000000] # 子集 sub_df['high_volume'] = 'big' # 赋值新列

运行完Pandas可能只是弹个SettingWithCopyWarning警告,代码不报错,但结果可能很微妙:sub_df里那列有时候加上了,有时候又是空值,取决于当时Pandas底层是返回了视图还是副本。这种不确定性最折磨人。

解决方案有两个,我建议直接养成习惯:

方法一,筛完马上.copy(),明确告诉Pandas我要一块独立的数据:

sub_df = df[df['volume'] > 1000000].copy() sub_df['high_volume'] = 'big'

方法二,用.loc在原始df上直接定位并赋值:

df.loc[df['volume'] > 1000000, 'high_volume'] = 'big'

第二种写法其实更贴近聚宽策略里的很多场景:在筛选条件下新增一列,比如给符合条件的行打标签。它的好处是不产生子表,不存在视图和副本的暧昧关系。真心建议刚开始用Pandas的朋友,把.copy()当成一个条件反射,宁可多复制一次,也不要被这个警告搞得怀疑计算结果。

3. 排序:从"看着顺眼"到"能直接用"之间隔着一个reset_index

3.1 单列排序:升序、降序和NaN的位置

排序用sort_values,先看最基础的单列排序:

df_sorted = df.sort_values('pct') # 升序,默认 df_sorted = df.sort_values('pct', ascending=False) # 降序

这里有个关键认知:sort_values默认返回一个新df,而不是在原df上改。早期版本支持inplace=True原地排序,但我不推荐,因为inplace在链式操作和后续引用时容易埋坑。就用接收返回值的方式,逻辑清晰。

另外一个容易被忽略的点是NaN。如果排序列里有缺失值,它们默认排到最后。如果你想把缺失值放在最前面,可以加na_position='first'。这在处理财务类数据时很好用——有些财务报表字段本来就不是每期都有,排序时缺失值乱了位置会影响人眼观察。

3.2 多列排序:行业、市值、涨幅的组合排序

当你想按多个字段依次排时,by传一个列表就够了:

df_sorted = df.sort_values( ['industry', 'pct'], ascending=[True, False] )

这个操作的含义是:先按industry升序排,行业相同的再按pct降序排。注意ascending列表的长度必须和by列表一致。再强调一下,行业大的排前面、行业内的涨幅大的排前面,这种"先分组再组内排序"的思路在聚宽选股里非常常见。

有时候你还会遇到"市值排名"这种需求。比如想在每个行业内按市值升序排列,取市值最小的那只,本质上也是多列排序问题。在聚宽里可以先取到行业和市值字段拼在df里,再直接sort_values(['industry', 'market_cap']),然后groupby取每组第一行。

3.3 排序后索引全乱了:reset_index的两个选择

这是排序里最容易被新手忽略的坑。我们原先的df索引是0、1、2……51,按pct降序排完,索引并不会自动重置成0、1、2……,而是原来的索引跟着行一起被挪动了。也就是说新df第一行的索引可能是37,第二行的索引可能是8,完全乱序。

乱索引的后果很实际:当你以为自己在取"新表里的第6行"时,很可能写成df_sorted.loc[5],结果取到的是"原索引为5的那一行",跟你想要的位置完全不同。

正确的做法是排序之后顺手重置索引:

df_sorted = df.sort_values('pct', ascending=False).reset_index(drop=True)

如果原来的索引还有保留价值,比如索引是日期,可以不加drop=True,这样原日期索引会变成一列普通数据保留下来。大多数场景下我都是加drop=True的,因为后续处理只需要一个干净的0到n-1索引。记住:排序、筛选之后,只要不关心原索引,就reset_index(drop=True)。

3.4 在聚宽里的实际用法:选topN和行业轮动

聚宽选股里最常见的两个排序场景,一个是选topN,一个是按行业分组取排名。

选topN很简单:

top10 = df.sort_values('pct', ascending=False).head(10)

这个head(10)必须放在排序之后才有意义。如果直接对原始df执行.head(10),取到的是原始顺序的前10行,完全不是涨幅最大的10行。看起来是玩笑,但真的有人会在这里翻车。

行业轮动场景则要结合分组:

df_sorted = df.sort_values( ['industry', 'pct'], ascending=[True, False] ).reset_index(drop=True) industry_first = df.groupby('industry').head(2)

这里groupby('industry').head(2)会先对行业分组,每个行业内部此时已经是按pct降序排列的,所以每组取到的前2条,就是行业内涨幅前2。排序、分组、取头部这几个操作配合起来,能解决很多选股策略的初步筛选问题。

4. 筛选:布尔索引、query和isin的实战组合

4.1 布尔索引:Pandas筛选的核心逻辑

筛选的本质是"按条件挑行",Pandas里最核心的写法是布尔索引。它分两步:第一步,构造一个每个元素都是True或False的Series;第二步,把这个布尔Series放进方括号里,Pandas只保留对应位置为True的行。

mask = df['pct'] > 0.05 # 这个mask是布尔Series df_masked = df[mask] # 取出pct大于5%的所有行

上面两步可以合并成一行:df[df['pct'] > 0.05]。这里容易混淆的是:df['pct'] > 0.05是一个Series,而df[df['pct'] > 0.05]才是筛选后的DataFrame。新手如果少套一层,拿到的结果完全不一样。

用一个生活比喻:你在一排人面前挨个问"身高超过170吗",得到一串"是/否"的答案,然后只把答案为"是"的人带走。布尔索引干的就是这件事。

4.2 多条件组合:&、|、~ 和括号

多条件组合是筛选里最容易报错的地方。最常见的错误写法是:

df[df['pct'] > 0.02 & df['volume'] > 1000000] # 会报错

原因在于Pandas的运算符优先级。&作为位运算符,和比较运算符>一起出现时,解析顺序和你想象中不一样,结果是先计算0.02 & df['volume']之类的怪异组合,然后报错或者得出莫名其妙的结果。

正确的写法是每个比较条件都套上括号:

df[(df['pct'] > 0.02) & (df['volume'] > 1000000)]

还有两个"或"和"非"的条件组合:

# 或:涨幅大于5% 或者 跌幅小于-3% df[(df['pct'] > 0.05) | (df['pct'] < -0.03)] # 非:成交量不大于1000000的行 df[~(df['volume'] > 1000000)]

这里还要提醒一句:在Pandas的布尔条件里,不要用Python的关键词and、or、not。它们是给单个对象用的,Series这种向量化对象必须用&、|、~。

4.3 query:条件多了以后更清爽的写法

条件稍微一多,上面的写法虽然正确,但可读性会直线下降。Pandas提供了query方法,支持用类似自然语言的表达式:

df.query('pct > 0.02 and volume > 1000000') df.query('pct > 0.05 or high > low * 1.05')

query的好处是条件串成一个字符串,一眼能看明白几个条件之间的逻辑关系。它还支持引用外部变量,用@前缀:

threshold = 0.03 df.query('pct > @threshold')

在聚宽环境里,如果列名是中文,用query也没问题,但需要加反引号,比如df.query('涨跌幅> 2')。我一般在研究Notebook里条件超过两个时就会切到query,代码短了,心情也好了。

4.4 isin:从股票池里挑出品

筛选除了基于数值条件,还有一种高频需求:判断某列的值在不在一个集合里。这时候用isin最合适。

比如我维护了一个股票池,存了几只目标标的的代码,想从全市场行情表里只留下这些标的的记录:

pool = ['000000.XSHE', '000001.XSHE'] # 这里是示意集合,实际用你的股票池 df_pool = df[df['symbol'].isin(pool)]

isin接收一个列表或集合,逐行判断该行的值是否在里面。如果要取反,加个~:

df_not_pool = df[~df['symbol'].isin(pool)]

isin和==的区别很直接:==只能匹配单个值,isin能匹配一组值。在你处理"成分股池""自选股池"这类场景时,isin几乎替代了手写一长串|条件。

4.5 筛选后保持数据干净的两个小习惯

第一,筛选后索引同样会乱。无论用布尔索引、query还是isin,得到的df索引都不是连续从0开始的。如果后续要把筛选结果和其它df合并,索引错位会非常难受。所以筛选之后,除非你有意保留原索引,否则直接reset_index(drop=True)。

第二,筛选结果如果需要继续修改,记得.copy()。前面提到的SettingWithCopyWarning,在筛选场景里出现的频率最高。我的习惯是:筛选、排序之后马上.copy(),后面再怎么折腾都不心虚。

5. 把三个操作串起来:聚宽里的一个选股小实验

5.1 实验任务:找出某只股票一个月里的"放量上涨日"

光说单个操作不容易有手感,我搭了一个小实验,把插入列、排序、筛选各用一遍。

任务:拉某只股票两个月日线,计算每天的涨跌幅和20日均量,筛选出"涨幅超过2%且成交量高于20日均量"的日子,最后按涨幅从高到低排序,取前5条。

这个任务很接近实际选股里"找放量突破日"的初步动作。

5.2 完整代码

import pandas as pd import numpy as np demo_symbol = '000000.XSHE' # 占位,请替换为目标标的 df = get_price( demo_symbol, start_date='2024-01-01', end_date='2024-03-01', frequency='daily', fields=['open', 'close', 'high', 'low', 'volume'] ) # 用assign一次性插入涨跌幅和20日均量两列 df = df.assign( pct=lambda x: x['close'].pct_change() * 100, vol_ma20=lambda x: x['volume'].rolling(20).mean() ) # 筛选:涨幅超过2%,且成交量高于自身20日均量 cond = (df['pct'] > 2.0) & (df['volume'] > df['vol_ma20']) # 排序:按涨幅降序,再重置索引 result = df[cond].sort_values('pct', ascending=False).reset_index(drop=True) # 展示前5条 print(result[['close', 'pct', 'volume', 'vol_ma20']].head(5))

5.3 代码运行后的输出长什么样

因为底层数据是占位符,我不放具体数字,只说明逻辑:输出的每一行都是符合"放量上涨"条件的交易日,pct列从大到小排列。如果某个交易日满足条件但pct恰好是NaN,比如第一行因为pct_change没有前值导致NaN,滚动均值也会让前19天算不出vol_ma20,这些缺省行会被自动过滤,因为NaN和任何数字比较都是False。

这个案例里,assign完成了插入列,布尔条件完成了筛选,sort_values + reset_index + head完成了排序和取前N。三件事各司其职,凑在一起就是一个能直接用在思路验证里的小流程。

5.4 如果要处理多只股票:先展开再处理

单只股票的df处理起来简单,但很多场景下,你可能想一次性处理股票池里的几十只标的。聚宽get_price传入股票列表时,返回的是带多层索引的DataFrame,行索引分别是日期和股票代码。

我的建议是拿到手先reset_index(),把索引展平成普通列,再按上面的逻辑处理:

df_multi = get_price( ['000000.XSHE', '000001.XSHE'], # 占位示意 start_date='2024-01-01', end_date='2024-02-01', frequency='daily', fields=['open', 'close', 'volume'] ) df_flat = df_multi.reset_index() df_flat.columns.name = None

展开之后,date和code都变成普通列,插入列、排序、筛选的操作就和单股票完全一致了。按股票分组做衍生指标时,用的是groupby逻辑。这个思路先记住,以后处理全市场数据会很顺手。

6. 我在聚宽里实操下来的几个补充坑

6.1 别把研究环境的数据习惯直接搬到回测框架里

聚宽研究环境里,get_price返回DataFrame,Pandas怎么玩都行。但到了策略文件的回测框架里,handle_data(context, data)中的data参数不是DataFrame,而是一个可以按股票代码取数据的对象,取出来的是单个股票的快照数据。很多人第一次写策略时,想在data上直接套布尔筛选,结果一脸懵。

我的经验是:回测框架里的数据交互逻辑和研究环境不同,先别急着在处理函数里写一堆Pandas筛选。正确做法是把研究环境里验证过的数据处理逻辑抽成独立函数,传入必要的数据后返回处理结果,再在策略里调用。这样既保证研究阶段的成果能复用,也避免回测阶段被数据格式的问题绊住。

6.2 向量化操作优先于for循环

聚宽研究环境的数据量不算大,但你可能会处理全市场几千只股票的某段历史数据。如果习惯用for循环逐行计算衍生指标,速度慢得会让等待变得非常痛苦。

Pandas的设计哲学是向量化:整列操作、整列运算。pct_change、rolling、apply、shift、cut这些工具基本覆盖了大部分计算需求。一条df['pct'] = df['close'].pct_change()能抵掉一个几十行的循环。遇到必须逐行判断的复杂逻辑,也建议先把问题拆解成几个向量化步骤,尽量不要走到循环那一步。

6.3 多标的DataFrame的索引层级问题

前面已经提过,get_price传列表时返回MultiIndex。这里再强调一个细节:如果你用reset_index()展开后,列名的层级可能会残留。

df_flat.columns.name = None

这行代码经常被忽略,但输出表格时,表头上方多了一行列名标签,看起来非常不专业。顺手清一下,表格干干净净。

另外,展开后的索引列,建议先确认名字是date和code,再决定要不要重命名。不同的聚宽接口版本返回的列名可能略有差异,实际操作时先打印df_flat.columns看一眼,比什么都强。

6.4 特征列记得存档

聚宽研究环境是Notebook形式,会话断开以后,变量就没了。你辛辛苦苦构造出来的涨跌幅、均量、布尔标记列,一旦内核重启,全部归零。重新拉数据、重新计算倒是不难,但浪费时间。

我现在的习惯是:每做完一步数据处理,如果结果值得保留,就立刻落盘。最简单的写法:

df.to_csv('demo_features.csv')

下次研究直接pd.read_csv读回来,省去重复拉数和计算的步骤。尤其是那些经过复杂筛选和排序的中间结果,存档的意义更大。这个习惯帮我省下了不少重复劳动时间。

这三个操作看着基础,但用熟了之后,你会发现在聚宽里研究任何策略,最难的部分往往不是策略思想本身,而是能不能快速把原始数据加工成适合做判断的形态。插入列、排序、筛选就是这条加工流水线上最常用的三把刀。把它们的细节抠清楚,后面写策略时就能把注意力放在真正有价值的逻辑判断上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询