简介:面向金融领域研究者、监管机构及金融网络分析学者,这份资源提供基于DebtRank模型的多层银行网络风险传染复现资料,可解决系统性风险传染评估与政策模拟问题。资源包仅含1个PDF文件,大小约1002KB,以中国银行业为背景,完整覆盖三层网络构建、冲击模拟与代码逐段解释。作者设计银行间借贷、交叉持股和共同资产持有三条风险传染渠道,模拟个体、行业及宏观经济冲击,识别四大国有商业银行为系统重要性机构,并指出制造业、交通运输业贷款和个人信贷需重点关注。已有152人学习,适合从事金融风险监测、政策模拟或学术复现的读者。内容还扩展了非线性叠加效应表达、资产价格反馈机制和系统重要性识别算法,可帮助读者深入掌握多层网络建模与DebtRank应用,并基于Python自行搭建评估系统。
1. 金融系统性风险传染模型:从单层网络到三层网络的认知升级
一个银行即使从未在银行间市场拆借过一分钱,也可能被同行的坏账拖垮——传染路径藏在共同资产持有里,而单层网络模型看不到这条路径。这篇基于中国银行业实证的论文,核心贡献是把传统的单层银行间借贷网络扩展成三层结构:银行间借贷、交叉持股、共同资产持有,并把经典的DebtRank算法改造成三层非线性叠加版本。配套的Python代码包完整实现了从网络构建、DebtRank传染计算、三类冲击模拟到系统重要性银行识别的全流程,每段代码都带中文注释和参数说明。对金融监管研究员、做压力测试的风控从业者,以及研究复杂网络应用的人来说,这套代码是直接能跑、能改、能接真实数据的底座。
2. 单层网络的盲区:为什么一定要引入多层结构
2.1 直接连接与间接连接:两种网络构建思路的分野
在系统性风险研究里,网络构建方式大致分成两条技术路线。第一条是基于真实连接的网络构建,直接观测银行间的业务和交易关系,比如利用银行间市场交易数据画出拆借关系图。这条路的数据可得性高,结果直观,但只能反映部分风险传染渠道——它天然忽略了银行之间通过其他业务形成的隐性关联。第二条是基于间接连接的网络构建,不直接看业务关系,而是通过股价波动传导、股价相关性、投资业务相似度这些间接信号来推断银行间的关联强度。这条路能捕捉到市场心理和协同行为,但网络构建方法更复杂,且推断出来的边带有较强假设。
这篇论文走的是一条折中路线:既不放弃直接连接的信息,又把间接连接纳入统一框架。它把中国银行业的真实关联拆成三个可观测的维度——银行间借贷、交叉持股、共同资产持有。这三个维度恰好覆盖了金融网络中最重要的三种相互作用:债权债务关系带来的直接传染、股权纽带带来的资产端风险传递、以及共同持仓带来的间接传染(当一个银行抛售资产导致价格下跌,持有相同资产的银行跟着受损)。单看任何一层都不完整,三层叠在一起才逼近真实系统。
2.2 三个邻接矩阵的定义与数据来源
模型的核心数据结构是三个n_banks × n_banks的邻接矩阵,对应代码里的interbank_network、cross_holding_network、common_asset_network。第一个矩阵是银行间借贷网络,interbank_network[i][j]表示银行i对银行j的拆借敞口,通常是按银行间市场成交数据聚合出来的,方向性很强,所以代码里用有向图处理。第二个矩阵是交叉持股网络,cross_holding_network[i][j]表示银行i持有银行j的股权比例,来自上市公司股东数据库(Wind或CSMAR都能拿到),同样是有向的。第三个矩阵是共同资产持有网络,common_asset_network[i][j]代表银行i和银行j在投资组合中的重叠程度,由银行资产负债表中的投资持仓数据计算出来,本质是无向的,所以代码里把它强制构造成对称矩阵,并对角线置零。
这三个矩阵的构建口径直接影响后面的传染计算结果,也是实际应用时最花时间的一步。我一般会建议先做归一化再入模型,否则借贷敞口的绝对值(可能上亿)和持股比例(0到1之间的小数)放在一起算impact时,量纲差异会直接把后两个渠道淹没掉。
2.3 非线性叠加:三种传染渠道为什么不能简单相加
把DebtRank扩展到三层,最核心的数学改动是三种传染渠道的合成方式。代码里用的公式是:total_impact = 1 - (1 - interbank_impact) × (1 - holding_impact) × (1 - asset_impact)。这不等于三个impact简单相加,而是一种"如果三个独立风险事件同时发生,至少发生一个的概率"式的合成逻辑。
这样做的好处体现在两个地方。第一,三个渠道的影响天然存在重叠——同一家银行的损失可能通过借贷渠道传过来,又通过共同资产渠道再传一次,简单相加会重复计算。第二,极端情况下三个渠道同时作用时,简单相加可能超过1,导致损失比例失真,而乘积形式的公式天然把total_impact压在0到1区间内。这样的非线性叠加设计,从数学上规避了"传染总效应超过100%"的尴尬局面。
3. DebtRank 扩展到三层网络:状态机、冲击向量与损失积累
3.1 DebtRank 的基本机制:h 与 s 状态向量怎么配合
DebtRank的原始思想是用两个向量描述每家银行在传染过程中的状态:h是累计损失比例(0到1之间),s是状态标记(0=未受影响,1=受影响但未违约,2=已违约)。这两个向量是配套走的,h记录损失数字,s记录银行处于哪个阶段,后面每一轮迭代都要参照上一轮的状态来决定当前轮怎么算。
h = np.zeros((self.contagion_steps + 1, self.n_banks)) # 银行损失比例 s = np.zeros((self.contagion_steps + 1, self.n_banks)) # 银行状态 # 初始冲击 h[0] = initial_shock s[0] = (h[0] > 0).astype(int)这段代码对应传染开始前的初始化。h的第一行存的就是初始冲击向量,s的第一行通过向量化比较得到——哪家银行初始损失大于0,状态就标记为1。注意这里有个隐含设定:初次冲击直接打到银行头上时,银行先处于"受影响"状态,只有当累计损失在后续传染中超过违约阈值,状态才会变成2。初始冲击本身不直接判定违约,这是DebtRank与普通阈值模型的一个显著区别。
3.2 三个 impact 的逐项计算与分母选择
进入传染主循环后,每轮迭代对每家未违约银行计算三种渠道的影响。关键代码是这样一段:
# 1. 银行间借贷影响 interbank_impact = np.sum(self.interbank_network[:, i] * h[t-1]) / self.liabilities[i] # 2. 交叉持股影响 holding_impact = np.sum(self.cross_holding_network[:, i] * h[t-1]) / self.equity[i] # 3. 共同资产影响 asset_impact = np.sum(self.common_asset_network[i, :] * h[t-1]) / self.assets[i]三个等式的结构完全相同,都是"别人受的损失 × 与我的关联强度"求和后,再除以一个规模分母。差别在分母的选择:借贷渠道除以银行i的总负债,因为拆借敞口最终是靠负债端来吸收的;持股渠道除以银行i的资本,因为股权价值下跌直接影响的是资本金;共同资产渠道除以总资产,因为资产价格的波动直接打在资产的账面价值上。这个分母选择不是拍脑袋,它对应的是损失在资产负债表中实际冲击的科目。如果换成统一的分母,三个渠道的传染强度对比就失去意义了。
3.3 非线性叠加与状态更新:为什么损失比例不会下降
三个impact算出来后,进入合成与更新阶段:
# 综合影响(非线性叠加) total_impact = 1 - (1 - interbank_impact) * (1 - holding_impact) * (1 - asset_impact) h[t, i] = min(h[t-1, i] + total_impact * (1 - h[t-1, i]), 1) # 更新银行状态 if h[t, i] > self.default_threshold: s[t, i] = 2 # 违约 elif h[t, i] > 0: s[t, i] = 1 # 受影响 else: s[t, i] = 0h的更新公式值得细看:h[t] = h[t-1] + total_impact × (1 - h[t-1])。这个形式保证了h单调递增且不会超过1——因为每次加上的增量都乘了(1 - h[t-1]),h越接近1,能继续增加的空间越小。这意味着模型里不存在"银行先受损后恢复"的场景,损失只积累不清偿,这是DebtRank作为系统性风险度量工具的保守性设计。状态更新则完全由h与default_threshold的比较驱动,阈值默认0.05,即资本损失超过5%就认为违约,这篇论文的设定是合理的。
4. 三类冲击模拟与系统重要性银行识别:代码实现与参数说明
4.1 个体银行冲击与行业冲击的差异:冲击向量的构造逻辑
冲击模拟是整个模型最有实用价值的部分。simulate_shocks函数用shock_type参数区分三类冲击,本质区别在初始冲击向量initial_shock的构造方式上:
if shock_type == 'individual': initial_shock[shock_target] = 0.5 # 假设50%的初始冲击 elif shock_type == 'industry': for i in range(self.n_banks): if i in shock_target: initial_shock[i] = 0.3 # 30%的初始冲击 elif shock_type == 'macro': initial_shock = np.full(self.n_banks, 0.2) # 20%的初始冲击个体银行冲击是把全部冲击集中到一家银行身上,冲击强度0.5意味着这家银行初始就损失一半资本,模拟单一银行出险的极端情形。行业冲击则需要传入一个银行索引列表,凡是该行业涉及的银行都施加0.3的初始损失,对应的是"某个行业的贷款集中违约"场景。宏观经济冲击最简单,所有银行一视同仁承受0.2的损失,模拟利率突变、宏观政策收紧这类全局性风险。这里0.5、0.3、0.2的数值是论文作者根据中国银行业实际数据校出来的,实际使用就该按压力测试的重度来调整。
4.2 系统重要性银行识别:资产加权损失与阈值判定
识别系统重要性银行用的是一种"逐个击破"策略。对每一家银行单独施加以1.0为强度的完全违约冲击,然后把算出来的全系统损失按资产规模加权求和,最后归一化:
systemic_importance = np.zeros(self.n_banks) for i in range(self.n_banks): initial_shock = np.zeros(self.n_banks) initial_shock[i] = 1.0 # 假设该银行完全违约 losses = self.calculate_debtrank(initial_shock) systemic_importance[i] = np.sum(losses * self.assets) # 以资产加权的总损失 systemic_importance /= np.sum(self.assets) return np.where(systemic_importance > threshold)[0].tolist()这里有个值得注意的细节:np.sum(losses * self.assets)做的是逐银行乘积再求和,用的是银行资产规模去放大损失比例,资产越大的银行即便自身损失比例不高,对系统的绝对冲击也越大。阈值threshold默认0.3,含义是"该银行违约导致全系统损失的资产加权比例超过30%"。在实际监管压力测试中,这个阈值往往要降下来配合监管容忍度,而且这轮跑完得到的排名稳定性,取决于网络矩阵的质量——矩阵如果噪声大,排名会漂移。
4.3 网络可视化:三层网络分开画,看传染路径更清晰
代码的最后一部分用networkx画三层网络结构图,分三个子图并排展示:银行间借贷用有向图,交叉持股用有向图,共同资产持有用无向图。因为三个网络密度差异很大,借贷网络往往稀疏而共同资产网络可能接近全连通,合在一起画会变成一坨线,分开画反而容易看出哪一层是稠密的传染通道。实际应用中我习惯再加一步——用节点大小映射银行的资产规模,用边的粗细映射敞口强度,这样一眼就能看出哪些大银行处在多个网络的枢纽位置。
5. 避坑指南:复现多层网络模型最容易翻车的五个地方
5.1 三个矩阵量纲差异导致占比失真
现象:跑出来的losses结果中,某个渠道的影响恒定为0或小到可以忽略,但数据本身明明有相关性。
原因:interbank_network的数值可能是几十亿级别的敞口,而cross_holding_network里是0.01这种持股比例,两个矩阵直接套进impact公式时,数值大的矩阵会天然主导结果。代码里除以的是liabilities、equity、assets这些不同量级的分母,但矩阵本身的量级差异依然会扭曲三个渠道的贡献对比。
解决:在build_networks之前先对三个矩阵做标准化。常见的做法是分别按行归一化,使每行的和等于1,或者按矩阵最大的元素缩放,把三个矩阵压到同一个量级区间。我一般会先把矩阵转成百分比口径,再进模型。
5.2 违约阈值 default_threshold 的敏感性判断失误
现象:把default_threshold从0.05改到0.1,传染结果变化剧烈,某些银行从"违约"直接变回"受影响"。
原因:这个阈值不是理论推导出来的,是作者依据巴塞尔协议对资本充足率的最低要求设定的经验值。不同国家的监管标准不同,改阈值本身合法,但系统性风险模型的结论对阈值敏感是正常现象,改完阈值后必须重新校准其他参数,否则结果不可比。
解决:每次调整阈值后,把传染结果与基准场景做对比,确认变化幅度是否在可接受范围内。如果阈值微调就导致结论反转,说明模型本身处在临界状态,要回头检查网络矩阵的质量,而不是继续调阈值。
5.3 h 值在传染中途被错误重置
现象:银行明明已经受了冲击,下一轮损失比例却变小了,传染过程出现"回血"。
原因:这是把多维数组和循环变量的生命周期搞混了。如果某轮循环里直接给h[t, i]赋值而不是累加,或者复用了上一轮的数组没有深拷贝,h的历史会被覆盖掉。代码里h是一个(contagion_steps+1, n_banks)的二维数组,行代表轮次,列代表银行,一旦用错索引维度,就会出现时间维度的数据串位。
解决:跑完传染后,把h的每一行打印出来逐轮检查。h[t]里所有元素必须大于等于h[t-1]的对应元素,如果有任意一处递减,检查更新公式是否被改成直接赋值了。这个检查我每次跑模型都会做,是判断传染逻辑是否正确的第一道防线。
5.4 共同资产矩阵对称性被破坏
现象:asset_impact的计算结果与理论不对称——银行A受到B的传染影响和B受到A的影响数值上差很多,但代码逻辑上它们应该相等。
原因:共同资产持有关系天然是对称的,银行A和银行B共同持有某个资产,A抛售影响B,B抛售同样影响A。但实际数据来源(比如投资组合重叠度计算)很容易因为数据口径不一致产生非对称矩阵。代码里已经有asset_matrix = (asset_matrix + asset_matrix.T) / 2这一步来强制对称,但如果你自己加载真实数据时直接赋值给模型,绕过了这个对称化步骤,就会踩坑。
解决:在build_networks之前,对共同资产矩阵先做一次对称化,并顺手把对角线元素置零。另外检查矩阵对角线上是否残留了大量非零值——对角线非零意味着银行"自己传染自己",会让模拟结果虚高。
5.5 传染步数 contagion_steps 与收敛性的拿捏
现象:把contagion_steps从10改成50,最终损失比例几乎没变化;改成5,结果却明显偏低。
原因:DebtRank的传染过程本质上会在有限步内收敛——当所有银行状态都变成2或不再新增受影响银行时,再多的步数也只是重复计算。10步在大多数场景下已经够用,但如果网络规模变大(真实数据可能有数十家银行),传染链条变长,10步可能不足以让风险传遍全网,结果就偏低了。
解决:每跑一个新的数据集,先做一个步数敏感性测试——分别用5、10、20、50跑一遍,看最终损失的差异。差异小于0.01%时选较小的步数以节省计算时间;差异大说明网络结构里的间接路径较长,要适当调大步数上限。这个测试在我调真实数据时每次必做,步数的选择直接影响压力测试结论的可靠性。
6. 进阶:广义均值叠加、资产价格反馈与蒙特卡洛识别
基础的DebtRank三层模型跑通之后,论文配套代码里还有三层进阶扩展。第一层是改进非线性叠加的数学表达,用广义均值函数替代固定的乘积公式:
p = 0.5 # 通过实证数据校准 non_impacts = 1 - np.array(impacts) combined = np.power(np.mean(np.power(non_impacts, p)), 1/p) return 1 - combinedp是调节"渠道间交互强度"的关键参数。p趋近负无穷时结果接近最小效应,趋近正无穷时接近最大效应,p=0.5表示三种传染渠道处于中等交互状态。这个参数理论上需要通过历史危机数据校准,我在实际使用中会先跑一组p从0.1到1.0的网格搜索,观察系统重要性银行的排名稳定性,选择排名变化最小的区间。
第二层是资产价格反馈机制,模拟多家银行同时抛售共同资产时的"火灾抛售"效应。代码用指数衰减函数描述价格下跌与抛售压力之间的关系:fire_sale_pressure越大,资产价格下跌比例越接近1,但边际效果递减。这一层扩展对捕捉2015年股灾和2022年债券波动这类极端场景特别有用,基础模型里资产价格是被动受冲击的,加上反馈后资产价格变成内生变量,传染过程会出现加速效应。
第三层是蒙特卡洛模拟与边际贡献拆解。随机生成上千个冲击场景,逐个计算每加一家银行受影响时对总损失的边际贡献,把所有场景的结果平均后给出系统重要性排名。这个做法比单点冲击更稳健——单点冲击只能回答"某一家银行单独出事的后果",蒙特卡洛能覆盖"多家银行同时出事的组合情形"。我在评估一家银行的系统重要性时,如果条件允许,都会以蒙特卡洛结果为主,单点冲击结果作交叉验证。
这套代码从输入数据到监管结论的链路比我预想的短——拿着银行间市场成交数据、股东数据库和资产负债表就能跑通全流程,得到的结果能直接支撑三个决策:哪些银行要纳入附加监管、哪些行业的贷款需要设定集中度上限、宏观经济冲击的容忍度应该定在什么压力水平。从那以后我再评估金融网络风险,都会强制走一遍基础DebtRank、敏感性测试、蒙特卡洛验证这三级流程。希望这个复现思路也能帮你少走一些弯路。
本文还有配套的精品资源,点击获取