☰
从零到Kaggle:Python机器学习项目代码整理实战记录
2026/10/15 12:44:08 网站建设 项目流程

我从头到尾把《Python机器学习及实践——从零开始通往Kaggle竞赛之路》这本书过完一遍之后,最大的感受不是"算法真多",而是"代码真散"。书里每个章节的示例代码单独看都能跑,但一旦你想把前面章节的数据处理流程用到后面章节的模型上,就得来回复制粘贴,改参数、改变量名,动不动就报错。这篇博文就是我自己在做"代码整理"这件事时的完整记录,包括我为什么整理、怎么整理、整理出了什么,以及整理过程中踩过的坑。如果你也正在读这本书,或者刚学完机器学习基础、准备打比赛但不知道代码该怎么组织,那这份整理思路应该能省你不少时间。

1. 这本书的定位与代码整理的初衷

1.1 这本书到底在讲什么

先说结论:这是一本非常适合"刚学会Python语法但还没碰过机器学习"的读者入门的书。它不追求把每个算法的数学原理推到极致,而是把机器学习实践链路串了一遍:从数据读取、数据清洗、特征工程,到经典模型训练、模型评估,再到末章进入Kaggle竞赛的实战流程。

我当初买这本书,就是因为它的标题里有两个关键词:"从零开始"和"Kaggle竞赛"。我想找的是一条能直接把"学过Python"转化为"能跑通一个完整机器学习项目"的路。读完确实达到了这个效果,但代价是代码层面的"散装感"非常明显。

1.2 为什么必须单独整理代码

书里的代码写法,用一句话概括就是"示例式写法"。什么意思?它为了让读者快速理解某个模型的用法,会把数据加载、预处理、模型初始化、训练、评估全部塞进一个脚本里。好处是初学者跟着敲一遍就能看到效果,坏处是当你真正要做自己的项目时,这些代码几乎没法复用。

我举个具体例子。书中用某个数据集做逻辑回归时,会这样写:

from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) model = LogisticRegression(max_iter=1000) model.fit(X_train, y_train) print(model.score(X_test, y_test))

看起来没问题是吧?但你换个数据集、换个模型的时候,问题就来了:数据里如果有缺失值怎么办?有类别型特征怎么办?要不要做标准化?模型分数如果很低,怎么判断是数据问题还是模型问题?这些在书里是分散在不同章节分别介绍的,并不会自动整合到你的项目代码里。

所以我把整本书的代码重新读了一遍,做了一次系统性的整理。目标不是"把书抄一遍",而是把书里的知识点转化为一套"我自己能直接拿去跑比赛"的项目骨架。

2. 从环境搭建到可复跑:代码依赖的梳理与冻结

整理代码的第一步不是写代码,而是把运行环境固定下来。机器学习库的版本更新速度非常快,同样的代码在不同版本下可能跑出完全不同的结果。这本书出版到现在,库的API已经发生过不少变化。

2.1 版本选型的心得

我当时用的是Python 3.8环境,核心库版本大致固定在这样一组:

库名版本范围用途
numpy1.19.x数值计算底层依赖
pandas1.2.x数据加载与处理
scikit-learn0.24.x机器学习模型与评估
matplotlib3.3.x可视化
xgboost1.3.x竞赛常用梯度提升模型
lightgbm3.1.x竞赛常用梯度提升模型

为什么选这些版本?因为这本书的主要代码基于scikit-learn实现,而0.24版本对train_test_split、Pipeline这些API的支持非常稳定。做代码整理时,我建议不要盲目追求最新版本,而是先固定下来,保证全书代码能原样跑通,再考虑升级。

2.2 requirements.txt 的整理方式

整理依赖的时候,我会把库分为两类。一类是"核心必装",就是不管跑哪个章节都必须有的库,例如numpy、pandas、scikit-learn和matplotlib。另一类是"按需安装",例如xgboost和lightgbm,只有跑到集成模型或竞赛实战环节才需要。

