第 3 章 · 02 半协方差 本节摘要:本节讲 提供的下行风险估计—— 。普通协方差把上涨与下跌一视同仁,但投资者的真实痛感是非对称的:下跌亏钱、上涨赚钱,只有下跌才算「风险」。半方差(semivariance)只对低于某个基准(默认无风险利率)的收益算协方差,更贴近「风险即亏损」的直觉。本节拆解它的数学定义(Estrada 2007 的成对 min 形式)、源码实现里的 、benchmark 参数如何选择,以及它与样本协方差的差异。最后指出它常与第 6 章的 配合——后者把半方差当真正的目标而非启发式近似。 内容来源:原项目源码 (函数 )、文档 ,汉化并套用体系化模板。 学习目标 阅读完本节,你应当能够: 解释半方差的直觉与「风险即亏损」的非对称视角。
本节摘要:本节讲
risk_models.py提供的下行风险估计——semicovariance。普通协方差把上涨与下跌一视同仁,但投资者的真实痛感是非对称的:下跌亏钱、上涨赚钱,只有下跌才算「风险」。半方差(semivariance)只对低于某个基准(默认无风险利率)的收益算协方差,更贴近「风险即亏损」的直觉。本节拆解它的数学定义(Estrada 2007 的成对 min 形式)、源码实现里的drops = np.fmin(returns - benchmark, 0)、benchmark 参数如何选择,以及它与样本协方差的差异。最后指出它常与第 6 章的EfficientSemivariance配合——后者把半方差当真正的目标而非启发式近似。
内容来源:原项目源码
pypfopt/risk_models.py(函数semicovariance)、文档docs/RiskModels.rst,汉化并套用体系化模板。
阅读完本节,你应当能够:
semicovariance 算半协方差矩阵,理解 benchmark 默认值。EfficientSemivariance(第 6 章)搭配。普通方差 σ² 把所有偏离均值的幅度一视同仁:涨 5% 和跌 5% 都贡献相同的「风险」。但这与投资者的真实感受相悖:
这就是下行风险(downside risk)的动机——只对不利方向的偏离算风险。半方差(semivariance)是其中最经典的度量,定义为「低于基准的收益的方差」。
💡 核心心法:半方差把「上涨波动」从风险里剔除——因为上涨是收益不是风险。这让风险度量更贴合投资者直觉,尤其适合「亏损厌恶」型(比亏损厌恶更甚,见前景理论)的投资者。
半方差的定义有多种微妙差异,主要在「成对」如何处理。PyPortfolioOpt 采纳 Estrada (2007) 的建议:
semicov_ij = (1/n) Σ_t min(r_i,t - B, 0) × min(r_j,t - B, 0)
注意是 min(r_i - B, 0) × min(r_j - B, 0),而不是 min(r_i × r_j, B)——前者分别对每只资产做下行截断,再相乘。docs/RiskModels.rst 的原文公式:
(1/n) Σ_i Σ_j min(r_i, B) min(r_j, B)
直观理解:只有当两只资产同时低于基准时,才贡献正的半协方差;只要有一只上涨(贡献 0),该项就为 0。这让半协方差矩阵聚焦「一起跌」的情景——这正是真正的尾部联动风险。
打开 pypfopt/risk_models.py,定位 semicovariance(源码 206-253 行):
def semicovariance( prices, returns_data=False, benchmark=0.000079, frequency=252, log_returns=False, **kwargs, ): ... if returns_data: returns = prices else: returns = returns_from_prices(prices, log_returns) drops = np.fmin(returns - benchmark, 0) T = drops.shape[0] return fix_nonpositive_semidefinite( (drops.T @ drops) / T * frequency, kwargs.get("fix_method", "spectral") )
关键点:
benchmark 默认 0.000079:文档注释说这是「日度无风险利率」,即 1.02^(1/252) - 1 ≈ 7.9e-5(年化 2% 转日度)。低于它的算「下行」。drops = np.fmin(returns - benchmark, 0):np.fmin 逐元素取「收益减基准」与 0 的较小值——正收益(高于基准)被截断为 0,负收益(低于基准)保留。这就是「下行截断」。(drops.T @ drops) / T:矩阵乘法 drops.T @ drops 就是成对的 min(r_i-B,0) × min(r_j-B,0) 之和,除以 T 得均值。这正是 Estrada 公式的向量化实现——简洁而高效。* frequency 年化 + PSD 修正:与 sample_cov 一致。💡 为何默认 benchmark 是 0.000079 而非 0:用 0 当基准会把「收益为正但低于无风险」的日子也算作「正常」,低估了下行风险。用无风险利率当基准,意味着「跑不赢无风险」就算下行——这更符合「机会成本」的视角。实务中可按你的无风险利率假设调整。
| 参数 | 默认 | 含义 | 备注 |
|---|---|---|---|
prices |
— | 价格 DataFrame | 必填 |
returns_data |
False |
True 表示输入已是收益 | 不应是对数收益 |
benchmark |
0.000079 |
下行基准(日度无风险) | 周期须匹配 frequency |
frequency |
252 |
年化系数 | 改 frequency 务必同步改 benchmark |
log_returns |
False |
是否用对数收益 | |
fix_method |
"spectral" |
PSD 修正方法 |
⚠️ benchmark 与 frequency 必须配套:日度数据用日度 benchmark(默认 0.000079);若改成月度(
frequency=12),benchmark 也要换成月度无风险利率(如年化 2% 转月度约0.02/12)。不配套会让「下行」的判定门槛错位。
默认调用:
from pypfopt import risk_models S_semi = risk_models.semicovariance(df)
cookbook 3 里就用它替代样本协方差:
from pypfopt import risk_models, expected_returns mu = expected_returns.capm_return(prices) S = risk_models.semicovariance(prices)
自定义基准(年化 3% 无风险,转日度):
S_semi = risk_models.semicovariance(df, benchmark=0.03/252)
通过统一入口(支持 semicovariance 与 semivariance 两种别名):
risk_models.risk_matrix(df, method="semicovariance") risk_models.risk_matrix(df, method="semivariance") # 别名
数值上半协方差矩阵通常小于样本协方差矩阵(因为只计入下行),且结构不同:
| 维度 | 样本协方差 | 半协方差 |
|---|---|---|
| 上行波动 | 计入 | 不计入 |
| 下行波动 | 计入 | 计入 |
| 「一起涨」的联动 | 计入 | 不计入 |
| 「一起跌」的联动 | 计入 | 计入(聚焦) |
| 投资者痛感贴合度 | 弱 | 强 |
💡 关键差异:半协方差矩阵不是「样本协方差的一半」。它结构不同——「一起涨」的资产对在半协方差里贡献为 0,但在样本协方差里贡献为正。这意味着优化器在两种矩阵下会选出不同的组合:用半协方差的组合更侧重规避「一起跌」的资产。
semicovariance 函数返回的矩阵可以直接喂给 EfficientFrontier,当作普通协方差用:
ef = EfficientFrontier(mu, S_semi) ef.max_sharpe()
但 docs/GeneralEfficientFrontier.rst 指出,这只是启发式近似——把半协方差当普通协方差做均值-方差优化,并不能得到真正在「均值-半方差空间」上有效的组合。
要得到精确的均值-半方差最优组合,应用第 6 章的 EfficientSemivariance 类——它把半方差写成凸问题(引入 2T+N 个变量),直接在均值-半方差前沿上求解。两者关系:
| 方法 | 实现 | 精度 | 成本 |
|---|---|---|---|
semicovariance + EfficientFrontier |
启发式近似 | 近似 | 低 |
EfficientSemivariance |
凸问题精确求解 | 精确 | 高(变量多) |
💡 何时用哪个:资产数少、追求快速原型,用
semicovariance+EfficientFrontier足够;严肃的下行风险优化,用EfficientSemivariance(第 6 章)。
(1/n) Σ min(r_i - B, 0) × min(r_j - B, 0)。drops = np.fmin(returns - benchmark, 0),再 drops.T @ drops / T × frequency。下一节,我们看指数协方差——把指数加权思想用到协方差上,对近期联动更敏感。