← 返回模块
3.2.1.2beta 可读 · 未来免费内容校验中内容版本 2026-05-27

聚合、轴与归约

3.2.1 · NumPy · 编程

周三上午十点,一家私募的研究员把上一课写好的 (252, 3) 沪深300 成分股日收益矩阵 returns 甩进 Jupyter,敲下 returns.mean(),得到一个标量 0.00042。他把这个数贴进周报,标题写「样本期内组合日均收益约 4 个基点」——这句话有问题。returns.mean() 在没有 axis 参数时会把整个矩阵展平成一维向量再取均值,等于同时把时间维和股票维一起塌掉。它既不是每只股票的日均收益(per-ticker mean),也不是每天的等权组合收益(equal-weighted portfolio return),只是个意义模糊的「全样本平均数」。这一节要解决的问题是:哪根轴塌掉,输出形状变成什么——把这条规则练到闭眼写对,这个 bug 就不会再出现。

轴的规则:axis=k 塌掉第 k 维

returns 看成 (T, N):T 行表示交易日(trading day),N 列表示股票(ticker)。NumPy 的归约(reduction)操作沿哪根轴执行,决定了输出的形状:

  • axis=0 —— 沿行方向塌掉,输出形状 (N,),对应「每只股票在 T 天上的统计量」。
  • axis=1 —— 沿列方向塌掉,输出形状 (T,),对应「每天 N 只股票的横截面统计量」。
  • axis=None(标量归约的默认值)—— 把所有元素一起塌掉,输出标量。
import numpy as np

returns = np.array([[0.01, -0.02, 0.005], [0.02, 0.01, -0.01], [-0.005, 0.015, 0.02], [0.008, -0.003, 0.011], [0.012, 0.004, -0.006]])

mean_per_ticker = returns.mean(axis=0)   # shape (3,),每只股票的日均收益
mean_per_day = returns.mean(axis=1)      # shape (5,),每天的等权组合收益
mean_overall = returns.mean()            # 标量,意义模糊

The rule is: axis=k removes dimension k from the output shape. 中文译为:axis=k 把输出的第 k 维「抽掉」。再加一个常用旋钮——keepdims=True 会把被塌掉的那一维保留为长度 1,方便后续广播(broadcasting)回去,例如 returns - returns.mean(axis=0, keepdims=True) 得到逐列去均值(demean)后的矩阵。

经典归约清单与 ddof

summeanstdvarminmax 的签名都是 (axis=None, keepdims=False, ...),沿轴行为完全一致。重点放在 ddof(degrees of freedom,自由度修正参数):默认 ddof=0 是总体公式,分母为 N;金融工作几乎一律取样本公式,ddof=1,分母为 N−1(Bessel 修正),与 np.cov 的默认行为一致,也与 2.2 课讲的样本方差 s2=1n1i=1n(xixˉ)2s^2 = \frac{1}{n-1}\sum_{i=1}^{n}(x_i - \bar{x})^2 对齐。

daily_std = returns.std(axis=0, ddof=1)   # 每只股票的样本日波动率,shape (3,)

argmin / argmax 返回的是位置而非数值——returns.argmax(axis=0) 给你每只股票在 T 天里收益最高那一天的行号(行号是时间索引,具体对应哪一天要回查交易日表;本例不指定年份,数据仅示意)。一个常见的搭配用法是 returns[returns.argmax(axis=0), np.arange(N)],借助高级索引把每列的「最佳一日」的数值取出来——这也是上一课讲过的 fancy indexing 在归约结果上的延伸。cumsum / cumprod 沿轴累积:把日对数收益(log return)拼成累计净值曲线用 np.cumsum(log_returns, axis=0),把日简单收益(simple return)拼成财富曲线(wealth path)用 np.cumprod(1 + simple_returns, axis=0)

simple_returns = np.array([0.01, -0.02, 0.005, 0.008, 0.012])
wealth = np.cumprod(1 + simple_returns)   # 末值即累计净值,初始 1.0

四个非归约工作马

  • np.where(condition, a, b) —— 向量化三元运算(vectorized ternary)。
  • np.clip(arr, lo, hi) —— 把数组取值压到 [lo, hi] 区间内,最常见的用法是缩尾(winsorize)。
  • np.diff(arr, axis=0) —— 沿轴一阶差分(first difference),等价于 prices[1:] - prices[:-1]
  • np.quantile(arr, q, axis=...) —— 分位数(quantile),q[0, 1]np.percentile 是同一函数的 [0, 100] 版本,新代码统一用 np.quantile
