news 2026/10/20 8:34:40

聚宽Pandas实战:插入列、排序与筛选避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
聚宽Pandas实战:插入列、排序与筛选避坑指南

如果你接触过聚宽(JoinQuant)平台,那你大概率会有同感:Pandas 的插入列、排序、筛选这些操作,单独拎出来都很简单,但一放到聚宽 API 返回的行情数据上,总会出现各种小意外。我最早用聚宽写策略时,就在df['新列'] = ...这行代码上被警告折腾过,也在多条件筛选用and还是&上卡了半个小时。这篇文章就是我的 Pandas 初体验记录,围绕三个最基础的操作展开:插入列、排序、筛选。适合刚装上聚宽、想在研究环境里快速上手 Pandas 的量化新人;就算你已经写过一阵子策略,再看一遍也能避掉一些我踩过的坑。

1. 动手前,先搞懂聚宽环境里的 DataFrame 长什么样

1.1 聚宽终端里,Pandas 其实早就在后台等着你

有件事值得先确认一下:聚宽的研究环境已经预装了 Pandas,所以你在 Notebook 里直接import pandas as pd就能跑,不用单独装包。但这里有个隐藏差异——平台内置的 Pandas 版本未必和你本地的完全一样,某些函数参数在老版本里行为会有点区别。我见过有人把本地跑得好好的代码原样粘到聚宽,结果报错,最后发现是sort_values的inplace=True语义变了。

所以我给自己定了个习惯:每次到一个新环境,先敲一行print(pd.__version__),心里有个数。别小看这一步,后面排查问题能少走弯路。

import pandas as pd import numpy as np print(pd.__version__)

1.2 拿到手的数据,本质上是个“带时间索引的表格”

聚宽 API 千千万,最常打交道的还是get_price()。它返回的对象是一个标准的 Pandas DataFrame:

  • 行索引是交易日,类型是DatetimeIndex;
  • 列一般包括open、close、high、low、volume、money、factor;
  • 单只股票返回的是单层索引,多只股票返回的可能是多层索引。

其中factor是复权因子,用来做前复权、后复权计算,这个字段在初学阶段可以先忽略,但对后续做回测很关键。

你可以把它理解成一张“Excel 表”,只是时间列被放在了索引里。正因为这样,很多 Pandas 操作就有了“按行标签走”的天然优势,比如df.loc['2024-01-02']直接取某一天的数据。

1.3 准备一份练习数据:只用三行代码

后面的章节都要拿数据说事,这里先准备好底稿。下面的代码会拉取一只股票从 2024 年初到 3 月初的日线行情:

from jqdata import * security = "000001.XSHE" # 占位代码,实际请换成你自己关注的标的 df = get_price(security, start_date="2024-01-01", end_date="2024-03-01", frequency="daily", fields=["open", "close", "high", "low", "volume", "money"]) df.head()

解释一下参数:

  • start_date/end_date:闭区间日期范围;
  • frequency:"daily"表示日线,可以换"minute"等;
  • fields:按需取列,不用全字段一股脑拉下来,省内存。

如果你想把范围放宽到多个标的,可以先对单个标的跑通流程,后面第五章我会演示多标的的处理套路。初学阶段建议先单标的练手,因为报错定位更简单。

2. 插入列:从“裸数据”到“可用数据”加的第一块砖

2.1 直接赋值最顺手,但要注意对齐

给 DataFrame 加新列,最直白的写法就是直接赋值:

df["涨跌幅"] = df["close"].pct_change() * 100 df.head()

这行代码做了两件事:先算当日相对前一日收盘价的涨跌幅,再把它作为一个新列塞进表里。Pandas 会自动按索引对齐,也就是说,新列的每一行都会和原始表同一日期的行对上。

但有几个细节新手容易栽:

  • 如果新列是一个长度不一致的普通列表,Pandas 会直接报错或填充 NaN,因为它希望新列要么是标量,要么是长度/索引匹配的 Series;
  • 直接赋一个标量值,比如df["market"] = "XSHE",Pandas 会沿着所有行广播,通常没啥问题;
  • 在循环里反复给同一个 DataFrame 赋值新列,效率很低,还会让代码难读,后面可以用批量计算。

