1. 为什么预处理在这份数据集上如此重要
如果你之前碰过信贷风控或者 Kaggle 上的信用评分题目,多半见过 Lending Club 这份公开数据集。它记录了平台从 2007 年到 2018 年前后的贷款申请与还款表现数据,包含借款金额、利率、年收入、负债率(DTI)、FICO 评分区间、就业年限、房屋所有权状态、贷款用途、当前贷款状态等上百个字段。很多初学者上手时最容易犯的错误,是拿到数据后先跑模型,结果发现特征矩阵里有大量 NaN,年收入出现几十万美元的极端值,负债率 DT I 甚至超过 100%,然后所有模型都报错或者效果一塌糊涂。
我最初接触这份数据时也走过弯路。那时我比较天真,看到dropna()能删掉 NaN 就直接删,结果训练集从 10 万条缩水到 3 万条,样本量损失一半以上。更麻烦的是,我后来才发现被删掉的大多是高风险贷款记录,因为信用历史不足的申请人往往缺失更多字段,直接把它们扔掉等于把模型最需要学习的"坏客户"样本全部清理掉了。做出来的模型上线一看,AUC 挺好看,但实际判断新样本时完全偏掉。
这件事给我的教训很深刻:对 Lending Club 这类真实业务数据做预处理,绝不能简单填空或者删行,而是要站在业务角度先回答三个问题——缺失是不是一种信号?异常值是噪音还是特征本身?预处理方式会不会造成信息泄漏?本文会沿着"缺失值分析—异常值分析—完整预处理管线"这条路径,把我在实际项目中验证过的处理思路、源码结构和踩坑经验完整展开。标题里提到的源码和数据,我会说明模块化拆分方式与迁移到其他信贷数据集的通用套路,希望给你一条可以直接照做的路线,也让你理解每个处理动作背后的"为什么"。
这份数据集的预处理之所以值得单独写一篇,还有一个现实原因:它的字段覆盖了申请信息、信用历史、贷款条款、还款表现四大类,字段类型既有连续数值也有海量分类,缺失模式也五花八门。如果你能把它彻底搞清楚,很多其他风控场景或者普通表格数据的清洗任务也就顺手解决了。
1.1 数据集字段全景:三类变量与它们的业务含义
我在实际分析中会把 Lending Club 的字段粗分成三组。第一组是申请端信息,比如loan_amnt(贷款金额)、term(期限)、int_rate(利率)、installment(月供)、annual_inc(年收入)、dti(负债率)。这些字段大多由借款人在申请时填写,缺失相对可控,异常值多半来自填写错误、极端收入或者虚构信息。
第二组是信用端信息,比如delinq_2yrs(两年内逾期次数)、inq_last_6mths(最近半年查询次数)、open_acc(开放信用账户数)、pub_rec(公共记录数量)、revol_util(循环额度使用率)、total_acc(总信用账户数),以及 FICO 评分区间fico_range_low和fico_range_high。这类字段由第三方征信机构提供,特点是整体缺失率不高,但个别细分项(比如mths_since_last_delinq距离上次逾期月数)缺失率可能高达 60% 以上,因为很多优质客户从来就没有逾期记录,系统自然没有填这项。
第三组是表现端标签,最核心的是loan_status,它标记了贷款是正常还款、逾期、坏账还是已结清。标签字段本身一般没有缺失,但它会决定你在后续异常值分析中是否需要分别查看不同状态下的数值分布。比如同样一笔年收入 30 万美元的申请,出现在已坏账样本里和出现在正常结清样本里,业务含义完全不同。
1.2 预处理不是"脏活",而是建模前的第一道特征工程
很多人把预处理当成建模前不得不做的体力活,我觉得这个认知必须纠正。预处理的质量直接影响特征工程和模型上限,尤其对于 Lending Club 这种记录条数多、字段类型杂、缺失和异常都很有规律的数据集,预处理本身就是在创造特征。
举个例子,revol_util代表循环贷额度使用率,正常范围在 0 到 100% 之间。如果某条记录算出来是 120%,或者填成了很小的负值,这可能不是普通噪声,而是说明申请人的信用账户状态异常,或者数据录入时发生了字段错位。如果你简单把异常行删掉,就丢掉了一条"信用使用过度"的信号;如果你把异常值原样喂给模型,树模型可能会根据这个异常区间学出一个其实没有业务依据的分叉。正确做法是校验异常边界,同时生成一个"是否异常"的二元标记列,把异常特征保留下来。
还有一点需要提前说:数据预处理必须保留一份"清洗日志"。你每删掉一行、填充一个 NaN、修正一个越界值,都应该知道是哪些字段、基于什么规则、影响了多少样本。我在后面会展示如何在源码中用规则和统计汇总自动输出这样的日志。这不仅能让你回头复核,也方便在团队协作或论文复现时交代清楚数据处理步骤。
2. 缺失值分析:先摸清缺失全貌,再决定填充策略
2.1 缺失率统计与缺失字段分类
拿到数据后第一步不是立刻填充,而是先跑一个缺失全景统计。我当时写了一段脚本,按列汇总缺失数量、缺失率和缺失率排名,然后人工逐一查看缺失率最高的前 20 个字段。这样做省时高效,因为上百个字段里大部分缺失率要么是 0,要么是 100%,真正需要你逐列决策的其实只有中间那十几个。
import pandas as pd import numpy as np def missing_overview(df): missing_count = df.isnull().sum() missing_rate = missing_count / len(df) overview = pd.DataFrame({ 'missing_count': missing_count, 'missing_rate': missing_rate }) overview = overview.sort_values(by='missing_rate', ascending=False) overview = overview[overview['missing_rate'] > 0] return overview # 假设 df 是加载后的 DataFrame overview = missing_overview(df) print(overview.head(20))这段代码不复杂,但跑完之后你要做一件比代码更重要的事:给缺失字段分门别类。我通常把缺失分为三类。
第一类叫"结构型缺失",特征是缺失率极高(超过 80%)并且缺失样本高度重叠。比如mths_since_last_delinq、mths_since_last_record这类"距上次逾期/距上次公共记录"的时间字段,大量样本没有逾期记录或公共记录,所以系统默认不填,实际含义是"该借款人无此事件"。这类字段要么直接删除,要么用 0、-1 或"Never"来显式表达无事件状态,而不能用平均值填充。
第二类叫"随机型缺失",缺失率不高,缺失样本与目标标签没有显著相关关系,可以视为可忽略的随机噪声,适合用中位数或众数进行保守填充。
第三类叫 "业务信号型缺失",缺失与否本身和贷款结果相关。我用 A 同学跑实验的时候验证过很典型的例子:某些低 FICO 申请记录会把部分征信字段留空,而坏账率在这些缺失样本上明显更高。此时"是否缺失"就是一条强特征,我会额外生成一个col_missing_flag列,保留缺失信息,再进行填充。
2.2 不同缺失类型的处理方案对比
| 缺失类型 | 典型字段 | 推荐处理 | 原因 |
|---|---|---|---|
| 结构型缺失 | mths_since_last_delinq、mths_since_last_record | 删除原字段或填 -1/0 | 大量样本缺失代表"无事件",均值填充会扭曲分布 |
| 随机型缺失 | emp_length、annual_inc部分乱码 | 中位数/众数填充 | 随机缺失对分布影响小,模型稳健 |
| 业务信号型缺失 | revol_util、collections_12_mths_ex_med | 生成缺失标记列后填充 | 缺失本身与坏账相关,必须保留信号 |
这里有一个很关键的经验:删除一个缺失率超过 90% 的字段是天经地义的事,但删除一个只有 10% 缺失的字段往往是偷懒。10% 缺失意味着你有 1 万条样本,哪怕直接放弃这些样本也会损失大量信息,而且未必随机。我见过很多初学者看到某列缺失率 5% 就顺手 drop,完全没想过生成一个缺失标记列带来的信息量。在 Lending Club 数据集上,"是否缺失"和坏账率之间的相关性经常比很多正经数值特征还高。
2.3 缺失值填充的 Python 实现思路
对于数值型字段,我习惯按不同的分组维度先做统计,再填充,而不是全表用一个中位数。举个实际例子:annual_inc(年收入)在某些记录里是 0 或者 NaN,如果用全表均值填充,低收入群体的收入可能被高估,而高收入群体也可能被拉低。我一般先按home_ownership和emp_length做分组,再取组内中位数填充缺失值。
def fill_median_by_group(df, col, group_cols): df = df.copy() group_median = df.groupby(group_cols)[col].transform('median') df[col + '_filled'] = df[col].fillna(group_median) return df # 示例:年收入按房屋所有权状态分组填充 df = fill_median_by_group(df, 'annual_inc', ['home_ownership'])这里要注意,分组填充会产生一个"填充痕迹"问题:填充进来的值终究是估计值,在后续建模时最好额外生成一个"是否填充"的标记列。这样模型不仅可以学到字段本身,还可以判断哪些样本的原值是缺失的。这个思路在信贷风控场景非常实用。
对于类别型字段,比如emp_length(就业年限),顺序枚举值包括<1 year、1 year、2 years……10+ years等。缺失的时候我通常不直接从文本映射到数字,而是保留缺失类别并单独编码。原因是"不填就业年限"的申请人可能就业不稳定,这一信息在业务上颇有价值。直接填众数反而会抹掉这层信号。
类别填充代码也很简单:
df['emp_length'].fillna('unknown', inplace=True)填充之后,如果你使用独热编码,未知类别会自然多出一个二进制列。如果用有序编码,记得把unknown单独设成一个与正常就业年限区间隔离的值,比如 -1,并确保后续模型不会把它当作真实的年限大小。
3. 异常值分析:数值异常与类别异常的两种排查路径
3.1 数值型异常值:箱线图与 IQR、Z-score 的组合判断
异常值处理的核心问题是:什么叫做"异常"?只看肉眼不靠谱,单靠一种统计方法也容易误判。我在这份数据上的做法,是用IQR 法和 Z-score 法搭配箱线图交叉确认,再结合业务可解释性做最终决策。
IQR 法的阈值很简单:
def detect_outliers_iqr(series): Q1 = series.quantile(0.25) Q3 = series.quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR return (series < lower_bound) | (series > upper_bound)Z-score 法适合接近正态分布的字段,公式是(x - mean) / std,绝对值超过 3 通常认为异常。但我要提醒你一个使用前提:Lending Club 的数据高度有偏,年收入、贷款金额、循环额度这些字段根本不是正态分布,Z-score 会因为你直接用原始均值而失效。更稳妥的做法是取对数后再算 Z-score,或者直接用分位数法。
from scipy import stats import numpy as np # 对年收入取对数后再做 z-score log_inc = np.log1p(df['annual_inc']) z_scores = np.abs(stats.zscore(log_inc)) outlier_mask = z_scores > 3我在实际项目中常把两种方法同时跑,然后取交集或者并集再人工抽查。如果两种方法都选中某一行,那它基本属于边界明显的异常;如果只有一种方法选中,需要进入业务判断环节。
3.2 类别型"异常":低样本类别与业务冲突记录
很多新手只盯着数值型字段,却忽略了类别字段里的异常。Lending Club 数据集里的addr_state(州代码)、home_ownership(房屋所有权)、purpose(贷款用途)、verification_status(验证状态)都可能出现低频类别。频率极低的类别(比如某州只有个位数样本)会让交叉验证时训练集和测试集分布不稳定,对编码也不友好,还容易过拟合。
处理低频类别,我不建议直接删行,因为那等于丢掉样本。更实用的办法是合并成"其他"类别,或者做目标编码时设置平滑项。代码大致是:
low_freq_cats = df['addr_state'].value_counts()[df['addr_state'].value_counts() < 100].index df['addr_state'] = df['addr_state'].replace(low_freq_cats, 'OTHER')还有一类更隐蔽的"类别异常"是业务冲突记录。比如term字段是36 months,但如果它同时标记为已全额付清,而还款记录显示只过了 10 期,那就是明显矛盾;再比如installment与loan_amnt、int_rate、term存在对应关系,理论上可以用 PMT 公式互相验证。我在自己的代码库里加了一个校验函数,对这类字段关系做一致性检查。每次跑完,系统会输出一份"疑似异常记录数"报表,样例如下:
| 校验规则 | 正常值范围 | 疑似异常记录数 | 处理动作 |
|---|---|---|---|
dti必须在 0-100 | 0-100 | 17 | 剔除或修正 |
annual_inc与installment比例 | 一般高于 2% | 23 | 结合业务审查 |
term类别合法性 | 36/60 months | 0 | 无需处理 |
fico_range_low≤fico_range_high | 恒成立 | 0 | 无需处理 |
3.3 异常值处理不是删光,而是"保留+标记+修正"
关于异常值,我最想强调的一点是:对于 Lending Club 这类信贷数据,异常值往往不是录入噪音,而是反映借款人极端信用行为的信号。年收入 50 万美金的人出现在 P2P 贷款里,可能是高净值客户借款用于投资;负债率 90% 以上的人申请贷款,则可能是极度依赖借贷的高风险用户。这两类极端样本恰恰是风控模型最需要学习的,"直接删掉异常行"等于把最有区分度的样本扔了。
所以我推荐的策略是三步走:
- 保留:先不删任何行。
- 标记:为每个数值型字段生成一个
is_outlier布尔标记。 - 修正或转换:对于明显的录入错误(如负的额度利用率),用合理的业务边界修正;对于合法的业务极端值,保留原始值并让模型看到标记列。
这个思路在源码里体现为:
def flag_outliers(df, numeric_cols): for col in numeric_cols: Q1 = df[col].quantile(0.25) Q3 = df[col].quantile(0.75) IQR = Q3 - Q1 lower = Q1 - 1.5 * IQR upper = Q3 + 1.5 * IQR df[col + '_outlier'] = ((df[col] < lower) | (df[col] > upper)).astype(int) return df加标记列之后,特征维度会大幅增加,树模型对这类 0/1 标记很敏感,效果往往比直接删除或者截尾更好。如果你最终选择截尾处理(例如把超过 99.9 分位数的值拉回 99.9 分位数),也请同时保留标记列,这样才不会丢失"该样本原本极端"的信息。
4. 预处理全流程串联:从分析结论到可建模数据集
4.1 数据清洗管线的整体顺序
预处理不能想到哪做到哪,一定要有一条固定的管线。我在多个项目里沉淀下来一套顺序,适用于 Lending Club 数据,也适用于大多数表格型信贷数据。具体是:
- 加载与原始备份:读取 CSV 后立刻保存一份原始备份,后续所有操作都在副本上进行。
- 字段类型矫正:把日期字段转成 datetime 类型,把金额字段转成 numeric,类别字段转成 category。这一步避免后面出现类型错误。
- 缺失值分析:先做缺失率统计,人工区分结构型、随机型、业务信号型缺失,再执行删除、填充、标记三类动作。
- 异常值分析:对数值型字段做 IQR/Z-score 组合检测,对类别字段做低频合并和业务一致性校验,生成异常标记列。
- 特征加工与编码:对类别字段做独热编码或有序编码,对日期字段计算距离变量(如首次贷款到数据发布时间之间的月份数)。
- 训练集与测试集切分:这一步必须放在填充和异常值处理之后,但放在标准化之前。切分后再在训练集上计算填充用的中位数、分位数,避免测试集信息泄漏。
- 输出清洗日志和清洗后数据。
为什么最后还要强调输出清洗日志?因为这种项目经常需要做多次迭代。有了日志,别人问你"那 2000 行到底为什么被标注为异常"时,你能快速给出依据,而不是哑口无言。
4.2 数据集划分与泄漏风险的规避
处理缺失值和异常值的时候,最隐蔽的错误是用到了全量数据的统计信息。比如你计算annual_inc的中位数来填充缺失值,是在全数据集上计算的,然后再切分训练集和测试集,这其实已经把测试集的分布信息带进了训练过程,属于温和的数据泄漏。
正确的做法是先切分:
from sklearn.model_selection import train_test_split train_df, test_df = train_test_split(df, test_size=0.2, random_state=42, stratify=df['loan_status'])然后再用训练集的统计量去填充训练集和测试集。实际项目中我会把填充函数设计成两个阶段:fit(df)计算中位数、众数、分位数等参数,transform(df)用参数填充新数据。这样测试集、验证集甚至未来线上数据都能复用同一套参数。源码里我保留了这种 fit-transform 模式,它能让你少踩非常多的坑。
关于切分还有一个细节:这份数据是时间跨度很长的逐笔贷款记录,如果你直接随机切分,会让未来的样本混入训练集。更稳妥的做法是按issue_d(贷款发布日期)排序后,用前 80% 的时间做训练、后 20% 做测试,模拟真实的"用过去预测未来"场景。如果你想做源码里的 baseline 复现,建议优先采用时间切分。
5. 源码结构解析:模块化实现与复用改造
5.1 模块划分:从加载到导出的清晰边界
我提供的源码不是一个大而全的 notebook,而是拆成多个文件的小工程。工程目录大致是这样:
lending_club_preprocess/ ├── config.py ├── load_data.py ├── missing_handler.py ├── outlier_handler.py ├── feature_engineering.py ├── split_data.py └── main.py其中config.py放所有可调参数,比如缺失率阈值、低频类别合并阈值、IQR 倍数、随机种子等。你拿到源码后,第一个要改的就是这个文件,不用去动其他 Python 文件。
load_data.py负责读取 CSV,做列名统一和类型转换。missing_handler.py和outlier_handler.py分别实现缺失值填充与异常值标记,它们都是可复用的类或函数。feature_engineering.py做编码和日期派生特征。split_data.py做时间切分。main.py把流程串起来并输出清洗日志。
这类模块化设计的好处是每部分都可以单独测试。你如果只想验证某一种填充策略,可以直接导入missing_handler里的类,把 DataFrame 传进去查看输出结果,而不需要把整条管线跑完。这种调试体验对大 dataframe 尤其关键。
5.2 如何把这份源码迁移到自己的项目
你手上如果还有别的信贷数据集,或者自己公司的脱敏数据,源码并不需要大改。核心动作是把config.py里的字段名映射改掉即可:
COLUMN_MAP = { 'annual_inc': 'ANNUAL_INCOME', 'dti': 'DEBT_TO_INCOME', # ... }然后重点检查几个地方:缺失率阈值是否合理,低频类别阈值按样本量调整,业务一致性校验规则是否仍然成立。其实这份源码最大的复用价值不在代码本身,而在"分析习惯":先看缺失类型,再标记异常,再切分,再 fit-transform。你只要沿着这个思路迁到新数据集,遇到任何异常情况都知道该在哪个环节排查。
迁移时还要注意一个细节:很多公司内部数据的时间跨度短,字段稀疏得多。这时候你应该删掉源码中那些对 Lending Club 特有字段写死的规则,比如 FICO 区间、季付次数等,保留通用策略即可。我在交付源码时也写了注释,标出哪些规则是数据集特化的,哪些是通用模板。
6. 实测中容易踩的坑:缺失值处理的隐性陷阱
6.1 全列缺失与"空字符串/占位符"误判
一个非常烦人的坑是:某些字段在 CSV 里看起来不是 NaN,而是空字符串''或者特殊占位符,比如-1、NA、N/A。isnull()检测不到它们,缺失率统计会被严重低估。
我当时处理emp_length的时候就吃过亏。CSV 里很多记录显示为n/a,但我第一次统计缺失率时没考虑它,结果把缺失当成正常类别,后续训练出来的模型明显异常。解决办法是在加载数据时统一指定 NA 值集合:
df = pd.read_csv('loan.csv', na_values=['', 'NA', 'N/A', 'n/a', 'null', 'NULL'])跑完之后再检查一次每列的 dtype 和 unique value 前几名,你会发现很多你以为很干净的数据,实际上藏着大量"伪装缺失"。这一步做扎实,比后面选多高级的填充算法都重要。
6.2 填充顺序造成的统计偏差
当你同时处理多个字段时,填充顺序会影响最终结果。例如annual_inc有缺失,emp_length也有缺失,你可能会先填充emp_length再分组建annual_inc;或者反过来,先填充annual_inc再填充emp_length。这两条路径算出来的结果在高相关字段之间是有差异的。
我建议按如下原则排序:先填充类别型字段,再填充数值型字段。因为数值型字段的分组填充往往会依赖类别字段。如果先填了数值型再填类别型,分组时反而会引入不必要的偏差。比如先填emp_length的缺失类别,再按emp_length分组填annual_inc;如果顺序反了,annual_inc缺失值的分组依据就变成了一个带有填充痕迹的字段,组内中位数的可靠性也会打折扣。
6.3 用交叉验证检验填充效果
有人可能会问:填充策略这么多,我怎么知道哪种最好?答案很简单:用交叉验证看模型效果,而不是凭感觉。我在对比不同填充方案时,会让同一种模型(比如 LightGBM)跑 5 折交叉验证,固定种子和超参数,只改变缺失值处理方式,最后比较 AUC 或 LogLoss。
比如对比"均值填充 + 缺失标记"、"中位数填充 + 缺失标记"、"分组中位数填充 + 缺失标记"三种策略,实测下来分组中位数通常优于全局均值,但差距有多大取决于字段缺失率和分布形态。值得强调的是,这种对比一定要在同一个验证集上做,而不是每次跑到不同模型上。否则你根本分不清是填充策略带来的提升,还是模型超参数随机波动带来的误差。
7. 异常值处理中的业务逻辑陷阱与实战取舍
7.1 单纯看分布会误杀真实极端样本
统计方法从来不带业务常识。annual_inc大于 100 万在 Lending Club 数据里虽然少见,但这些人在现实中的确存在——他们可能是医生、企业主、高级管理人员。如果你只看 IQR,把超过上界 1.5 倍 IQR 的记录全标记为异常,并用删除策略处理,那么高收入群体样本会整体消失,后续模型在预测高收入人群时会非常不稳定,因为训练数据里根本没有对应的极端收入空间。
我的做法是,在自动标记完异常值后,人工抽查异常样本的比例和特征分布。比如年收入异常高的样本通常伴随着高 FICO、低 DTI、贷款金额较大,那么这些记录就不该删,最多考虑 Winsorize 到 99 分位数。如果是那种年收入为 0 或者负数的记录,明显是录入错误,此时才考虑修正剔除。
实际项目里,我把异常样本分成三类:可解释极端值、录入错误、业务欺诈嫌疑。可解释极端值保留并标记;录入错误按业务边界修正;业务欺诈嫌疑样本单独剔除并记录原因。这样处理既保住了真实信号,又清理了噪声。
7.2 分箱或归一化前必须先处理异常值
这里的"处理"不一定指删除,而是指把异常值控制在模型能稳定学习的范围内。如果你在归一化前不修正极端值,MinMaxScaler会把 99% 的样本压缩到一个极窄区间,因为最大最小值都被那几条极端记录拉远了。树模型对量纲不敏感,但神经网络、逻辑回归和 SVM 对这些极端值非常敏感。
如果你用的是标准化(Z-score),极端值同样会让均值往极端方向偏移,导致中心化后的小值全部变成负的且幅度异常。常用的替代方案是对数值型字段做log1p转换后再标准化,可以大幅压低长尾。代码示例:
from sklearn.preprocessing import StandardScaler inc_log = np.log1p(df['annual_inc'].clip(lower=0)) df['annual_inc_log'] = StandardScaler().fit_transform(inc_log.values.reshape(-1, 1))这里的clip(lower=0)也属于对异常值的修正:把负收入这种录入错误先拉回 0,再取对数,避免log1p遇到负数产生无意义结果。整个过程顺序很重要:先修正、再转换、最后归一化。
7.3 异常值处理的经验阈值速查
我在多个数据集上试下来,整理了一张经验阈值表,可以在没有额外业务指导时作为起点。需要强调的是,这些阈值不是通用定律,你每次都要用自己的数据重新计算四分位数和分布形状。
| 字段 | 经验合理区间 | 处理建议 |
|---|---|---|
loan_amnt | 1000 - 40000 | 高于 40000 多为合格借款人,标记后保留 |
annual_inc | 0 - 1500000 | 0 直接修正,超高收入标记后保留 |
dti | 0 - 100 | 超过 100 为严重异常,剔除或修正 |
revol_util | 0 - 100 | 小于 0 修正为 0,大于 100 标记后评估 |
installment | 与 PMT 公式偏差在 10% 以内 | 偏差过大时核查字段关系是否错位 |
open_acc | 0 - 80 | 远大于 80 标记并人工审核 |
在实战中,最让我印象深刻的是一批dti超过 200 的记录。从纯统计角度它们显然是异常值,但打开原始记录一看,很多对应着非常高的贷款金额和很低的年收入,业务上大概率属于高风险申请。我没有简单把它们删除,而是单独生成一个dti_extreme标记列并保留记录。最终模型对这个标记的依赖度排在所有特征的前几名,说明极端值里确实藏着预测信号。
7.4 异常值标记列之间的交互:避免二次泄漏
最后补充一个容易被忽略的点:当你为多个特征分别生成异常标记列后,这些标记列也会进入模型。这时候必须注意,某些标记列之间可能是高度相关的。比如annual_inc异常标记和dti异常标记经常同时为 1,因为它们本来就同属一个借款人的财务状态描述。多个强相关标记列同时入模,虽然不至于像完全共线性那样直接爆表,但会让树模型的特征重要性得分分散,还让结果解释变得困难。
我的建议是:先用相关性矩阵跑一遍所有异常标记列,若两列相关系数超过 0.7,只保留其中业务解释更强的一列;再把剩下的标记列放入模型。这一步不是必需,但在做特征重要性分析时能让你少很多困惑。异常值处理这件事,最终的衡量标准不是"你删掉了多少异常值",而是"你在不丢失业务信号的前提下,把多少噪声变成了模型可以理解的信息"。多花一点时间在这些交互分析上,往往比多试几个算法更有回报。