资讯动态

聚宽量化研究必备:Pandas插入列、排序与筛选实战

发布时间:2026/10/15 6:01:30 来源:尧图企业网站定制
第一次在聚宽里拉数据的时候我看着那几十行行情记录脑子里只有一个念头接下来怎么做涨跌幅怎么做排名怎么把涨幅超过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_date2024-01-01, end_date2024-03-01, frequencydaily, 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这些是float64volume大概率是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%。如果你想展示成百分数可以再乘100df[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.022.2 insert需要控制列顺序时再用直接赋值只能追加到末尾但有些场景下你希望字段按某个顺序排列比如把股票代码放到最左边当标识列。这时候用insertdf.insert(0, symbol, demo_symbol)insert的第一个参数是位置0就代表放在最前面第二个参数是列名第三个是值。注意insert是在原df上原地修改不像直接赋值那样每次生成新浅拷贝。它还支持allow_duplicates参数允许你创建重复列名不过日常使用中我基本不开这个选项重复列名在后续按列名取值时容易引发混乱。什么样的场景我会优先用insert主要是标识优先、数据在后的要求。比如最终要输出一张表来观察我希望第一列是标的代码第二列是日期然后才是行情和指标列。这种视觉顺序用insert控制起来最直观。2.3 assign适合一口气增加多个列如果一次要新增好几个字段逐一赋值代码会显得很啰嗦。Pandas提供的assign方法可以一口气加多列并且支持链式写法df df.assign( log_closelambda x: x[close].apply(np.log), ma5lambda x: x[close].rolling(5).mean(), ma20lambda 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_index3.1 单列排序升序、降序和NaN的位置排序用sort_values先看最基础的单列排序df_sorted df.sort_values(pct) # 升序默认 df_sorted df.sort_values(pct, ascendingFalse) # 降序这里有个关键认知sort_values默认返回一个新df而不是在原df上改。早期版本支持inplaceTrue原地排序但我不推荐因为inplace在链式操作和后续引用时容易埋坑。就用接收返回值的方式逻辑清晰。另外一个容易被忽略的点是NaN。如果排序列里有缺失值它们默认排到最后。如果你想把缺失值放在最前面可以加na_positionfirst。这在处理财务类数据时很好用——有些财务报表字段本来就不是每期都有排序时缺失值乱了位置会影响人眼观察。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, ascendingFalse).reset_index(dropTrue)如果原来的索引还有保留价值比如索引是日期可以不加dropTrue这样原日期索引会变成一列普通数据保留下来。大多数场景下我都是加dropTrue的因为后续处理只需要一个干净的0到n-1索引。记住排序、筛选之后只要不关心原索引就reset_index(dropTrue)。3.4 在聚宽里的实际用法选topN和行业轮动聚宽选股里最常见的两个排序场景一个是选topN一个是按行业分组取排名。选topN很简单top10 df.sort_values(pct, ascendingFalse).head(10)这个head(10)必须放在排序之后才有意义。如果直接对原始df执行.head(10)取到的是原始顺序的前10行完全不是涨幅最大的10行。看起来是玩笑但真的有人会在这里翻车。行业轮动场景则要结合分组df_sorted df.sort_values( [industry, pct], ascending[True, False] ).reset_index(dropTrue) 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(dropTrue)。第二筛选结果如果需要继续修改记得.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_date2024-01-01, end_date2024-03-01, frequencydaily, fields[open, close, high, low, volume] ) # 用assign一次性插入涨跌幅和20日均量两列 df df.assign( pctlambda x: x[close].pct_change() * 100, vol_ma20lambda x: x[volume].rolling(20).mean() ) # 筛选涨幅超过2%且成交量高于自身20日均量 cond (df[pct] 2.0) (df[volume] df[vol_ma20]) # 排序按涨幅降序再重置索引 result df[cond].sort_values(pct, ascendingFalse).reset_index(dropTrue) # 展示前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_date2024-01-01, end_date2024-02-01, frequencydaily, fields[open, close, volume] ) df_flat df_multi.reset_index() df_flat.columns.name None展开之后date和code都变成普通列插入列、排序、筛选的操作就和单股票完全一致了。按股票分组做衍生指标时用的是groupby逻辑。这个思路先记住以后处理全市场数据会很顺手。6. 我在聚宽里实操下来的几个补充坑6.1 别把研究环境的数据习惯直接搬到回测框架里聚宽研究环境里get_price返回DataFramePandas怎么玩都行。但到了策略文件的回测框架里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读回来省去重复拉数和计算的步骤。尤其是那些经过复杂筛选和排序的中间结果存档的意义更大。这个习惯帮我省下了不少重复劳动时间。这三个操作看着基础但用熟了之后你会发现在聚宽里研究任何策略最难的部分往往不是策略思想本身而是能不能快速把原始数据加工成适合做判断的形态。插入列、排序、筛选就是这条加工流水线上最常用的三把刀。把它们的细节抠清楚后面写策略时就能把注意力放在真正有价值的逻辑判断上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