我看过不少人在这里栽跟头:拿两个不同索引的 DataFrame 做减法,得到一个全 NaN 的新列,然后画图发现一片空白,折腾半天才反应过来是索引没对齐。

2.2 想插到指定位置,用 insert

直接赋值新列会追加到 DataFrame 末尾。如果你在意列的顺序,比如想把新算的“涨跌幅”挪到close旁边,方便肉眼核对,那就要用insert():

df.insert(loc=1, column="昨收", value=df["close"].shift(1)) df.head()

loc参数表示新列插入的位置,数字从 0 开始;column是列名;value可以是 Series、标量或数组。这个函数不会覆盖已有列,如果同名列已经存在,会报错提醒你。

使用场景还挺常见的:我自己做复盘表的时候,习惯把“昨收、今开、涨幅”排在最前面,一眼扫过去就能看出今天走势。这时候insert比直接赋值靠谱得多。

2.3 多条数据合并时,用 concat

插入列还有一种常见场景是“横向拼接两个 DataFrame”,比如把价格数据和成交量指标合并到一个表里。此时可以用pd.concat:

price_df = df[["close", "high", "low"]] volume_df = df[["volume", "money"]] merged = pd.concat([price_df, volume_df], axis=1) merged.head()

axis=1表示横向拼,axis=0是纵向拼。拼接时如果两个表的索引不完全一致,默认会做“外连接”,缺失部分补 NaN;如果你想严格保留交集,可以加join="inner"。

这个操作在获取多个字段、多个接口数据时非常好用。聚宽有些数据接口返回的字段维度不同,没法用简单赋值整合,我就会开一个结果表格,然后多次 concat 进去。

2.4 实战演示:算涨跌幅和 5 日均线

光讲函数太干,我直接演示一个最常见的组合:给行情表加上“涨跌幅”和“5 日均线”两列。

df["return"] = df["close"].pct_change() df["ma5"] = df["close"].rolling(5).mean() df["振幅"] = (df["high"] - df["low"]) / df["close"] * 100 df.head(10)

这段代码背后有三个小知识:

  • pct_change()计算的是“当前值相对上一个非空值的百分比变化”,第一行因为没有“上一个值”,得到的是 NaN;
  • rolling(5).mean()在窗口不足 5 天时也是 NaN,需要第 5 天才开始有有效值;
  • 振幅这个公式是我自己常用的粗略算法,不是标准定义,但用来横向比较足够。

做完之后,你可以顺手看一下df.info(),检查新增列的类型和有没有大量 NaN。类型不对后面排序、筛选都会出奇怪问题。

3. 排序:让数据按照你的逻辑排队

3.1 sort_values 按列排序,最常用

拿到行情数据后,最常干的一件事是排序。比如我想看整段时间里哪一天的涨幅最大,直接对return列排序:

df.sort_values(by="return", ascending=False)

这里ascending=False表示降序,把涨幅最大的放最前面。没传ascending时默认升序,也就是从最小的开始排。

有几个隐藏参数值得知道:

  • na_position:NaN 排在最前还是最后。默认是"last",在降序找出最大涨幅时,NaN 会沉底,不会干扰顶部的有效数据;
  • kind:指定排序算法,日常不用管,了解有"quicksort"/"mergesort"即可;
  • inplace:老版本常用df.sort_values(..., inplace=True),新版本更推荐重新赋值,避免一些链式操作的坑。

排序之后有个小细节很容易忽略:行的索引顺序不会跟着排序重新编号,而是保留原来位置的行号。所以你会看到结果表里 index 是乱的,比如 60、2、35……这时候如果想把结果当成新表使用,最好重置索引:

result = df.sort_values(by="return", ascending=False).reset_index(drop=True)

drop=True表示丢掉原来的行号列,只保留从 0 编号的新索引。这个小坑我在第五节组合实战里还会遇到。

3.2 sort_index 按时间排序,别忽略

