4、向量化计算入门:NumPy基础、向量化 vs 循环、广播机制、ufunc函数
做量化回测,说白了就是跟数据打交道。
K线数据、订单簿、因子矩阵……动不动就是几百万行。我刚入行那会儿,傻乎乎地用纯Python写循环算指标,一个简单的移动平均线跑了快两分钟。后来被前辈骂了一顿,扔给我一句话:「去学NumPy,别再用Python写循环了。」
嗯,从那以后,我的回测速度直接起飞。
这一章,我们就来啃下向量化计算这块硬骨头。你想想看,如果能把回测速度提升100倍,是不是很爽?
4.1 NumPy基础:为什么量化离不开它?
NumPy,全称 Numerical Python,是Python科学计算的基石。我个人习惯把它比作「高性能数组引擎」。
为什么量化离不开它?因为回测系统里,所有数据本质上都是多维数组。
- 一维数组:单只股票的收盘价序列
- 二维数组:多只股票在某段时间内的OHLCV数据
- 三维数组:多只股票、多个时间窗口、多个因子值
先看个最简单的例子,创建一个数组:
import numpy as np
# 从列表创建
prices = np.array([100.5, 101.2, 102.8, 103.1, 102.5])
print(prices)
# 输出: [100.5 101.2 102.8 103.1 102.5]
# 创建全零数组
zeros = np.zeros(5)
print(zeros)
# 输出: [0. 0. 0. 0. 0.]
# 创建等差数列
range_arr = np.arange(0, 10, 2)
print(range_arr)
# 输出: [0 2 4 6 8]
我在项目中遇到过一个问题:用Python列表存了100万条行情数据,结果内存直接爆了。换成NumPy数组后,内存占用直接降到原来的1/5。为什么?因为NumPy数组在内存里是连续存储的,而且每个元素类型固定,不像Python列表那样每个元素都是一个对象。
核心概念:ndarray
NumPy的核心数据结构叫ndarray(N-dimensional array)。它有几个关键属性:
ndim:维度数shape:形状,比如 (1000000,) 或 (1000, 5)dtype:数据类型,比如 float64、int32size:元素总数
4.2 向量化 vs 循环:速度差距有多大?
这是本章最核心的内容。说白了,向量化就是用数组运算代替逐元素循环。
你想想看,如果我们要计算两只股票的价格差:
# Python循环方式(慢!)
def calc_diff_loop(a, b):
result = []
for i in range(len(a)):
result.append(a[i] - b[i])
return result
# NumPy向量化方式(快!)
def calc_diff_vectorized(a, b):
return a - b
# 测试一下
import time
a = np.random.rand(1000000)
b = np.random.rand(1000000)
start = time.time()
diff_loop = calc_diff_loop(a, b)
print(f"循环耗时: {time.time() - start:.4f}秒")
start = time.time()
diff_vec = calc_diff_vectorized(a, b)
print(f"向量化耗时: {time.time() - start:.4f}秒")
结果会让你震惊。在我的机器上,循环用了0.8秒,向量化只用了0.003秒。差距接近300倍。
为什么会这样?
因为Python循环每次迭代都要做类型检查、对象创建、内存分配。而NumPy的向量化操作,底层是用C语言写的,直接在连续内存上做批量计算,省掉了所有Python层面的开销。
我的经验之谈
我曾经把一个用纯Python写的因子计算模块,全部改成NumPy向量化实现。原来跑一次要45分钟,改完后只要40秒。那天我提前下班了,哈哈。
4.3 广播机制:不同形状数组也能一起算
广播(Broadcasting)是NumPy最强大的特性之一,也是最容易踩坑的地方。
简单说,广播允许不同形状的数组进行算术运算。NumPy会自动扩展较小的数组,使其形状与较大数组匹配。
看个例子:
# 标量广播
prices = np.array([100, 101, 102, 103, 104])
# 每个价格都减去100
normalized = prices - 100
print(normalized)
# 输出: [0 1 2 3 4]
# 一维数组广播到二维
a = np.array([[1, 2, 3],
[4, 5, 6]])
b = np.array([10, 20, 30])
# b会自动扩展成 [[10, 20, 30],
# [10, 20, 30]]
result = a + b
print(result)
# 输出: [[11 22 33]
# [14 25 36]]
广播的规则其实很简单,就三条:
- 如果两个数组的维度数不同,就在维度较少的数组前面补1
- 如果两个数组在某个维度上的大小不一致,但其中一个为1,则可以广播
- 如果两个数组在某个维度上的大小不一致,且都不为1,则报错
我曾经踩过的坑
有一次我在计算因子暴露度时,把一个 (1000, 10) 的因子矩阵和一个 (1000,) 的权重向量做乘法。我本意是做逐行乘法,结果因为广播机制,NumPy自动把权重向量扩展成了 (1000, 1000) 的矩阵,内存直接炸了。后来我加了 reshape 操作才搞定。
记住:永远检查你的数组形状。用 array.shape 看一眼,能省很多麻烦。
4.4 ufunc函数:全称通用函数
ufunc,全称 universal function,是NumPy里对数组进行逐元素操作的函数。说白了,就是「向量化版本的数学函数」。
常见的ufunc分为几类:
| 类别 | 函数 | 说明 |
|---|---|---|
| 算术运算 | add, subtract, multiply, divide | 加减乘除 |
| 三角函数 | sin, cos, tan, arcsin | 三角函数及其反函数 |
| 指数对数 | exp, log, log10, log2 | 指数和对数运算 |
| 比较运算 | greater, less, equal, not_equal | 返回布尔数组 |
| 统计函数 | mean, std, var, sum, max, min | 聚合统计 |
看个实际例子,计算股票的日收益率:
# 假设有5天的收盘价
close_prices = np.array([100, 102, 101, 105, 108])
# 计算日收益率: (close[t] - close[t-1]) / close[t-1]
# 用ufunc实现
daily_returns = np.diff(close_prices) / close_prices[:-1]
print(daily_returns)
# 输出: [0.02 -0.00980392 0.03960396 0.02857143]
# 也可以用np.divide
daily_returns = np.divide(np.diff(close_prices), close_prices[:-1])
print(daily_returns)
# 输出一样
ufunc还有一个很实用的特性:reduce 和 accumulate。
# reduce:连续应用操作
# 计算所有收益率的乘积(累计收益率)
cum_return = np.multiply.reduce(1 + daily_returns)
print(cum_return)
# 输出: 1.08,相当于 (108/100)
# accumulate:保留中间结果
# 计算每日累计净值
cum_net = np.multiply.accumulate(np.concatenate([[1], 1 + daily_returns]))
print(cum_net)
# 输出: [1. 1.02 1.01 1.05 1.08]
小技巧
在回测系统里,我经常用 np.where 来做条件筛选。比如找出所有收益率大于2%的日期:
high_return_days = np.where(daily_returns > 0.02)
print(high_return_days)
# 输出: (array([0, 3]),)
这个函数比Python的列表推导式快太多了。
4.5 本章知识体系
为了让你更直观地理解本章的知识结构,我画了一张图:
这张图把本章的四个核心知识点串起来了。从NumPy基础出发,到向量化与循环的性能对比,再到广播机制和ufunc函数。每个知识点都不是孤立的,它们共同构成了向量化计算的完整体系。
4.6 实战小练习
光看不练假把式。给你留个小任务:
假设你有100万条股票价格数据,需要计算20日移动平均线。分别用Python循环和NumPy向量化实现,看看速度差多少。
# 提示:NumPy实现可以用 np.convolve 或者滑动窗口
prices = np.random.rand(1000000)
# 你的代码写在这里
# ...
嗯,我相信你试过之后,就再也不想写Python循环了。
本章小结
- NumPy的ndarray是量化回测的数据基石,内存效率远高于Python列表
- 向量化操作比Python循环快几十到几百倍,核心原因是C语言底层实现
- 广播机制让不同形状的数组可以一起运算,但要注意形状匹配
- ufunc函数提供了丰富的向量化数学操作,reduce和accumulate非常实用
记住一句话:在回测系统里,能用向量化就别用循环,能用NumPy就别用纯Python。这是我从无数次加班改代码中总结出来的血泪教训。
公众号:蓝海资料掘金营,微信deep3321