【Python学习打卡-Day10】从数据到模型:我的第一个完整机器学习项目!
前言
各位小伙伴,今天的心情无比激动!我们终于迎来了第10天的学习,这也是整个学习计划的第一个高潮——完成一个端到端的机器学习项目。从拿到原始数据开始,我们亲手将它一步步清洗、转换、预处理,最终送入多个机器学习模型中,并对结果进行了专业的评估。
这不仅仅是代码的堆砌,更是一次思维的升华。我们学习了如何像真正的算法工程师一样,思考异常值处理、警惕数据泄露,并深刻理解了分类报告中精确率(Precision) 与 **召回率(Recall)**背后的业务含义。
今天,我将带大家完整走一遍这个激动人心的全流程,用的是大家熟悉的信用违约预测数据集。准备好了吗?让我们发车!
一、数据预处理:终极全流程回顾
这是我们之前所有工作的集大成。一个干净、可用于建模的数据集是如何诞生的?
1.1 导入库与数据加载
老规矩,先准备好我们的“瑞士军刀”。
# 导入所有需要的库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
# 设置绘图样式与中文显示
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 加载数据 (请确保路径正确)
try:
data = pd.read_csv('E:/study/PythonStudy/python60-days-challenge-master/data.csv')
except FileNotFoundError:
print("错误:数据文件未找到!")
exit()
print("数据加载成功!")
1.2 清洗与编码:让数据变得“听话”
这个过程包括了重命名、删除无用列、以及对文本特征进行数值化编码。
# --- 1.2.1 特征名汉化与删除无用列 ---
feature_name_mapping = { 'Annual Income': '年收入', ... } # (此处省略完整映射)
data = data.rename(columns=feature_name_mapping)
data = data.drop(columns=['Id'])
# --- 1.2.2 序数与名义特征编码 ---
# 房屋所有权 (有序)
data['房屋所有权'] = data['房屋所有权'].replace({"Rent": 0, "Have Mortgage": 1, "Home Mortgage": 1, "Own Home": 2})
# 当前工作年限 (有序)
data['当前工作年限'] = data['当前工作年限'].replace({"< 1 year": 0, ... , "10+ years": 10}) # (省略)
# 贷款期限 (有序)
data['贷款期限'] = data['贷款期限'].replace({"Short Term": 0, "Long Term": 1})
# 贷款目的 (名义,但为简化流程采用标签编码)
# 老师提到“法无定则”,这里为了方便,我们暂时用标签编码。
# 在更严格的项目中,通常会使用独热编码(One-Hot)
purpose_map = {"debt consolidation": 0, "other": 1, ... } # (省略)
data['贷款目的'] = data['贷款目的'].replace(purpose_map)
print("特征清洗与编码完成!")
1.3 缺失值填充
我们采用众数来填充所有存在缺失值的列。
missing_cols = ['年收入', '当前工作年限', '距上次拖欠月数', '破产次数', '信用评分']
for col in missing_cols:
mode_val = data[col].mode()[0]
data[col].fillna(mode_val, inplace=True)
print("缺失值填充完成!")
1.4 【新知识】异常值处理
异常值可能会严重影响模型(尤其是线性模型和SVM)的性能。我们今天学习了最经典的**IQR(四分位距)**方法来处理它。
原理:一个值如果小于 Q1 - 1.5 * IQR 或大于 Q3 + 1.5 * IQR,就被认为是异常值。
我们以“年收入”为例进行处理:
# --- 1.4.1 绘制箱线图观察异常值 ---
plt.figure(figsize=(10, 6))
sns.boxplot(x=data['年收入'])
plt.title('处理前:年收入分布')
plt.show()
# --- 1.4.2 使用IQR方法移除异常值 ---
col = '年收入'
Q1 = data[col].quantile(0.25)
Q3 = data[col].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 筛选在边界内的数据
initial_rows = len(data)
data = data[(data[col] >= lower_bound) & (data[col] <= upper_bound)]
final_rows = len(data)
print(f"异常值处理完成:移除了 {initial_rows - final_rows} 行数据。")
# --- 1.4.3 再次绘制箱线图观察 ---
plt.figure(figsize=(10, 6))
sns.boxplot(x=data['年收入'])
plt.title('处理后:年收入分布')
plt.show()
深度思考:为什么处理后箱线图还有“异常点”?
这是一个非常棒的问题!因为箱线图的Q1,Q3和边界是根据当前数据动态计算的。当我们移除了第一批“极端”异常值后,数据的分布变得更集中,新的Q1和Q3会被重新计算,从而产生新的、更严格的边界。这使得之前的一些“正常值”现在看起来像是“异常值”。这提醒我们,异常值处理不是一个无限递归的过程,通常只做一次即可。
二、机器学习建模与评估
数据准备就绪,真正的大戏开场了!
2.1 核心原则:防止“数据泄露”!
这是今天最最最重要的知识点!数据泄露 (Data Leakage) 指的是在训练过程中,模型不小心“看到”了测试集的信息,导致我们对模型的评估过于乐观,以为它很厉害,但一到真实未知数据上就原形毕露。
老师的考试比喻:
- 训练集 = 平时做的练习题
- 测试集 = 最终的考试题
- 归一化/标准化 = 划定分数标准(比如均值和方差)
如果你在划分练习题和考试题之前,就用所有题目的平均分和难度(均值和标准差)来给自己评分,那你其实已经知道了考试题的大概难度范围。这不就是作弊吗?
正确流程:
- 先将数据划分为训练集和测试集。
- 仅在训练集上计算归一化的参数(如最大/最小值)。
- 用训练集学到的参数,分别对训练集和测试集进行归一化。
2.2 数据划分与归一化
严格遵守“先划分,后处理”的原则。
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
# 1. 划分特征 (X) 和标签 (y)
X = data.drop('信用违约', axis=1)
y = data['信用违约']
# 2. 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
# 使用 stratify=y 保证训练集和测试集中违约比例与原始数据一致,在非平衡数据中很重要!
# 3. 数据归一化(只对连续特征)
continuous_features = ['年收入', '信用历史年限', '最大开放信用额度', ...] # (省略)
scaler = MinMaxScaler()
# 核心步骤!
X_train[continuous_features] = scaler.fit_transform(X_train[continuous_features])
X_test[continuous_features] = scaler.transform(X_test[continuous_features])
print("数据划分与归一化完成!")
print(f"训练集形状: {X_train.shape}, 测试集形状: {X_test.shape}")
2.3 模型“三行代码”与评估
对于 scikit-learn 中的绝大多数模型,训练和预测都遵循简单的三步:
- 实例化模型:
model = ModelClass() - 训练模型:
model.fit(X_train, y_train) - 预测结果:
y_pred = model.predict(X_test)
我们以逻辑回归为例,展示完整的评估流程。
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix
# 1. 实例化
logreg_model = LogisticRegression(random_state=42)
# 2. 训练
logreg_model.fit(X_train, y_train)
# 3. 预测
logreg_pred = logreg_model.predict(X_test)
# 4. 评估
print("\n逻辑回归 分类报告:")
print(classification_report(y_test, logreg_pred))
print("逻辑回归 混淆矩阵:")
print(confusion_matrix(y_test, logreg_pred))
逻辑回归 分类报告:
precision recall f1-score support
0 0.77 0.99 0.87 997
1 0.89 0.28 0.42 400
accuracy 0.78 1397
macro avg 0.83 0.63 0.64 1397
weighted avg 0.81 0.78 0.74 1397
如何解读这份报告?(以类别1“违约”为例)
- Precision (精确率) = 0.89:在所有被模型**预测为“违约”**的客户中,有 89% 是真的违约了。—— “宁可放过,不可杀错”,模型的预测很准,被它标为“违约”的,大概率真是坏客户。
- Recall (召回率) = 0.28:在所有**真正“违约”**的客户中,模型只成功识别出了 28%。—— “宁可错杀,不可放过”,模型漏掉了很多坏客户。
- F1-score = 0.42:精确率和召回率的调和平均数,一个综合指标。
结论:这个逻辑回归模型是一个“保守派”,它追求高精确率,但牺牲了召回率。在风控场景中,我们可能更关心召回率(找到所有坏人),因为漏掉一个坏客户的代价远高于错判一个好客户。
三、模型大比拼:谁是真正的预测王者?
我们测试了包括 SVM、KNN、随机森林、XGBoost 在内的多种模型。为了方便对比,我将它们对**正类(违约客户)**的评估指标以及总体准确率整理如下:
| 模型名称 | 准确率 (Accuracy) | 精确率 (Precision) | 召回率 (Recall) | F1值 (F1-score) |
|---|---|---|---|---|
| 朴素贝叶斯 | 0.4324 | 0.3328 | 0.9775 | 0.4965 |
| XGBoost | 0.7652 | 0.6552 | 0.3800 | 0.4810 |
| LightGBM | 0.7674 | 0.6761 | 0.3600 | 0.4698 |
| KNN | 0.7366 | 0.5615 | 0.3650 | 0.4424 |
| 随机森林 | 0.7767 | 0.7716 | 0.3125 | 0.4448 |
| 决策树 | 0.6722 | 0.4300 | 0.4450 | 0.4373 |
| 逻辑回归 | 0.7831 | 0.8943 | 0.2750 | 0.4207 |
| SVM | 0.7688 | 0.9639 | 0.2000 | 0.3313 |
结果分析:
- 综合表现 (F1-score):朴素贝叶斯 的 F1 分数最高,这得益于其极高的召回率。它几乎把所有违约的人都找出来了。
- 召回率之王:朴素贝叶斯 的召回率达到了惊人的 0.98!如果你是一个“宁可错杀一千,不放过一个”的风控经理,这是你的首选。但代价是它的精确率很低,会误伤很多好客户。
- 精确率之王:SVM 和 逻辑回归 的精确率极高,它们预测的“坏人”基本都是真坏人,但它们漏掉了大量的坏人。
- 平衡性:XGBoost 和 LightGBM 在精确率和召回率之间取得了相对不错的平衡,虽然单项都不是最高,但整体稳健,这也是它们在竞赛中如此流行的原因。
总结与作业
Day 10 是令人收获满满的一天。我们不仅将数据分析的全流程跑通,还踏入了机器学习建模的大门。最重要的收获是:
- 流程化思维:建立了一套从数据清洗到模型评估的完整工作流。
- 严谨性:深刻理解了“数据泄露”的危害,并掌握了正确的预处理顺序。
- 批判性思维:学会了如何解读分类报告,并根据业务需求(看重Precision还是Recall)来选择合适的模型,而不是盲目追求高准确率。
今日作业:
请对“心脏病数据集”采用今天学到的完整机器学习流程进行建模和评估。
这是一个绝佳的练习机会!看看你能否独立复现今天的全部操作,并分析不同模型在预测心脏病时的表现。
感谢 @浙大疏锦行 老师的带领,让我们从一个数据小白,成长为能够独立完成一个简单机器学习项目的学习者。这趟旅程,越来越精彩了!
更多推荐

所有评论(0)