这样区分有什么好处?如果你只是想先跑通书前面的基础章节,没必要装一堆大型库。如果你直接准备Kaggle,那可以把重心放在集成模型库上。我把这两类分别写到两个requirements文件里:requirements-core.txt和requirements-extra.txt,实践下来非常清晰。

2.3 目录结构的重新规划

代码整理里最值得做的一件事,就是把原先"一个notebook打天下"变成"一个项目分模块"。我最终采用的目录结构长这样:

ml_practice/ ├── data/ │ ├── raw/ # 原始数据,不做任何修改 │ └── processed/ # 经过处理后的数据 ├── src/ │ ├── __init__.py │ ├── data_loader.py # 数据加载 │ ├── preprocess.py # 数据预处理 │ ├── features.py # 特征工程 │ ├── models.py # 模型训练与预测 │ ├── evaluate.py # 模型评估 │ └── utils.py # 公共工具函数 ├── notebooks/ # 交互式探索实验 ├── models/ # 保存训练好的模型 └── submissions/ # Kaggle提交文件存放

这套目录结构其实不是抄来的,而是我按照"数据流"的思路自己推出来的:原始数据进来,经过加载、清洗、特征工程,变成模型能吃的训练集,模型训练完保存,评估完生成提交文件。每个环节对应一个独立模块,目录就是整条流水线的映射。整理完以后再跑书里的案例,就不再是翻书找代码了,而是直接from src.models import train_classifier。

3. 数据结构化:训练集、验证集与测试集的规范管理

这本书的一大贡献是让我意识到:在真实项目里,数据切分不是一个小步骤,而是一个需要严肃对待的环节。数据切分方式直接决定了你评估出来的模型分数是"真能力"还是"虚假繁荣"。

3.1 别把数据切分当成一件小事

书里最常用的代码是train_test_split,一行搞定。但在我整理代码时,我把所有切分逻辑统一封装成一个函数,并且强制固定随机种子。原因很简单:如果不固定随机种子,每次运行切分结果都不同,你昨天调的模型参数可能今天就失效了,你还会误以为是代码写错了。

这是我在整理代码时写的统一切分函数:

import numpy as np from sklearn.model_selection import train_test_split SEED = 42 def split_data(X, y, test_size=0.2, stratify=None, random_state=SEED): """统一数据切分入口,保证可复现。""" return train_test_split( X, y, test_size=test_size, random_state=random_state, stratify=stratify )

stratify参数非常关键。分类任务中,如果类别本身不平衡,比如正样本占10%,你不做分层抽样,切出来的测试集可能只有几十个正样本,模型分数波动会非常大。书里没有特别强调这一点,但实际做竞赛时,这是必踩的坑。

3.2 交叉验证与数据泄漏问题

单纯一次切分得到的评估结果,运气成分很大。我整理代码时把交叉验证也封装成了一个统一模块。分类问题我默认使用StratifiedKFold,回归问题使用KFold,然后用一个开关控制是否打乱数据顺序。

from sklearn.model_selection import StratifiedKFold, KFold def get_cv_splitter(X, y=None, n_splits=5, classification=True): if classification and y is not None: return StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=SEED) return KFold(n_splits=n_splits, shuffle=True, random_state=SEED)

比切分更要命的是数据泄漏。这个概念书里有提到,但我真正理解是在整理预处理代码时:如果你先把整个数据集做标准化,再切分训练集和测试集,那么测试集的均值和方差就已经参与了训练集的转换过程,这会让你在测试集上的评估结果虚高。正确做法是先把数据切分好,然后在训练集上fit预处理器,再在验证集和测试集上只做transform。

我用scikit-learn的Pipeline解决了这个问题。把预处理和模型放进同一个流水线,交叉验证时每一折都只在这折的训练部分进行拟合,从机制上杜绝数据泄漏。

4. 特征工程的代码沉淀:从数据清洗到特征编码

这本书把特征工程分成了几个独立场景:缺失值处理、类别特征编码、特征缩放。每个部分单独看都不难,但组合在一起,代码就很容易失控。我整理的重点是把这些操作变成可组合的"转换器"。

4.1 缺失值处理的取舍