有时候你需要按时间索引排序,而不是按某一列的值。最典型的是后面章节说的多标的数据合并后,行顺序乱了,得用索引恢复时间先后:

df.sort_index(ascending=True)

这个操作不会动任何列的数据,只是把行按照索引排序。对于聚宽返回的行情数据来说,默认就是按时间升序排列的,但只要你做了一次筛选或拼接,顺序就未必可靠,养成排序索引的习惯很有必要。

如果你想把时间索引变成普通列,方便后续按日期字符串筛选,可以这样:

df_reset = df.reset_index() df_reset.head()

重置后,时间那一列默认叫index,你可以自己重命名成date。后面多标的拼接时,这个操作几乎是必经之路。

3.3 多重排序:先“板块”后“涨幅”

实际分析中,单一排序往往不够。比如你要做板块内比较,希望先按标的代码分组,再按涨幅降序排,那就需要多重排序:

df.sort_values(by=["code", "return"], ascending=[True, False])

注意这个操作的两个细节:

  • by列表里放的是排序关键字,顺序代表优先级,"code"是主排序,"return"是次排序;
  • ascending列表要和by一一对应,True表示升序,False表示降序。

如果你用的是多层索引 DataFrame,也可以直接用sort_index(level=[0, 1])按层级排序,但可读性不如sort_values清晰。我建议初学阶段先掌握sort_values的多重写法就够了。

3.4 只取前几名:nlargest 比 sort 更高效

有时候你并不想关心完整排序结果,只想知道“涨幅最大的 5 天”或者“成交量最大的 10 天”。这时用nlargest和nsmallest比“先 sort 再 head”更高效:

df.nlargest(5, columns="return")

这个函数只找出前 N 个最大值的行,不需要把整张表排一遍,数据量大的时候性能差距很明显。如果你用的是聚宽研究环境拉多年全市场数据,这种细节能帮你省不少内存。

同理,找最小值用nsmallest:

df.nsmallest(3, columns="volume")

日常复盘时,这两个函数是我使用频率很高的工具,比“sort_values 后切片”更直白。

4. 筛选:从一批数据里挑出你真正想要的

4.1 单条件筛选:布尔索引进场

筛选是 Pandas 里最有价值的一类操作,因为它直接帮你做“选股”的雏形。最简单的单条件筛选长这样:

df[df["close"] > 10]

方括号里放的是一个布尔 Series,结果只有满足条件的行会被保留。原理你可以理解成:Pandas 拿着这张“哪些行满足条件”的清单,把对应行挑出来。

为了避免和按列名取数的用法混淆,我推荐用.loc写法:

df.loc[df["close"] > 10]

.loc明确表示这里是按标签、布尔条件取数,某种程度上比df[...]更严谨一些。当然两种写法结果一样,看你个人习惯。

4.2 多条件筛选与 &、|、~ 的优先顺序

多条件筛选是新手的重灾区,我当年也在这里卡了很久。假设你想找“涨幅超过 3% 且成交量大于平均成交量”的日子,直觉可能会写:

# 错误示范:不要这么写 df[(df["return"] > 3) & (df["volume"] > df["volume"].mean())]

这里有两个关键点:

  • 必须用&(按位与),不能用 Python 的and;
  • 每个条件都要单独用小括号括起来,否则会报错或者得到错误结果。

为什么不能用and?因为and是 Python 层面的短路运算符,它要求两边的值都是可明确判断真假的一个布尔值,而 Series 是一组布尔值,Pandas 不知道你到底想判断“所有元素都满足”还是“任意一个满足”,所以会抛异常:The truth value of a Series is ambiguous。

同理,取反操作要用~:

df.loc[~(df["close"] < 10)]

如果要实现“或”的条件,用|。务必记住:&、|、~是位运算符,它们对布尔 Series 的每个元素逐个操作,所以必须用小括号保证运算优先级。

4.3 更舒服的筛选:between、isin、query

除了布尔索引,Pandas 还提供了一些“语法糖”,能让代码更可读。

区间筛选用between:

df[df["close"].between(8, 12)]

