资讯动态

Lending Club信贷数据预处理实战:缺失值填充与异常值标记全解析

发布时间:2026/10/16 4:18:19 来源:尧图企业网站定制
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(bymissing_rate, ascendingFalse) 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, inplaceTrue)填充之后如果你使用独热编码未知类别会自然多出一个二进制列。如果用有序编码记得把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-1000-10017剔除或修正annual_inc与installment比例一般高于 2%23结合业务审查term类别合法性36/60 months0无需处理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_size0.2, random_state42, stratifydf[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(lower0)) df[annual_inc_log] StandardScaler().fit_transform(inc_log.values.reshape(-1, 1))这里的clip(lower0)也属于对异常值的修正把负收入这种录入错误先拉回 0再取对数避免log1p遇到负数产生无意义结果。整个过程顺序很重要先修正、再转换、最后归一化。7.3 异常值处理的经验阈值速查我在多个数据集上试下来整理了一张经验阈值表可以在没有额外业务指导时作为起点。需要强调的是这些阈值不是通用定律你每次都要用自己的数据重新计算四分位数和分布形状。字段经验合理区间处理建议loan_amnt1000 - 40000高于 40000 多为合格借款人标记后保留annual_inc0 - 15000000 直接修正超高收入标记后保留dti0 - 100超过 100 为严重异常剔除或修正revol_util0 - 100小于 0 修正为 0大于 100 标记后评估installment与 PMT 公式偏差在 10% 以内偏差过大时核查字段关系是否错位open_acc0 - 80远大于 80 标记并人工审核在实战中最让我印象深刻的是一批dti超过 200 的记录。从纯统计角度它们显然是异常值但打开原始记录一看很多对应着非常高的贷款金额和很低的年收入业务上大概率属于高风险申请。我没有简单把它们删除而是单独生成一个dti_extreme标记列并保留记录。最终模型对这个标记的依赖度排在所有特征的前几名说明极端值里确实藏着预测信号。7.4 异常值标记列之间的交互避免二次泄漏最后补充一个容易被忽略的点当你为多个特征分别生成异常标记列后这些标记列也会进入模型。这时候必须注意某些标记列之间可能是高度相关的。比如annual_inc异常标记和dti异常标记经常同时为 1因为它们本来就同属一个借款人的财务状态描述。多个强相关标记列同时入模虽然不至于像完全共线性那样直接爆表但会让树模型的特征重要性得分分散还让结果解释变得困难。我的建议是先用相关性矩阵跑一遍所有异常标记列若两列相关系数超过 0.7只保留其中业务解释更强的一列再把剩下的标记列放入模型。这一步不是必需但在做特征重要性分析时能让你少很多困惑。异常值处理这件事最终的衡量标准不是你删掉了多少异常值而是你在不丢失业务信号的前提下把多少噪声变成了模型可以理解的信息。多花一点时间在这些交互分析上往往比多试几个算法更有回报。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