signal = np.where(returns > 0, 1.0, -1.0)
winsorized = np.clip(returns, -0.05, 0.05)

np.quantile(losses, 0.05) 给你每只股票 5% 分位的日收益,可作为「单行 VaR 风格摘要」(illustrative one-line summary)——这只是示例,生产级在险价值(value at risk, VaR)的方法学(参数化、历史模拟、Monte-Carlo)留给 4.4.2。缩尾切位(1/99 还是 5/95)的方法学讨论同样不在本课范围内,4.2.2 信号构建会展开。

缺失值与布尔归约

真实 A 股数据有 ​停牌​ 日和数据缺口。假设 600519.SH 在第三个交易日因故停牌,把它写成 np.nan 看会发生什么:

returns_with_gap = returns.copy()
returns_with_gap[2, 0] = np.nan
bad = returns_with_gap.mean(axis=0)            # 第 0 列变成 nan,污染整列
good = np.nanmean(returns_with_gap, axis=0)    # 跳过 nan 取均值

np.nansumnp.nanstdnp.nanvarnp.nanminnp.nanmaxnp.nanpercentile 同理。一个权衡:若一只票超过 ~20% 的天数是 NaN,应当回查数据源而不是默默 nanmean 过去——少量缺口可以容忍,结构性缺失说明上游有问题。掩码数组(np.ma)也能做同样的事,但在 2026 年的工程实践里,更通用的缺失值范式是下个模块的 Pandas DataFrame + dropna / fillna,本课不展开。布尔归约 np.any(mask, axis=...) / np.all(mask, axis=...) 用于回答「今天是否​​任一​​股票跌超 5%」 / 「这周是否​​所有​​股票都收阳」,结果分别是 (T,)(N,) 形状的布尔向量。

协方差矩阵:通往下一课的桥

cov_matrix = np.cov(returns, rowvar=False)

rowvar=False 是 NumPy 最容易忘记的旋钮:np.cov 默认把​​行​​当变量,但我们的 (T, N) 收益矩阵约定​​列​​为变量(每列一只股票,T 个时间观测)。设 N = 3,则 cov_matrix 的输出形状是 (N, N) = (3, 3),对角线即每只股票的样本方差,与 returns.var(axis=0, ddof=1) 在数值上完全一致——这就是上面强调 ddof=1 的实际理由。​​协方差矩阵​​(covariance matrix)是组合优化(portfolio optimization)与因子模型(factor model)的中心输入。如果你想要的是相关系数矩阵,把上面这行换成 np.corrcoef(returns, rowvar=False) 即可(数值上等价于把每列标准化后再算协方差)。

练习

Exercise

给定一个二维 NumPy 数组 returns,形状为 (T, N),表示 N 只股票在 T 个交易日上的日简单收益(不含 NaN)。请写出​​四个一行表达式​​:(1) 每只股票的日均收益,输出形状 (N,);(2) 每只股票带 Bessel 修正的样本日波动率,输出形状 (N,);(3) 每天的等权组合收益,输出形状 (T,);(4) 等权组合从初始净值 1.0 起的累计财富曲线,输出形状 (T,)。每个表达式后再写一行 assert,用 .shape 检查输出形状是否符合预期。

提示
每只股票的统计量 = 沿时间塌掉 → axis=0;每天的组合收益 = 沿股票塌掉 → axis=1
提示
财富曲线 = cumprod(1 + 每日组合收益);在 cumprod 内部仍然要写 axis=0,沿时间轴累积。

到这里你已经能在不写循环的前提下,对一张 (T, N) 收益矩阵做出全部一阶统计量,并把 np.cov(returns, rowvar=False) 这个 (N, N) 协方差矩阵作为「下一段流水线的输入」交出去。下一课把它真正接到 np.linalg:用 np.linalg.lstsq 跑沪深300 ETF 对单只股票的 OLS 市场 β 回归,用 np.linalg.eig 对协方差矩阵做主成分分析(principal component analysis, PCA);同时引入 np.random.default_rng(seed) 的现代生成器接口,模拟几何布朗运动(geometric Brownian motion, GBM)的价格路径。一句话总结:先把轴的方向想清楚,再让 NumPy 替你跑那一行。