
简介在金融风控与银行风险管理中不良贷款率的变动归因是信贷决策和压降方案的核心环节。面对GDP、M2、社融等高度相关的宏观指标直接进行多元线性回归常因多重共线性导致系数失真、符号反转。因子分析通过KMO检验评估适用性并利用主成分法将高维相关指标浓缩为少数公共因子从而有效规避共线性问题。将因子得分作为新的自变量代入回归模型不仅能保留原始信息还能让系数的业务含义更清晰。这一“因子分析降维多元线性回归归因”的组合广泛应用于银行资产质量季度分析、区域风险监测及不良贷款成因量化等场景为识别宏观景气、风险抵补与信贷结构等潜在驱动因素提供了可复现的建模路径。1. 为什么银行不良贷款归因大家最后都回到多元线性回归和因子分析这套组合做银行风险分析的人应该都有体会不良贷款率一抬头领导第一个问题就是“到底哪个因素把它推上去的”。直接拿十几个宏观和经营指标去对不良贷款率做多元线性回归结果往往是VIF爆表、符号反向、删一个样本系数就翻案。原因是这些指标彼此高度相关——GDP增速、M2、社融、制造业景气度本来就是一篮子事放一起回归等于自己和自己打架。因子分析在这条路径里的角色是把一堆互相缠绕的指标浓缩成少数几个公共因子例如宏观景气因子、资产质量因子、经营效率因子再用这几个互不相关的因子去做多元线性回归。这样既保住了信息量又躲开了多重共线性回归系数的业务含义也更容易讲给信贷审批和合规听。这套“因子分析降维多元线性回归归因”的组合是银行业不良贷款影响研究的成熟做法适合三类人银行风险管理部做季度归因分析的分行信贷条线做压降方案的数据人员以及金融风控方向需要把模型写成可复现报告的从业者。2. 先定指标集和数据口径不良贷款的“因”与“果”该怎么量化很多人拿到这类课题第一反应是找数据、跑模型。实际上最耽误时间的不是建模而是确定因变量和自变量口径。口径定错后面所有统计检验都是正确的废话。2.1 因变量选什么不良贷款率、不良余额还是不良生成率不良贷款的定义在银行业内是清晰的按五级分类标准次级、可疑、损失三类贷款合称不良贷款。常见做法是用不良贷款率不良贷款余额/贷款总额做因变量因为它是监管报送和市场观察的基准口径解释起来不费劲。但用不良贷款率有一个隐患它是一个时点存量指标天然带自相关性——上季度不良率已经很高这个季度即便新生成不良很少率值也会惯性维持。所以做回归归因时如果模型算出R方高达0.98先别高兴大概率是存量惯性在起作用而不是你选的因子解释力强。更严格的替代方案是用不良生成率新增不良/期初贷款余额或不良贷款余额的一阶差分做因变量截掉惯性再看因子影响。我一般会两组都跑一组用水平值一组用生成率对比结果是否一致。时间口径也要统一。宏观指标通常是季度同比或月度累计而银行不良贷款率是季末时点数。常见做法是把宏观指标统一折算成季度数值并且对宏观变量做滞后处理——GDP增速下降不会立刻把当期不良推高通常滞后一到两个季度才体现在资产质量上。滞后阶数怎么定可以用相关性扫描或经济逻辑判断后面会细说。2.2 自变量的候选池从宏观、地区到银行自身经营自变量选择决定了因子分析能抽出什么。我的习惯是先把候选指标按四个维度铺开宁可多一些反正后面有因子分析过滤相关性。宏观与货币环境GDP同比增速、工业增加值增速、PMI、M2同比、社会融资规模存量同比、CPI。这类指标反映经济冷热经济下行时企业偿债能力整体走弱不良生成率随之抬升。区域与行业结构如果用的是某省或某市的数据要放区域内GDP增速、商品房销售面积增速、区域主导行业景气指数。银行不良贷款的行业集中度很高区域产业结构单一的行往往死磕一个行业行业景气的边际变化比宏观总量更敏感。银行自身经营指标拨备覆盖率、资本充足率、存贷比、贷款拨备率、最大十家客户贷款集中度、房地产贷款占比、制造业贷款占比、逾期贷款占比。这些指标一部分是风险抵补能力一部分是信贷结构暴露度都是因子分析里容易归类清晰的变量。客户与信贷政策代理变量关注类贷款率、逾期90天以上贷款与不良贷款比值、新增贷款平均利率、个人住房贷款占比。用它们做解释变量时要注意有些指标和因变量是同向恶化的镜像指标比如逾期率放进模型会出现内生性的争论。稳妥做法是把这类指标作为稳健性检验的一部分不进主模型。2.3 数据清洗与标准化先把 13 个指标的脏数据收拾干净银行历史数据常见的坑有三个口径调整比如某年重组贷款口径变了、缺失值连片、季节性与量纲差异。我一般按下面这段逻辑处理。import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler df pd.read_csv(bank_data.csv, parse_dates[date]) df df.sort_values(date).set_index(date) # 单指标缺失率超过 20% 的整列剔除避免插值填出假规律 valid_cols df.isna().mean() 0.20 df df.loc[:, valid_cols] # 剩余缺失用线性插值填充金融时序平滑变化比较适用 df df.interpolate(methodlinear, limit_directionboth) df df.dropna() # 因子分析对量纲敏感做 z-score 标准化保留均值和标准差备用 feature_cols [c for c in df.columns if c not in [npl_ratio, npl_balance]] scaler StandardScaler() X_scaled scaler.fit_transform(df[feature_cols]) X_scaled pd.DataFrame(X_scaled, columnsfeature_cols, indexdf.index) print(X_scaled.describe().T[[mean, std]])逻辑说明缺失率超过20%的列直接删是因为银行指标缺了五分之一以上插值出来的“规律”基本是编的。线性插值适合季度的、平滑变化的经营类指标如果遇到监管口径调整产生的断裂值插值会失败要做分段处理。标准化用z-score把不同量纲的指标比如亿元资产规模和百分比不良率拉到同一个尺度因子分析才不会让绝对数值大的变量垄断主成分。参数说明缺失率阈值20%偏保守样本多时可以收紧到10%插值方法选linear如果数据有明显趋势也可以尝试polynomial二次多项式但要小心外推部分。标准化之后需要留一个字段记录每个原始变量的均值和标准差后续做因子业务映射和回归结果还原时要用。2.4 样本量要求因子分析不是数据少也能跑的算法这是最容易被忽略的一道硬门槛。KMO检验和因子载荷估计都依赖样本量经验上样本量至少是变量数的5到10倍最好不低于100。一家城商行按季度算10年才40个样本放13个指标进去因子载荷会抖到完全无法解释。所以这类课题如果只拿单家银行做样本量是不够的。常见做法是用面板数据选取同一区域的几家或十几家银行每家按季度取数分析对象变成“银行-季度”个体样本量就上来了。代价是引入了银行个体差异需要在建模时处理个体效应通常可以按银行类型国有行、股份行、城商行、农商行做分组对比或者在回归里加个体虚拟变量。如果坚持只用单家银行建议把模型简化为因子分析只抽2个因子、回归里最多放4个解释变量不要贪多。3. 因子分析降维从 13 个指标到 3-4 个公共因子的落地步骤指标池备好后先不急着建模用因子分析方法搞清楚这些指标背后到底有几个共同力量在驱动。3.1 适用性检验KMO 和 Bartlett 球形检验过不了别硬做KMOKaiser-Meyer-Olkin衡量的是变量间偏相关的大小。KMO值在0.6到1之间才适合做因子分析低于0.5属于“无法接受”说明指标之间缺乏共同方差抽出来的因子解释力会很差。Bartlett球形检验的原假设是相关矩阵为单位阵——也就是变量互相独立如果p值大于0.05说明数据做因子分析没有意义。import pandas as pd import numpy as np from scipy.stats import chi2 def kmo_test(corr): n corr.shape[0] inv_corr np.linalg.inv(corr) diag_inv np.diag(1 / np.sqrt(np.diag(inv_corr))) partial_corr -diag_inv inv_corr diag_inv np.fill_diagonal(partial_corr, 0) corr_sq corr.values**2 partial_sq partial_corr**2 kmo_num corr_sq.sum().sum() kmo_den kmo_num partial_sq.sum().sum() kmo kmo_num / kmo_den det_corr np.linalg.det(corr.values) n_obs X_scaled.shape[0] p corr.shape[0] bartlett_stat -(n_obs - 1 - (2*p 5)/6) * np.log(det_corr) df p * (p - 1) / 2 p_value chi2.sf(bartlett_stat, df) return kmo, bartlett_stat, p_value corr X_scaled.corr() kmo, bartlett_stat, p_value kmo_test(corr) print(fKMO: {kmo:.3f}, Bartlett chi2: {bartlett_stat:.2f}, p: {p_value:.3e})逻辑说明KMO的分子是变量间简单相关系数平方和分母再叠加上偏相关系数平方和。若变量间真的存在公共因子偏相关会明显小于简单相关KMO就接近1。Bartlett统计量利用相关矩阵的行列式——变量高度相关时行列式趋于0ln(det)负得很大统计量就大p值自然显著。参数说明这里手工算KMO和Bartlett而不是直接调包是为了让你理解指标的含义。实际项目里也可以用factor_analyzer.factor_analyzer的calculate_kmo和calculate_bartlett_sphericity结果一样。如果KMO低于0.5优先删掉那些独特性高、和其他变量几乎不相关的指标而不是硬着头皮继续。3.2 提取因子个数特征值大于1 还是看累计方差解释率因子提取常用主成分法。判断因子个数有两个并用的标准特征值大于1Kaiser准则以及累计方差解释率超过70%-80%。特征值大于1的含义是这个因子单独解释的方差超过一个原始变量的方差留它下来不是在做多余的浓缩。from sklearn.decomposition import PCA import matplotlib.pyplot as plt pca_full PCA() pca_full.fit(X_scaled) eigenvalues pca_full.explained_variance_ var_ratio pca_full.explained_variance_ratio_ for i, (eig, var) in enumerate(zip(eigenvalues, var_ratio)): cum var_ratio[:i1].sum() print(f因子{i1}: 特征值{eig:.3f}, 方差解释率{var:.2%}, 累计{cum:.2%})逻辑说明PCA输出的explained_variance_就是各主成分的特征值。只取特征值大于1的分量代表这个因子比单个变量更能浓缩信息。银行不良贷款影响因素数据里通常前3到4个因子累计解释率在75%左右再往下特征值跌到0.8以下解释率增量不到5%抽出来只会让因子命名变困难。参数说明n_components不设就是为了看全部特征值再回来圈定。实际建模时一般保留累计解释率超过80%的因子数或者特征值大于1的因子数两者冲突时优先考虑解释率。3.3 因子旋转与命名让矩阵里的业务含义显形初始因子载荷矩阵往往所有变量在第一因子上都有高载荷看不出区分度。这时候做方差最大化正交旋转varimax让每个变量尽量只在一个因子上有高载荷其他因子上接近0因子结构才清晰。from factor_analyzer import FactorAnalyzer fa FactorAnalyzer(n_factors4, rotationvarimax, methodprincipal) fa.fit(X_scaled) loadings pd.DataFrame(fa.loadings_, indexfeature_cols, columns[因子1, 因子2, 因子3, 因子4]) print(loadings.round(3))逻辑说明factor_analyzer的FactorAnalyzer直接封装了主成分法提取和varimax正交旋转。methodprincipal指定用主成分法rotationvarimax是方差最大正交旋转。输出的是旋转后的载荷矩阵每一行是一个原始指标每一列是一个公共因子。参数说明n_factors可以按前面PCA的结果调整如果第4个因子特征值只有0.6就改成3。旋转方法默认varimax即可如果理论上认为因子之间应该相关比如“宏观景气”和“资产质量”天然相关可以用oblimin斜交旋转但坏处是因子得分之间的相关性会回来后面做多元回归会再引入共线性所以主模型我都不用斜交。因子命名是这一步的重头。通常跑出来的结果会有这样的模式GDP增速、PMI、M2、社融存量增速在因子1上载荷都超过0.7可以命名“宏观景气因子”拨备覆盖率、资本充足率、存贷比在因子2上载荷高命名“风险抵补因子”房地产贷款占比、制造业贷款占比、客户集中度在因子3上载荷集中命名“信贷结构因子”逾期率、关注率在因子4上高载荷命名“存量风险因子”。命名规则记住一条载荷超过0.5的指标在这个因子上才有解释资格低于0.4的不参与命名讨论。这一步一定要留底稿后面向业务汇报因子含义全靠它。4. 多元线性回归建模用浓缩后的因子解释不良贷款率因子分析解决了自变量之间的共线性接下来才是真正的回归归因。这里有一个很多新手会走偏的地方直接用旋转后的因子得分做自变量还是原始指标缩回去做回归答案是用因子得分。4.1 因子得分怎么算以及一个必须注意的时间对齐问题因子得分是把原始标准化数据投影到因子空间得到的坐标值。factor_analyzer里有现成接口。df_factor pd.DataFrame(fa.transform(X_scaled), columns[factor_macro, factor_capital, factor_structure, factor_risk], indexdf.index) df_model pd.concat([df[[npl_ratio, npl_balance]], df_factor], axis1) df_model df_model.join(df[gdp_yoy].shift(2).rename(gdp_yoy_lag2)) print(df_model.corr().round(3))逻辑说明fa.transform把原始标准化数据映射成因子得分数据维度就从13个指标压到4个因子。为什么要跟原始gdp_yoy的滞后项并到一起因为宏观因子虽然浓缩了宏观信息但它是当期值不良贷款率是存量当期GDP增速对当期不良率的影响大概率还没传导完。我习惯在模型里显式加入一两个代表性宏观变量的滞后值用来验证“宏观因子滞后一期或两期才显著”的直觉。如果你不想让原始变量混进因子模型也可以把整体宏观因子做滞后但显式滞后一个有代表性的指标更好解释。参数说明shift(2)是滞后两期季度数据里大约对应半年传导周期。滞后一期可能还不够因为从企业现金流恶化到贷款五级分类下调再到银行报表确认通常有一个监管暴露的延迟。具体滞后几期可以通过对比滞后0、1、2期的模型AIC或R方来定不要拍脑袋。4.2 拟合回归别急着看R方先看系数符号和显著性import statsmodels.api as sm X df_model[[factor_macro, factor_capital, factor_structure, factor_risk, gdp_yoy_lag2]] X sm.add_constant(X) y df_model[npl_ratio] model sm.OLS(y, X).fit() print(model.summary())逻辑说明statsmodels的OLS输出包含了R方、调整R方、各系数t检验的p值、F统计量比sklearn的LinearRegression更适合统计归因场景。这里因变量是npl_ratio四个因子得分由于正交性彼此相关性接近0再加上一个滞后的宏观指标解释存量传导。系数正负是重点宏观景气因子系数为负说明经济景气度高时不良率下降风险抵补因子系数为负说明拨备越充裕不良率越低信贷结构因子符号取决于你选的主导指标如果因子在房地产贷款占比上载荷高可能为正或负要结合该行的实际敞口解释。参数说明sm.add_constant是为模型加入截距项OLS默认不包含截距银行不良归因模型必须带截距否则系数估计会偏。如果发现因子的t值小于1.5先检查是不是因子得分本身携带的信息太弱而不是直接删因子。4.3 回归诊断VIF、DW、白检验和残差正态性四个必看项因子得分正交VIF理论上接近1但滞后宏观变量和宏观因子之间可能会重新引入相关性所以要再查一遍。DW统计量用来判断残差是否自相关——不良贷款率这种惯性强的时序数据最容易翻车的就是这里。白检验White检验判断异方差银行数据里不同资产规模的机构方差差异很大容易出现异方差。from statsmodels.stats.outliers_influence import variance_inflation_factor from statsmodels.stats.stattools import durbin_watson from statsmodels.stats.diagnostic import het_white vif_data pd.DataFrame({ var: X.columns, vif: [variance_inflation_factor(X.values, i) for i in range(X.shape[1])] }) print(VIF:\n, vif_data) print(DW:, round(durbin_watson(model.resid), 3)) white_test het_white(model.resid, X) print(White p-value:, round(white_test[1], 3)) import scipy.stats as stats import matplotlib.pyplot as plt stats.probplot(model.resid, distnorm, plotplt) plt.title(Residual Q-Q Plot) plt.show()逻辑说明:VIF用方差膨胀因子公式计算每个解释变量以其他解释变量为自变量做回归VIF 1/(1-R²)。VIF大于10说明共线性严重大于5要警惕。DW值在2附近说明残差无自相关明显小于1.5则有正向自相关说明模型漏掉了时间惯性项。White检验的p值如果小于0.05残差存在异方差常见对策是使用稳健标准误statsmodels里把fit()改成fit(cov_typeHC1)就行。参数说明Q-Q图判断残差正态性——OLS本身不要求残差严格正态但t检验和F检验在样本量不大时依赖这个假设。散点如果偏离直线两端翘起说明残差重尾对应的t检验p值可信度下降可以考虑bootstrap或稳健标准误再做结论。5. 做这类课题的避坑清单数据、方法与解释的5个高频翻车点这类课题我在实际交付和评审里见过的翻车点远比教科书里的“显著/不显著”要具体。挑五个高频的写下来每个按现象、原因、解决三步说清楚。5.1 R方高得离谱0.97的拟合优度是惊喜还是陷阱现象多元线性回归跑出来R方0.97调整R方也超过0.95看起来模型完美但一放进新季度数据预测就明显偏大。原因不良贷款率是存量指标上期不良率本身解释了当期不良率的绝大部分模型里如果没有包含因变量滞后项残差会高度自相关R方被存量惯性虚高抬起来了。解决换因变量为不良生成率或者在模型中加入npl_ratio的滞后一期作为控制变量再看其他因子的边际贡献。加了滞后项之后原来显著的宏观因子可能变得不显著这是正常的——说明宏观影响已经有一部分通过存量不良体现出来了。5.2 KMO只有0.48强行抽因子导致因子无法命名现象KMO检验没过但为了赶报告硬抽3个因子结果第一因子上挤了9个变量第二、第三因子加载不足3个变量命名成了一个四不像。原因指标池里混进了几个和其他变量几乎没有共同方差的“独立变量”它们稀释了公共因子方差。解决回到指标池把和大多数变量偏相关都很低的指标逐个剔除再复测KMO。常见的剔除对象是CPI和存贷比——CPI受外部大宗商品影响大存贷比是银行主动调节项在相当多数据集里跟其他指标相关性很弱。5.3 因子旋转选错或载荷阈值太低业务解释一团浆糊现象用oblimin斜交旋转因子1和因子2得分相关性高达0.6放到回归里VIF重新飙高;或者按载荷大于0.3的阈值去命名每个因子都有七八个变量归属说不清它到底代表什么业务含义。原因斜交旋转允许因子相关因子得分之间的共线性当然会回来;阈值太低则把边缘相关都纳入命名范围淹没核心变量。解决主模型一律用varimax正交旋转载荷阈值至少取0.5。如果业务上一定要看斜交结果把它作为稳健性检验放到附录不进主模型。5.4 想跳过因子得分直接拿原始指标做回归现象因子模型解释业务含义比较绕有人干脆把原始标准化指标直接丢进多元线性回归结果VIF超过30GDP增速和工业增加值两个变量系数一正一负互相抵消。原因原始指标相关性太高最小二乘估计的系数方差被共线性放大符号自然不稳定。解决按因子得分做回归需要解释原始变量时用因子载荷矩阵做映射——比如“宏观景气因子上升1个单位不良率下降0.3个百分点”转换成“GDP增速上升1个百分点约带动宏观景气因子上升0.4个单位传导到不良率下降约0.12个百分点”。如果评审硬要求原始变量结果可以用岭回归或者偏最小二乘做对照。5.5 样本量不足还硬报告显著性删两个样本结果全变现象30几个样本、10个解释变量某个因子的p值0.04报告写了“显著影响”。结果换一个季度区间删除两个异常季度的数据p值变成0.3。原因样本量低于解释变量数的10到15倍时t检验的统计功效极低系数估计的标准误巨大显著性更多是偶然。解决统一采用银行-季度面板扩样本或者接受现实把结论表述从“显著影响”降级为“方向性一致”。更稳的做法是跑Bootstrap重复抽样500次观察系数正负的比例如果正负各半就别报显著了。6. 验证模型与进阶处理让回归结果在业务上站得住模型跑通只是第一步。银行内部评审最常问的一句话是“你这个模型换个时间段还成立吗换家银行还成立吗”这一章就是解决这两个问题的。6.1 滚动窗口验证前八成拟合、后两成预测from sklearn.metrics import mean_squared_error import numpy as np n len(df_model) cut int(n * 0.8) train, test df_model.iloc[:cut], df_model.iloc[cut:] X_train sm.add_constant(train[[factor_macro, factor_capital, factor_structure, factor_risk, gdp_yoy_lag2]]) y_train train[npl_ratio] X_test sm.add_constant(test[[factor_macro, factor_capital, factor_structure, factor_risk, gdp_yoy_lag2]]) y_test test[npl_ratio] model_cv sm.OLS(y_train, X_train).fit() pred model_cv.predict(X_test) rmse np.sqrt(mean_squared_error(y_test, pred)) print(RMSE:, round(rmse, 4)) print(测试期不良率均值:, round(y_test.mean(), 4))逻辑说明滚动窗口用前80%样本拟合后20%样本预测看RMSE与测试期不良率均值的比值。如果RMSE超过均值的10%-15%说明模型泛化能力偏弱前面那些显著系数只能解释历史不能指导未来的信贷政策。参数说明切分比例80/20是银行时序模型里常用的折中切法样本更少时可以改用扩展窗口每次多加入一个季度重新拟合而不是固定一段训练集。6.2 三种对照实验Lasso选变量、偏最小二乘、正则化岭回归因子得分回归已经消除了共线性但评审专家常要求“你能不能证明因子分析比直接用原始变量更好”。我会做三个对照第一把原始标准化变量直接丢进OLS如果VIF爆表说明确实需要降维第二用Lasso跑一遍原始变量看它选出的非零变量是不是恰好落在因子分析的高载荷变量集合里第三用岭回归跑原始变量对比系数符号方向是否与因子回归解释一致。三个方向的结论能互相印证讨论业务含义时才有底气。6.3 输出可业务解释的表格而不是只贴回归摘要表格是给业务部门汇报的最终交付物。建议按下面这个结构出表每个因子的命名、载荷较高的代表性指标、回归系数、p值、业务解读。业务解读不要写“因子2显著负向影响不良率”要写“资本充足率每提高1个百分点通过风险抵补因子传导不良率约下降0.08个百分点”。我自己做这类项目有个习惯所有结论必须能落到一句“假如……就会……”的话。比如“假如制造业PMI连续两个季度低于49宏观景气因子会拖累不良率上行大约滞后两到三个季度体现”。这样的句子评审组听得懂也会真正被业务拿去用。反过来如果模型结论只能写“因子分析表明存在某种共同影响因素”那这个模型对银行不良贷款的压降没有任何指导意义等于白做。我早期做区域城商行不良归因时模型检验全过汇报时被会审问“你第二个因子到底对应信贷审批还是贷后管理”当场答不上来。从那以后每个因子都会提前画一张“原始指标-因子载荷-业务动作”的映射表解释不通的因子宁愿删掉重提。希望你跑数据之前先做这一步能省掉后面一大轮返工。希望帮到你。本文还有配套的精品资源点击获取