这个写法等价于(df["close"] >= 8) & (df["close"] <= 12),但明显更简洁。注意是闭区间。

多值匹配用isin:

target_codes = ["代码A", "代码B"] df[df["code"].isin(target_codes)]

这个在按股票池过滤时特别常用。比如你想只看某几个行业里的标的,先把它们的代码放进列表,然后用isin一次性过滤。

如果条件比较多,布尔索引会变得很臃肿,此时可以试试query:

df.query("return > 3 and volume > 1000000")

query的字符串写法下,and是允许的,不必再加括号,读起来更接近自然语言。但我个人建议:如果条件逻辑复杂,还是老老实实写布尔索引,因为调试时可以直接看到每个条件的中间结果。

4.4 按行筛选和按列筛选,别搞混了

筛选有两个方向,初学者特别容易混:

  • 按行筛选:df[df["return"] > 0],得到的是部分行;
  • 按列筛选:df[["close", "return"]],得到的是部分列。

区别是方括号里放的东西不一样。放的是一列名字的列表,它取列;放的是一个布尔 Series,它取行。还有个小细节:df["close"]返回的是 Series,而df[["close"]]返回的是 DataFrame。如果你后面还要对结果做.columns之类的操作,这个区别会影响行为。

5. 组合实战:一个“连续上涨观察列表”是怎么做出来的

5.1 需求拆解:找连续两天上涨的标的

前面几个章节是零件,这一节我们组装起来。假设我现在想做一个小工具:选出最近一段时间里“连续两天上涨”的标的,并且按涨幅从高到低排个序,作为第二天开盘前的观察列表。

这不是完整的交易策略,但足够练手,而且正好覆盖了插入列、排序、筛选三个核心操作。需求拆解一下:

  1. 拉取多个标的的日线收盘价;
  2. 计算每只标的的每日涨跌幅;
  3. 判断“今天上涨”且“昨天也上涨”;
  4. 筛选出满足条件的记录;
  5. 按涨幅降序排序,取前几名。

5.2 数据准备:多标的数据怎么组织

先准备一个代码列表,这里用占位符表示,实际替换成你关注的标的:

securities = ["标的A代码", "标的B代码", "标的C代码"] frames = [] for sec in securities: t = get_price(sec, start_date="2024-01-01", end_date="2024-05-01", frequency="daily", fields=["close", "open", "high", "low", "volume"]) t["code"] = sec frames.append(t) df_all = pd.concat(frames).reset_index(drop=True) df_all.head()

这段代码做了几件事:

  • 用循环逐个取数,避免一次传多个标的时返回多层索引带来的复杂度;
  • 给每个 DataFrame 加一列code,记录标的代码;
  • 用pd.concat(frames)做纵向拼接,把不同标的的行情放到同一张表里;
  • 重置索引,让时间不再是索引,而变成普通列。

经过这几步,df_all的表结构就是:index(原时间序列)、close、open等列、以及code列。名字看着有点乱,可以顺手重命名:

df_all = df_all.rename(columns={"index": "date"})

5.3 插入列、筛选、排序一气呵成

有了多标的统一表,后面就顺畅了。先算每只标的的涨跌幅:

df_all["return"] = df_all.groupby("code")["close"].pct_change() * 100

这里用了groupby("code"),是为了让每只标的单独计算涨跌,而不是跨标的算前一只和后一只的价差。这是多标的处理里非常关键的一步,不做分组的话,结果一定错。

接着判断“昨天是否也涨”。把涨跌幅这一列按标的分组平移一行:

df_all["prev_return"] = df_all.groupby("code")["return"].shift(1)

现在筛选条件就清晰了:今天涨、昨天也涨:

cond = (df_all["return"] > 0) & (df_all["prev_return"] > 0) df_signal = df_all.loc[cond]

最后排序取前几名:

df_signal = df_signal.sort_values(by="return", ascending=False) df_signal.reset_index(drop=True, inplace=True) df_signal.head(10)

这个组合流程并不复杂,但它把第二节的插入列(return和prev_return)、第三节的排序(sort_values)、第四节的筛选(布尔索引)全部串了起来。如果每一步都吃透了,你再看聚宽社区里很多选股逻辑,就会发现基本都是这种套路。

