分位数回归与CoVaR计算:从VaR到ΔCoVaR的Python实现

发布时间:2026/9/15 18:26:15
分位数回归与CoVaR计算:从VaR到ΔCoVaR的Python实现 做风险管理的人应该都有过这种体会单看一家机构的VaR总觉得差点意思。监管问你的问题往往不是“你自己会不会亏”而是“如果你这家机构出了问题别人会不会跟着遭殃”。这个“你出事我也受伤”的溢出效应就是CoVaR要回答的问题。而计算静态CoVaR最常用的工具就是分位数回归Quantile Regression。这篇文章我会从CoVaR的定义讲起解释为什么分位数回归天然适合做这件事然后直接给出一套可复现的Python代码用模拟数据完整走一遍静态CoVaR的计算流程包含VaR、CoVaR和ΔCoVaR。最后再聊几个我自己在实际落地时踩过的坑比如收益率方向、分位数选择、置信区间怎么算。如果你正在做金融机构风险计量、写系统性风险相关的论文或者要给压力测试模型补一块溢出风险指标这篇文章可以直接当参考脚本用。1. 静态CoVaR在度量什么从“自己亏多少”到“你亏完我也跟着亏”1.1 VaR和CoVaR的差别VaR的定义很直白给定置信水平q一家机构的收益率分布左尾分位数就是它的VaR。比如某银行日收益率R_i的5%分位数是-2.3%意思是有5%的概率日收益率比-2.3%还差。这个数只描述“自己”的风险不涉及别人。CoVaR的全称是Conditional VaR意思是“条件风险价值”。CoVaR_q^{j|i}表示当机构i的收益率正好处于它的VaR水平时机构j收益率的q分位数是多少。写成公式就是CoVaR_q^{j|i} Q_q(R_j | R_i VaR_q^i)这里Q_q表示条件分位数函数。i通常是被当作风险源的机构j是受影响的一方。监管上常把j当成整个金融系统i当成一家重要银行这样算出来的就是“某银行处于困境时整个金融系统的尾部风险”。如果你只算VaR你看到的是单点风险如果你算CoVaR你看到的是尾部传染风险。后者才是系统性风险里更关心的东西。1.2 为什么用“条件分位数”而不是“条件相关系数”很多人第一反应是算两个机构收益率的相关性不就行了相关性高不就说明风险传染强这个思路在常态下有一定道理但在尾部往往失真。金融数据最典型的特征就是“常态相关性很低压力时期相关性猛涨”。2008年金融危机里很多资产在平时相关系数只有0.3市场一崩就跳到0.8以上。用全样本相关性去度量尾部传染等于用一个平均值掩盖了最危险的那段关系。OLS线性回归也有类似问题。OLS估计的是条件均值也就是“平均来看j跟着i变多少”。但CoVaR要的是“当i已经在5%分位数的极端困境时j的5%分位数在哪里”。均值关系和尾部关系经常不一致甚至可能符号都相反。分位数回归的优势就在这里它不估计均值而是直接估计被解释变量的某个分位数。你想知道j的5%分位数就把损失函数里的q设成0.05回归出来的系数就是“j的5%分位数如何随i变化”。1.3 静态CoVaR和动态CoVaR的区别“静态CoVaR”这几个字容易让人误会以为它不引入任何宏观变量。其实不是。静态是指整个样本期内分位数回归的系数是固定不变的。我们用全样本一次性估计出系数然后用这些常数系数计算VaR和CoVaR。动态CoVaR则通常用滚动窗口、状态变量或GARCH类模型让系数或波动率随时间变化。在Adrian和Brunnermeier那套经典框架里他们会控制一些宏观状态变量比如市场波动率、利率期限结构、信用利差等。即便加了这些控制变量只要系数是全样本估计的它仍然是静态CoVaR。这样做的好处是结果稳定、可解释性强适合监管压力测试和横截面比较。2. 分位数回归为什么是计算静态CoVaR的天然工具2.1 分位数回归的损失函数与OLS的本质区别分位数回归的估计思路是极小化一个非对称的绝对误差损失函数ρ_q(u) u × (q - I(u 0))其中I(u 0)是指示函数当残差为负时取1。这个函数也叫“检查函数”。当q0.5时正负残差的权重一样退化成最小绝对离差估计估计的是中位数。当q0.05时负残差的权重是0.95正残差的权重是0.05也就是说模型会狠狠惩罚“实际值低于预测值”的样本从而把预测线压到条件分布的5%分位数附近。OLS的损失函数是平方误差它天然会被极端值拉走估计的是条件均值。分位数回归用绝对误差的非对称加权对极端值更稳健而且能刻画整个条件分布。这正是CoVaR需要的我要的不是“平均传染程度”而是“最差5%情况下的传染程度”。2.2 从两个分位数回归推出CoVaR计算静态CoVaR一般分两步。第一步先对风险源机构i做分位数回归估计它在q分位数下的VaR。假设我们控制一个宏观状态变量M模型是VaR_{i,t}(q) α_i(q) γ_i(q) × M_t第二步对机构j的收益率做分位数回归解释变量里放入机构i的当期收益率和宏观状态变量Q_q(R_{j,t} | R_{i,t}, M_t) α_{j|i}(q) β_{j|i}(q) × R_{i,t} γ_{j|i}(q) × M_t然后把第一步算出来的VaR_{i,t}(q)作为R_{i,t}代入第二步得到压力状态下的CoVaRCoVaR_q^{j|i}(M_t) α_{j|i}(q) β_{j|i}(q) × VaR_{i,t}(q) γ_{j|i}(q) × M_t但光有压力状态还不够最好再算一个“正常状态”作为基准。正常状态一般取机构i收益率的中位数也就是50%分位数。把VaR_{i,t}(0.5)代入第二步公式得到正常状态下的CoVaRΔCoVaR_q^{j|i}(M_t) CoVaR_q^{j|i}(M_t) - CoVaR^{j|i at median}(M_t)这个差值就是机构i从正常状态滑落到q分位数尾部状态时机构j尾部风险额外恶化的部分。通常在收益率口径下ΔCoVaR是负数绝对值越大代表溢出风险越强。2.3 为什么要用两步而不是一步有人可能会问为什么不直接把R_j的q分位数回归里的X_i换成“机构i是否处于VaR”的虚拟变量这样也能得到条件效应。技术上可以但信息损失很大。把连续的VaR值作为解释变量能保留i自身尾部深度带来的边际影响。真实数据里机构i的VaR不是固定常数会随宏观环境变化。两步法的好处是既把VaR的估计过程显式化又能在CoVaR中体现i的风险状态逻辑更接近风险传染的真实路径。3. Python手写静态CoVaR数据准备、核心代码与结果解读3.1 环境与数据准备我用的环境是Python 3.10加statsmodels 0.14。先安装依赖pip install pandas numpy statsmodels下面用模拟数据演示核心目的是把计算链路跑通。真实项目里把X_i、X_j和M换成你自己的数据即可。模拟数据我设置了这样的关系机构i受宏观变量M影响机构j既受i影响也受M影响所以j的尾部风险里既有共同暴露也有i的溢出成分。import numpy as np import pandas as pd import statsmodels.api as sm np.random.seed(2024) n 1200 # 宏观状态变量 M np.random.normal(0, 1, n) # 机构i的收益率 error_i np.random.normal(0, 0.6, n) X_i 0.01 0.4 * M error_i # 机构j的收益率受i影响也受M影响 error_j np.random.normal(0, 0.7, n) X_j 0.005 0.7 * X_i 0.25 * M error_j data pd.DataFrame({ M: M, X_i: X_i, X_j: X_j })如果你用的是真实数据一般是价格序列需要先转成收益率returns prices.pct_change().dropna() # 或者 log return returns np.log(prices / prices.shift(1)).dropna()不同机构的数据经常不在同一张表里需要按日期对齐df pd.concat([r_i, r_j, macro], axis1, joininner).dropna()这一步很关键千万别忽略。回归对样本对齐极其敏感漏掉的日期会导致变量之间错位尤其是收益率数据错一个交易日结果就完全不是那么回事。3.2 封装分位数回归函数我习惯把分位数回归封装成一个函数后面换q值、换变量都方便def quantile_fit(y, x, q): X sm.add_constant(x) model sm.QuantReg(y, X).fit(qq) return model, X这里sm.add_constant是给解释变量加截距项。分位数回归的截距项含义是“当所有解释变量为0时的条件分位数”不能随便去掉。3.3 计算VaR、CoVaR、ΔCoVaR先算机构i在5%分位数下的VaR以及50%分位数下的VaR作为正常状态基准q 0.05 # 机构i的条件分位数回归 model_i_5, X_i_design quantile_fit(data[X_i], data[[M]], q) model_i_50, _ quantile_fit(data[X_i], data[[M]], 0.50) # 预测VaR var_i_5 model_i_5.predict(X_i_design) var_i_50 model_i_50.predict(X_i_design)第二步对机构j做分位数回归解释变量放X_i和M# 机构j的条件分位数回归 model_j_5, X_j_design quantile_fit(data[X_j], data[[X_i, M]], q) # 压力状态把X_i替换成机构i的VaR_5 X_stress X_j_design.copy() X_stress[X_i] var_i_5.values covar_stress model_j_5.predict(X_stress) # 正常状态把X_i替换成机构i的VaR_50 X_normal X_j_design.copy() X_normal[X_i] var_i_50.values covar_normal model_j_5.predict(X_normal) # 溢出风险 delta_covar covar_stress - covar_normal最后汇总成一个DataFrameresult pd.DataFrame({ VaR_i_stress: var_i_5, CoVaR_j_stress: covar_stress, CoVaR_j_normal: covar_normal, DeltaCoVaR: delta_covar }) print(result.describe()) print(平均DeltaCoVaR:, delta_covar.mean())我跑出来的结果大致长这样VaR_i_stress CoVaR_j_stress CoVaR_j_normal DeltaCoVaR mean -0.978 -1.366 -0.676 -0.690 std 0.400 0.532 0.532 0.133 min -2.130 -2.680 -1.990 -1.100 max -0.010 -0.210 0.450 -0.120注意这是模拟数据下的近似输出具体数值会随随机种子变化。核心看符号和量级DeltaCoVaR的均值约-0.69说明机构i处于5%尾部状态时机构j的5%分位收益率进一步下移0.69个百分点。我这里用的是收益率口径所以负值表示风险恶化。如果你习惯用“损失率”口径可以把收益率取负再算那样CoVaR和ΔCoVaR都是正数含义等价但符号解释千万别混。3.4 结果怎么读分位数回归的系数表里最有价值的是model_j_5中X_i的系数。它表示机构i的收益率每变化1个单位机构j的5%分位数收益率变化多少个单位。在这个模拟数据里X_i系数接近0.7说明尾部传染效应很强。再看ΔCoVaR这个值已经扣掉了正常状态下的共同暴露所以比单纯看相关系数更干净。它衡量的是“从正常状态跌到尾部状态”这个额外冲击对j的影响。如果做横截面比较可以把每家公司作为风险源i计算它对系统或其他机构的ΔCoVaR然后排序。排序结果就是“谁一旦出事对别人的尾部风险冲击最大”。这就是系统性风险重要机构识别的基本逻辑。3.5 怎么给ΔCoVaR算置信区间很多人算完ΔCoVaR只给个点估计这不够。监管报送或论文里通常需要显著性判断。不建议直接看回归系数的p值因为ΔCoVaR是两个预测值之差它的标准误不是单一回归系数能代表的。更稳妥的方法是bootstrapdef bootstrap_delta_coVaR(data, q0.05, n_boot500): deltas [] for _ in range(n_boot): boot_data data.sample(frac1, replaceTrue).reset_index(dropTrue) try: model_i_5, X_i_db quantile_fit(boot_data[X_i], boot_data[[M]], q) model_i_50, _ quantile_fit(boot_data[X_i], boot_data[[M]], 0.50) var_5 model_i_5.predict(X_i_db) var_50 model_i_50.predict(X_i_db) model_j_5, X_j_db quantile_fit(boot_data[X_j], boot_data[[X_i, M]], q) X_stress_b X_j_db.copy() X_stress_b[X_i] var_5.values stress_b model_j_5.predict(X_stress_b) X_normal_b X_j_db.copy() X_normal_b[X_i] var_50.values normal_b model_j_5.predict(X_normal_b) deltas.append((stress_b - normal_b).mean()) except Exception: continue return np.array(deltas) boot_deltas bootstrap_delta_coVaR(data, q0.05, n_boot500) print(95% CI:, np.percentile(boot_deltas, [2.5, 97.5]))如果置信区间不包含0就可以认为溢出效应在统计上显著。4. 落地中容易踩的五个坑符号、样本、交叉与控制变量4.1 收益率方向千万别搞反这是最基础也最容易出问题的地方。CoVaR的定义里R可以是收益率也可以是损失率两种口径下公式符号正好相反。如果R是收益率那么尾部在左边5%分位数是负值VaR、CoVaR、ΔCoVaR通常都是负数。如果R是损失率尾部在右边95%分位数是正值VaR、CoVaR就变成正数。我见过不少新手在同一套代码里一会儿用收益率一会儿用损失率最后ΔCoVaR的符号混乱结论完全写反。建议从一开始就统一数据用收益率小数全程保留负号只在最后报告时加上文字说明“风险恶化约0.7个百分点”。4.2 分位数越低样本量越不能省q0.05在实证里最常见。如果数据是日频1200个样本大约5年够用。但如果你想看1%尾部也就是q0.01那条件分位数估计对样本量极其敏感。0.01分位数的有效观测数量太少回归系数方差会变得很大。这个时候有两个选择要么用更长的样本期要么老老实实用q0.05。不要为了追求“极端尾部”而硬上q0.01最后估计出来的系数可能就由几个异常点决定换个样本期就崩。我个人的习惯是样本少于1000时用5%样本超过2000时才会尝试1%作为稳健性检验。4.3 不要只看回归系数的显著性要看ΔCoVaR的置信区间在实证分析里model_j_5的X_i系数显著不代表ΔCoVaR一定显著。因为ΔCoVaR还包含了第一步VaR估计的不确定性。如果你只报告模型系数的p值相当于忽略了一部分估计误差。所以算完点估计后最好补一个bootstrap置信区间。这也是论文审稿人和监管机构常问的问题你的ΔCoVaR到底稳不稳4.4 条件分位数“交叉”问题分位数回归有一个常见毛病不同分位数下的条件预测线可能交叉。比如用q0.05估计出来的系数可能在某些样本点上预测出的值比q0.5还高这在逻辑上说不通因为5%分位数不可能高于中位数。出现交叉时不要急着直接改数据先看看是不是样本量不足、解释变量过多或者存在极端离群值。如果只是个别点交叉通常不影响整体结论。如果大面积交叉可能说明模型设定有问题需要简化变量或改用非参数分位数回归。4.5 静态CoVaR不等于因果传染最后说一个容易被过度解读的问题。回归里X_i系数显著只能说明“机构i处于尾部时机构j的尾部也跟着恶化”不能直接说“i把风险传染给了j”。因为两家机构可能同时受到同一个宏观因子影响模型里虽然加了M但M不可能穷尽所有共同暴露。所以静态CoVaR更准确的表述是“尾部共同风险溢出度量”而不是严格的因果推断。写论文或报告时尽量用“关联”“溢出”“暴露”这类词别拍胸脯说“某机构导致了某机构的风险”。实际做项目时我会同时算好几个版本的CoVaR全样本静态、按年份拆分的子样本、加了不同宏观控制变量的版本。如果结论在不同设定下都稳定那才敢拿去给领导或监管看。单跑一版就下结论多少有点碰运气。