4、必备Python库(上):NumPy基础:创建数组、索引与切片、数学运算
做量化选股,说白了就是跟数据打交道。股票价格、成交量、财务指标……这些数据动辄成千上万条。你要是用纯Python的列表去处理,跑个回测能等到你怀疑人生。这时候,NumPy就该登场了。
我个人习惯把NumPy叫做「Python数据处理的发动机」。它底层用C语言实现,运算速度比纯Python快几十倍甚至上百倍。今天我们就来啃下这块硬骨头,把NumPy最核心的三个技能拿下:创建数组、索引与切片、数学运算。
1. 创建数组:你的数据容器
NumPy的核心数据结构叫ndarray,也就是N维数组。你可以把它想象成一个超级版的Python列表,但人家是专门为数值计算设计的。
创建数组最直接的方式,就是从Python列表转换过来:
import numpy as np
# 从列表创建一维数组
prices = np.array([28.5, 29.1, 28.8, 30.2, 29.9])
print(prices)
# 输出: [28.5 29.1 28.8 30.2 29.9]
# 创建二维数组(比如:3只股票,5天的收盘价)
stock_data = np.array([
[28.5, 29.1, 28.8, 30.2, 29.9],
[15.2, 15.8, 15.5, 16.1, 15.9],
[42.3, 43.1, 42.8, 44.0, 43.5]
])
print(stock_data.shape) # 输出: (3, 5)
dtype属性。NumPy默认会用float64,但如果你确定数据是整数,可以指定dtype=np.int32,能省不少内存。我在处理全市场5000多只股票的历史数据时,这个习惯帮我省下了好几个G的内存。
除了手动输入,NumPy还提供了一些快速生成数组的函数,写量化策略时特别常用:
# 生成等差数列(比如:模拟时间序列)
time_points = np.arange(0, 10, 0.5) # 从0到10,步长0.5
print(time_points)
# 输出: [0. 0.5 1. 1.5 2. 2.5 3. 3.5 4. 4.5 5. 5.5 6. 6.5 7. 7.5 8. 8.5 9. 9.5]
# 生成全0数组(比如:初始化持仓矩阵)
positions = np.zeros((3, 5)) # 3只股票,5天,初始仓位全为0
print(positions)
# 生成全1数组(比如:初始化权重)
weights = np.ones(3) / 3 # 等权重配置
print(weights) # 输出: [0.33333333 0.33333333 0.33333333]
# 生成随机数(蒙特卡洛模拟常用)
np.random.seed(42) # 固定随机种子,保证结果可复现
random_returns = np.random.randn(1000) # 生成1000个标准正态分布的随机收益率
np.random.seed(),结果每次跑出来的结果都不一样,排查了半天才发现是随机数没固定。做量化研究,可复现性是底线,记得设种子!
2. 索引与切片:精准定位你的数据
数组建好了,怎么取数据?NumPy的索引和Python列表很像,但功能更强大。你想想看,股票数据通常是二维的——行是股票,列是时间。怎么快速取出某只股票的全部价格?
# 沿用上面的 stock_data (3只股票 x 5天)
# 取出第一只股票的全部价格
stock_1 = stock_data[0, :] # 第0行,所有列
print(stock_1) # 输出: [28.5 29.1 28.8 30.2 29.9]
# 取出所有股票第3天的价格
day_3 = stock_data[:, 2] # 所有行,第2列(索引从0开始)
print(day_3) # 输出: [28.8 15.5 42.8]
# 取出前2只股票,后3天的数据
sub_data = stock_data[:2, 2:5]
print(sub_data)
# 输出:
# [[28.8 30.2 29.9]
# [15.5 16.1 15.9]]
这里有个坑,我刚开始学的时候踩过——切片返回的是视图,不是副本。什么意思?就是你修改切片后的数据,原数组也会跟着变。
# 踩坑演示
temp = stock_data[0, :] # 取第一只股票
temp[0] = 999 # 修改切片数据
print(stock_data[0, 0]) # 输出: 999.0 —— 原数组也被改了!
.copy() 方法。temp = stock_data[0, :].copy()
NumPy还有一种特别酷的索引方式——布尔索引。这在量化选股中太实用了,比如筛选出所有涨幅超过5%的股票:
# 假设这是10只股票的日收益率
returns = np.array([0.02, -0.01, 0.06, 0.03, -0.02, 0.08, 0.01, -0.03, 0.05, 0.04])
# 找出收益率大于5%的股票
high_returns = returns[returns > 0.05]
print(high_returns) # 输出: [0.06 0.08]
# 找出收益率在2%到5%之间的股票
mid_returns = returns[(returns >= 0.02) & (returns <= 0.05)]
print(mid_returns) # 输出: [0.02 0.03 0.05 0.04]
3. 数学运算:让数据飞起来
终于到了最爽的部分。NumPy的数学运算,说白了就是「向量化操作」——不用写循环,直接对整个数组做运算。速度?那是真的快。
# 基础运算:逐元素操作
prices = np.array([28.5, 29.1, 28.8, 30.2, 29.9])
# 计算每日收益率(百分比变化)
daily_returns = (prices[1:] - prices[:-1]) / prices[:-1]
print(daily_returns)
# 输出: [ 0.02105 -0.01031 0.04861 -0.00993]
# 计算对数收益率(量化中更常用)
log_returns = np.diff(np.log(prices))
print(log_returns)
为什么量化中常用对数收益率?因为对数收益率具有可加性,多期收益率直接相加就行。嗯,这里不展开讲,你记住这个结论就好。
NumPy还提供了丰富的统计函数,写策略信号时几乎天天用:
# 统计函数
data = np.array([28.5, 29.1, 28.8, 30.2, 29.9, 31.0, 30.5])
print(f"均值: {np.mean(data):.2f}") # 输出: 均值: 29.71
print(f"标准差: {np.std(data):.2f}") # 输出: 标准差: 0.86
print(f"最大值: {np.max(data):.2f}") # 输出: 最大值: 31.00
print(f"最小值: {np.min(data):.2f}") # 输出: 最小值: 28.50
# 计算滚动均值(简单移动平均线)
def simple_moving_average(data, window=3):
weights = np.ones(window) / window
return np.convolve(data, weights, mode='valid')
sma_3 = simple_moving_average(data, 3)
print(sma_3) # 输出: [28.8 29.37 29.63 30.37 30.47]
NumPy最强大的特性之一。不同形状的数组做运算时,NumPy会自动扩展小数组的维度。
比如:你想给每只股票的价格都加上一个基准偏移量:
prices = np.array([[28.5, 29.1], [15.2, 15.8]])bias = np.array([0.1, 0.2])result = prices + bias # 广播自动完成结果就是第一列加0.1,第二列加0.2。不用写循环,不用手动扩展,NumPy全帮你搞定。
矩阵运算在量化投资中也经常用到,比如计算投资组合的方差:
# 矩阵乘法
# 假设我们有3只股票的权重向量和协方差矩阵
weights = np.array([0.4, 0.3, 0.3])
cov_matrix = np.array([
[0.1, 0.02, 0.01],
[0.02, 0.08, 0.03],
[0.01, 0.03, 0.12]
])
# 计算组合方差:w^T * Σ * w
portfolio_variance = weights.T @ cov_matrix @ weights
print(f"组合方差: {portfolio_variance:.4f}")
# 输出: 组合方差: 0.0589
@ 运算符做矩阵乘法,比 np.dot() 更直观。但要注意,* 是逐元素乘法,@ 才是矩阵乘法。这两个搞混了,算出来的结果会完全不对。我曾经在写均值-方差优化时搞混过一次,优化出来的权重全是错的,排查了整整一个下午。
好了,NumPy的基础就讲到这里。创建数组、索引切片、数学运算——这三个技能掌握了,你已经能处理大部分量化数据了。下一节我们会继续讲NumPy的高级功能,包括如何用NumPy高效处理时间序列数据。
记住,做量化选股,数据处理的效率决定了你的策略迭代速度。NumPy就是你手中的利器,多用、多练,自然就熟练了。