MCM520 ← 资料站首页 泰迪杯 2020 D 题「金融风控分类」优秀范文(一):数据理解与逻辑回归从零实现 打开交互阅读器 →

泰迪杯 2020 D 题「金融风控分类」优秀范文(一):数据理解与逻辑回归从零实现

摘要

信贷违约识别是金融风控中最典型的二分类问题:给定借款人的多维特征,模型需要判断其未来是否会发生违约,从而为授信额度、定价与贷后管理提供依据。本篇聚焦该问题的数据基础与基线模型,采用一组确定性的合成信贷样本(样本量 N=200,其中违约标签 label=0 共 70 例、占比 35.0%,正常履约标签 label=1 共 130 例、占比 65.0%)展开分析。我们首先刻画了样本类别构成与六个风险特征在两类群体间的分布差异,随后从零实现了逻辑回归模型:以 Sigmoid 函数将线性组合映射为违约概率,采用二元交叉熵作为损失函数、以梯度下降(学习率 0.5、迭代 200 轮)进行参数估计,并对特征做 Z-score 标准化以保证梯度稳定。训练结束后损失收敛至 0.3498,训练集 AUC 达到 0.9077、测试集 AUC 达到 0.8973;在默认判定阈值 0.5 下,测试集整体准确率为 0.8500,对正常类的精确率、召回率、F1 分别为 0.8636、0.9268、0.8941。需要指出的是,少数类(违约)在阈值 0.5 下的召回率仅为 0.6842,说明在不加任何不平衡处理的情形下,模型对高风险客户的捕获能力有限——这正是后续第二篇、第三篇需要进一步处理的核心动机。本篇所有数字均可由 tools/gen_tidy2020a.py 确定性复现。

一、问题重述

金融风控中的信贷违约识别,要求利用客户的人口属性、收入负债、历史信用行为等特征,建立可解释的二分类模型,将新客户划分为"违约(高风险)"与"正常(低风险)"两类。题目的现实背景是:银行与消费金融机构在授信决策中,必须同时兼顾"尽量拦截坏客户"与"尽量不误伤好客户"两个有时相互冲突的目标。本篇作为系列范文的第一篇,集中回答以下子问题:

  • 子问题 1(数据构成):样本规模、类别比例如何?是否存在类别不平衡?六维特征在两类客户之间是否存在系统性差异?
  • 子问题 2(基线模型):如何不依赖任何第三方机器学习库、仅用标准库从零实现一个可用的逻辑回归分类器?
  • 子问题 3(训练机制):梯度下降如何迭代更新参数?损失函数是否稳定收敛?标准化对收敛有何作用?
  • 子问题 4(结果解释):学到的系数应如何解读?决策边界在特征空间中呈现何种形态?模型预测的概率分布是否合理?

二、假设与符号说明

为在合理范围内简化建模,作如下假设与约定:

  1. 样本独立同分布,且训练集与测试集来自同一总体,不存在概念漂移;
  2. 六个特征均为数值型、已做基本清洗,缺失值在此合成数据中不出现;
  3. 标签定义固定:label=0 表示违约客户(风险类,建模关心的"正事件"),label=1 表示正常履约客户;
  4. 逻辑回归假定特征经线性组合后通过对数几率(logit)与类别概率相关;
  5. 训练集(140 例,占 70%)用于估计参数,测试集(60 例,占 30%)仅用于独立评估,二者不重叠。

符号约定:设特征向量为 x=(x₁,…,x₆),模型参数为权重 w=(w₁,…,w₆) 与截距 b;Sigmoid 函数 σ(z)=1/(1+e⁻ᶻ);z=b+Σⱼ wⱼx̃ⱼ 其中 x̃ⱼ 为标准化后的特征;交叉熵损失 L=−(1/n)Σᵢ[yᵢ·ln pᵢ+(1−yᵢ)·ln(1−pᵢ)],pᵢ=σ(zᵢ) 为样本 i 属于正常类(label=1)的预测概率;AUC 为受试者工作特征曲线下面积。

三、模型建立

3.1 二分类逻辑回归

逻辑回归通过线性组合 z=b+Σⱼ wⱼx̃ⱼ 刻画"对数几率",再用 Sigmoid 将其压缩到 (0,1) 区间,得到样本属于正常类的概率:

p = P(label=1 ∣ x) = σ(z) = 1 / (1 + e⁻ᶻ)

判定规则为:当 p ≥ 0.5 时判为正常(label=1),否则判为违约(label=0)。该框架天然输出 calibrated 概率,便于后续以阈值调节风险偏好(见第三篇)。

3.2 交叉熵损失与梯度下降

以二元交叉熵作为损失函数,对第 i 个样本有:

ℓᵢ = −[yᵢ·ln pᵢ + (1−yᵢ)·ln(1−pᵢ)]

对权重 wⱼ 的梯度为 ∂ℓᵢ/∂wⱼ = (pᵢ − yᵢ)·x̃ᵢⱼ,对截距 b 的梯度为 (pᵢ − yᵢ)。采用批量梯度下降,每轮以全部训练样本的平均梯度更新参数:

wⱼ ← wⱼ − η·(1/n)Σᵢ(pᵢ − yᵢ)·x̃ᵢⱼ, b ← b − η·(1/n)Σᵢ(pᵢ − yᵢ)

其中 η=0.5 为学习率,迭代上限 200 轮。梯度下降使经验风险(平均交叉熵)单调下降直至收敛。

3.3 标准化与决策边界

不同特征量纲差异巨大(如"月收入"在 02 万元、"历史逾期次数"在 05 次),直接代入会使梯度方向被量纲主导、收敛缓慢。因此对训练集各特征做 Z-score 标准化 x̃ⱼ=(xⱼ−μⱼ)/σⱼ(μⱼ、σⱼ 由训练集估计并复用于测试集),使各维度具有相近尺度。在二维子空间 (x₁ 年龄, x₂ 月收入) 中,决策边界由 σ(z)=0.5 即 z=0 给出,是一条直线:b + w₁·(x₁−μ₁)/σ₁ + w₂·(x₂−μ₂)/σ₂ = 0,可在原特征平面直接绘制。

四、求解各子问题

4.1 数据构成与类别平衡

本数据集共 200 例,违约 70 例(35.0%)、正常 130 例(65.0%),呈现轻度类别不平衡——违约客户为少数类。按 7:3 划分为训练集 140 例与测试集 60 例,二者类别比例与总体一致。类别构成由图 1 直观呈现,这种"坏客户少、好客户多"的分布正是信贷场景的真实写照,也提示我们在评估指标上不能只看整体准确率,而必须关注少数类(违约)的召回能力。

Z-Score标准化

为消除量纲影响,对数据进行标准化处理:

zi=xi−μσz_i = \frac{x_i - \mu}{\sigma}

其中 μ\mu 为均值,σ\sigma 为标准差。标准化后数据均值为0、标准差为1,便于不同量纲指标的比较与融合。

4.2 特征按标签的分布差异

六个特征在两类群体间的均值存在清晰差异(见图 2):违约群体的年龄均值更低(37.57 岁 vs 43.60 岁)、月收入更低(0.658 万 vs 1.164 万)、负债收入比更高(0.432 vs 0.262)、历史逾期次数更多(1.798 次 vs 0.645 次)、额度使用率略高(0.479 vs 0.402)、居住稳定年数更短(3.511 年 vs 5.505 年)。这些差异共同构成了模型可学习的信号:低风险客户通常收入更高、负债更轻、信用历史更干净、生活更稳定。整体来看,月收入、负债收入比、历史逾期次数三类特征的类间区分度最强,预期在模型中权重最大。

4.3 逻辑回归从零训练

按上述流程从零实现梯度下降。损失曲线(图 3)显示交叉熵从初始约 0.69 平稳下降至 0.3498,200 轮内已充分收敛,未出现震荡或发散,说明学习率 0.5 与标准化组合合理。训练集 AUC 为 0.9077,测试集 AUC 为 0.8973,二者接近,表明模型泛化良好、无显著过拟合。学到的标准化系数见图 5:月收入(w₂=1.4042)、居住稳定年数(w₆=1.0781)、历史逾期次数(w₄=−0.9942)、负债收入比(w₃=−0.7130)绝对值最大,与 4.2 中"区分度最强"的直观判断一致;年龄(w₁=0.2679)与额度使用率(w₅=−0.2563)影响相对较弱。截距 b=1.1153 为正,说明在特征取均值时样本更偏向正常类,符合总体正常占比更高的先验。

4.4 决策边界与概率分布

图 4 在原特征平面 (年龄, 月收入) 上绘出两类散点与由系数反推的决策边界:年轻且低收入区域落入违约半平面,年长且高收入区域落入正常半平面,边界斜穿两类重叠区,符合逻辑回归线性可分但存在重叠的设定。图 6 给出 Sigmoid 函数形状,说明当对数几率远离 0 时预测概率迅速趋近 0 或 1,而接近边界时则对特征变化敏感。图 8 进一步展示测试集预测概率的分布:正常客户的概率质量集中在高位(0.81.0 区间),违约客户概率质量集中在低位(0.00.2 区间),但中间 0.2~0.8 区间两类仍有交叠,印证了"适度重叠、非完全可分离"的数据特性,也解释了为何单一阈值难以同时兼顾两类召回。

五、结果分析

逻辑回归在本数据上取得了 0.8973 的测试 AUC 与 0.8500 的整体准确率,作为仅依赖线性边界的基线模型是令人满意的。系数的符号与现实金融直觉完全吻合:收入越高、居住越稳定 → 违约概率越低(正系数);负债比越高、逾期越多、额度使用越满 → 违约概率越高(负系数)。这赋予模型良好的可解释性,风控人员可直接依据"哪些特征把分数拉低"定位风险点。

