做金融风控类的机器学习项目,如果说建模阶段是"炼丹",那数据分析就是"选矿"。矿没选好,后面怎么调参都白搭。天池的金融风控-贷款违约预测系列走到"(二)—— 数据分析",讲的就是把这份贷款违约数据从一堆CSV变成一套能指导特征工程的信息地图的过程。这篇文章我就把这个环节的完整思路展开来写:从字段识别、目标变量拆解,到缺失值、分布体检、相关性分析,再到容易被忽略的隐藏信息和几个新手必踩的坑。无论你是第一次碰信贷数据,还是已经在其他数据集上有过建模经验,这篇文章都能帮你把EDA阶段做得更扎实,而不是简单跑几张图就交差。
1. 拿到贷款违约数据的第一件事:先摸清字段的家底
不管你在哪个平台参加贷款违约预测类的比赛,拿到数据后最忌讳的就是直接train.describe()一把梭。我的习惯永远是先看 shape、再看 columns,然后用 dtypes 对字段做一个粗分类,搞清楚这份数据到底在描述什么业务。
用 pandas 读进来之后,我通常这样打印全局信息:
import pandas as pd train = pd.read_csv('train.csv') test = pd.read_csv('testA.csv') print(train.shape, test.shape) print(train.columns.tolist()) print(train.dtypes.value_counts())这份数据的量级我记得大概是训练集八十万条、测试集二十万条左右,特征在四五十个的量级,具体数字以你本地读入为准。特征类型并不复杂,但业务含义跨度很大,我建议按下面这种方式把字段分成几类来理解。
| 字段类型 | 代表性字段 | 说明 |
|---|---|---|
| 连续数值类 | loanAmnt、interestRate、annualIncome、dti | 贷款金额、利率、年收入、债务收入比 |
| 有序类别类 | grade、subGrade、term | 信用评级、分期期数 |
| 无序类别类 | homeOwnership、purpose、applicationType | 房产状况、借款用途、申请类型 |
| 时间日期类 | issueDate、earliesCreditLine | 放款日期、最早信用记录日期 |
| 匿名数值类 | 以n开头的若干列 | 已脱敏或加工过的数值特征 |
这种分类不是纸上谈兵,它直接决定了后面每一步分析怎么做。连续数值类字段要做分布体检和异常值处理;有序类别类字段可以编码后参与相关性计算;无序类别类字段更多依靠分组统计来观察规律;时间日期类字段往往藏着趋势信息和可衍生的人工特征;匿名数值类字段则要格外小心,因为你不知道它的原始含义,只能通过分布和相关性去推测。
我特别想强调一点:理解字段的业务含义比跑任何统计量都重要。比如 annualIncome 代表年收入,就业年限代表工作稳定性,dti 代表债务收入比,这些在银行风控里都是评估还款能力的基础指标。你不一定要有信贷行业从业经验,但至少要能从字段名推断出"这个变量大概在描述什么风险维度"。
还有一个非常实用的动作:检查是否存在全表只有一个取值的字段。我记得当时数据里就有一个策略枚举类字段,全表都是同一个值,这种列提供不了任何区分度,后面直接删掉就好。这种"单值字段"在竞赛数据里很常见,删掉之前先确认一下训练集和测试集里的取值是否一致,如果测试集里有新取值,那就要另当别论了。
2. 目标变量的分布真相:不平衡率决定后续一切决策
数据分析阶段最核心的锚点就是目标变量 y,也就是 isDefault。在贷款违约预测里,0 代表正常还款,1 代表违约。拿到数据后的第一个统计动作,永远是看这个二分类标签的比例。
train['isDefault'].value_counts(normalize=True)我当时看到的结果是典型的信贷场景分布:违约样本占比处于个位数百分比水平,正常样本占绝对多数。这种不平衡率会给后面的建模带来一系列连锁反应,最直接的一点就是——不能用准确率 Accuracy 作为主要评价指标。假设违约率只有 5%,那模型什么都不学,全部预测为 0,准确率也有 95%,但这显然没有任何风控价值。所以这个比赛以及绝大多数金融风控任务,都会选择 AUC、KS 这类对不平衡不那么敏感、更关注排序能力的指标。
但这里我想多说一句:指标选择只是不平衡问题的冰山一角。真正影响后续建模质量的,是你要不要做采样、怎么设定预测阈值、以及如何在模型里让少数类样本的犯错代价被体现出来。这些当然不是数据分析阶段就能全部解决的,但你在看分布图的时候就要有这个意识。
除了看整体比例,我强烈建议做两个交叉分析。
第一个是按时间看违约率的变化。通过解析 issueDate 的年月,按月统计放款量和违约率,你能看到这条业务的宏观走势。比如某一段时间放款量激增,同时违约率明显上行,那很可能意味着那段时间平台放宽了审核标准,或者客群结构发生了变化。如果你后面要做时间序列相关的特征,这个观察就是地基。
第二个是按信用评级 grade 看违约率。信贷业务里有个非常稳定的常识:信用等级越差,违约风险越高。我当时做了分组统计之后,发现违约率从 A 级到 G 级呈现出清晰的单调上升趋势,评级越低的群体违约率越高。这种单调关系一方面验证了数据的业务一致性——说明这份数据的字段不是瞎编的,确实符合信贷逻辑;另一方面也暗示着 grade 和 subGrade 这两个字段很可能在模型里占据重要位置。
| 信用等级区间 | 违约率特征 |
|---|---|
| A类 | 最低 |
| B类 | 较低 |
| C-D类 | 中等 |
| E类及以下 | 明显升高 |
这种分组统计的价值在于"业务验证"。如果一份信贷数据的违约率和信用评级没有关系,那我反而要怀疑数据质量或者评级字段的定义有问题。反过来,如果关系符合常识,你就可以放心地把这个字段作为后续特征工程的重要原料。
3. 缺失值分析:在征信场景里,空白往往比数字更有话讲
信贷数据几乎没有哪份是没有缺失值的,关键在于你怎么看待缺失。很多新手拿到缺失率统计之后,第一反应就是"要不要删掉""要不要填充",但我的看法是:缺失值在风控场景里本身就是一种信息。
先上代码:
missing = train.isnull().mean().sort_values(ascending=False) missing[missing > 0].head(20)把缺失率按降序排出来,你会看到有些字段的缺失率可能超过 70%,有些在 30% 到 70% 之间,有些只有不到 5%。面对这张清单,最重要的动作不是急着决定填充策略,而是给每个高缺失字段贴一个标签:这个字段是"业务上天然会缺失",还是"采集过程中人为导致的技术缺失"?
举个例子,如果一个字段和房产估值有关,那么对于没有房产的申请人,这个字段缺失是合理的,这种缺失本身就在告诉你"此人名下没有房产"。再比如工作年限字段,如果平台在早期阶段没有采集这一项,那后加入的申请人大概率也会有缺失,这种缺失就和技术采集团队的历史有关,信号价值弱一些。两者对模型的含义是完全不同的。
我在这里要重点强调一个在风控业务里非常经典的观点:申请人连资料都填不齐,这件事本身就值得模型注意。办过贷款或者信用卡的人应该有体会,银行让你填的资料越全、提供的证明越多,说明审核越严格、资质越被看好;反过来,借款人如果连必填信息都大面积缺漏,违约概率往往是偏高的。所以在后面的特征工程里,我会给高缺失字段额外生成一个"是否缺失"的布尔特征,把缺失本身交给模型去学习,而不是简单粗暴地填一个默认值掩盖掉。
初步的处理策略我一般这样划分:
| 缺失率范围 | 建议处理思路 |
|---|---|
| 高于70% | 谨慎看待,优先做成缺失标志特征,再考虑是否删除 |
| 30%-70% | 保留缺失标志,同时按分布或业务逻辑做填充 |
| 低于30% | 按字段类型正常填充,均值、中位数、众数或模型填充均可 |
这里有一个容易忽略的细节:填充不是越精确越好,而是要和后续模型兼容。决策树类模型本身能较好地处理缺失,很多实现里会自动把缺失值分到最优分支;而线性模型、神经网络则完全不能接受 NaN。所以到底填不填、怎么填,取决于你打算把哪类模型作为主力。我的建议是,在数据分析阶段先把缺失率排名、缺失类型搞清楚,至于具体的填充方案,等模型框架定了再动手也不迟。
4. 数值特征的分布体检:偏态、长尾和异常值的信号
看完了缺失值,接下来要盯着一堆数值特征看分布。这一步的核心目的有两个:一是找出明显的偏态和长尾特征,为后面的变换预留思路;二是揪出那些"不合理"的异常值,判断它们是脏数据还是真实风险信号。
最有效率的做法是先跑一个 describe:
train[['loanAmnt', 'interestRate', 'annualIncome', 'dti']].describe()把均值和中位数对比着看。如果均值明显大于中位数,说明分布右偏、存在长尾。我当时印象比较深的是 annualIncome 这类收入字段,少数高收入样本会把均值拉得很高,而大多数人的收入集中在一个相对低的区间。这种长尾分布直接喂给线性模型会带来麻烦,要么取对数压缩尺度,要么按分位数截断。对于树模型来说虽然没有那么敏感,但做好尺度变换始终是一种更稳妥的做法。
再来看异常值。这里要举一个我几乎每次做信贷数据都会遇到的例子:dti,也就是债务收入比。常识告诉我们 dti 超过 100 意味着负债比收入还高,这在业务上已经属于极端危险信号;如果出现负数或者超过几百的值,那就多半是采集或者计算过程出了问题。面对这类异常值,我的原则是:不要急着删除,而是先看看这些异常样本的违约率是否显著高于正常样本。如果违约率确实更高,说明即使是"脏数据",也携带了和风险高度相关的信息,后面可以采用截断而非直接删除的方式来处理。
还有一个典型的"非纯数值"特征需要注意,比如就业年限 employmentLength。这份数据里它很可能不是数字,而是类似"10+年"“<1年”这种字符串。处理方式很简单:把"<1年"解析成 0,"10+年"解析成一个合理上限比如 10,中间的数字转成整数。这种小动作看起来不起眼,但如果不做,这个字段就只能当类别特征处理,白白损失了它本来应有的排序信息。
在做完单字段分布体检之后,别忘了做一件事:对比训练集和测试集的分布。方法很简单,把同一个特征在两个数据集里的 describe 或者直方图拉出来并排看一眼。如果某些特征在两个集里的分布差异很大,就要警惕了。一种常见的情况是测试集来自更晚的时间窗口,而这段时间客群结构发生了变化,导致特征分布整体漂移。这种漂移特征在后续交叉验证里是最容易让模型翻车的,因为你很可能在训练集上表现很好,上线或者提交后效果骤降。数据分析阶段提早发现这些问题,绝对能让你后面少掉不少头发。
5. 相关性矩阵里的隐语:从系数排名到匿名特征的共线性陷阱
分布看完了,下一步就是特征之间的关系。大多数人的习惯是直接画一个相关性热力图,然后对着花花绿绿的色块感叹"好多相关"。但我想说的是:热力图只能给你一个全局印象,真正能指导特征工程的是有目的的相关性分析,建议分两步走。
第一步,看每个特征与目标变量 isDefault 的相关性:
corr_with_target = train.corr()['isDefault'].sort_values() print(corr_with_target.head(10)) print(corr_with_target.tail(10))这一正一负两个列表,能快速告诉你哪些字段对违约风险的影响最大。我记得当时跑出来,利率类、循环额度使用率这类字段排在显著相关的位置,这非常符合信贷业务直觉:借款利率高的人,往往是因为资质不够好、被定价更高,风险当然更大;循环额度使用率很高的人,说明长期处于"刷爆信用卡"的状态,资金链紧张,违约概率上升。这一步的产出其实是在帮你验证"哪些特征在业务上本来就应该重要",而不是盲目相信计算出来的数字。
第二步,看特征之间的相关性,尤其是共线性陷阱。信贷数据里经常出现的现象是:利率和信用评级高度相关,因为评级本来就是决定利率的依据之一;而一堆匿名特征之间也可能存在极强的正相关,因为它们很可能都是从同一套底层变量加工出来的衍生体。比如一组以 n 开头的匿名数值列,如果它们两两之间的相关系数超过了 0.8,那基本可以断定它们在信息上高度重叠。这种高相关特征对树模型来说不至于导致模型崩溃,但会把特征重要性分散掉,让你分不清到底哪一个才是幕后推手;对线性模型和依赖特征降维的方案来说,则会引发回归系数不稳、解释性变差的问题,处理时可以考虑合并、去重或者做 PCA。
相关性分析还有一个非常值得挖掘的隐藏宝藏,那就是时间字段。这份数据里有 issueDate(放款日期)和 earliesCreditLine(最早信用记录时间)。我第一次做的时候差点把这两个字段直接丢了,后来才发现它们能衍生出很实用的特征:从 issueDate 提取出年月,可以按月观察放款规模和违约率的趋势;用 issueDate 和 earliesCreditLine 做差,可以算出一个人的"信用历史长度"。信用历史长的人在风控视角下通常更稳定,因为银行能看到他在足够长的时间内怎么还款,这个特征在后续模型里往往有不错的解释力。
需要提醒的是,相关系数捕捉的是线性关系,它不代表事情的全貌。比如某个匿名特征和违约率的关系可能是非线性的——在区间 A 内风险很低,在区间 B 内风险骤升,这种模式在相关系数上可能只体现出一个微弱的负相关。所以相关性榜单只是第一轮筛选器,真正要挖非线性关系和交互效应,还需要借助分箱统计、特征交叉分析这类手段,这也是为什么我说只看一张热力图远远不够。
6. 数据分析阶段最常见的五个坑,每一个都影响建模结果
这部分我想聊点实际的。数据竞赛也好,真实风控项目也好,我在这个阶段看到过太多人踩进同样的坑里,有些坑甚至我自己也踩过。把它们写下来,帮大家避一避。
第一个坑:把测试集混进来一起做统计。有人图省事,直接把训练和测试拼接成一个 DataFrame 再去看分布和缺失率。这样做最致命的问题是你可能在不知不觉中把测试集的信息"泄漏"到了训练集里。举个最简单的例子,你在某个字段的缺失率统计里看到了测试集的信息,然后在填充时人为加了偏向,模型的泛化能力就会受影响。正确的做法是全程分开看,最多只做分布对比,不要合并。
第二个坑:只用 Pearson 相关系数走天下。Pearson 只能捕捉线性关系,对单调非线性关系虽然也有一定敏感度,但对 U 型、倒 U 型这类关系基本无效。遇到和违约率呈非线性关系的特征,相关系数可能低得可怜,这时候不要急着删除它。建议多算一个 Spearman 秩相关系数做对照,或者直接做分箱统计看趋势,不然很容易把好特征误杀。
第三个坑:缺失值一刀切。要么全部填充 0,要么全部填充均值,要么缺失率超过某个阈值就整列删除。我在前面已经反复强调过,缺失在风控场景里本身可能是信号。不分青红皂白地填充,等于把这个信号从数据里抹掉了。哪怕你最后决定填充,也应该同时保留一个"该样本此字段是否缺失"的标志特征,让模型自己决定怎么用。
第四个坑:忽略时间泄漏。这份数据有明确的放款日期,那就不能随便打乱样本做交叉验证。比如你按随机切分训练集和验证集,等于允许模型"偷看未来",验证集上的指标会虚高,但提交到测试集或者上线之后效果就会大幅缩水。正确的做法是严格按时间顺序切分,用过去预测未来,这样才能真实反映模型在实际业务中的表现。这个坑在竞赛里尤其隐蔽,很多人前期分数很高,到了后期才发现是时间泄漏在帮忙。
第五个坑:分析结论不沉淀。EDA 做的事情非常多,字段含义、缺失率、分布特征、相关性排名、异常值情况、交叉验证结论,这些信息如果只是散落在聊天记录和临时代码里,等到特征工程阶段你会发现自己又要重新看一遍数据。我现在的习惯是,数据分析阶段结束之后,在项目文档里整理一页"字段观察清单",记录每个字段的类型、缺失率、分布特征、与目标变量的关系、初步处理建议以及字段可信度。这份文档在后面模型迭代里会被反复用到,花两小时整理,能省后面两天的事。
最后一个附加提醒,也算半个坑:不要把所有异常值都当成脏数据处理。在风控业务里,极端值背后往往站着的是真正的坏客户。一份收入极低却申请了巨额贷款的数据,在统计上可能是离群点,在真实世界里却可能是一个高风险借款人的特征。对于这类异常值,先看它与违约率的关系再决定是截断、删除还是单独做成特征。
说到这里,其实这个项目的核心方法论已经很清晰了:把每个特征当成一个业务变量去理解,而不是当成一串数字去运算。这份贷款违约数据里的信息量比想象中大得多,但能不能挖出来,取决于你愿不愿意在建模之前,先花足够的时间把数据读懂。尤其是时间字段、匿名特征、缺失值这些角落里藏着的细节,往往才是区分高手和新手的分水岭。数据分析和后续特征工程之间没有明确的边界,你在分析阶段埋下的每一分思考,都会在模型效果里得到回报。