ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

评分卡特征工程:分箱与WOE编码实战指南

评分卡特征工程:分箱与WOE编码实战指南 前两篇把字段级的脏活干完了缺失值、异常值、格式统一、重复样本都处理得差不多今天这篇part.3要聊的是评分卡建模里最有标志性的预处理环节——分箱与WOE编码。很多新手在这里容易卡住原因在于它不像普通的机器学习预处理那样标准化就完事它带着很强的业务逻辑和风控经验在里面。如果你已经准备好动手建一张评分卡这一篇值得认真读完。先说清楚这篇要解决什么问题。普通的机器学习项目里连续特征最常见的预处理方式是标准化或者归一化然后直接丢给模型。但评分卡模型不一样它底层的算法通常是逻辑回归逻辑回归本身是一个线性模型如果直接把年龄、收入这种连续变量扔进去模型学到的是一条线性关系——年龄每增加一岁违约概率线性变化。可真实业务里年龄和违约率往往不是线性关系典型的是U型或者倒U型年轻人和老年人违约率都偏高中年人反而稳定。直接用原始值建模模型很难捕捉这种非线性而且一旦某个极端值特别离谱回归系数会被带偏得很厉害。分箱加WOE编码就是为了解决这些问题把连续变量离散化成分箱再用WOE编码把每一箱的风险水平量化出来这样进入模型的特征既光滑又带着业务含义。如果你把评分卡的完整流程走一遍顺序大概是数据清洗、字段筛选、分箱、WOE编码、IV值筛选、逻辑回归训练、模型评估、评分映射。前两篇讲的是数据清洗part.3要动的就是“分箱、WOE编码、IV值筛选”这一段属于特征工程里最关键的一公里。这篇文章我打算把分箱的几种主流方法、WOE和IV的计算逻辑、完整的Python实现以及我在实际项目中踩过的坑一次性讲清楚。1. 先把part.3的定位说清楚分箱与WOE编码在整个评分卡流程里的位置1.1 为什么评分卡不能像普通机器学习那样直接标准化很多从普通机器学习转过来做评分卡的人第一反应是“还用这么麻烦吗直接StandardScaler不就行了”。我一开始也这么干过后来被现实教育了。标准化确实能解决量纲问题但它解决不了三个更本质的问题。第一个是业务可解释性。评分卡最终要面对的不是机器学习工程师而是业务部门、风控审批人员和监管机构。你跟他们说“这个客户的收入标准化后是1.37”没人听得懂。你说“这个客户收入在1万到2万区间这个区间的坏账率是3.2%对应的分数段是这里”大家都明白。分箱天然自带业务含义这是原始连续数值做不到的。第二个是异常值鲁棒性。逻辑回归对极端值非常敏感一个收入1000万的样本能把整个特征的系数带偏。分箱之后极端值会被单独分到一个箱里它的影响力被限制在一个箱子内部不再干扰其他样本的拟合。这个特性在实际业务里太重要了信贷数据里收入、负债这种长尾分布极其常见。第三个是非线性关系的捕捉。前面说的年龄和违约率的U型关系分箱之后模型可以给每个年龄段一个独立的WOE值相当于以分段常数的形式逼近任意形状的函数关系。这比强行用线性拟合要灵活得多。1.2 从分箱到WOE再到IV的完整逻辑链条整个评分卡预处理的逻辑链条是这样的先分箱把连续变量或离散变量划分成若干区间再计算每个箱子的WOE值衡量这个箱子相对于整体平均水平是好是坏然后通过每个箱子的WOE和样本分布计算IV值评估这个变量整体的预测能力最后用IV值筛选特征把预测能力强的变量留下来进入模型。这个链条中分箱是基础分箱质量直接决定WOE的质量WOE的质量又决定IV值能否真实反映变量预测力。所以别急着跳到WOE计算分箱这步要舍得花时间。我见过不少同学直接拿pd.qcut分个箱就开始算WOE结果分出来的箱单调性一塌糊涂IV值也虚高这种特征进了模型后期的稳定性一定出问题。还有一点要提前说WOE编码之后特征和标签的关系被强制变成了线性关系这也是逻辑回归能发挥效力的前提。逻辑回归本身是个线性分类器它拟合的是特征与对数几率的线性关系。如果原始特征和标签是对数线性关系逻辑回归自然表现好如果不是就需要靠WOE编码来“掰直”。所以整条链路的最终目的就是把任意形态的关系转化成一个逻辑回归能高效建模的形态。2. 从等距到卡方评分卡分箱的3种主流方法实战2.1 等距分箱和等频分箱简单但够用先聊最简单的两种等距分箱和等频分箱。等距分箱就是按数值范围切成等宽的区间比如年龄0到100岁每10岁一个箱一共10箱。用pandas的话一行代码import pandas as pd # 等距分箱年龄按20到60每10岁一个箱 age_bins pd.cut(df[age], bins[18, 25, 35, 45, 55, 80], rightFalse)等频分箱是按样本量切分每个箱子里的样本数量大致相等。用的是pd.qcut# 等频分箱按收入分成5箱每箱约20%样本 income_bins pd.qcut(df[income], q5, duplicatesdrop)这两种方法的好处是快、简单、无监督不需要标签。但问题也很明显分箱完全没考虑和目标变量的关系切出来的箱子好坏样本分布可能很不均匀甚至出现“坏样本集中在一个箱、其他箱几乎全好”的情况。比如等距分箱年龄20到30岁坏账率3%30到40岁坏账率1.5%40到50岁坏账率2%这种来回跳的趋势WOE编码之后单调性很差对逻辑回归不友好。所以这两种方法在我实际项目中主要用于两种场景一是初筛阶段快速看变量的趋势走向二是作为有监督分箱方法的初始分箱。真正建模用的分箱我一般会用下面两种有监督的方法。2.2 决策树分箱借力sklearn自动找最优切分点决策树分箱是风控建模里非常实用的一种方法。原理一句话用单个特征去拟合目标变量训练一个深度受限的决策树树的分裂点就是候选的好箱子边界。决策树每一次分裂都在找能够让目标变量区分度最大的切分点这正好就是我们分箱想要的效果。具体实现也很简单import numpy as np from sklearn.tree import DecisionTreeClassifier def tree_binning(X, y, max_leaf_nodes5, min_samples_leaf0.05): 决策树分箱返回箱子边界 X: 一维特征需要reshape成二维 y: 二分类标签0-好客户 1-坏客户 model DecisionTreeClassifier( max_leaf_nodesmax_leaf_nodes, min_samples_leafmin_samples_leaf, random_state42 ) model.fit(np.asarray(X).reshape(-1, 1), y) # 提取树的分裂阈值 thresholds [] tree model.tree_ for i in range(tree.node_count): if tree.children_left[i] ! tree.children_right[i]: # 非叶子节点 thresholds.append(tree.threshold[i]) thresholds sorted(set([round(t, 4) for t in thresholds if t ! -2])) bins [-np.inf] thresholds [np.inf] return bins, model # 使用示例 bins, model tree_binning(df[age], df[is_bad], max_leaf_nodes5) df[age_bin] pd.cut(df[age], binsbins)这里的max_leaf_nodes和min_samples_leaf是两个需要调的关键参数。max_leaf_nodes控制最终分多少个箱min_samples_leaf是叶子节点的最小样本占比用来防止某些箱样本太少。我一般习惯把min_samples_leaf设成0.05也就是说每个箱至少要包含5%的样本这样可以保证后面算WOE时每个箱的统计量不至于太脆弱。决策树分箱跑起来要注意一个细节树对缺失值有自己的一套处理逻辑如果你的数据还没有处理缺失值可以先填充再分箱或者单独把缺失值作为一个特殊的箱。评分卡里更常见的做法是让缺失值单独成一箱这样模型可以自动学到“缺失本身就是一种风险信号”——某些场景下收入缺失确实比任何收入数值都更能反映客户的资质问题。2.3 卡方分箱有监督分箱里的经典方案卡方分箱是评分卡教科书里绕不开的一个方法。它的思路是初始时把每个不同的值都当成一个独立的箱然后反复合并相邻的两个箱子合并的准则是卡方检验的p值最大也就是说两个箱子的好坏样本分布最相似可以合并成一个。重复这个过程直到箱子数量达到预设值。卡方分箱没有sklearn内置实现需要自己写。这里我给出一个简化但可用的版本import pandas as pd import numpy as np from scipy.stats import chi2_contingency def chi_merge(X, y, max_groups5, min_samples_leaf0.05): 卡方分箱 X: 一维连续特征 y: 二分类标签 df pd.DataFrame({X: X, y: y}) # 先做等频初始分箱避免每个值一个箱导致卡方计算太慢 try: df[bin] pd.qcut(df[X], q100, duplicatesdrop) except Exception: df[bin] pd.cut(df[X], bins20) # 统计每个初始箱的好坏样本数 grouped df.groupby(bin, observedTrue)[y].agg([sum, count]) grouped[bad] grouped[sum].astype(int) grouped[good] (grouped[count] - grouped[bad]).astype(int) while len(grouped) max_groups: # 对相邻两箱计算卡方统计量 chi2_values [] for i in range(len(grouped) - 1): table np.array([ [grouped.iloc[i][good], grouped.iloc[i][bad]], [grouped.iloc[i1][good], grouped.iloc[i1][bad]] ]) # 如果某箱好或坏为0卡方值设为无穷大避免除零 if table.min() 0: chi2_values.append(np.inf) else: chi2, p, _, _ chi2_contingency(table, correctionFalse) chi2_values.append(chi2) # 合并卡方值最小的相邻两箱 min_idx np.argmin(chi2_values) idx_to_merge grouped.index[min_idx:min_idx2] merged_bad grouped.loc[idx_to_merge, bad].sum() merged_good grouped.loc[idx_to_merge, good].sum() # 构造新的一行替换原来的两行 new_row pd.DataFrame([{bad: merged_bad, good: merged_good}], index[fmerged_{min_idx}]) grouped pd.concat([ grouped.drop(indexidx_to_merge), new_row ]).sort_index() # 提取实际箱边界取每个箱的X最小值 boundaries [] for bin_interval in grouped.index: if hasattr(bin_interval, left): boundaries.append(bin_interval.left) boundaries sorted(set(boundaries)) bins [-np.inf] boundaries[1:] [np.inf] return bins这段代码重点关注两件事第一初始分箱用了等频100箱一定要先做初始分箱否则对连续值每个唯一值当一箱计算量会非常恐怖1000万条样本的连续特征可能有几百万个唯 一值卡方合并会跑死。第二当某个箱的好样本或坏样本数量为0时把卡方值设为无穷大这样它们永远不会被合并避免出现WOE为无穷的极端情况。卡方分箱的优点是能自动找到和目标变量区分度高的切分点比盲目等频更科学但它也有一个缺点它只考虑局部相邻箱的差异不保证最终结果单调。所以卡方分箱跑完以后一定要检查每个箱的坏样本率是否单调递增或递减如果不单调就手动合并相邻的箱或者调整max_groups参数重新跑。2.4 三种分箱方法怎么选我的经验备忘分箱方法优点缺点适用场景等距分箱简单快速无需标签不关注样本分布和目标变量初期探索、快速看趋势等频分箱每箱样本量均衡可能把数值跨度很大的样本分到一箱作为有监督分箱的初始箱决策树分箱自动找最优切分点速度快切分点可能过拟合需限制深度大多数连续变量首选卡方分箱经典有监督方法可解释性强计算量大不保证单调对分箱方式有审计要求的场景实际项目中我的默认组合是先用决策树分箱处理大多数连续变量效率高、效果稳定对需要向监管解释分箱依据的重要因子再用卡方分箱做一版两个结果对比验证等距和等频只用来做探索性分析。当然这个组合不是绝对的样本量小的数据集决策树很容易过拟合这种情况下卡方分箱更稳妥。总之分箱的核心目标是在“信息损失小”和“每箱样本量足够”之间找平衡别一味追求箱数多箱数越多过拟合风险越高。3. WOE编码与IV值计算从公式到Python实现3.1 WOE公式和业务含义正负号千万别搞反分箱完成后下一步就是把每个箱转成WOE值。WOE全称Weight of Evidence直译是“证据权重”。它的公式长这样WOE_i ln(坏样本占比 / 好样本占比) ln((B_i / B_total) / (G_i / G_total))其中B_i是第i个箱的坏样本数B_total是全体坏样本数G_i是第i个箱的好样本数G_total是全体好样本数。用文字说就是这个箱里的坏样本占全体坏样本的比例和好样本占全体好样本的比例两者相除再取对数。如果WOE是正数说明这个箱里的坏样本占比高于好样本占比也就是这个箱比整体平均水平更“坏”如果是负数说明这个箱里好样本相对更多风险更低。注意这个符号方向和很多人的直觉相反因为分母是好样本占比。我见过不止一个新手把正负号搞反结果做出来的评分卡高风险客户反而给高分。判断正负号的一个简便方法是看坏账率坏账率高于平均值的箱子WOE应该为正反之应该为负。还有一点要提醒有些资料里的WOE公式分子分母写的是好样本除以坏样本不是错是定义方向不同。但你在评分卡里一定要保持WOE方向统一并且清楚它和后面评分公式的符号关系。我自己习惯统一用“坏/好”这个方向后面积分映射的时候逻辑清晰。3.2 IV值计算和筛选阈值怎么判断变量好坏IV值全称Information Value信息量它是在WOE基础上加权求和得到的。公式IV Σ (坏样本占比 - 好样本占比) * WOE_i注意是“坏占比减好占比”保留这个符号在后续解读变量方向和计算模型系数时会用到。IV值越高说明这个变量区分好坏客户的能力越强。行业里比较通用的经验阈值IV值范围预测能力判断小于0.02几乎无预测力建议直接剔除0.02到0.1较弱可结合业务判断是否保留0.1到0.3中等比较理想的筛选区间0.3到0.5较强非常有价值的变量大于0.5需要警惕可能存在过拟合或数据泄漏这个阈值表是经验值不是硬标准不同业务会稍微浮动。比如反欺诈场景里IV大于0.5的变量还挺常见的因为欺诈本身就是强信号但在普通信贷评分卡里如果一个变量IV超过0.5我会本能地怀疑它是不是间接包含了标签信息比如“逾期次数”这种特征本质上已经是结果的一部分放进评分卡就是数据泄漏。3.3 WOE和IV的完整Python实现直接上代码这段代码我在项目里用了很多次改个列名就能复用import pandas as pd import numpy as np def calc_woe_iv(df, feature, targetis_bad): 计算分箱后特征的WOE和IV df: 包含分箱结果和标签的DataFrame feature: 分箱后的特征列名已有bin target: 标签列名1表示坏客户 # 统计每箱好坏样本 grouped df.groupby(feature, observedTrue)[target].agg([sum, count]) grouped.columns [bad, total] grouped[good] grouped[total] - grouped[bad] # 全局好坏样本数 total_bad grouped[bad].sum() total_good grouped[good].sum() # 计算占比加上平滑项防止除零 grouped[bad_dist] grouped[bad] / total_bad grouped[good_dist] grouped[good] / total_good # 注意这里做平滑把0替换成一个极小值 eps 1e-6 grouped[bad_dist_smooth] grouped[bad_dist].replace(0, eps) grouped[good_dist_smooth] grouped[good_dist].replace(0, eps) # WOE grouped[woe] np.log(grouped[bad_dist_smooth] / grouped[good_dist_smooth]) # IV grouped[iv] (grouped[bad_dist] - grouped[good_dist]) * grouped[woe] iv_total grouped[iv].sum() return grouped, iv_total # 使用示例 df[age_bin] pd.cut(df[age], binsbins) woe_table, iv_value calc_woe_iv(df, age_bin) print(woe_table) print(IV , iv_value)代码里的平滑项是必须的。实际数据里经常出现某个箱好坏样本为0的情况如果没有平滑log里面会出现0/正数结果是负无穷IV算出来直接变成无穷大。平滑之后这个箱的WOE会变成一个很大的负数或正数虽然还是会影响IV但至少不会直接报错。当然平滑只是兜底策略最好的做法是出现空箱时就手动合并到相邻箱。还有一个细节容易被忽略在算WOE和IV之前要确保分箱特征和标签在同一个DataFrame里并且已经去掉ID类字段和标签本身。有些同学手一抖把is_bad也当成特征算了一遍得到IV等于几十的“神变量”这明显就是泄漏了。排查方法很简单IV异常高的变量优先怀疑是不是和标签有直接函数关系。4. 完整流程演练手工搭建一份评分卡预处理流水线4.1 构造一份模拟数据并跑通全流程光说公式不练假把式我用一份模拟数据把从分箱到IV筛选的完整流程跑一遍。模拟数据包含5个特征年龄、收入、负债率、信贷申请笔数、账户开立时长标签是is_bad好客户0坏客户1。样本量1万条。import numpy as np import pandas as pd from sklearn.tree import DecisionTreeClassifier np.random.seed(42) n 10000 # 模拟特征 age np.random.normal(35, 10, n).clip(18, 65) income np.random.lognormal(4.5, 0.8, n) * 1000 # 右偏分布 debt_ratio np.random.beta(2, 5, n) * 0.8 # 负债率 apply_count np.random.poisson(3, n) 1 # 申请笔数 account_age np.random.exponential(5, n).clip(0, 20) # 账户龄 # 构造好坏标签坏概率与特征成一定关系 logit -0.5 0.04 * (age - 35)**2 - 0.02 * (age - 35) \ - 0.0003 * income / 10000 \ 3 * debt_ratio \ 0.15 * apply_count \ - 0.08 * account_age prob 1 / (1 np.exp(-logit)) is_bad np.random.binomial(1, prob.clip(0.01, 0.99)) df pd.DataFrame({ age: age, income: income, debt_ratio: debt_ratio, apply_count: apply_count, account_age: account_age, is_bad: is_bad }) print(df.head()) print(坏样本占比:, df[is_bad].mean())这份模拟数据里我故意把年龄设置成了非线性关系二次项这样分箱的优势能体现得更明显。逻辑回归直接用原始年龄变量去拟合一个二次关系效果肯定不如分箱后好。收入用了对数正态分布也就是说大部分样本集中在低收入区间少量高收入长尾这类特征如果直接标准化建模长尾样本的杠杆效应非常明显。接下来对每个连续特征做决策树分箱然后用上面的calc_woe_iv算WOE和IVdef process_feature(df, feature, targetis_bad, max_leaf_nodes5): 对一个连续特征做分箱、WOE、IV的完整流程 bins, _ tree_binning(df[feature], df[target], max_leaf_nodesmax_leaf_nodes) bin_col feature _bin df[bin_col] pd.cut(df[feature], binsbins) woe_table, iv calc_woe_iv(df, bin_col, target) return woe_table, iv results {} for feat in [age, income, debt_ratio, apply_count, account_age]: woe_table, iv process_feature(df, feat) results[feat] iv print(f{feat} IV {iv:.4f}) # 按IV排序 for feat, iv in sorted(results.items(), keylambda x: x[1], reverseTrue): print(f{feat}: {iv:.4f})跑完之后你会发现debt_ratio和age的IV明显高于其他变量apply_count可能IV很低接近0.02这种变量就可以考虑剔除了。这就是用IV做特征筛选的标准动作。4.2 如何把WOE映射到训练集防止数据穿越分箱算完WOE之后要把WOE值映射回原数据替换掉原始连续值形成可以直接进入逻辑回归的特征矩阵。这一步有两个常见的坑。第一个坑是训练集内部穿越。如果你先在整个数据集上计算WOE再切训练测试集那测试集的特征分布信息就已经泄漏到训练过程里了后续评估结果会虚高。正确做法是先在训练集上分箱并计算WOE表然后把同样的箱边界和WOE映射到测试集上。具体来说训练集算好woe_table后测试集先按相同的bins做pd.cut再通过map把woe值映射过去。第二个坑是测试集里出现了训练集没有的箱。比如某特征在训练集里最大值是100分箱边界最右是100但测试集里出现了一个值150pd.cut会把它归于NaN。处理方式是分箱边界最后一段设为np.inf或者在映射时对NaN样本做一个特殊处理要么落到最近的一箱要么单独设一个WOE值。我一般用np.inf边界确保任何数值都能落到最后一箱。映射的代码长这样# 训练集上已经得到 bins 和 woe_table # 把woe_table变成字典注意index是Interval woe_map woe_table[woe].to_dict() # 训练集映射 X_train[feat _woe] pd.cut(X_train[feat], binsbins).map(woe_map) # 测试集映射先确保边界覆盖 bins_ext [-np.inf] [b for b in bins if b ! -np.inf and b ! np.inf] [np.inf] X_test[feat _woe] pd.cut(X_test[feat], binsbins_ext).map(woe_map) # 对NaN值兜底用WOE为0即该箱无差异 X_test[feat _woe] X_test[feat _woe].fillna(0)关于fillna(0)解释一下WOE为0代表这个箱子好坏分布和整体一致也就是没有额外风险信息这在新箱出现时是一个中性的兜底选择。实际项目里如果测试集出现大量映射不到的箱说明时间外样本分布发生了漂移这本身就是重要的监控信号不能简单当成bug处理。4.3 分箱结果不单调时怎么调手动合并和重分箱在评分卡建模中分箱后的坏账率应该呈现单调递增或递减的趋势这是评分卡可解释性的基本要求。如果你算出来的箱坏账率来回跳比如【3.2%、2.1%、4.5%、2.8%】说明这个变量的分箱没有捕捉到稳定的风险规律这种变量放进逻辑回归里系数解释会很困难。面对不单调的分箱我的处理顺序是先尝试减少箱数。箱数太多的时候相邻箱合并往往能恢复单调性。还是不行就换分箱方法决策树换卡方分箱或者反过来。再不行就要回头看看这个变量本身是否和目标之间确实是复杂关系比如前面说的U型关系这种情况下可以手动把两个低风险箱合并、两个高风险箱放两头人为构造单调性。实在无法单调的话就需要结合业务判断是保留还是剔除。千万别为了单调而强行合并箱那样会丢失太多信息IV值会显著下降得不偿失。下面是一个手动合并的示意代码# 假设age_bin有5个箱箱3的坏账率出现回落 # 手动把箱2和箱3合并 def manual_merge_bins(df, bin_col, merge_map): merge_map: 字典key是原箱value是新箱名 df[bin_col _merged] df[bin_col].map(merge_map) return df合并以后重新计算WOE和IV你会发现IV值会下降一些但单调性变好了逻辑回归的系数解释也顺了。这是评分卡建模里经常要做的权衡IV高但不单调和IV略低但单调我几乎总是选后者因为评分卡的核心价值是可解释和稳定不是测试集上的AUC最高。5. 评分卡预处理高频问题与排查技巧实录5.1 分箱后某箱坏样本为0WOE无穷大怎么办这是评分卡建模中最常见的问题没有之一。某个箱全好或者全坏log里面算出来就是正负无穷。前面给的代码在处理这个问题时用了一个eps平滑这能让程序不报错但逻辑上并不能完全解决问题——平滑之后的WOE值依然非常大可能导致这一箱的分值和其他箱差距离谱。更好的处理方式有两种。第一种是合并相邻箱把坏样本为0的箱和旁边风险最接近的箱合并。第二种是这箱确实有业务意义比如“收入为0”的客户很少但一旦出现风险极高那就算WOE很大也保留因为它在业务上是强信号模型会自动学到这个极端特征带来的高分数变化。实务里我通常先看这个箱的样本量如果样本量小于总体的1%优先考虑合并如果样本量足够大且业务含义清晰可以保留。判断的关键一句话WOE值再大也只能反映这个箱相对整体的差异但模型给分高低还受到整体截距项的牵制不会一箱定生死。5.2 IV虚高先别高兴可能已经泄漏了前面提过IV大于0.5要警惕。我遇到过好几个项目的变量IV算出来超过1一查发现是拿“近3个月逾期次数”这类特征预测“当前是否逾期”。这种特征和标签本质上描述的是同一件事放进评分卡没有任何价值反而会让模型在训练集上表现完美、上线后一塌糊涂。检查泄漏有个简单的经验把所有特征的IV排序后看那些异常高的变量逐一问自己“这个特征是在评分申请时点就能拿到的吗”。如果答案是“需要等到客户还款之后才能知道”那基本就是泄漏了。还有一种隐蔽的泄漏是特征本身包含了未来信息比如“当前账户余额”在高频风控里会用到但在申请评分卡场景评分时点和数据采集时点必须严格对齐。牢记一句话特征必须是在决策时点已知的信息这是评分卡建模的红线。5.3 测试集WOE和训练集差异大模型稳定性出问题模型上线之后监控的主要指标之一就是特征WOE的漂移。训练集和测试集或上线后的样本之间同一个箱子的WOE应该大体一致。如果某个箱子的WOE从训练时的-0.3漂移到了上线后的0.8说明这个特征的风险含义已经变了需要重点关注。排查方法也比较简单按箱对比训练集和验证集的坏账率画一个双折线图。如果两个折线不平行甚至交叉说明该特征存在稳定性问题。处理选项一般有两种一是检查是不是分箱过细导致某一箱样本太少统计波动大二是变量本身确实在时间维度上发生了变化这时候考虑把它从模型中拿掉或者重新训练模型。我自己在建模时会专门留一份时间外样本Out-of-Time样本来验证WOE的稳定性这比随机测试集更能暴露问题。这里多说一句分箱时不要只看IV最大就把箱数设得很高。箱数越多每个箱的样本越少WOE估计的方差越大稳定性越差。一个经验值是每箱坏样本数尽量不低于30个总箱数在4到6个比较合适。这也是为什么我在前面决策树分箱时max_leaf_nodes默认给5的原因。5.4 缺失值在分箱里的单独处理很多做机器学习的同学习惯先插补缺失值再进模型但评分卡里把缺失单独成箱是更常见也更符合业务的做法。原因是缺失本身可能携带风险信息比如“收入未知”的客户往往收入不稳定违约率更高。如果直接插补成均值或中位数会抹掉这个信号。操作上如果一个特征缺失率不高比如5%以下可以把缺失单独设成一个箱然后照常计算WOE。如果缺失率达到30%以上我倾向于先判断这个缺失是随机缺失还是信息性缺失随机缺失考虑用其他特征插补信息性缺失就让缺失成箱。另外要注意后续逻辑回归时WOE编码已经把所有箱变成了数值缺失被编码成一个具体数值不会存在模型不支持缺失的问题所以这一步可以安心做。5.5 分箱太碎的另一个后果训练集表现好但上线崩盘最后这个坑我说得细一点。分箱过细导致过拟合是评分卡建模里最隐蔽的坑。表面看分箱越细每个区间内的样本越同质训练集上的IV和AUC会很好看。但到了新数据上这些细分的箱子边界往往是噪音不是信号表现就会明显下滑。我个人的经验法则是建模样本量在1万左右时每个特征的箱数控制在4到6箱样本量到10万级别可以放宽到6到8箱。极端情况下不要超过10箱超过之后解释难度和过拟合风险都会大幅上升。另一个经验是分箱完毕务必做一遍样本外验证把训练好的WOE映射方式原封不动搬到验证集上比较IV值是否大幅度缩水缩水超过30%基本说明分箱过拟合了。这一步看似多花了几分钟但能帮你避开无数次上线后的半夜告警。回到这篇part.3的定位从分箱方法的选择到WOE和IV的计算再到一点点的坑坑洼洼这基本就是评分卡建模中特征预处理的全部核心操作了。我平时带着团队做评分卡项目花在分箱和WOE上的时间往往不少于建模和评估的时间。很多看起来花哨的模型调优都不如把分箱这一步做扎实来得多。数据处理得干净、分箱分得合理、WOE算得稳妥后面的模型训练反而是水到渠成的事。
返回列表