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、int32
  • size:元素总数

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
  2. 如果两个数组在某个维度上的大小不一致,但其中一个为1,则可以广播
  3. 如果两个数组在某个维度上的大小不一致,且都不为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还有一个很实用的特性:reduceaccumulate

# 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向量化计算 NumPy基础 ndarray创建 shape/dtype 向量化 vs 循环 性能差距300x C语言底层 广播机制 自动扩展 形状匹配规则 ufunc函数 逐元素操作 reduce/accumulate

这张图把本章的四个核心知识点串起来了。从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