书里介绍了删除缺失行和用均值填充两种方式。实操下来,这两种方式都不是万能的。均值填充对数值型特征的干扰相对小,但如果缺失比例超过30%,填充本身就会引入大量噪声。我在整理时把缺失值处理写成一个带参数的函数:

def fill_missing(df, columns, strategy="mean", fill_value=None): """缺失值填充统一入口。strategy支持mean/median/most_frequent/constant。""" from sklearn.impute import SimpleImputer imputer = SimpleImputer(strategy=strategy, fill_value=fill_value) df[columns] = imputer.fit_transform(df[columns]) return df

这里有一个容易忽略的细节:填充缺失值时,如果是在交叉验证流程里,同样应该只对训练部分做fit,否则测试集的缺失值填充也会用到全量数据的统计量。用Pipeline包一层就能解决,但如果你是在函数里手动处理,就要格外小心。

4.2 类别编码与特征缩放的正确姿势

书里讲到One-Hot编码的时候,我用得很顺手,但后来发现pd.get_dummies()有一个隐患:如果测试集里某个类别在训练集里没见过,get_dummies会直接多出一列,导致训练和预测的特征维度不一致。整理代码后,我改用scikit-learn的OneHotEncoder或OrdinalEncoder,因为它们能记住训练时见过的类别集合,新类别会被编码成全零向量。

特征缩放这块,书里介绍了StandardScaler和MinMaxScaler。我对这两个的选择原则很简单:如果后续模型是线性模型、SVM、神经网络这类对特征尺度敏感的模型,一律用StandardScaler;如果是树模型,比如随机森林和梯度提升树,缩不缩放影响不大,因为树模型只关心特征的相对顺序,不关心绝对数值。

把这个逻辑沉淀成代码时,我会定义一个根据模型类型自动选择预处理方式的函数:

def make_preprocessor(numeric_features, category_features, scaler="standard"): from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder, StandardScaler, MinMaxScaler scaler_obj = StandardScaler() if scaler == "standard" else MinMaxScaler() preprocessor = ColumnTransformer( transformers=[ ("num", scaler_obj, numeric_features), ("cat", OneHotEncoder(handle_unknown="ignore"), category_features) ] ) return preprocessor

这么做以后,数值列和类别列就能放进同一个转换器里统一处理,后续接任何模型都只管fit_transform一次。

5. 模型代码的模块化封装:从模板代码到可复用函数库

书里每个模型的章节结构几乎一样:初始化模型对象、调用fit、调用score、打印结果。如果我只是原样照抄,那整理完还是一堆重复代码。所以这一部分我的核心工作是"去重"。

5.1 模型训练流程的封装

我封装了一个统一的模型训练接口,支持分类和回归两种任务。你只需要传入模型实例、训练数据和评估指标名称,它自动完成训练、预测和指标计算,并且把结果以字典形式返回。

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score CLASSIFICATION_METRICS = { "accuracy": accuracy_score, "precision": precision_score, "recall": recall_score, "f1": f1_score, "auc": roc_auc_score, } REGRESSION_METRICS = { "mse": mean_squared_error, "mae": mean_absolute_error, "r2": r2_score, } def evaluate_model(model, X_train, y_train, X_test, y_test, task="classification"): model.fit(X_train, y_train) y_pred = model.predict(X_test) metrics = (CLASSIFICATION_METRICS if task == "classification" else REGRESSION_METRICS) results = {} if task == "classification": y_prob = model.predict_proba(X_test)[:, 1] results["auc"] = roc_auc_score(y_test, y_prob) for name, func in metrics.items(): results[name] = func(y_test, y_pred) return results

封装完以后,书里那些"先用逻辑回归跑一遍,再用决策树跑一遍,再用SVM跑一遍"的重复代码,就变成了对同一个函数传入不同模型对象的循环。跑书里的对比实验时,我能直接得到一张包含多个模型、多个指标的对比表,一眼看出谁好谁差。

5.2 参数搜索的正确打开方式

