7.8 向量化思维:别写慢循环
课前导读
本章目标
学完能解决什么困惑:为什么量化研究里经常说“少写逐行循环”,以及如何用 pandas 的整列计算表达投资规则。
适合谁学习:已经知道 DataFrame 是表格,准备把 A 股观察规则写成可重复计算流程的读者。
难度:intermediate
预计用时:约 35 分钟
本节的关键词不是“炫技”。
我们不追求把代码写得让人看不懂。
我们追求的是:同一条规则,每次都能用同一种方式计算。
你可以把向量化想成“整列计算,而不是逐格手算”。
在表格软件里,如果你想算 1000 行收盘价涨跌幅,你不会一个格子一个格子按计算器。
你会写一个公式,然后向下填充整列。
pandas 的向量化思维也是类似。
它让你对一整列数据说话。
知识正文
1. 类比:逐格手算与整列盖章
想象你在整理 500 只 A 股的日线数据。
每只股票有几百个交易日。
如果你逐行问:“今天收盘价比昨天高吗?”
你的大脑很快会被细节淹没。
逐行循环就像拿着印章,一个格子一个格子盖。
向量化就像把整列名单交给机器,一次性按规则处理。
它不是偷懒。
它是把注意力从“怎么挪到下一行”,转回“规则本身是否正确”。
量化研究最怕的不是慢一点。
更怕的是循环里藏着状态错误、漏算第一行、错用未来数据。
向量化能减少这类人为细节。
2. 术语铺垫:什么是向量化
向量可以先理解成“一整列数字”。
向量化就是对一整列或多列数字一次性运算。
在 pandas 里,一列通常叫 Series。
一张表叫 DataFrame。
当你写 df["close"].pct_change() 时,你不是只计算某一天。
你是在告诉 pandas:请对整列收盘价计算相邻两天的变化。
当你写 df["close"] > df["ma20"] 时,得到的不是一个答案。
得到的是一整列 True 或 False。
这列 True 或 False 可以被理解成“条件检查清单”。
它也常被叫作布尔条件。
新手不必急着记英文。
你只要理解:规则可以变成整列的是与否。
3. loop 写法:像逐行翻账本
先看一个直观但容易变长的写法。
returns = []
last_close = None
for close in df["close"]:
if last_close is None:
returns.append(0)
else:
returns.append(close / last_close - 1)
last_close = close
df["return_loop"] = returns这段代码能表达每日收益率。
它的优点是接近人类一步一步想问题。
但当规则变多时,循环里会出现很多临时变量。
比如昨天的收盘价、前天的信号、是否持仓、累计费用。
新手很容易在某一行忘记更新状态。
也容易把今天才知道的信息用到昨天。
这不是说循环不能用。
而是说入门阶段应优先把简单规则写成整列计算。
4. vectorized 写法:像整列填公式
同样的收益率,用 pandas 可以更直接。
df["return_vec"] = df["close"].pct_change().fillna(0)
df["ma20"] = df["close"].rolling(20).mean()
df["ma60"] = df["close"].rolling(60).mean()
df["trend_ok"] = df["ma20"] > df["ma60"]pct_change() 表示计算相邻两行的百分比变化。
rolling(20).mean() 表示用最近 20 个交易日求平均。
df["ma20"] > df["ma60"] 表示整列比较。
这几行代码读起来像规则说明。
收盘价计算每日收益。
收盘价计算 20 日均线。
收盘价计算 60 日均线。
20 日均线高于 60 日均线时,趋势条件为真。
这种写法更接近研究记录。
别人读代码时,更容易复现你的规则。
5. 为什么少写慢循环
第一,少写循环能减少人为状态错误。
量化研究里,今天、昨天、上一个信号、当前持仓都很容易混在一起。
整列计算把时间关系暴露在列名和函数里。
第二,少写循环通常更快。
pandas 和 NumPy 底层已经针对整列计算做了很多优化。
但本课程不把速度当作炫耀点。
对于新手来说,更重要的是规则清楚。
第三,整列计算更方便复查。
当你生成 ma20、ma60、trend_ok 这些列后,可以直接抽样检查。
你可以问:某一天为什么信号为真。
答案就在同一行附近。
第四,整列计算更容易画图和写报告。
上一节的收益曲线、回撤曲线,都依赖清晰的列。
如果所有逻辑都藏在循环内部,复盘会困难很多。
6. A 股案例:均线条件的一列化表达
假设你想观察一个非常朴素的条件。
当某只 A 股的 20 日均线高于 60 日均线时,认为中期趋势较强。
这不是买入建议。
它只是一个可计算的观察条件。
用向量化表达时,你可以这样拆分。
df["ma20"] = df["close"].rolling(20).mean()
df["ma60"] = df["close"].rolling(60).mean()
df["trend_filter"] = df["ma20"] > df["ma60"]
df["usable_signal"] = df["trend_filter"].shift(1).fillna(False)这里最值得注意的是 shift(1)。
它表示把信号往后挪一天。
为什么要挪?
因为当天收盘后你才知道当天的收盘价和均线结果。
如果你假装当天开盘前就知道当天收盘后的条件,就偷看了未来。
在 A 股研究里,这个错误非常常见。
向量化不是只为了快。
它也能帮助你明确“信息在什么时候可见”。
风险提醒
没有 shift() 的信号不一定错,但你必须解释清楚它代表何时可见,不能把收盘后才知道的结果当成盘中已经知道。
7. 从规则到列的思考顺序
请把一句投资观察话,拆成几列。
“最近趋势较强”可以拆成 ma20、ma60、ma20 > ma60。
“今日放量”可以拆成 volume、volume_ma5、volume > volume_ma5。
“避免 ST 股票”可以拆成 is_st 和 is_st == False。
“只看成交额较高的股票”可以拆成 amount 和 amount > threshold。
每个条件变成一列后,复查就容易了。
你能看到哪一天满足,哪一天不满足。
你也能解释为什么某只股票被筛入或筛出。
这就是可复现性的来源。
不是因为代码高级。
而是因为每一步都有证据。
flowchart TD
A[一句观察想法] --> B[拆成数据列]
B --> C[整列计算条件]
C --> D[生成信号列]
D --> E[抽样复查]
E --> F[写入研究记录]8. 常用向量化工具清单
pct_change():计算相邻两行的涨跌幅。
rolling().mean():计算滚动平均,例如 20 日均线。
shift():把数据向后或向前移动,用来处理信息可得时间。
where():按条件保留或替换数据。
布尔条件:用一整列 True 或 False 表达规则是否满足。
列间运算:让收盘价、成交量、均线等列互相比较。
这些工具不需要一次全背下来。
你只需要在研究中反复使用。
用多了,就会自然形成“先想列,再想规则”的习惯。
9. 常见误区
误区一:为了不用循环,写出自己也看不懂的一长串代码。
可读性永远优先。
如果一行代码太长,可以拆成几列。
误区二:把所有问题都硬塞进向量化。
有些复杂状态以后会在回测系统中处理。
本章只学习研究表格的基础表达。
误区三:忘记处理第一行和缺失值。
例如 pct_change() 第一行没有前一天,会产生缺失。
这时要明确填 0、保留缺失,还是从第二行开始观察。
误区四:忽略索引对齐。
不同股票、不同日期的数据合并时,日期必须对齐。
否则计算结果可能看起来正常,实际已经错位。
交互实验室
练习 1:把一句话改写成列
请把下面这句话拆成列名。
“只观察收盘价高于 20 日均线,并且成交量高于 5 日平均成交量的交易日。”
你至少应该写出这些列。
close:收盘价。
ma20:20 日均线。
volume:成交量。
volume_ma5:5 日平均成交量。
price_ok:收盘价是否高于 20 日均线。
volume_ok:成交量是否高于 5 日平均成交量。
final_ok:两个条件是否同时满足。
练习 2:比较 loop 与向量化
请阅读下面两句。
“我每天看一行,判断今天是否满足条件。”
“我先把所有日期的条件一次性算成一列,再抽样检查。”
写下你认为第二种方式的两个好处。
一个好处应和复查有关。
另一个好处应和减少人为错误有关。
练习 3:检查信息可得性
假设你用当天收盘价计算信号。
请回答:这个信号最早应该用于当天,还是下一个交易日。
如果你选择下一个交易日,请在代码里写出 shift(1)。
如果你选择当天,请写出你的合理假设。
这个练习不是考语法。
它是在训练你不要偷看未来。
核心复盘
知识清单:
- [ ] 我理解向量化是整列计算,不是为了写得更神秘。
- [ ] 我能看懂
pct_change()、rolling()、shift()在量化研究中的基本用途。 - [ ] 我知道布尔条件可以把投资规则变成一列 True 或 False。
- [ ] 我会用
shift(1)思考信号何时可见,避免偷看未来。 - [ ] 我知道可读性优先,必要时可以把复杂表达拆成多列。
金句总结
少写循环,不是为了炫耀速度,而是为了让规则更清楚、更容易复查。