然而必须正视一个短板:在默认阈值 0.5 下,违约类(少数类)的召回率仅为 0.6842,意味着约三成真实违约客户被误判为正常。这并非模型"能力不够"(训练/测试 AUC 均接近 0.9),而是因为阈值 0.5 以"正常类"为默认判定,对少数类天然不利,加之数据本身轻度不平衡。图 8 中两类概率分布的交叠区间即对应于这一代价。若业务上"漏掉一个坏客户"的损失远高于"误拒一个好客户",则应下调判定阈值或引入类别权重/重采样——这正是本系列第二篇、第三篇的主线。

六、图表

图1 样本类别构成(违约70 / 正常130,违约率0.350)
图2 各特征按标签的均值对比(违约 vs 正常)
图3 逻辑回归训练损失(交叉熵)下降曲线(200 轮收敛至0.3498)
图4 逻辑回归决策边界(年龄 vs 月收入)
图5 逻辑回归系数(标准化特征,绿正红负)
图6 Sigmoid 激活函数 σ(z)=1/(1+e^-z) 示意
图7 训练—测试切分与建模流程
图8 预测违约概率分布(按真实标签分箱)

七、灵敏度分析

对逻辑回归的训练机制做如下灵敏度考察。第一,学习率 η 的稳健性:η=0.5 时损失平滑收敛;若将 η 提高到 2.0 以上,初期梯度可能过大导致损失在 0.69 附近剧烈跳动甚至数值溢出(Sigmoid 输入被截断),说明标准化前提下的"适中学习率"是关键,本篇取值保守且有效。第二,标准化必要性:若跳过 Z-score 直接以原始量纲训练,月收入(量级 1)与历史逾期(量级 1)还尚可,但年龄(量级 40)会主导梯度方向,需大幅降低学习率才能收敛,训练效率显著下降,印证了 3.3 的论断。第三,迭代轮数的边际收益:损失在前 50 轮已下降约 80%,后 150 轮仅做微调,说明 200 轮上限对本题规模充裕,进一步增加轮数收益甚微。第四,训练/测试划分的随机性:由于数据由固定随机种子生成且划分确定,上述指标在不同运行间完全一致,保证了结论的可复现性。综上,基线逻辑回归对合理的超参数选择具有稳健性,其性能瓶颈主要来自数据本身的类间重叠与轻度不平衡,而非训练机制缺陷。

八、模型优缺点

本篇模型的优点:结构透明、可解释性强,系数符号直接对应业务风险因子;输出校准概率,便于以阈值灵活调节风险偏好;从零实现仅依赖标准库,无第三方依赖,便于教学与审计。缺点:仅能刻画特征间的线性决策边界,难以拟合复杂的非线性交互;对特征尺度敏感,必须配合标准化;在类别不平衡时少数类召回不足,需要后续的不平衡处理(见第三篇);且未引入正则化,在更高维或强相关特征场景下存在过拟合隐患。

九、结论

本篇针对信贷违约识别的数据基础与基线模型给出完整求解:在 200 例合成样本(违约率 35.0%)上,从零实现的逻辑回归经 200 轮梯度下降将交叉熵损失收敛至 0.3498,测试集 AUC 达 0.8973、准确率 0.8500,系数方向与金融直觉一致、可解释性强。同时揭示了基线模型在默认阈值下对少数类(违约)召回偏低(0.6842)的结构性短板,为后续引入决策树对比、评分卡与类别不平衡处理指明了方向。

十、参考文献

[1] 周志华. 机器学习(逻辑回归与梯度下降章节). 清华大学出版社.
[2] 信用评分模型技术:从逻辑回归到机器学习(二分类与概率校准).
[3] 金融风控建模实践:特征工程、样本不平衡与阈值策略.

参考文献

[1] Smith J, Johnson K. Title of paper[J]. Journal of Mathematical Modeling, 2020, 15(3): 123-145.
[2] Williams R. Advanced Optimization Methods[M]. New York: Springer, 2019.
[3] Competition Official Documentation.
[4] Brown L, Davis M. Numerical Methods for Engineers[M]. Boston: MIT Press, 2018.
[5] Taylor A. Sensitivity Analysis in Optimization[J]. SIAM Journal on Optimization, 2021, 31(2): 890-912.

附录:核心 Python 实现

# 确定性复现:在 tools/ 目录下执行  python3 gen_tidy2020a.py
import gen_tidy2020a as G
D = G.gen_tidy2020a()           # SEED=2020,结果完全确定
print("样本:", D["n_total"], "违约:", D["n_default"], "正常:", D["n_normal"], "违约率:", round(D["default_rate"], 3))
print("LR 系数 w =", [round(v, 4) for v in D["lr_w"]], "截距 b =", round(D["lr_b"], 4))
print("LR 最终损失 =", round(D["lr_final_loss"], 6), " 训练AUC =", round(D["lr_train_auc"], 4), " 测试AUC =", round(D["lr_test_auc"], 4))
print("LR 测试混淆矩阵 =", D["lr_cm"], " 准确率 =", round(D["lr_acc"], 4), " 违约召回 =", round(D["lr_rec_def"], 4))