数据分析与挖掘实战
写在前面
好,话不多说,咱们直接开始。
数据分析和挖掘重点就是:数据探索(数据清洗+初步分析可视化)->特征工程->模型选择->调参优化这几个步骤,学完本节你能在心里建立一套流程,对每个步骤都有个大概了解,知道这步为什么这么做,以及如果不这么做会怎样。
上案例:信用卡违约风险预测
题目描述:给定数据集
credit.csv,包含某银行部分信用卡用户数据及违约情况。字段包括”income”、”age”、”gender”、”limit”、”default_fre”、”province”、”default”等。需建立违约风险模型,预测 results.csv 中新客户是否存在违约风险。步骤一:数据探索
- 读取
credit.csv,查看数据基本信息- 按照合理方法处理异常值
- 填充表中空值
- 性别用【0、1】替换,省份用 One-Hot 编码替换
步骤二:数据分析与挖掘
- 画出反映不同特征条件下的违约情况
- 按照 7:3 切分训练集、测试集
- 建立用户违约风险模型
步骤三:调参与优化
- 模型参数调优
- 提交预测模型
- 系统将执行模型,预测 results.csv 中的新用户风险,输出 F1 值并自动评分
我们按数据分析和挖掘的流程走:数据探索 → 特征工程 → 模型选择 → 调参优化。
第一步:数据探索
1.1 读取数据——把数据先拿进来
1 | import pandas as pd |
就这么简单,不过一般数据格式有很多种,比如 Excel、SQL数据库 等。你根据实际情况选择合适的读取姿势:
1 | # 读 Excel |
**read_excel**——它是 pandas 读 Excel 的主力函数,常用以下几个参数可增强读取功能:
1 | # 完整一点的写法 |
注意:
- 身份证号、手机号这种长数字串读进来很容易变成
1.23E+18科学计数法,必须用dtype=str强制按字符串读,否则后面匹配就废了。- Excel 单元格里有公式的话,pandas 默认读到的是公式计算后的值,不是公式本身。如果单元格显示空白但实际有公式,可能读到 NaN,注意区分。
分块读取这个技巧,平时用不到,但特定场景里是加分项。
例子:”几 GB 的 CSV 怎么读”?
你就说 chunksize 参数,再补一句 Dask 或者 Modin 也行,显得你工具箱很丰富。
小白的疑问:分块读取出来的数据和上面
df = pd.read_csv()读出来的处理方式一样吗?我看它根本没用df来保存啊。好问题。答案是——不一样,处理姿势有本质区别:
pd.read_csv()一次性把整张表读进内存,赋值给一个df,后面所有操作(fillna、groupby、describe…)都直接对这个df调用,因为数据是”完整的一坨”。chunksize返回的不是 DataFrame,而是一个迭代器(TextFileReader),每次next吐出来的是一小块 DataFrame(chunk)。它没有”全局 df”的概念,你只能在循环里对每一块单独处理。这意味着凡是需要”看全局”的操作都搞不定,比如:
df.describe()看不出整体统计量,只能逐块算完再合并df.dropna(subset=['age'])只能在块内删,删不干净- 排序、全表去重、相关性热力图——一个块算不出来
正确的姿势是”逐块处理 + 累加合并”:
1 | # 例子:统计几千万行数据里每个省份的违约人数 |
一句话总结:**普通读取”先全量进内存再处理”,分块读取”边读边处理再合并”**。能用普通读取就别用分块,分块是为内存兜底的最后一招。
1.2 三板斧:shape、info()、describe()
数据读进来之后,千万别急着跑模型。你还连数据长啥样子都不知道着,有的人一上来就 fit(),结果到后面发现有空值没处理、数据类型不对,全部白做。先花两分钟做这三件事:
1 | # 第一斧:看看数据规模——几行几列 |
shape 告诉你数据规模。1000 行和 100 万行的处理策略完全不同——小数据在预处理的就不能直接删除异常值了,数据本来就少,而大数据得考虑内存和计算成本。
1 | # 第二斧:看看每列的类型和空值情况 |
info() 是最关键的一步。它会告诉你:
- 每列叫什么名字
- 每列的数据类型(int64、float64、object……)
- 每列有多少非空值——这一行直接暴露哪些列有缺失
如果 info() 显示某列 non-null 只有 800 而总行数是 1000,那就是有 200 个缺失值,后面必须处理。
1 | # 第三斧:看看数值列的统计摘要 |
describe() 会输出每列的均值mean、标准差std、最小值min、最大值max、四分位数25%、50%、75%。
重点看最小值和最大值。如果 age 列最小值是 -3,那肯定是脏数据;如果收入最大值是 9999999,可能是异常值或者占位符。
实战经验:describe() 里的 50% 就是中位数。如果均值比中位数大很多,说明数据右偏,不是正态分布——这在收入类字段里非常常见,后面填缺失值时要用到这个判断。
1.3 异常值处理——数据清洗的第一关
异常值处理我建议放在空值处理前,因为异常值可能影响数据分布,从而影响空值填充的结果。
在处理前先最好备份一下读取的数据,避免你后面想对照一下的时候只能重新读取。
1 | df_backup = df.copy() |
题目说”按照合理方法处理异常值”,”合理”两个字很关键——你得说出你用的什么方法、为什么合理。
第一步:可视化辅助判断
刚才通过 describe() 看到大概的数据异常情况,现在通过可视化更精准地看一下数据情况。
1 | import matplotlib.pyplot as plt |
画图不是浪费时间,是帮你验证判断。如果箱线图里那些”异常点”其实是一条合理的长尾分布,你就不该处理它们。
什么叫”长尾分布”?为什么就不能处理?
长尾分布指的是——大部分数据集中在一个范围内,但右边拖着一条长长的”尾巴”,零星地分布着一些大值。收入、财富、城市人口、电商单品销量都是这种形状。
长尾上的点虽然被 IQR 判定为”异常”,但其实是真实存在的样本,不是脏数据。比如马云的资产在你的样本里就是那个”异常点”,可他确实是用户,把他删了或盖帽成”普通收入”——模型反而学不到”超高净值用户的违约模式”。
怎么判断一个点是”真异常”还是”长尾”?看直方图。如果直方图呈现一个明显的尖峰 + 右边慢慢拖长(像滑梯),那它是长尾分布;如果点是孤立地飘在远处(中间完全断开),那才是真异常。本例里 income 有多个
9999999,明显是系统上限占位符——这种才是真异常,不是长尾。
顺便说说怎么读箱线图——这图信息量比你想的大:
1 | ┌──┐ |
:图上是不是没有画出 Q1-1.5IQR 和 Q3+1.5IQR 这两条线?
对——箱线图不会直接画出这两条边界线。它们是隐含的判断标准:
- 上须线:延伸到”不超过 Q3+1.5*IQR”的最大实际数据点(如果最大点本身就小于这个边界,须线就停在最大点)
- 下须线:延伸到”不低于 Q1-1.5*IQR”的最小实际数据点(同理)
- 离群点:超出须线范围的点才会被单独标出来(用 ● 符号)
读图四步走:
- 看中位数线在箱体的位置——居中说明对称;偏上说明数据集中在高值区间(左偏);偏下说明集中在低值区间(右偏)。
- 看箱体长度(IQR)——长说明数据分散,短说明数据集中。
- 看上下须的对称性——上须长下须短就是右偏(典型如收入);左右对称就是近似正态。
- 看离群点的数量和方向——只在上方有成堆离群点,往往是长尾分布(不要乱删);上下都有少量孤立点,更可能是真异常。
回到本例:income 既有
9999999这种”孤立在上方且数值明显不连续”的点(系统占位符,要处理),也可能有真正的高收入人群(如果呈现长尾,保留)。先画图,凭分布形态决策,不要无脑删。
我们注意到income列有多个9999999,这看着可能是达到系统上限默认取成这个数字了,这是异常值,如果直接删除异常值会丢失数据,样本量变少,异常值如果给换成中位数又会丢失这个异常值的含义,更好的办法是盖帽法。
盖帽法保留数据点,只是把极端值拉回到正常范围
计算异常值边界常用的有IQR 法和 3σ 法。
IQR 法(四分位数法)分两步:先用箱线图判断异常值,计算异常值的边界,再用盖帽法拉回正常范围。
1 | # 计算 IQR |
为什么这里用
3 * IQR而不是常见的1.5 * IQR?这两个数有啥讲究?这两个数都来自统计学经验,背后是对”异常 vs 极端”的分级:
| 系数 | 严格程度 | 适用场景 |
| ——– | ————————————- |
| 1.5IQR | 宽松 | 标准箱线图默认值;数据干净、想抓更多异常值 |
| 3IQR | 严格 | 数据本身分散或有长尾;只想抓”极端异常” |直觉理解:在正态分布下,1.5IQR 大概对应 2.7σ(覆盖 99.3% 数据),3IQR 大概对应 4.7σ(覆盖 99.999% 数据)。也就是说:
- 用 1.5:1000 个点里平均会误判 7 个为异常——抓得狠,但会误伤;
- 用 3:10000 个点里才平均误判 1 个——只抓最离谱的,不会乱杀。
本例为什么选 3 而不是 1.5?因为收入是右偏长尾数据,用 1.5 的话一大批真实存在的高收入人群会被误判为异常,盖帽会把他们的收入拉低,丢失业务信息。换 3*IQR 之后,只有
9999999这种明显不合理的系统占位符会被识别出来。怎么选?给个口诀:
- 数据近似正态、干净 → 用 1.5(标准做法,箱线图默认)
- 数据右偏、有长尾 → 用 3(避免误伤)
- 保守一点没错的 → 先用 1.5 画图看,离群点太多就放宽到 3,再盖帽
1 | # 盖帽法:把超出范围的值拉回边界 |
如果你通过上面的可视化看到数据是近似正态分布,那么就可以用3σ 法。
1 | # 3σ 法:计算均值和标准差,定义异常值边界 |
3σ 法假设数据正态分布,超过 3 倍标准差的就是异常值。但收入这种右偏数据用 3σ 法不合适——右偏分布的均值被拉高,标准差也大,用 3σ 法可能漏掉很多真正的异常值。
实战经验:IQR 法是万能选手,3σ 法只适合正态数据。不确定就用 IQR,不会出错。
现在把异常值问题解决了,下面处理缺失值。
1.4 缺失值处理——数据清洗的第二关
通过刚才的 info(),发现数据有空值,空值不能放着不管,先看看到底哪些列有空值、有多少:
1 | print(df.isnull().sum()) |
假设输出是这样的:
1 | income 120 |
income 缺了 120 条,age 缺了 5 条,limit 缺了 30 条。怎么办?
原则一:缺得少且不重要的列,直接删行。 age 只缺 5 条,占总数据 0.5%,删了也不心疼,不过你要是数据本身就少你还是别删,想办法填充缺失值更好:
1 | # 删除age列空值的行 |
原则二:缺得多但重要的列,要填充。 填什么值?这是有讲究的:
1 | # 收入用中位数填充——不是均值! |
为什么收入用中位数而不是均值?因为刚看到数据中收入是右偏分布——少数人收入特别高,把均值拉上去了。比如 10 个人里 9 个收入 5000,1 个收入 500000,均值是 54500,中位数是 5000。你用 54500 去填一个普通人的缺失收入,直接就给填飞了。
还有些缺失值填充策略了解一下,每种都有它的”主场”:
① 均值填充——适合近似正态分布的数值列
1 | # 身高这种数据,多数人集中在 165~175,分布对称,均值能代表"典型值" |
适用场景:身高、考试成绩、温度这种分布对称、没明显偏斜的连续值。判断方法——describe() 看均值和 50%(中位数)接近不接近,接近就用均值。
② 中位数填充——适合有偏(尤其右偏)的数值列
1 | # 收入、房价这种右偏数据,少数巨富把均值拉飞了,得用中位数 |
适用场景:收入、资产、消费金额这类”少数值特别大”的字段。右偏数据的中位数远小于均值,更贴近”普通人”。
③ 众数填充——适合分类变量
1 | # 性别、职业、教育水平这种类别型字段,没有"均值"的概念,用出现最多的那个值填 |
适用场景:类别型字段,比如职业、城市、学历。注意 mode() 返回的是 Series(可能不止一个众数),所以要加 [0] 取第一个。
④ 前向/后向填充——时间序列的专属技能
1 | # 股票收盘价、传感器温度——昨天的值对今天有强参考意义 |
适用场景:时间序列(温度、股价、PM2.5、传感器读数)。逻辑是”数据有惯性,离得近的样本值相近”。注意 ffill 只能往前看,第一个值缺失就填不了,这时候要配合 bfill 用。
怎么选?给个决策顺序:类别用众数 → 数值看分布(正态用均值、偏态用中位数)→ 时间序列用 ffill。
还有一种更干脆的做法——直接删列,不过请尽量避免,可能关键信息就在这列中,缺失值本身就是一种特征,例如:
“缺失即信号”的几种典型场景:
- 贷款申请表里的”配偶收入”列:填了的人通常配偶也有稳定收入,违约率低;没填的人往往单身或配偶无业,违约率反而高。这列缺不缺失,本身就是一个强信号。
这种情况下,与其删列或乱填,不如新建一列”是否缺失”的标记,把”缺失”这个信号保留下来:
1 | # 先把"是否缺失"这个信号单独存一列 |
真的要删列的话,怎么操作——
1 | # 如果某列缺失超过 40%,直接扔掉这列 |
⚠️ 重点提醒:
dropna的thresh参数是”至少要有多少个非空值才保留这列“。
1.4 分类变量编码——非数字值转为数字编码
题目说”性别用【0、1】替换,省份用 One-Hot 编码替换”,这是因为汉字不能直接用模型处理,必须先转换成数字。
Label Encoding(标签编码)——把类别映射成整数:
1 | # 性别:假设原始值是 '男'、'女' |
为什么性别用 Label Encoding?因为性别是二分类变量,只有两个值,0 和 1 就够了,不需要搞成两个列。
One-Hot Encoding(独热编码)——把每个类别变成一个 0/1 列:
1 | # 省份:有多个类别,用 One-Hot Encoding |
drop_first=True 是什么意思?比如省份有北京、上海、广东三个值,One-Hot 会生成三列:province_北京、province_上海、province_广东。但三列其实用两列就能表示了,第三列能用另外两列算出来(三列加起来肯定等于 1),这就产生了多重共线性——模型会被搞混。drop_first=True 删掉第一列,留两列就能完整表达三个类别。
生活化类比:你让两个员工 A 和 B 一起完成一项任务,但他们的工作内容 100% 重叠——做完了你根本分不清谁的功劳大,发奖金时怎么分都不公平。最干脆的办法是”去掉一个”(drop_first),让剩下的人独立完成,功劳清清楚楚。
注意:树模型(决策树、随机森林、XGBoost)对共线性不敏感,因为它们是按特征排序切分的,不靠权重线性组合。但线性模型(逻辑回归、线性回归)和神经网络对它非常敏感——只要用了这些模型,One-Hot 必须删一列。
什么时候用 Label Encoding,什么时候用 One-Hot?
- 有序类别(如学历:高中 < 本科 < 硕士):用 Label Encoding,保序关系
- 无序类别且类别少(如性别、是否吸烟):Label Encoding 即可(二值)或 One-Hot
- 无序类别且类别多(如省份、职业):One-Hot,否则模型会误以为类别之间有大小关系
为什么 Label Encoding 会引入”假大小关系”?
Label Encoding 的逻辑很简单:把类别按出现顺序(或字母顺序)映射成 0, 1, 2, 3… 这样的整数。问题是——这些数字本来只是”标签”,但很多模型会把它们当数值来算,这种强加的数学结构会让模型学出错误的模式——比如”省份值在顺序中越靠后违约率越高”这种毫无依据的规律。
哪些模型会中招?
- 会中招:线性模型(逻辑回归、线性回归)、SVM、KNN、神经网络——它们直接把特征当数值算距离或权重。
- 不太中招:树模型(决策树、随机森林、XGBoost)——它们只看”以哪个值做切分点”,不关心数值大小,所以 Label Encoding 对树模型勉强能接受。但即便用树模型,One-Hot 在多类别时也会更好(除非维度爆炸)。
这就是为什么对无序多类别,One-Hot 是默认选择——每个类别一个独立列,互不干扰,模型绝不会把”是上海”和”是北京”建立任何数值关系。
拓展分析:用 One-Hot 编码省份,会不会因为测试集出现训练集没见过的省份而导致编码失败?
会,这是一个非常常见的工程坑。sklearn 的
OneHotEncoder默认遇到没见过的类别会直接报错:
1
2
3
4
5
6 # 训练集只有 北京、上海、广东
ohe.fit(X_train[['province']]) # 学到了 3 个类别
# 测试集出现了一个 '新疆'
ohe.transform(X_test[['province']])
# ❌ ValueError: Found unknown categories ['新疆'] in column 0这就是未知类别问题——训练集分布不等于生产环境分布,新省份随时可能出现。两种典型解法:
1
2
3
4
5
6
7 # 解法一:handle_unknown='ignore' —— 未知类别所有 One-Hot 列全填 0
ohe = OneHotEncoder(handle_unknown='ignore', drop='first')
# 解法二:把低频类别预先合并成 '其他',未来新省份也归入 '其他'
counts = df['province'].value_counts()
rare = counts[counts < 50].index # 出现少于 50 次的省份
df['province'] = df['province'].replace(rare, '其他')解法一简单但丢失信息——新省份的所有 One-Hot 列都是 0,等价于”哪个已知省份都不是”,模型完全没法学到它的特征。解法二更工程化,但需要预先知道哪些类别会高频出现。生产环境里推荐两者结合:先合并罕见类别,再开
handle_unknown='ignore'兜底。如果省份有 34 个省,One-Hot 出来 33 列,会不会维度爆炸?答案是:会的。这时候可以考虑目标编码(Target Encoding),用每个省份的平均违约率来替代省份名。但考试里一般不会问这么深,用 One-Hot 就行。
针对这种情况现实工程中最常用的还是 Target Encoding——它既不增加维度,又能保留类别的预测信号,配合交叉验证防过拟合效果很好。sklearn 里有现成的
category_encoders库或者 1.4 版本后的TargetEncoder:
1
2
3
4
5 from sklearn.preprocessing import TargetEncoder
te = TargetEncoder(target_type='binary', random_state=42)
df['province_encoded'] = te.fit_transform(df[['province']], df['default'])
# 省份名直接变成"该省平均违约率",比如北京→0.12,海南→0.25注意 Target Encoding 必须用交叉验证的方式 fit(sklearn 内部默认做了),否则会用标签信息”作弊”,导致严重过拟合——训练集上的”北京违约率”包含了当前样本自己的标签,等于偷看答案。
1.6 数据类型转换——小细节别翻车
1 | # 确保 default 列是整数类型(标签列必须是 int) |
这步看起来无聊,但因为数据类型不对导致模型报错的人太多了。object 类型的列如果忘了编码,sklearn 会直接抛 ValueError。
第二步:数据分析与挖掘
刚才进行了数据预处理,可以认为干净数据已经准备好了,下面开始读懂数据。
2.1 可视化——画图不是炫技,是找线索
题目说”画出反映不同特征条件下的违约情况”,意思是要你分层看违约率,找出哪些特征跟违约强相关。
收入与违约率的关系——分箱后看趋势
1 | # 将收入分成 5 个区间 |
如果你看到低收入组的违约率明显高于高收入组,这意味着收入是违约的有效预测特征,后面建模时收入特征权重应该比较大。反过来,如果违约率在各收入区间差不多,那收入这个特征可能对模型帮助不大。
性别与违约率——分组柱状图
1 | gender_default = df.groupby('gender')['default'].mean() |
信用额度与违约率——箱线图对比
1 | sns.boxplot(x='default', y='limit', data=df) |
如果违约用户的额度中位数明显低于非违约用户,说明额度低的用户更容易违约——这符合业务直觉。
相关性热力图——全局视角
1 | plt.figure(figsize=(10, 8)) |
热力图看什么?两件事:
- 特征与标签的相关性:哪个特征和
default的相关性绝对值最大,哪个就是最有预测力的特征,值越接近 1,说明越相关(正相关和负相关)。 - 特征之间的相关性:如果两个特征之间相关性 > 0.8,说明它们信息重叠(多重共线性),模型会不稳定,需要酌情去掉一个
为啥要去掉一个?
为啥信息重叠就要删? 直觉上:两个特征相关性 0.95,等于它们说的是”同一件事”。比如
income和monthly_income(年收入 / 12),它俩一起进模型——
- 冗余信息:模型学一遍就够了,两个都留等于把信号放大 2 倍,但有效信息没增加。
- 权重打架:模型分不清”该把重要性记到谁头上”,结果两个权重都给得很小,单独看哪个都不重要,加起来又很强——可解释性崩坏。
- 数值不稳定:线性模型的权重求解依赖矩阵求逆,高度共线会让矩阵接近奇异,求出来的权重对训练集扰动极其敏感。
可视化的解读能力是加分项。给你一张图,不是让你描述图上有什么,而是问你”这说明什么”——你要能从图里读出业务含义,然后决定下一步操作。
常用可视化方法怎么选?
- 想看一个变量的分布 → 数值用直方图,类别用柱状图;既要分布又要异常值,用箱线图。
- 想看一个变量在不同组下的差异 → 分组箱线图(数值)或分组柱状图(聚合后的统计量)。
- 想看两个变量之间的关系 → 散点图;如果点太密,换成 hexbin 或 2D 直方图。
- 想看时间趋势 → 折线图;多变量同时画就叠加,颜色区分。
- 想全局扫一眼相关性 → 热力图;要细看每对关系用 pairplot。
- 想看占比 → 饼图(仅 2~5 类);类别多直接换横向柱状图。
举几个例子:
1 | # 例子 1:想看"不同省份的违约率谁高谁低"——分类 × 比例 → 柱状图 |
常见误区提醒:
- 饼图滥用:饼图只适合类别极少(2~5 个)且需要强调”占比”的场景。10 个类别的饼图是灾难——人眼很难比较相邻扇形的大小。
- 折线图用在无序数据上:把不同省份画成折线图,会暗示”上海 → 北京 → 广东”有先后顺序,但实际没有。无序类别一律用柱状图。
- 散点图点太密:大数据量直接
scatter会糊成一团,加alpha=0.3透明化,或换成 hexbin(df.plot.hexbin)。- 箱线图看不出分布密度:箱线图只显示分位数,分布是多峰还是单峰看不出来——这时候小提琴图更好(密度+箱体合体)。
- 3D 图炫技:3D 散点图看着酷,但人眼很难在 3D 空间准确比较深度,最后还是看 2D 投影。能用 2D 就别上 3D。
2.2 特征工程——数据科学的真正核心
有人说”数据和特征决定了机器学习的上限,模型和算法只是逼近这个上限”。特征工程就是帮你把上限往上推的操作。
标准化 vs 归一化——本质区别
1 | from sklearn.preprocessing import StandardScaler, MinMaxScaler |
区别是什么?
| 操作 | 公式 | 结果 | 适用场景 |
|---|---|---|---|
| 标准化 | (x - μ) / σ | 均值0,标准差1 | 数据近似正态;SVM、逻辑回归、神经网络 |
| 归一化 | (x - min) / (max - min) | 压缩到 [0,1] | 数据范围差异大;KNN、神经网络 |
实战经验:树模型(决策树、随机森林、XGBoost)不需要标准化和归一化,因为树模型是按特征值排序做切分的,尺度变化不影响排序结果。但 SVM 和 KNN 就必须做,因为它们依赖距离计算。问到”哪些模型需要特征缩放”,记住这条就行。
特征分箱——把连续变量变离散
1 | # 年龄分成 3 组:青年、中年、老年 |
分箱什么时候有用?当特征和标签之间不是简单线性关系的时候。比如年龄和违约率可能是 U 型关系(年轻人和老年人违约率高,中年人低),线性模型抓不住这种关系,分箱之后反而能学到。
PCA 降维——降了个寂寞还是真有用?
1 | from sklearn.decomposition import PCA |
PCA 降维的核心判断:累计方差解释率要达到 85% 以上,否则你丢掉的信息太多,等于降了个寂寞。
“方差解释率”和”累计方差解释率”到底是个啥?
先理清概念:PCA 的本质是把原始数据投影到新的坐标系上,新坐标轴(主成分)按”能解释多少原始数据的方差”排序。方差越大,说明这根轴能承载的信息越多。
- 方差解释率(explained variance ratio):单个主成分能解释原始数据总方差的比例。
- 比如 PC1 的方差解释率是 0.45,意思是”原始数据里 45% 的信息(方差)被 PC1 这根轴承载了”。
- 计算方式:主成分的方差 / 所有原始特征的总方差之和。
- 累计方差解释率:前 k 个主成分的方差解释率之和。
- 比如 PC1=0.45, PC2=0.25, PC3=0.15 → 前 3 个主成分累计解释率 = 0.45+0.25+0.15 = 0.85(85%)。
- 意思是:”用 3 个主成分就能保留原始数据 85% 的信息”。
**直觉理解——“信息”在这里等价于”方差”**:
- 为什么方差大 = 信息多?因为方差衡量的是”数据的丰富程度”。如果一个特征全是同一个值(方差=0),那它对预测没帮助——“所有人收入都是 5000”这条信息等于没信息。
- PCA 把方差大的方向排在前面,本质上是”优先保留信息量大的维度”,扔掉方差小的维度(信息量小,丢了不心疼)。
为什么要看累计方差解释率?
- PCA 降维是把 20 个原始特征压缩成 5 个主成分。压缩完了你得回答一个问题:”压缩后的 5 个主成分,到底保留了原始数据多少信息?”
- 答案就在累计方差解释率里。如果前 5 个主成分累计解释率是 85%,说明”原始数据 85% 的信息被压缩进这 5 个维度了,剩下 15% 被扔了”。
- 如果累计解释率只有 60%,说明你扔了 40% 的信息——降维太狠,模型效果会大幅下降。
85% 这个阈值怎么来的? 经验值:
- 70% 太低,丢掉的信息可能包含关键信号
- 85% 是工程上的”安全线”,大部分场景下效果还行
- 90%+ 更保守,但主成分数量会增加,降维效果减弱
怎么看、怎么选? 用代码输出一目了然:
1
2
3
4
5
6
7
8
9
10
11
12 # 输出每个主成分的方差解释率和累计值
print("各主成分方差解释率:", pca.explained_variance_ratio_)
print("累计方差解释率:", pca.explained_variance_ratio_.cumsum())
# 画图看"拐点"
import matplotlib.pyplot as plt
plt.plot(range(1, len(pca.explained_variance_ratio_) + 1),
pca.explained_variance_ratio_.cumsum(), 'b-o')
plt.xlabel('主成分数量')
plt.ylabel('累计方差解释率')
plt.axhline(y=0.85, color='r', linestyle='--') # 标出 85% 线
plt.show()图上会看到一条曲线:前几个主成分累计解释率上升很快(斜率大),后面越来越慢(斜率变小)。拐点之后的主成分解释力已经很弱,可以扔掉。85% 的线帮你找到”保留几个主成分合适”的答案。
一句话总结:方差解释率是”单个主成分承载了多少信息”,累计方差解释率是”前几个主成分一共承载了多少信息”。降维前先看累计解释率,保证 >=85%,否则别降。
PCA 为什么之前要标准化?
因为 PCA 找的是方差最大的方向,如果特征量纲不同(收入上万、年龄几十),收入那个维度天然方差大,PCA 会优先保留收入方向,忽略年龄方向——这不是因为年龄不重要,纯粹是因为单位不同。
特征交叉——两个弱特征组合成强特征
1 | # 收入 × 额度 = 实际可用杠杆 |
这个特征什么意思?一个人的额度占收入的比例。如果额度远超收入,违约风险自然高。这种业务驱动的特征交叉,往往比 PCA 这种纯数学降维更有效。
2.3 数据集划分——7:3 还是 8:2?
题目说”按照 7:3 切分训练集、测试集”,那咱就 7:3:
1 | from sklearn.model_selection import train_test_split |
注意 stratify=y 这个参数——这叫分层抽样,保证训练集和测试集里正负样本的比例一致。分类问题必须加,不加的话万一测试集里违约样本全被分走了,你怎么评估?
test_size表示测试集占比。
random_state=42 表示以固定数值42作为随机种子,确保结果可复现。
train_test_split方法还有个参数,叫shuffle,用于随机打乱数据。
时间序列数据不能随便 shuffle! 如果数据是按时间排的(比如每月的交易记录),你 train_test_split 默认会打乱顺序,导致”用未来数据预测过去”——这叫数据泄漏。时间序列要用 shuffle=False 或者按时间截断:
1 | # 时间序列切分——不能 shuffle |
为什么是 7:3 或 8:2?这是经验值。训练数据太少模型学不好,测试数据太少评估不可靠。数据量特别大(几十万条)时可以用 9:1,因为测试集哪怕只有 10% 也够评估了。
2.4 不平衡数据处理——SMOTE 过采样
先看看正负样本比例:
1 | print(y_train.value_counts()) |
如果违约样本只占 5%,那模型只要全部预测”不违约”就有 95% 的准确率——但这个模型毫无用处。这就是类别不平衡问题。
SMOTE 过采样——用 imblearn 三行代码搞定:
1 | from imblearn.over_sampling import SMOTE |
SMOTE 的原理:找到少数类样本的 K 近邻,在连线上随机插值生成新样本。不是简单复制,是”合成”新的少数类样本。
问:SMOTE 会不会导致过拟合?
- 会,如果你在过采样之后才做交叉验证的话。因为过采样生成的样本是基于训练集的,如果这些合成样本也出现在验证集里,验证集就不独立了——这又是数据泄漏。
- 正确做法:在每个 fold 内部单独做 SMOTE,而不是先 SMOTE 再分 fold。sklearn 的 Pipeline 配合 imblearn 的
make_pipeline可以实现。
欠采样作为对比:
1 | from imblearn.under_sampling import RandomUnderSampler |
欠采样是随机删掉多数类样本,让正负样本平衡。缺点是丢数据,优点是训练快。考试里数据量通常不大,优先用 SMOTE。
2.5 算法选择与建模——先跑 Baseline 再上 Boosting
不要一上来就 XGBoost,调了半天参数,结果发现数据没洗干净,F1 值还不如逻辑回归。正确的策略是:先跑一个简单模型看效果,再逐步升级。
Baseline:逻辑回归
1 | from sklearn.linear_model import LogisticRegression |
逻辑回归的输出有概率,可解释性好,训练快。它是你的锚点——后面的模型至少要比它好才算有意义。
升级:随机森林
1 | from sklearn.ensemble import RandomForestClassifier |
终极大招:XGBoost
1 | from xgboost import XGBClassifier |
LightGBM 代码:
1 | from lightgbm import LGBMClassifier |
各算法的”出场时机”:
| 算法 | 优点 | 缺点 | 什么时候用 |
|---|---|---|---|
| 逻辑回归 | 快、可解释 | 只能学线性关系 | Baseline;需要解释性的场景 |
| 决策树 | 可视化直观 | 容易过拟合 | 理解数据结构;特征选择 |
| 随机森林 | 稳定、抗过拟合 | 慢、模型大 | 中等数据量;特征多 |
| XGBoost | 比赛霸主 | 调参复杂 | 追求极致效果 |
| LightGBM | 比 XGBoost 更快 | 小数据容易过拟合 | 大数据量;训练时间有限 |
比赛策略:先跑逻辑回归当 baseline(5 分钟),再跑随机森林(10 分钟),最后 XGBoost 精调(剩下时间全给它)。别上来就死磕 XGBoost,你不确定数据是否干净,调了白调。
2.6 模型评估——不只是看准确率
1 | from sklearn.metrics import ( |
混淆矩阵——一切评估指标的基础
1 | cm = confusion_matrix(y_test, y_pred_xgb) |
输出长这样:
1 | 预测=0 预测=1 |
从混淆矩阵衍生出四个核心指标:
| 指标 | 公式 | 直觉 | 什么时候重要 |
|---|---|---|---|
| 准确率 | (TP+TN) / 全部 | 整体对不对 | 正负样本均衡时 |
| 精确率 | TP / (TP+FP) | 预测为正的有多少真为正 | 误报代价大时(如垃圾邮件) |
| 召回率 | TP / (TP+FN) | 真正的有多少被找到 | 漏报代价大时(如癌症检测) |
| F1 值 | 2 × P × R / (P+R) | 精确率和召回率的调和平均 | 正负样本不均衡时 |
问”F1 为什么用调和平均不用算术平均?”
因为调和平均对短板更敏感。精确率 0.9、召回率 0.1,算术平均 0.5 看着还行,调和平均只有 0.18,直接暴露问题。
ROC 曲线与 AUC 值
1 | y_prob = xgb.predict_proba(X_test)[:, 1] # 预测为正类的概率 |
AUC 的直觉含义:随机取一个正样本和一个负样本,模型把正样本排在前面的概率。AUC = 0.5 就是瞎猜,AUC = 1.0 就是完美。
对数损失(Log Loss)
1 | logloss = log_loss(y_test, y_prob) |
Log Loss 衡量的是预测概率和真实标签之间的差距,比准确率更严格。准确率只看你预测的类别对不对,Log Loss 还看你预测的概率准不准,关心的是你”猜得有多自信、有多准”,哪怕你两次都猜对了,一个 51% 勉强对、一个 99% 稳稳对,Log Loss 会给后者更低的(更好的)分数。
Log Loss 什么时候比准确率更有参考价值?当你的下游任务需要概率输出时。比如风险控制,你要根据违约概率决定授信额度,不只是判断”违约/不违约”,这时候 Log Loss 更能反映模型质量。
第三步:调参与优化
3.1 交叉验证——不只是 cross_val_score 一行代码
1 | from sklearn.model_selection import cross_val_score, StratifiedKFold |
为什么用 StratifiedKFold 而不是普通 KFold?因为分类问题里每个 fold 的正负比例要一致,否则某个 fold 里全是不违约样本,F1 值没法算。
cross_val_score的参数:
- estimator:模型实例,这里用的是上面实例化的XGboBoost模型xgb
- X:特征矩阵,这里可以看到训练集和测试集都是用的原始训练集进行划分的,保证不会进行数据泄露
- y:目标变量,这里用的是原始训练集的目标变量
- cv:交叉验证策略,这里用的是5折分层交叉验证
- scoring:评估指标,这里用的是F1值
交叉验证的深层意义:单次 train_test_split 的结果有随机性,可能你运气好分到一个简单的测试集,F1 值虚高。交叉验证跑 5 次,看均值和方差,均值高且方差小说明模型真的稳。
工程考量:交叉验证的 fold 数怎么选?
- 5 折:最常用,计算成本和评估稳定性的平衡
- 10 折:数据量小时用,充分利用每条数据
3.2 网格搜索——暴力但可靠
1 | from sklearn.model_selection import GridSearchCV |
网格搜索的问题:3 × 3 × 3 × 3 = 81 种组合,每种跑 5 折交叉验证,就是 405 次训练。网格搜索相当于暴力遍历所有可能,参数多的时候,跑到天荒地老。
3.3 随机搜索——够快够用
1 | from sklearn.model_selection import RandomizedSearchCV |
50 种组合 vs 81 种,省了不少时间。而且随机搜索有个理论保证:如果某个参数真的很重要,随机搜索大概率也能搜到好的值。
随机性能大大减少尝试的次数,缺点是可能错过最优解。
3.4 贝叶斯优化——比赛里的秘密武器
1 | import optuna |
贝叶斯优化和网格搜索/随机搜索的本质区别:它会学习。每一轮试验的结果会被用来指导下一轮搜索的方向——上一轮发现 max_depth=5 比 max_depth=3 好,下一轮就会在 5 附近多探索。所以同样 50 次试验,贝叶斯优化大概率比随机搜索找到更好的参数。
Optuna 是 Kaggle 比赛里几乎人手一个的库。考试里如果时间允许,用 Optuna 跑 50~100 轮,效果通常比手动调参好一截。
贝叶斯优化的原理(为什么它能“预测”更好的值?)
- 网格搜索(Grid Search):像一张渔网把整个池塘捞一遍。傻瓜但全面,池子大了就捞不动。
- 随机搜索(Random Search):像闭着眼睛往池塘里乱撒网。偶尔能抓到大鱼,但纯看运气。
贝叶斯优化的核心思想:它会“看菜吃饭”,根据过去的经验来指导下一次的撒网位置。它主要依靠两个核心组件:
- 代理模型(Surrogate Model)—— 也就是“记忆大脑”
贝叶斯优化内部维护着一个概率模型(通常是高斯过程 Gaussian Process)。刚开始,它什么都不懂,随便试几组参数(比如试了 max_depth=3得分 0.6,max_depth=8得分 0.8)。随着试验次数增加,这个“大脑”会根据已有的结果,拟合出一条“参数与分数关系的地形图”。这张图不仅能告诉你“哪个位置现在看起来最高”(均值),还能告诉你“哪个位置你还不太确定,可能有惊喜”(方差/不确定性)。- 采集函数(Acquisition Function)—— 也就是“导航仪”
有了地形图后,导航仪会综合两个因素来决定下一次去哪试:
- 开采(Exploitation):去目前已知分数最高的区域附近(因为我已经知道那里不错)。
- 探矿(Exploration):去目前还很“陌生”、不确定性很高的区域(万一那里藏着更高的分数呢?)。
✨ 这就是为什么说它“会学习”:
上一轮发现 max_depth=5比 max_depth=3好,贝叶斯优化不会像随机搜索那样无视这个信息,而是会在 5 附近(比如 4, 5, 6)多分配一些试验机会,同时偶尔去 9 或 10 看看会不会有意外之喜。
3.5 学习曲线——判断欠拟合还是过拟合的利器
1 | from sklearn.model_selection import learning_curve |
怎么解读学习曲线?
- 欠拟合:训练集和验证集的 F1 都低,而且很接近——模型太简单了,学不到东西。对策:换更复杂的模型、加特征、减少正则化。
- 过拟合:训练集 F1 很高,验证集 F1 很低,差距大——模型把训练集背下来了但泛化不了。对策:加正则化、减特征、增大训练数据、Early Stopping。
- 刚刚好:两条曲线逐渐靠近并趋于平稳,且 F1 值可接受。
这图太好使了,判断模型是欠拟合还是过拟合,你就画这个图,一画就懂。
3.6 用最优模型做预测并提交
1 | # 用调完参的最优模型 |
考试最后 5 分钟最容易犯的错误:训练数据做了标准化、One-Hot,但预测数据忘了做同样的处理。结果模型输入格式不对,直接报错或者出垃圾结果。一定要用 Pipeline 把预处理和建模绑在一起,这样就不会漏步骤:
1 | from sklearn.pipeline import Pipeline |
ColumnTransformer:它的任务是对不同的列,施加不同的处理魔法
- ‘num’:别名,方便日志打印
- StandardScaler():对数值特征进行的处理
- numeric_features:待处理的数值特征列名列表
课后加餐:四个补充练习
练习一:Titanic 生存预测——练完整的二分类流程
经典入门题。数据包含乘客的年龄、性别、舱位等级、登船港口等,预测是否生还。
1 | import pandas as pd |
Titanic 的核心考点:完整的端到端二分类流程——从缺失值处理、编码、特征选择到建模评估,一步不落。
练习二:Iris 多分类——练决策边界可视化和多分类评估
1 | from sklearn.datasets import load_iris |
Iris 的核心考点:多分类的评估——每个类别分别算精确率、召回率、F1,看 classification_report;决策边界可视化——直观理解模型是怎么划分不同类别的。
练习三:房价预测——练回归算法对比和连续值预测
1 | from sklearn.datasets import fetch_california_housing |
房价预测的核心考点:回归问题的评估指标——
| 指标 | 含义 | 越小/大越好 |
|---|---|---|
| MAE | 平均绝对误差 | 越小越好 |
| RMSE | 均方根误差 | 越小越好 |
| R² | 决定系数 | 越接近 1 越好 |
R² = 1 表示完美预测,R² = 0 表示和预测均值一样差,R² < 0 表示比预测均值还差。
练习四:信用卡欺诈检测——专门练不平衡数据处理和 SMOTE
1 | from sklearn.datasets import make_classification |
欺诈检测的核心考点:极度不平衡数据下的处理策略对比。三种方案的适用场景:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 不处理 | 简单 | 少数类召回率极低 |
| SMOTE | 效果好 | 可过拟合;需要 Pipeline |
| class_weight | 不增加数据量 | 效果不如 SMOTE 但更稳健 |
实战建议:先试
class_weight='balanced',效果不够再上 SMOTE。大部分情况下class_weight就够用了。
全流程速查表
考试前花 5 分钟过一遍这张表,脑子里就有完整的流水线了:
1 | 1. 读取数据 → shape / info() / describe() |
写在最后
走完这一遍,你应该有了三样东西:
- 一条清晰的流水线:拿到任何脏数据,脑子里立刻跑出”清洗→探索→建模→调优”的步骤顺序
- 每个环节的工具包:知道用什么库、什么函数、什么参数——fillna 用中位数、One-Hot 要 drop_first、分类问题 stratify、调参先随机搜索再 Optuna
- 避坑的肌肉记忆:收入别用均值填、树模型不用标准化、SMOTE 要放 Pipeline 里、预测新数据预处理要和训练一致
这么长的流程都学完了,给自己点个赞吧!