做信贷风控的朋友应该都有这种感觉:客户信息就在那儿躺着,收入、年龄、职业、历史借还款记录、征信查询次数,数据量不小,可真要拿起来判断“这个人到底会不会违约”,心里没底。过去靠人工规则和经验打分,规则多了以后互相矛盾,维护成本越来越高。后来我开始用LightGBM跑客户信息与违约表现的建模,算是把这条路彻底走通了。这篇文章就是把这套思路完整拆开讲清楚:数据怎么准备、特征怎么做、模型参数怎么调、业务上怎么落地,以及实操里最容易踩的坑。适合刚接触风控建模的算法工程师,也适合已经在用LightGBM但觉得效果还能再进一步的分析师。
1. 场景理解与建模方案设计
1.1 把业务问题翻译成机器学习问题
“客户信息及违约表现”听起来像是一堆字段和一张标签表,但落到建模之前,第一件事是先搞清楚业务定义。
违约表现不是“能不能贷”这种模糊表述,而是一个有明确时间口径的二分类目标变量。以我参与过的某消费信贷项目为例,我们把违约定义为:客户在放款后90天内出现至少一次逾期30天以上的记录。这个定义不是拍脑袋,它直接决定了模型学到的规律是否有业务价值。如果表现期拉得太长,比如用12个月的逾期来定义,样本里坏客户少得可怜,建模效果容易被极端样本带偏;如果表现期太短,比如7天内逾期就算坏,很多只是忘记还款的优质客户会被误伤,模型区分度也会下降。
选定标签之后,还要特别注意一个词:观察点。所有客户特征取数的时间点必须统一在放款之前,而标签取的是放款之后的表现。这个时间隔离关系是后面所有特征工程的基石,也是新手最容易犯错误的地方。
1.2 为什么选择LightGBM
当时我们团队对比过逻辑回归、XGBoost和LightGBM。逻辑回归的优点是解释性强、上线方便,但在变量较多且非线性关系明显时,特征工程工作量非常大,得手动做WOE变换、交叉组合,几十个特征做下来人已经疲惫了。XGBoost效果不错,可是训练速度和内存占用在千万级样本上有点吃力。LightGBM用基于直方图的决策树算法,训练速度比传统的预排序方法快了好几倍,同时支持原生类别特征、稀疏特征处理、缺失值自动处理,对于风控这种表格型的、带有大量分类变量的场景,契合度非常高。
我个人的体会是,LightGBM真正解决的是“迭代效率”问题。风控数据的分布经常因为政策或者外部环境影响而变化,模型隔段时间就得重新训练。同样一份数据,XGBoost要跑两小时,LightGBM跑十几分钟,这就意味着一天之内可以跑几十次不同特征的组合实验。模型调优本质上是一场实验游戏,跑得越快,能试的方向就越多。
1.3 方案选型背后的思考脉络
整体方案分成四个环节:数据清洗、特征工程、模型训练与调参、评估与落地。每个环节之间是严格的先后关系,但实际执行中需要反复回溯。比如跑完第一版模型发现特征重要性里有一列“最近1个月征信查询次数”排在最前面,你就要回去检查这个特征是不是存在标签泄漏的风险——它可能包含了客户在审批期间的行为,这类特征一旦用了,线上效果就会大打折扣。
LightGBM本身不要求特征做归一化或标准化,这对风控场景非常友好,因为客户收入、年龄等特征的量纲差异很大,传统模型往往要花心思处理,到了LightGBM这里,直接丢进去就行。但这也带来了另一个问题:特征的量纲差异会导致模型对某些特征过度敏感吗?答案是决策树模型通过分裂点的搜索天然规避了这一点,它不关心特征的绝对数值,只关心排序关系。
2. 数据准备与特征工程实操
2.1 原始客户信息长什么样
客户信息这个概念在风控里覆盖的范围很广。从我处理过的数据看,一般可以归纳成几类:身份属性(年龄、性别、婚姻状态、学历)、经济能力(月收入、资产状况、负债比例)、历史行为(信用卡还款记录、贷款记录、查询次数)、社交与行为数据(App使用习惯、设备型号、常驻地区)。每一类特征在违约预测中的意义不一样,身份属性和经济能力属于静态变量,历史行为则接近动态变量。
举个例子,某客户的月收入是8000元,但它的信用卡最近6个月的平均使用额度达到了7000元,这说明负债压力很大。如果只看单一收入或者单一使用额度,模型很难捕捉这种风险迹象,所以在特征工程阶段,我们特意构造了“使用额度/月收入”这种比例型特征,结果重要性排名非常靠前。
2.2 数据清洗的四个步骤
拿到原始数据之后不要急着建模,清洗是第一步。我通常按固定的顺序操作:
第一步处理重复样本。客户ID完全相同的记录只保留最新一条,注意是时间上最新的一条,而不是随便去重。第二步处理缺失值。LightGBM能容忍缺失值,但容忍不代表你可以乱来。缺失率超过60%的特征直接删除,缺失率在20%到60%之间的特征保留并用一个特殊值填充,缺失率低于20%的直接让模型自己处理。第三步处理异常值。年龄小于16或大于80的、收入是负数的,这些记录要么删除,要么用分位数截断替换。第四步做标签对齐,确保每一行样本的时间窗口一致,避免“用一个已经知道了结果的样本去训练”。
这里有一个很多人忽略的细节:用户ID本身能不能作为特征?绝对不能。像手机号、身份证号、设备ID这种标识性字段,一旦进入模型,LightGBM会把它当成超高基数的类别特征,分裂出来的规则表面上有效,实际一上线就全面失效——因为新客户的ID从未出现在训练集里,模型找不到对应关系。这类字段的唯一用途是做样本关联,而不是做特征。
2.3 特征工程:决定模型效果的上限
特征工程这块值得多花时间。LightGBM虽然号称自带“特征组合能力”,但它做的是被动的分裂组合,主动构造的、贴合业务理解的特征仍然更有优势。
数值型特征方面,我建议做三件事:一是分箱离散化,比如把月收入切成5档,让模型更容易找到非线性边界;二是比例型特征,例如负债收入比、额度使用率、近6个月平均还款金额/月收入;三是差分和统计量特征,比如最近一期账单金额相比于前3个月平均账单金额的差额。对类别特征,比如职业、行业、地区,直接用LightGBM的categorical_feature参数传入列名,模型内部会做类别展开。
时序特征在客户信息里特别重要。客户过去12个月的查询次数、近3个月内贷款申请个数、M1(逾期30天以内)出现的频率等,这些变量本质上反映的是客户近期的资金紧张程度和借款意图。我一般在建模前跑一遍单变量分析,用IV值(信息价值)筛选变量,保留IV大于0.02的字段作为候选集,既减轻模型负担,也避免无关噪声干扰训练。
2.4 一个工程上的提醒:注意训练/测试分布一致性
在风控场景里,样本往往存在时间上的趋势变化。比如某一时期为了冲业务量放宽了审批标准,导致当期整体风险上升。这种由业务策略造成的分布漂移,会让LightGBM在训练集上表现很好、在最近的新样本上效果骤降。处理办法是在划分训练集和测试集时,按时间排序后切分,而不是随机切分。不能“过去测试未来”这种穿越式的评估。
3. LightGBM训练流程与核心参数调优
3.1 数据集划分与不平衡处理
建模前需要把样本分成训练、验证和测试三个集合。比例上我用过7比1比2,也用过8比1比1,关键不是比例而是划分方式。训练集和验证集用来调参和早停,测试集留到最后,只做一次评估,防止你反复看测试集结果导致信息泄漏。
信贷违约样本天然是不平衡的,坏客户占比通常只有3%到10%。如果直接训练,模型会倾向于把所有人都预测为“好客户”,因为这样整体准确率也很高。这时候需要设定LightGBM的scale_pos_weight参数,它的计算方式通常是负样本数除以正样本数。比如好客户9万、坏客户1万,scale_pos_weight就是9,这会让模型在计算梯度时给坏客户更大的权重。
除了等权重,也可以给坏客户样本配权重列,权重列的好处是你可以结合业务实际人为设定“把坏客户抓出来的代价”更高一些。我试过把坏客户权重设为5、好客户设为1,训练出来的模型在同样的召回率下,坏客户的抓出率能提升两到三个百分点,但要注意对精确率的折损,这个权衡应该在测试集上评估。
3.2 关键参数深度解析
LightGBM的参数非常多,但实战中高频使用的核心参数也就十个左右。我整理了一张表,按重要程度排序:
| 参数 | 作用 | 调参建议 |
|---|---|---|
| learning_rate | 学习率,控制每棵树的贡献 | 常用0.01到0.1,追求精度就调小,代价是训练更久 |
| num_leaves | 每棵树的最大叶子数 | 核心复杂度参数,建议小于2^max_depth,常用31到127 |
| max_depth | 树的最大深度 | 一般配合num_leaves一起用,防止过拟合设为5到8 |
| min_child_samples | 叶子节点最小样本数 | 越大越保守,风控场景常用50到200 |
| subsample | 样本采样比例 | 0.7到0.9,降低方差 |
| colsample_bytree | 特征采样比例 | 0.5到0.9,特征多的时候很管用 |
| reg_alpha | L1正则化系数 | 默认0,特征多时可调大 |
| reg_lambda | L2正则化系数 | 常用0到5,抑制过拟合 |
| scale_pos_weight | 正负样本权重调整 | 负样本数/正样本数 |
| early_stopping_rounds | 早停轮数 | 验证集指标n轮不提升就停止 |
这些参数之间存在很强的联动关系。比如num_leaves设得很大而max_depth又很浅,叶子数的上限其实被max_depth限制住了,实际不起作用。我习惯的调整顺序是:先固定一个较小的learning_rate,然后调num_leaves和max_depth,确定模型复杂度;接着调subsample和colsample_bytree,观察验证集AUC变化;最后再动正则化系数。
3.3 训练流程实测记录
下面这段代码是我在一份千万级客户样本上实际跑过的流程骨架,可以做参考。
import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score, classification_report # 假设 data 已清洗并完成特征工程 feature_cols = [col for col in train_data.columns if col not in ['customer_id', 'label']] X = train_data[feature_cols] y = train_data['label'] # 按时间顺序切分,避免随机切分造成时序污染 # 假设数据已有 order_ts 字段表示观测时间点 split_idx = int(len(X) * 0.8) X_train, X_val = X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_val = y.iloc[:split_idx], y.iloc[split_idx:] # 统计正负样本比例 neg_count = (y_train == 0).sum() pos_count = (y_train == 1).sum() params = { 'objective': 'binary', 'metric': 'auc', 'learning_rate': 0.05, 'num_leaves': 63, 'max_depth': 7, 'min_child_samples': 80, 'subsample': 0.8, 'colsample_bytree': 0.7, 'reg_lambda': 2.0, 'scale_pos_weight': neg_count / pos_count, 'verbose': -1, 'seed': 42 } d_train = lgb.Dataset(X_train, label=y_train) d_val = lgb.Dataset(X_val, label=y_val) model = lgb.train( params, d_train, num_boost_round=2000, valid_sets=[d_val], callbacks=[lgb.early_stopping(stopping_rounds=100), lgb.log_evaluation(200)] ) # 输出特征重要性 importance = pd.Series(model.feature_importance('gain'), index=feature_cols).sort_values(ascending=False) print(importance.head(20))实际操作中,我遇到过一次很有趣的情况:同样的参数,把subsample从0.8调整到0.6之后,验证集AUC反而从0.86降到了0.85,这让我一度怀疑参数设置有问题。后来分析了指标曲线才发现,0.6的采样比例让每一棵树看到的样本都更少,模型的方差降了,但偏差也升上来了,在树的数量不够多的情况下表现反而变差。所以在调参时,每一次只动一个参数,并且记录完整日志,不要为了调参而调参。
3.4 类别特征的传入细节
LightGBM支持原生类别特征,但是传入方式有讲究。如果特征列里有字符串,它会自动当作类别;如果是整数编码的类别,你需要显式在参数里指定。比如:
categorical_features = ['industry_code', 'region_code'] model = lgb.train(params, d_train, categorical_feature=categorical_features)如果忘了指定整数编码的类别列,LightGBM会把它当成数值型连续变量,导致模型在“行业代码=37”和“行业代码=73”之间做无意义的毛估。还要注意类别编码需要保持一致,训练集和测试集里如果出现了新的类别,可以用一个特殊编码处理,否则模型预测阶段会对没见过的新值报错。
4. 效果评估与业务落地
4.1 指标选择不能只看AUC
AUC是风控建模里最常被提起的评价指标,但它不是唯一标准,甚至不是最优标准。AUC衡量的是整体排序能力,可以理解为随机抽一个正样本和一个负样本,模型给出的分数能把正样本排在负样本前面的概率。但信贷风控真正关心的是在某个得分阈值之下,能不能把足够多的坏客户拦在外面,同时不误伤太多好客户。
我更看重KS值(Kolmogorov-Smirnov statistic),它是正负样本累计分布曲线之间的最大距离,反映了模型区分好坏客户的能力。一个合格的风控模型KS值通常在0.3以上,0.4到0.5算相当优秀。计算方式是把所有样本按模型预测分数从低到高排序,按每十分位累计统计好坏客户比例,然后找到最大差值。LightGBM训练过程打印的metric里可以直接加ks,也可以自己写代码算。
除了KS,还可以看每十分位的坏账密度。把预测分数分成10档,从最低分档到最高分档,统计每一档里坏客户的占比。好的模型应该是分数最低的档里坏账率最高,分数最高的档里坏账率趋近于零。如果出现中间某个档位坏账率反转,说明模型某些区间内的排序能力不足,需要检查特征或者换模型结构。
4.2 分数阈值选择与业务成本
模型输出的原始预测概率是一个从0到1的值,但业务决策需要一个阈值,分数高于阈值通过,低于则拒绝。这个阈值不应该是0.5,因为坏样本占比本来就低,0.5的阈值会导致坏客户抓出率极低。
我一般是画一条“通过率 vs 坏账率”曲线,让业务方给出可接受的坏账上限,然后倒推阈值。举个例子,如果全年坏账率容忍上限是3%,在验证集上通过率在85%时坏账率恰好是2.8%,那就把阈值定在对应的分数点上。这个方法看似简单,但比单纯追求AUC有意义得多,因为AUC更高的模型不代表它在目标通过率下坏账率就更低,必须要实测。
还有一种进阶做法是引入阈值滑动的成本矩阵。假设审核一个好客户的成本是50元,拒绝一个好客户损失的是潜在收益200元,放掉一个坏客户的成本是1000元,基于这些数字可以算出每个得分区间的最优决策边界。LightGBM的预测分数天然就是连续的,配合成本矩阵做决策,比简单设一个阈值要精细得多。
4.3 从LightGBM分数到评分卡
很多信贷机构上线模型时并不能直接使用LightGBM输出的浮点概率,合规和解释性要求往往需要把分数映射成整数信用评分。映射公式一般是:
score = offset + factor * log(odds)
其中odds是违约与不违约的比率,由LightGBM的预测概率经过转换得到:p = 1 / (1 + exp(-raw_score))。实际操作用的是LightGBM输出的leaf index和权重组合得到的边际原始分数,然后用一个线性变换映射到300到850的整数区间。
这一步要注意的是映射过程中要保留单调性:原始概率越高,映射后的分数必须越高。比例因子和偏移量需要提前设定好,比如“当违约概率为1%时,分数为600;违约概率每翻一倍,分数降低50”。有了这套映射,业务人员拿着分数可以直接解释客户的信用等级,而不是对着一个0.87的浮点数发呆。
4.4 线上部署与监控要点
模型训练完只是开始,上线之后要盯的事情反而更多。第一是特征监控,每条线请求进来,线上特征分布和训练集分布的一致性要实时统计,一旦发现某个特征的空值率或者分位数出现明显偏移,就要触发预警。常用指标是PSI(群体稳定性指标),PSI大于0.1说明特征有轻微漂移,大于0.25就说明分布发生了显著变化。第二是分数监控,模型输出的分数分布如果整体右移或者左移,说明客群结构变了或者评分逻辑失效了。第三是线下线上一致性比对,用同一批样本分别走离线特征流程和在线推理流程,比对分数差值,差值过大通常说明线上特征代码和离线版本没对齐。
我在一次项目里就吃到过线上线不一致的亏。特征是某大数据服务商提供的评分结果,离线的时候用的是T日的数据文件,上线之后却因为数据接口晚了一天,导致线上特征实际是T-1日的。虽然只是24小时的差,但对一些时效性很强的变量来说,差异足以让模型区分度从0.38降到0.29。
5. 常见问题排查与避坑实录
5.1 问题速查表
项目做了多了之后,我把常见问题整理成了一张速查表,平时排障照着查,能省很多时间:
| 问题 | 可能原因 | 排查思路 |
|---|---|---|
| 训练集AUC超高(0.99)但测试集很低 | 特征穿越或标签泄漏 | 检查每个特征的取数时间点是否晚于观察点 |
| 模型几乎把所有样本判为好客户 | 正负样本不平衡未处理或阈值过高 | 检查scale_pos_weight,用验证集重新寻找阈值 |
| 特征重要性第一名是ID类字段 | 关联字段被误当成特征 | 删除ID类字段 |
| 测试集与训练集分数分布差异大 | 时间窗口漂移或客群政策变化 | 计算各特征PSI,定位漂移源头 |
| 模型上线后收益率持续下降 | 线上特征与离线特征不一致 | 抽取样本比对线上线下特征值 |
| 预测概率全部集中在0.1以下 | 缺少数值重要特征或树数量过多 | 检查特征列表,减少num_boost_round |
5.2 踩过最有代表性的三个坑
第一个坑是标签穿越。刚开始做建模时,我把客户的“历史逾期次数”直接作为一个特征,没有确认这个数据包含的逾期是发生在观察点之前还是之后。LightGBM训练完,验证集AUC高达0.93,团队成员都很兴奋。后来复查数据才发现,样本表里这一列其实是截至当前时间的最新逾期次数,包含了跟随标签周期一起发生的逾期。换句话说,模型学会了“如果一个客户被标记为逾期,那么他的逾期次数一定大于0”,相当于用了一部分答案去猜答案。这一轮模型直接作废,之后所有特征的时间口径全部要经过代码审计。
第二个坑是类别特征编码不一致。训练集里行业代码是0到99的整数编码,线上推理时新客户来了一个104的未知编码。模型在构造树的时候没有见过这个值,特征分裂直接走了默认分支,分数被推向极端。后来我统一改成了哈希编码加转成字符串类别的方式处理,再遇到新值,模型也能平稳应对。
第三个坑是评估阶段用随机切分而不是时间切分。随机切分的问题在于,它让训练集和测试集的客群分布高度同构,模型在测试集上的表现是被高估的。真实场景里,模型上线后面对的是未来的客群,而未来的客群受信贷政策、宏观经济等因素影响,和过去并不完全一样。我后来坚持按月份切分样本,评估的时候特别留意最后三个月的表现,宁可分数低一点,也要保证它在线上的真实表现和离线评估差距不要太大。
5.3 深度解析:类别不平衡下的策略误区
很多新手看到好客户占比90%、坏客户占比10%,第一反应是用过采样算法(比如SMOTE)把坏客户样本复制几份。这个做法在树模型上其实意义不大,因为决策树的分裂本质上是找分裂点和区间,复制样本并不会带来新的决策信息,只是让同一批样本的权重变大了。LightGBM本身支持的scale_pos_weight和自定义样本权重就已经能解决这个问题,根本不需要SMOTE。
但权重调太大也会带来副作用。一次实验里我把scale_pos_weight从10调成20后,训练集上坏客户的召回率从0.3涨到了0.55,但好客户的误杀率也从0.05涨到了0.18。线上算一笔账,多抓出来的坏客户造成的亏损减少,远不及被误杀的好客户带来的利润损失。所以类别不平衡的调整方向和幅度,一定要结合业务成本来定,不能只盯着召回率这个数字。
5.4 一个辅助提效技巧:特征重要性的二次利用
LightGBM的特征重要性直接来自分裂增益的总和,可以用它来做特征迭代的加速器。第一版模型跑完,把重要性排名后50位的特征全部去掉,重新训练一次。通常模型AUC不会下降太多,甚至可能提升。原因是树模型在大量无关特征存在时,分裂时会分散选择一些没有预测力的特征,削弱了主特征的分裂次数。等到特征数量精简到合理范围后,再回头审视一下重要性靠前的特征,思考它们背后代表的业务逻辑,如果逻辑成立,就继续保留;如果逻辑说不通,就要小心是不是踩了泄漏的坑。
这个方法的额外好处是,给业务方解释模型时非常方便。你可以直接说:我们模型的判断依据主要是近3个月负债变化、历史还款行为和申请查询密度这几个维度,听起来可信度远高于“我们有一套复杂的机器学习模型”。
我个人在实际操作中的体会是,LightGBM本身并不难上手,真正拉开建模水平差距的,是建模之前对业务的理解深度和对数据的敬畏程度。客户信息及违约表现这个项目看着简单,但把时间口径、标签定义、特征分布这些基础工作做扎实了,模型效果自然就上去了。哪怕最后没有用上复杂的技巧,只要把这几条基本线守住,跑出来的模型也一定不会差。以后再遇到类似的信用风险建模项目,我会先问自己三个问题:标签定义有没有歧义?特征是否严格早于观察点?客群分布变化有没有监控?这三个问题没有解决好,用什么模型都白搭。