5.4 把结果用起来:回测和监控场景

拿到df_signal后,下一步可以做很多事:

  • 打印出来复盘,大概扫一眼哪些代码连续走强;
  • 导出成 CSV 或存到本地,后续人工确认;
  • 把筛选条件写进聚宽策略的handle_data里,变成自动下单逻辑。

我自己更推荐先在研究环境里跑通,再移植到回测。因为研究环境调试方便,可以随时打印中间结果,而回测环境里数据量大、执行循环频繁,不适合做复杂 Pandas 操作。策略里应该尽量轻量,重活放在研究阶段。

6. 聚宽环境里常见的 Pandas 报错与避坑笔记

6.1 SettingWithCopyWarning 烦人警告

我在一开始提到的坑,这里专门展开说。当你写出这样的代码时:

# 容易触发 Warning 的写法 df_filtered = df[df["close"] > 10] df_filtered["flag"] = 1

Pandas 大概率会弹出SettingWithCopyWarning,翻译过来就是:“你修改的可能是原表的一个副本,不一定真的写进去了。”这种警告不会让程序崩溃,但结果可能不符合预期。

解决办法很简单:在切片之后显式复制:

df_filtered = df[df["close"] > 10].copy() df_filtered["flag"] = 1

加.copy()看起来像个仪式感,实际上它明确告诉 Pandas:这是一张独立的新表,随便改。这是我个人最推荐的“保命”习惯——任何时候要对筛选结果新增列、修改值,先copy()再说。

提示:不要对链式索引结果直接赋值,比如df[df["close"]>10]["flag"]=1。Pandas 官方文档也建议避免链式赋值。

6.2 时间索引与时区带来的日期偏移

聚宽返回的行情索引是DatetimeIndex,一般不带时区名称,但不同接口混用时,偶尔会遇到带时区的时间和没带时区的时间合并后对不上的问题。

我遇到的情况是:把get_price的结果和另一个带时区的时间列做merge,结果发现日期总差 8 小时。排查后才发现是时区信息不一致。

通用解法是把时间列统一成纯日期,或者统一转换成无时区时间戳:

df["date"] = pd.to_datetime(df["date"]).dt.tz_localize(None)

如果你只是想按月份筛选,更简单的做法是把月份直接取出来当普通列:

df["month"] = df.index.month df.loc[df["month"] == 2]

这样彻底避开时区问题,也能满足大多数按月回看的场景。

6.3 NaN 处理:dropna 与 fillna 的取舍

插入列时,pct_change()和rolling()会在头部产生一批 NaN。NaN 不处理,排序时它们会堆积到最后,筛选时很多条件会把它们挡在外面,但如果你直接用这些列做回测信号,策略就会出问题。

处理思路无非两个方向:

  • 删除:df.dropna(subset=["return"]),去掉缺失行;
  • 填充:df.fillna(0),把 NaN 填成 0。

在计算涨跌幅时,第一行本来就没有参考值,填 0 和删掉都不影响后面分析。但在计算均线时,前 4 天的均线本身就不该存在,用dropna()更合理。关键是想清楚:这个 NaN 是“计算还没完成”还是“数据本身缺失”。前者建议填充,后者建议删除。

6.4 命名冲突和列名规范

聚宽 API 自带一些字段名,比如close、high、low、volume、factor。你新增列时,如果取名太随意,可能会覆盖或混淆原有语义。

我自己的规范是:

  • 新增计算指标加前缀,比如my_ma5、signal_roc;
  • 中文列名能用,但不建议大量使用,尤其在query和某些底层函数里,中文字符串容易出隐性编码问题;
  • 关键计算列用英文短横线风格,比如daily_return,阅读起来比涨跌幅更通用。

表格整理一下常见报错和对应的解决办法:

