4. NumPy 基础:数组创建、数组运算、随机数生成、统计函数

在量化回测中,坏账率哨兵需要高效处理大量时间序列数据与随机模拟。NumPy 作为 Python 科学计算的核心库,提供了高性能的多维数组对象与丰富的数学函数。本章将围绕数组创建、运算、随机数生成与统计函数四个模块,构建回测分析的数据基础。

4.1 数组创建:从列表到结构化数据

NumPy 数组(ndarray)是存储同类型数据的多维容器。创建数组的常用方式包括:

创建方式 函数/语法 示例 说明
从列表转换 np.array() np.array([1,2,3]) 最基础方式,自动推断 dtype
全零数组 np.zeros() np.zeros((3,4)) 常用于初始化权重或占位
全一数组 np.ones() np.ones((2,3)) 用于单位矩阵或累加基值
等差序列 np.arange() np.arange(0,10,2) 类似 range,返回数组
等间隔数列 np.linspace() np.linspace(0,1,5) 指定点数,自动计算步长
单位矩阵 np.eye() np.eye(3) 对角线为 1,其余为 0
import numpy as np

# 创建坏账率模拟数据(5个时间点,3个资产组合)
bad_rates = np.array([[0.02, 0.03, 0.01],
                      [0.04, 0.05, 0.02],
                      [0.03, 0.04, 0.03],
                      [0.05, 0.06, 0.02],
                      [0.04, 0.05, 0.04]])
print(bad_rates.shape)  # (5, 3)
print(bad_rates.dtype)  # float64
回测应用提示: 使用 np.zeros((n_days, n_assets)) 预分配坏账率矩阵,可避免循环中动态扩展数组带来的性能损失。

4.2 数组运算:向量化加速回测计算

NumPy 的向量化运算避免了 Python 原生循环,是回测性能的关键。核心运算分为三类:

  • 算术运算: 加、减、乘、除、幂运算,逐元素执行。
  • 比较运算: 返回布尔数组,用于条件筛选与阈值判断。
  • 广播机制: 不同形状数组运算时自动扩展维度。
# 算术运算:计算坏账率变化
rates = np.array([0.02, 0.04, 0.03, 0.05, 0.04])
threshold = 0.035

# 向量化比较
exceed_mask = rates > threshold
print(exceed_mask)  # [False  True False  True  True]

# 广播:每个资产组合的坏账率偏移
offsets = np.array([0.005, -0.002, 0.001])
adjusted_rates = bad_rates + offsets  # 形状 (5,3) + (3,) → (5,3)
print(adjusted_rates[:2])
性能对比: 处理 100 万笔贷款数据时,向量化运算比 Python 循环快 50~100 倍,直接决定回测哨兵能否实时响应。
NumPy 数组运算与广播机制 数组 A (5,3) 数组 B (5,3) 结果 C = A + B (5,3) 数组 A (5,3) 数组 B (3,) → 广播为 (5,3) 结果 C = A + B (5,3) 广播规则: 从尾部维度开始比较,维度为 1 或缺失时自动扩展。例如 (5,3) + (3,) → (5,3) + (1,3) → (5,3)。

4.3 随机数生成:蒙特卡洛模拟坏账路径

量化回测中,随机数用于生成坏账率的可能演化路径,评估极端风险。NumPy 的 random 模块提供多种分布:

函数 分布类型 回测应用场景
np.random.rand() 均匀分布 [0,1) 随机抽样、违约事件触发
np.random.randn() 标准正态分布 坏账率波动建模
np.random.binomial() 二项分布 违约次数模拟
np.random.poisson() 泊松分布 低频高损事件计数
np.random.seed() 随机种子 保证回测结果可复现
# 设置随机种子,确保结果可复现
np.random.seed(42)

# 模拟 1000 次蒙特卡洛路径,每次 252 个交易日
n_paths = 1000
n_days = 252
base_rate = 0.03
volatility = 0.01

# 生成正态随机增量
increments = np.random.randn(n_paths, n_days) * volatility
# 累积坏账率路径(简化模型)
bad_rate_paths = base_rate + np.cumsum(increments, axis=1)

print(bad_rate_paths.shape)  # (1000, 252)
print("第 1 条路径最后一天坏账率:", bad_rate_paths[0, -1])
种子管理: 在回测框架中,务必在每次模拟前调用 np.random.seed(),否则不同运行批次的结果不可比较,导致策略评估失效。

4.4 统计函数:描述与推断坏账分布

NumPy 提供丰富的统计函数,用于描述坏账率数据的集中趋势、离散程度与分布形态:

  • 集中趋势: np.mean()、np.median()、np.percentile()
  • 离散程度: np.std()、np.var()、np.ptp()(极差)
  • 相关性: np.corrcoef()、np.cov()
  • 累积与聚合: np.cumsum()、np.sum()、np.max()、np.min()
# 对蒙特卡洛模拟结果进行统计
final_rates = bad_rate_paths[:, -1]  # 所有路径的最后一天坏账率

mean_rate = np.mean(final_rates)
std_rate = np.std(final_rates)
p95 = np.percentile(final_rates, 95)
p99 = np.percentile(final_rates, 99)

print(f"平均坏账率: {mean_rate:.4f}")
print(f"标准差: {std_rate:.4f}")
print(f"95% 分位点: {p95:.4f}")
print(f"99% 分位点: {p99:.4f}")

# 计算不同资产组合的坏账率相关性
corr_matrix = np.corrcoef(bad_rates.T)  # 转置为 (3,5) → (3,3) 相关矩阵
print("资产组合坏账率相关系数矩阵:\n", corr_matrix)
坏账率统计分布与风险分位点 均值 95% 分位 99% 分位 风险哨兵解读: 99% 分位点坏账率 = 4.8% 意味着:在最坏的 1% 情景下,坏账率不会超过 4.8%,这是设置预警阈值的关键依据。 坏账率 (%)

4.5 综合实战:坏账率哨兵统计模块

将数组创建、运算、随机数与统计函数整合为一个哨兵统计模块,用于实时监控坏账率异常:

class BadRateSentinel:
    """坏账率哨兵统计模块"""
    def __init__(self, base_rate=0.03, window=60, threshold_percentile=95):
        self.base_rate = base_rate
        self.window = window
        self.threshold_percentile = threshold_percentile
        self.history = np.array([])

    def update(self, new_rate):
        """更新坏账率观测值"""
        self.history = np.append(self.history, new_rate)
        if len(self.history) > self.window:
            self.history = self.history[-self.window:]

    def check_alert(self):
        """检查是否触发预警"""
        if len(self.history) < 30:
            return False, "数据不足"
        current = self.history[-1]
        threshold = np.percentile(self.history, self.threshold_percentile)
        if current > threshold:
            return True, f"坏账率 {current:.4f} 超过 {self.threshold_percentile}% 分位阈值 {threshold:.4f}"
        return False, "正常"

# 模拟使用
sentinel = BadRateSentinel()
np.random.seed(2024)
for day in range(100):
    rate = 0.03 + np.random.randn() * 0.005
    sentinel.update(rate)
    if day % 20 == 0:
        alert, msg = sentinel.check_alert()
        print(f"第 {day+1} 天: {msg}")