书里介绍网格搜索时,我一开始是这么用的:把所有参数组合全部列出来,GridSearchCV跑一遍。但实际整理代码时发现两个问题。第一,参数组合数量太大时,运行时间可能从几分钟飙到几小时。第二,网格搜索默认用准确率作为评分指标,但在不平衡数据集上,准确率没有参考价值。

我的调整是:优先使用RandomizedSearchCV。网格搜索是把每个参数的所有候选值做笛卡尔积,随机搜索则是从参数分布里随机采样组合,在参数量多的时候效率高得多。另一个关键点是自定义评分指标。分类问题我一般直接指定scoring="roc_auc",如果多分类则用f1_macro,而不是默认的accuracy。

5.3 模型持久化的时机与方法

把模型保存到磁盘是在整理竞赛代码时才意识到的重要步骤。你不可能每次预测都重新训练一次模型,尤其当你尝试了大量参数组合之后,只有最优的那几个模型值得保留。

用joblib保存模型是对的,但更推荐把整个Pipeline保存下来,而不是只保存裸模型。因为Pipeline里包含了预处理逻辑、特征编码逻辑和模型本身。预测新数据时,一份pipeline.joblib就够用了,不需要在预测脚本里再写一遍标准化和编码的代码。

from joblib import dump, load # 保存 dump(pipeline, "models/best_model.joblib") # 加载 pipeline = load("models/best_model.joblib") y_pred = pipeline.predict(X_new)

我想强调一个教训:不要在训练脚本里同时做模型调参和模型保存。调参时要保存所有候选模型的评估结果,最后只把最优模型序列化。我当时图省事,每跑完一组参数就覆盖保存一次,结果最优模型反而弄丢了。

6. Kaggle竞赛思路的代码复盘

这本书的最后部分把前面所有知识汇到一个竞赛场景里。我整理代码时没有满足于"跑通官方示例",而是按照竞赛的完整闭环重写了一版:数据探索、特征工程、模型训练、交叉验证、生成提交文件。这个闭环跑通以后,我最大的收获不是在排行榜上拿多高的分,而是搞清楚了竞赛代码和平时练习代码的本质区别。

6.1 从本地练习到提交文件的规范流程

竞赛代码里最容易被忽略的是提交文件的格式。书中的示例代码会明确告诉你:提交文件需要两列,一列是ID,一列是预测值,而且要保证顺序和测试集的行顺序一致。这个细节我一开始没在意,结果提交上去格式错误,直接零分。

整理代码时,我把提交文件生成逻辑单独提取成一个函数,并且在写入前自动检查行数是否与测试集一致:

def make_submission(test_ids, y_pred, output_path="submissions/submission.csv"): import pandas as pd submission = pd.DataFrame({ "id": test_ids, "target": y_pred }) assert len(submission) == len(test_ids), "行数不一致,请检查预测结果" submission.to_csv(output_path, index=False) print(f"已生成提交文件: {output_path}")

6.2 本地验证分数与线上分数不一致怎么办

我一直记得第一次参加竞赛练习时的心态:本地交叉验证AUC是0.88,自信满满地提交,结果线上分数只有0.82。当时整个人是懵的。后来在整理这本书的竞赛代码时,我才真正理解了这个差距的来源。

本地分数高而线上分数低,常见原因有三类。第一,过拟合。交叉验证时的每一折模型过于贴合训练数据,遇到新数据泛化能力就下降。第二,随机种子。如果只切分了一次训练集和测试集,那这次切分的随机性可能让验证集特别好预测。第三,数据分布差异。测试集的分布和训练集可能不完全一样,尤其当训练数据里某个特征的取值范围和测试集差异很大时,预测效果必然下降。

我整理代码后,遇到这类问题会优先做两件事:一是把单一交叉验证改成多次重复交叉验证,例如用RepeatedStratifiedKFold跑5次,取平均分,这样能过滤掉不少随机噪声;二是去检查特征分布,用seaborn的kdeplot把训练集和测试集的关键特征分布画在一起对比,分布差异明显的特征优先考虑删除或做重缩放。

6.3 竞赛实战中集成模型的地位

