📎 配套代码:
第03讲_按标签取数.py
📊 配套数据:本讲用课件内的小样本(2024-01-02 真实收盘快照),无需外部数据
🎬 开场:位置是会变的
本讲的样本是 2024-01-02 的真实收盘数据(取自本地 tushare
pro_bar),只取四五只股票是为了每个值都看得清。
上一讲用位置取数,close[0] 取第一只股票的价格。看这段代码:
codes = ["600519", "000858", "601318", "000001"] close = np.array([1685.01, 136.00, 39.47, 9.21]) close[0] # 1685.01 —— 贵州茅台
现在你要按价格排个序(研究里再常见不过的动作):
order = np.argsort(close) # 升序 codes = [codes[i] for i in order] close = close[order] close[0] # 9.21 ← 变成平安银行了
代码一个字没改,close[0] 指向的股票变了。
位置只是数据当前排列顺序的一个副产品。排序、插入、删除、合并、筛选——这些动作都会重排位置。而你真正想指的那个东西(贵州茅台)从来没变过,变的只是它排第几。
这就是标签取数存在的理由:你说的是”茅台”,不是”第 0 个”。
在量化里,这个问题格外要命,因为你几乎总在做重排数据的动作:
| 你做的事 | 位置发生了什么 |
|---|---|
| 按市值排序选股 | 全部重排 |
| 剔除 ST 股 | 后面所有股票的位置前移 |
| 两张表按代码合并 | 顺序取决于合并方式 |
| 按日期筛选 | 行位置全变 |
| 新股上市 | 插入新行,后面全移位 |
只要你的代码里写着”第 3 列”,任何一次重排都可能让它悄悄指向别的东西。而且不报错。
一、🧰 字典:最基本的标签取数
Python 里最常用的标签容器就是字典——用名字存,用名字取:
close = {"600519": 1685.01, "000858": 136.00, "601318": 39.47} close["600519"] # 1685.01
不管字典里的键是什么顺序,close["600519"] 永远给你茅台的价格。
取不到怎么办
直接用 [] 取一个不存在的键会报 KeyError。多数时候你不希望程序就这么停掉:
close["999999"] # ❌ KeyError close.get("999999") # None —— 取不到给 None close.get("999999", 0.0) # 0.0 —— 取不到给你指定的默认值
.get() 在处理真实数据时很有用:一批股票里有几只当天停牌、数据源没给,用 .get(code, np.nan) 可以让流程继续走下去,同时把缺失标记成 NaN,而不是让整个脚本崩掉。
WARNING: ⚠️ 但”不崩掉”不等于”没问题”
.get(code, 0.0)把缺失填成 0,代码不报错了——可 0 元的股价是个荒谬的数字,它会一路流进你的收益率计算。默认值填什么,是个需要想清楚的决定:填np.nan通常比填 0 安全,因为 NaN 会在后续计算里保持”缺失”这个状态,而 0 会伪装成一个正常数字。
常用操作
"600519" in close # True —— 判断有没有 close.keys() # 所有标签 close.values() # 所有值 close.items() # 成对拿出来,常用来遍历
for code, px in close.items(): print(code, px)
用推导式批量取
给定一个股票池,把它们的价格都取出来:
pool = ["600519", "601318"] {code: close[code] for code in pool} # 假设都存在 {code: close.get(code, np.nan) for code in pool} # 更稳妥
🎮 随堂快练
QUESTION: 你有一个字典
mktcap(代码 → 市值),和一个股票池列表pool。写代码取出池子里所有股票的市值,池子里可能有几只不在字典中。
TIP: 👉 答案{c: mktcap.get(c, np.nan) for c in pool}用
.get(c, np.nan)而不是mktcap[c]——后者遇到不存在的代码会直接KeyError中断。填np.nan而不是0,因为市值为 0 的股票在后续排序、分组里会被当成”最小市值”,而 NaN 会被正确识别为缺失。
二、🧰 结构化数组:给数组的列起名字
字典解决了”按名字取”,但它有个短板:装不了二维表格。
行情数据是这样的:每一行是一条记录,每条记录有代码、收盘价、成交量几个字段。用字典表示会很别扭。
numpy 的结构化数组正好补这个位置——给数组的每一列起个名字:
dt = np.dtype([("code", "U6"), ("close", "f8"), ("vol", "i8")]) arr = np.array([("600519", 1685.01, 32156), ("000858", 136.00, 215269)], dtype=dt)
dtype 在这里描述了一条记录的结构:code 是 6 个字符的字符串、close 是 8 字节浮点、vol 是 8 字节整数。
取数就有了两个方向:
arr["close"] # 按字段名取一整列 → [1685.01 136. ] arr["code"] # → ['600519' '000858'] arr[0] # 按位置取一整行 → ('600519', 1685.01, 32156) arr[0]["close"] # 先取行再取字段 → 1685.01
这已经很接近 DataFrame 了:按名字取列、按位置取行。区别在于 DataFrame 的行也有标签,而且功能多得多。
按字段取是视图
np.shares_memory(arr, arr["close"]) # True
取一整列不复制数据,改它会改到原数组。这和上一讲的规律一致:字段在内存里是等间隔排列的(每条记录占固定字节数,同一字段每次跳过一条记录的长度),所以能用窗口表达。
🎮 随堂快练
QUESTION: 用上面的
arr,写出:① 所有股票的成交量 ② 第二条记录的代码 ③ 判断arr["close"]改动后会不会影响arr
TIP: 👉 答案
①arr["vol"]②arr[1]["code"](或arr["code"][1]) ③ 会影响,因为按字段取返回的是视图,np.shares_memory返回True。要独立副本得写arr["close"].copy()。
三、🤔 位置还是标签:怎么选
两种方式都能取到同一个数,但适用场景不同。
| 按位置 | 按标签 | |
|---|---|---|
| 你说的话 | “第 3 个” | “叫 close 的那个” |
| 数据重排后 | 失效(指向别的东西) | 仍然正确 |
| 需要知道 | 它排第几 | 它叫什么 |
| 适合 | 数据顺序本身有意义(时间序列的”最近 20 天”) | 数据顺序无意义(哪只股票、哪个字段) |
| 出错方式 | 静默指错,不报错 | 取不到会 KeyError,报错
|
最后一行很关键:标签取数错了会报错,位置取数错了不会。
close["600520"](代码写错一位)立刻 KeyError,你马上知道。而 close[3](本该是 2)安静地返回一个数字,你可能到最后都发现不了。
什么时候该用哪个
用位置:维度本身就是有序的,且顺序有含义。
- 时间序列的”最近 250 天”:
px[-250:] - 训练集/测试集切分:
px[:split]/px[split:]
用标签:这个维度只是”一堆东西”,顺序是任意的。
- 哪只股票:
d["600519"] - 哪个字段:
arr["close"]
行情矩阵正好是这两者的组合——行(时间)用位置,列(股票)用标签。这正是 pandas 的 DataFrame 要做的事:给行和列都配上标签,同时保留按位置取的能力。
IMPORTANT: 🔑 pandas 的设计动机
到第 05–07 讲你会看到,pandas 把这两套方式做成了两个明确的入口:
df.iloc[0]—— 按位置(i = integer)df.loc["600519"]—— 按标签分成两个名字,就是为了不让你搞混。numpy 只有一套
[],你得自己记住方括号里的东西是位置还是别的什么;pandas 强迫你说清楚。
🎮 随堂快练
QUESTION: 下面两个需求,各该用位置还是标签?
① 取某只股票最近 60 个交易日的收盘价
② 从一张有 30 个字段的行情表里取出”收盘价”和”成交量”两列
TIP: 👉 答案
① 两者都用:股票用标签(哪只)、时间用位置(最近 60 天)。
② 标签。字段的排列顺序是任意的——数据源换个版本、多加一个字段,位置就变了。写arr"close","vol"而不是arr[:, [3, 7]]。
🏋️ 训练营
QUESTION: 🟢 训练 1:你有
close = {"600519": 1685.01, "000858": 136.00}。写出:① 取茅台的价格 ② 取一个可能不存在的代码,取不到时返回 NaN ③ 判断 “601318” 在不在里面
TIP: 👉 参考
①close["600519"]②close.get(code, np.nan)③"601318" in close
QUESTION: 🟡 训练 2:下面这段代码想”取出市值最大的 3 只股票的代码”,但有个隐患。指出来。
order = np.argsort(mktcap)[::-1] # 按市值降序 top3 = codes[:3] # 取前 3 个代码TIP: 👉 参考
order算出来了,但根本没用上。codes[:3]取的还是原始顺序的前三只,和市值毫无关系。
正确写法要用order去重排codes:top3 = [codes[i] for i in order[:3]]这个 bug 不报错,还会给你三个看起来很正常的股票代码——这正是位置取数最典型的出错方式:你以为位置反映了某种顺序,其实没有。
QUESTION: 🔴 训练 3:你要把两个数据源的价格对上——A 源是字典
{代码: 价格},B 源是两个平行的列表codes_b和prices_b(第 i 个代码对应第 i 个价格)。写代码找出两边价格差异超过 1% 的股票。
TIP: 👉 参考b = dict(zip(codes_b, prices_b)) # 先把 B 源也变成字典 diff = {c: abs(a_price[c] - b[c]) / b[c] for c in a_price.keys() & b.keys() # 只比较两边都有的 if abs(a_price[c] - b[c]) / b[c] > 0.01}关键有两点:
① 先把平行列表转成字典(dict(zip(...)))。两个平行列表靠位置对应,非常脆弱——任何一方排序变了,对应关系就全错,而且不报错。
② 用keys() & keys()取交集,只比较两边都有的股票。直接遍历 A 的键去查 B 会KeyError。
🐛 常见坑
- ⚠️ 算了排序却没用上:
np.argsort返回的是位置数组,你得拿它去重排数据,光算不用等于没做。 - ⚠️ 两个平行列表靠位置对应:任何一方重排,对应关系全错且不报错。及早转成字典或结构化数组。
- ⚠️
.get()的默认值填 0:0 会伪装成正常数字流进后续计算。填np.nan更安全。 - ⚠️ 字段位置写死:
arr[:, 3]在数据源加一个字段后就指向别处。用字段名。 - ⚠️ 结构化数组按字段取是视图:改它会改原数组,要独立副本得
.copy()。
✍️ 作业
- 把开场那段代码跑一遍:建
codes和close,打印close[0];然后按价格升序重排,再打印close[0],确认它变了。 - 用
dict(zip(codes, close))把它们转成字典,重复上面的排序操作,验证d["600519"]不受影响。 - 造一个有
code / close / vol三个字段的结构化数组,分别按字段名和按行位置取数,并用np.shares_memory验证按字段取是不是视图。 - 思考题:既然标签取数更安全,为什么不干脆全部用标签、彻底抛弃位置?(提示:想想”最近 20 个交易日”这个需求——你知道那 20 天分别是哪天吗?如果知道,写起来方便吗?)
🔮 下讲预告:第 04 讲——按条件取数。前两讲你都得先知道”要第几个”或”叫什么名字”。但研究里更多的情况是:你不知道是哪些,只知道它们满足什么条件——涨幅超过 5% 的、市值前 20% 的、剔除掉 ST 的。下一讲讲布尔掩码,以及一个在 pandas 里会反复出现的陷阱。