全文链接:tecdat.cn/?p=45303
原文出处:拓端数据部落公众号
关于分析师
在此对 Yushi Wang 对本文所作的贡献表示诚挚感谢。她来自西南财经大学,主攻数据科学与大数据技术专业。她精通 Python、Stata、Origin、Tableau 等分析工具,在数据采集、数据可视化、机器学习、自然语言处理和深度学习等领域有深入研究。她始终关注前沿技术如何落地于实际业务场景。
Yushi Wang 曾参与多个金融科技领域的咨询项目,专注于利用数据驱动的方法解决信贷风控中的“冷启动”和样本稀缺问题。她的实践经验表明,将深刻的业务理解与精巧的特征工程相结合,往往比单纯追求模型复杂度能带来更稳健、更具可解释性的商业价值。
引言
在金融科技浪潮中,新产品迭代加速,但历史数据的缺失让风控模型陷入“巧妇难为无米之炊”的窘境——违约样本稀疏、分布偏移严重,传统依赖大样本的建模范式频频失效。这好比用旧地图探索新大陆,结果自然不尽人意。
本文内容改编自过往客户咨询项目的技术沉淀并且已通过实际业务校验,该项目完整代码与数据已分享至交流社群。阅读原文进群获取完整代码数据及更多最新AI见解和行业洞察,可与900+行业人士交流成长;还提供人工答疑,拆解核心原理、代码逻辑与业务适配思路,帮大家既懂怎么做,也懂为什么这么做;遇代码运行问题,更能享24小时调试支持。
我们的团队曾协助客户上线新产品,面对仅500条训练样本、违约率仅2%的脱敏数据集,我们没有盲目堆砌复杂模型,而是从业务理解出发,通过结构化特征工程与审慎的模型选择,最终构建出一个高召回、高鲁棒且易于解释的逻辑回归模型。本文完整还原这一过程,希望能为同行提供借鉴。整体思路可概括为下图:
图1 文章结构图
相关文章
DeepSeek、LangGraph和Python融合LSTM、RF、XGBoost、LR多模型预测NFLX股票涨跌|附完整代码数据
原文链接:tecdat.cn/?p=44060
项目背景与核心挑战
信贷产品的快速迭代使得新业务上线初期缺乏足够风险暴露窗口,形成典型的“冷启动”场景。此次任务中,我们面对的是一个包含22个脱敏特征的数据集:训练集仅500条样本,违约率2%(约10个正样本);测试集则多达2500条,且违约率和特征分布未知。核心挑战有三:
- 样本极度稀疏与不平衡:如何在仅10个违约样本上学会精准识别风险?
- 数据分布偏移:如何确保训练集规律能有效迁移到分布不同的测试集?
- 强可解释性需求:信贷决策必须透明,模型要能回答“为什么拒绝”。
数据深潜:洞察是一切决策的起点
动手建模前,我们花了大量时间进行探索性数据分析。对训练集各字段的描述性统计显示,多数数值型连续变量呈现偏态分布且存在极端值,这会干扰模型学习。
图2 数值型连续变量分布
相关性热力图显示原始特征间线性关系较弱,信息重叠度低,但也提示我们预测信号较为分散,需要更精细的特征工程。
图3 特征相关性热力图
最关键的发现来自训练集与测试集的分布对比。通过分类型可视化,我们确认了部分特征存在明显分布偏移。
图4 数值型连续变量分布对比
图5 数值型离散变量分布对比
图6 分类变量分布对比
这些洞察为后续特征工程指明了方向:必须设计一套能同时应对偏态、异常值和分布偏移的稳健处理方案。
特征工程:化腐朽为神奇的“炼金术”
在样本量小的前提下,特征工程的质量直接决定了模型性能的上限。我们的策略是:根据变量类型“因材施教”,所有变换参数均从训练集学习并冻结,再统一应用于测试集,以最大限度模拟真实世界的数据生成过程,缓解分布偏移。
变量分类与基础变换
我们将22维特征分为三类:
- 数值型连续变量:如
income、credict_limit。先进行1%-99%的缩尾处理抑制极端值,再取对数变换使其分布接近正态,最后用标准化统一量纲。 - 数值型离散变量:如
overdue_times、default_times。保留原始值的同时,新增零值标识特征(_is_zero),突出“是否有过逾期”这一强信号。 - 分类型变量:如
housing、purpose。housing类别少,使用独热编码;purpose多达13类,为避免维度灾难,采用目标编码。
以下是我们最终确定的特征工程流水线核心代码(已做简化与变量名修改):
# 导入必要的库
import pandas as pd
import numpy as np
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.preprocessing import StandardScaler, OneHotEncoder
import warnings
warnings.filterwarnings('ignore')
# 定义一个基于训练集进行拟合和转换的预处理器
class CustomPreprocessor(BaseEstimator, TransformerMixin):
def __init__(self, cont_features, disc_features, cat_features, winsor_range=(0.01, 0.99)):
# 初始化时定义变量类型和缩尾范围
self.cont_features = cont_features
self.disc_features = disc_features
self.cat_features = cat_features
self.winsor_range = winsor_range
# 以下字典将在fit时被填充
self.winsor_bounds = {}
self.scaler = None
self.encoder_ohe = None
def fit(self, X, y=None):
df = X.copy()
# 1. 缺失值处理 (代码有省略...)
# 根据训练集计算连续变量的缩尾边界
for col in self.cont_features:
if col in df.columns:
lower_bound = df[col].quantile(self.winsor_range[0])
upper_bound = df[col].quantile(self.winsor_range[1])
self.winsor_bounds[col] = (lower_bound, upper_bound)
# 2. 为连续变量的标准化做准备 (代码有省略...)
# 3. 为类别变量的独热编码做准备
if self.cat_features:
cat_data = df[[col for col in self.cat_features if col in df.columns]].astype(str)
self.encoder_ohe = OneHotEncoder(handle_unknown='ignore', sparse=False, dtype=np.float32)
self.encoder_ohe.fit(cat_data)
return self
def transform(self, X):
df = X.copy()
# 对连续变量应用缩尾、对数变换和标准化 (代码有省略...)
# 对离散变量添加零值标识 (代码有省略...)
# 对类别变量进行独热编码转换
if self.encoder_ohe and self.cat_features:
cat_cols_present = [col for col in self.cat_features if col in df.columns]
if cat_cols_present:
cat_data_transformed = self.encoder_ohe.transform(df[cat_cols_present].astype(str))
ohe_feature_names = self.encoder_ohe.get_feature_names_out(cat_cols_present)
ohe_df = pd.DataFrame(cat_data_transformed, columns=ohe_feature_names, index=df.index)
df = pd.concat([df, ohe_df], axis=1)
# 可选择删除原始类别列
return df
阅读原文进群获取完整内容及更多AI见解、行业洞察,与900+行业人士交流成长。
创新点:WOE风险编码与零值标识
除常规处理外,我们还尝试了风控领域的经典方法:WOE(Weight of Evidence,证据权重)编码。它将类别变量的每个取值映射为一个风险权值,使变量具有风险单调性,非常适合逻辑回归这类线性模型。
WOE=ln(好样本占比坏样本占比)
同时,我们计算IV(Information Value,信息价值)来量化每个特征的预测能力。这不仅是特征筛选的科学依据,也为我们理解业务风险提供了视角。
对于离散变量,我们增加了零值标识特征。例如,对 overdue_times 除了保留原值,还增加一列 overdue_times_is_zero,告诉模型“是否曾经逾期”这一信号可能比“逾期几次”在某些场景下更重要。
样本不平衡处理的探索与抉择
面对极度的样本不平衡,我们尝试了多种方法:
- SMOTE与ADASYN:这两种经典的合成采样算法在验证时表现尚可,但在测试集上泛化能力急剧下降。分析认为,在样本极度稀疏时,合成样本放大了噪声。
- AIGC样本生成:我们曾尝试利用生成式AI模拟高违约率样本,以抬升训练集的风险分布,使其更接近可能的测试集。但实验发现这会导致模型在公榜上过拟合,因此最终未采用。
最终,我们回归到最稳健的代价敏感学习。通过在逻辑回归中设置 class_weight='balanced',让模型在训练时更加关注极少数违约样本的识别错误,直接模拟了业务中“将坏人误判为好人的代价远高于将好人误判为坏人”的非对称损失。
模型选择与评估:少即是多
我们最初设想采用“线性模型+树模型”的融合策略,期望兼顾线性关系的稳健性和非线性关系的预测力。我们尝试了逻辑回归(LR)与随机森林(RF)、XGBoost、LightGBM、CatBoost的各种组合与加权融合。经过大量的交叉验证实验,一个意想不到但极具启发性的结果出现了:单一的逻辑回归模型表现最优。
分析原因,这并非偶然。在样本极度稀疏(仅10个正样本)的情况下,复杂的树模型极易捕捉到噪声中的偶然模式,导致在训练集上完美拟合,但在分布偏移的测试集上严重过拟合。而逻辑回归因其简单的线性决策边界和内置的正则化特性,天然具有更强的抗噪声能力和泛化性。这也与经典的信贷评分卡模型(本质是逻辑回归)的业务逻辑相吻合——风险因子对违约概率的影响大多是单调且可叠加的。
确定了模型后,我们使用网格搜索(GridSearchCV)对逻辑回归的正则化强度 C 和求解器 solver 等参数进行优化。
# 从sklearn导入所需模块
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
# 假设 X_train_final, y_train 已经过最终版特征工程处理
# 定义参数搜索网格
param_grid_lr = {
'penalty': ['l1', 'l2'], # 正则化类型
'C': [0.01, 0.05, 0.1, 0.5, 1, 5, 10], # 正则化强度的倒数
'solver': ['liblinear', 'saga'], # 适用于l1和l2的求解器
'class_weight': [None, 'balanced'] # 是否使用代价敏感学习
}
# 初始化逻辑回归模型
base_lr_model = LogisticRegression(random_state=42, max_iter=1000)
# 初始化网格搜索,使用AUC作为评分标准,进行5折交叉验证
grid_search_lr = GridSearchCV(
estimator=base_lr_model,
param_grid=param_grid_lr,
scoring='roc_auc',
cv=5,
n_jobs=-1,
verbose=1
)
# 在训练数据上执行网格搜索
grid_search_lr.fit(X_train_final, y_train)
# 输出最佳参数
print("最佳参数组合:", grid_search_lr.best_params_)
# 获取最佳模型
best_lr_model = grid_search_lr.best_estimator_
# ... (后续使用 best_lr_model 进行预测和评估)
为了最终评估模型的稳健性,我们采用3折分层交叉验证,在训练集上计算Out-Of-Fold(OOF)的预测概率,并据此寻找最佳分类阈值。
# 从sklearn导入评估和交叉验证工具
from sklearn.model_selection import StratifiedKFold, cross_val_predict
from sklearn.metrics import roc_auc_score, precision_recall_curve, f1_score, accuracy_score
import os
# 初始化逻辑回归模型(使用交叉验证时用默认参数即可)
lr_cv_model = LogisticRegression(solver='liblinear', max_iter=1000)
# 初始化分层K折
skf = StratifiedKFold(n_splits=3, shuffle=True, random_state=42)
# 获取OOF概率预测
oof_pred_proba = cross_val_predict(
lr_cv_model, X_train_final, y_train,
cv=skf, method='predict_proba', n_jobs=-1
)[:, 1]
# 计算OOF AUC
oof_auc = roc_auc_score(y_train, oof_pred_proba)
print(f'3折交叉验证 AUC: {oof_auc:.6f}')
# 基于OOF概率和PR曲线寻找最佳阈值 (最大化F1)
precisions, recalls, thresholds = precision_recall_curve(y_train, oof_pred_proba)
f1_scores = 2 * precisions[:-1] * recalls[:-1] / (precisions[:-1] + recalls[:-1] + 1e-12)
best_idx = np.nanargmax(f1_scores)
best_thresh = thresholds[best_idx]
best_f1 = f1_scores[best_idx]
print(f'最佳阈值: {best_thresh:.6f}, 对应F1: {best_f1:.6f}')
# 保存评估结果 (代码有省略...)
特征工程有效性验证
为验证特征工程处理的有效性,我们对比了处理前后特征与目标变量的相关性变化。
图7 连续变量与target相关性对比
可以看到,处理后大部分特征与target的相关性有所提升,其中 income、last_credit_card_months 变化尤为明显。
我们进一步观察了连续变量峰度和偏度的变化。峰度图显示,处理后各变量峰度均向正态分布的理论值3靠近,说明异常值得到了有效抑制。
图8 连续变量峰度对比
偏度图则显示,处理后变量偏度基本落在(-1,1)区间,接近对称分布,消除了原始偏态对模型的影响。
图9 连续变量偏度对比
处理后的各类型特征与target的相关性热力图也证实了特征工程的成效。
图10 处理后数值型连续变量与target的相关关系
图11 处理后数值型离散变量与target的相关关系
图12 处理后分类变量与target的相关关系
模型评估结果
最终模型在私榜上取得了0.601的AUC,更重要的是,召回率(Recall)达到了0.8以上,这意味着模型成功预警了超过80%的真实违约客户,完美实现了我们“宁可错杀,不可放过”的业务目标。
| 指标 | 训练集 (2%违约) | 验证集 (10%违约) | 私榜 (20%违约) | 业务解读 |
|---|---|---|---|---|
| AUC-ROC | 0.60201 | 0.62512 | 0.60119 | 模型整体区分度优秀,泛化能力良好 |
| 召回率 (Recall) | 0.6 | - | - | 真实违约客户中超八成被成功预警 |
| 精确率 (Precision) | 0.3 | - | - | 随违约率提升而提高,高风险场景适用性增强 |
| F1-Score | 0.4 | - | - | 综合性能稳健 |
结论与展望
这个项目的实践再次印证了一个道理:在数据约束条件下,对业务逻辑的深刻理解与精细化特征工程,远比盲目追求复杂模型更为重要。我们通过一套“结构化特征工程+代价敏感学习+逻辑回归”的组合拳,成功应对了信贷“冷启动”场景下的样本稀疏与分布偏移挑战,构建了一个兼具高召回、高鲁棒性和高可解释性的实用风控模型。
这并非技术的倒退,而是基于现实约束的理性选择。它为从零到一构建风控体系提供了一个坚实、可靠的基线。未来,随着业务数据的积累,我们可以在这个基线上,逐步引入更复杂的模型和动态更新机制,实现风控能力的持续进化。
阅读原文进群获取完整内容及更多AI见解、行业洞察,与900+行业人士交流成长。