这本书的末尾部分用了不少篇幅介绍梯度提升树模型,包括XGBoost和LightGBM。我当时比较疑惑:为什么书里前面花了大量篇幅讲逻辑回归、SVM、朴素贝叶斯,到了竞赛环节却都换成了集成模型?

整理代码之后我有了答案:竞赛要的是"预测精度最大化",而集成模型自带处理非线性关系、特征交互和缺失值的能力,在很多标准表格数据上就是比单一模型强。但这也带来一个陷阱:如果不理解集成模型的参数含义,直接套默认参数,效果可能还不如调好的逻辑回归。

我整理代码时把每个模型的常用参数网格单独写成了字典,方便搜索时引用。例如LightGBM的核心参数我关注n_estimators、learning_rate、num_leaves、max_depth和min_child_samples。这些参数决定了树的复杂度,而树的复杂度直接决定模型的过拟合程度。如果你拿到的数据集只有几千行,把num_leaves调到50以上,多半是灾难。

7. 代码整理过程中踩过的坑和给后来者的建议

最后这部分,我把整理代码时踩过的一些比较典型的坑集中说下,这些坑不一定写在书里,但只要你动手整理代码,大概率会遇到。

7.1 Jupyter Notebook和脚本的边界

书里的大部分代码示例都以交互式为主,我最初也习惯一键运行所有单元格。但整理到后面,我发现Notebook有一个严重问题:单元格的执行顺序不是固定的,你可能先跑了后面的单元格,回头再改前面的变量,然后整个环境状态就乱了,出问题时根本不知道是哪个变量被覆盖了。

我后来给自己定了一个规则:Notebook只用来做数据探索和画图,所有模型训练和数据处理流程全部写进src目录下的脚本里。脚本天然是自上而下顺序执行的,不会出现"我忘了运行第3个单元格"这种问题。两个工具各有分工,别混着用。

7.2 随机种子的全局管理

这本书里不止一次强调随机种子,但我在整理代码时发现:随机种子涉及的地方远不止train_test_split。模型初始化、数据采样、交叉验证时的数据打乱,甚至特征选择和降维算法里都有随机性。只固定一处,其他地方的随机性依然会让结果不可复现。

我的做法是在utils.py里定义一个全局的SEED常量,并且在程序入口处用以下方式统一固定,每次写代码都主动传入random_state=SEED。整理完以后,同一份代码在不同时间跑出来的结果完全一致,这对后续调参的意义非常重大。

7.3 代码注释也要分类整理

代码整理不等于把代码"摆得整齐",更重要的是让未来的自己看得懂。我给每个模块的公共函数都写了统一的注释格式:功能说明、输入参数、输出结果、使用示例。尤其那些参数较多的函数,比如预处理和模型训练接口,没有注释的话,隔两周再看,你真的会因为一个参数的作用而翻书查半天。

还有一点,注释里最好写清楚"为什么"。比如我固定随机种子,注释会写"为了保证结果可复现,所有随机操作统一使用该种子",而不是只写"seed=42"。这样你的代码库就相当于一份可执行的笔记,配合原书阅读时效率会高很多。

7.4 给后来者的快速上手建议

如果你也想整理这本书的代码,我的建议是不要一上来就追求完美架构。先跟着书把每个示例原样跑通,然后再动手重构。因为只有你亲自跑过一遍,你才知道哪些代码是核心流程、哪些代码只是演示用的临时写法。

整理顺序我建议这样:先把数据加载和预处理抽成模块,因为这是所有章节共用的部分。然后做模型训练和评估的封装,跑通一个完整案例。最后再处理竞赛提交和模型保存。这三步走完,你的代码库已经具备参加入门级竞赛的能力了。

最终你会发现,整理代码本身就是一个"二次学习"的过程。书里一笔带过的逻辑,你在整理时会不自觉地去追根究底。这个过程比单纯读书带来的提升大得多。我现在遇到新的数据集,已经习惯用整理好的这套代码库直接起步,把省下来的时间用在特征工程和参数调优上,这可能是这本书带给我最大的回报。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询