现象原因解决办法
The truth value of a Series is ambiguous多条件时用了and/or改成&/|,每个条件加括号
SettingWithCopyWarning对切片结果直接赋值筛选后先.copy()
新增列全为 NaN索引没有对齐统一索引,或使用.values取出底层数据
排序后 index 乱序行号保留原位排序后.reset_index(drop=True)
多标的数据涨幅算错没按标的groupby使用groupby("code")后再算pct_change
时间列对不上时区信息不一致统一转成无时区时间戳或取年月字段

这些小问题在聚宽实战里非常常见。我的习惯是:每次从聚宽 API 拿到数据,不急着分析,先执行三步——df.shape看大小,df.dtypes看类型,df.head()看内容。不管后续是插入列、排序还是筛选,这三步都能在第一时间把数据问题暴露出来。

最后再分享一个小技巧:当你写完一段 Pandas 处理代码,先挑一个最小数据集跑通,再放到全量数据上执行。我在聚宽上更多时候不是被语法难住,而是被“数据规模大了之后的内存与性能问题”拖住。把流程从小处打通,再放大执行,能省下大量调试时间。

Pandas 初体验的这三个操作,练完一遍你就能感受到,它没有想象中复杂,但每一个细节都值得亲手敲一遍。希望这篇能帮你在聚宽平台上少踩几个坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 13:41:51

Python单元测试(unittest)实战指南:从入门到项目落地

项目标题: "Python单元测试&#xff08;unittest&#xff09;实战指南"我做了快十年Python开发&#xff0c;前五年几乎不写测试&#xff0c;总觉得那是“浪费时间”&#xff0c;代码能跑就行。直到有一次接手一个历史项目&#xff0c;改一个看似无关紧要的工具函数&a…

作者头像 李华
网站建设 2026/10/18 11:38:15

边缘检测算法详解:从梯度算子到Canny与轮廓提取

1. 边缘检测到底在检测什么&#xff0c;为什么"梯度"是最合适的数学工具1.1 灰度突变&#xff1a;边缘在人眼与数学之间的统一表达先问一个看起来有点傻的问题&#xff1a;人眼是怎么看出"这里有一条边"的&#xff1f;你看一张黑白棋盘图&#xff0c;黑色格…

作者头像 李华
网站建设 2026/10/19 4:46:13

PF6000拧紧控制器配置与四步拧紧工艺落地指南

简介&#xff1a;本资源是阿特拉斯科普柯&#xff08;Atlas Copco&#xff09;Power Focus 6000智能拧紧控制器的官方中文说明书&#xff0c;面向工业自动化产线工程师、设备维护技术人员及智能制造系统集成人员&#xff0c;用于指导设备配置、安全操作与日常维护。文档覆盖从硬…

作者头像 李华
网站建设 2026/10/20 16:04:33

Spring Boot整合Hadoop:校园二手交易系统毕设项目实战拆解

校园二手交易这个场景&#xff0c;在每年的毕业设计里几乎都是TOP3的热门选题。但大多数人的项目都停留在"SSMJSPMySQL"这种十年前的技术栈上&#xff0c;答辩时老师一问分布式就哑火。今天要拆解的这个项目比较特别&#xff1a;Java Spring Boot做后端业务层&#x…

作者头像 李华
网站建设 2026/10/18 19:20:22

F2FS为何是NAND闪存设备的必需文件系统

1. 项目概述&#xff1a;为什么F2FS在NAND闪存设备上不是“可选”&#xff0c;而是“必须”你手里的SSD、U盘、手机存储、车载记录仪卡、工控设备SD卡——只要底层是NAND Flash&#xff0c;就绕不开一个根本矛盾&#xff1a;NAND物理特性与传统文件系统设计逻辑的天然错配。这不…

作者头像 李华
网站建设 2026/10/18 9:53:08

C语言scanf缓冲区机制与格式串陷阱:从原理到工程实践

写C语言这些年&#xff0c;scanf是我见过讨论最两极化的库函数之一。新手觉得它不过是个输入函数&#xff0c;老手却对它又爱又恨。很多看似玄学的bug&#xff0c;其实都源于对缓冲区机制和格式串规则的误解。这篇文章不会停留在“scanf怎么用”&#xff0c;而是把我在实际项目…

作者头像 李华