ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Scikit-learn实战:分类模型从数据清洗到调参完整记录

Scikit-learn实战:分类模型从数据清洗到调参完整记录 用Scikit-learn做分类模型从数据清洗到调参的一整套实战记录先说一个我在带新人时经常遇到的场景很多刚接触机器学习的同学第一反应是去背《西瓜书》里的公式或者刷吴恩达的视频课理论倒是背得头头是道但真拿到一份业务数据时却不知道从哪下手。其实这很正常因为机器学习入门最关键的坎不在算法原理而在于从原始数据到一个能用的模型这条完整链路。这篇文章我就用Scikit-learn这个最成熟的Python机器学习库带你把这条链路完整走一遍核心任务锁定在分类模型上。我会用一份真实业务场景的数据集做实战演示从数据处理、特征工程、模型训练到评估调优全部覆盖适合那些已经会Python基础语法、想正经入门机器学习但还没完整跑通一个项目的读者。你不需要先啃完所有数学推导跟着操作就能理解每个环节为什么这么做。我选择Scikit-learn作为入门工具是因为它把机器学习最核心的流程封装得非常干净API设计统一、文档质量极高、内置了大量玩具数据集和预处理工具而且模型接口的风格从分类到回归几乎完全一致。学会了这一套以后再接触XGBoost、深度学习框架你会发现思路都是通的。1. 项目准备选对数据集等于成功了一半1.1 为什么用员工离职预测而不是鸢尾花经典入门教程里几乎都会用鸢尾花数据集Iris三个类别、四个特征、150条样本用来演示分类流程确实够用。但说实话它离真实业务太远了学完之后你依然不知道分类模型到底能在工作中解决什么问题。我在这篇文章里换了一个更有代表性的数据集——企业员工离职预测。这是许多数据科学竞赛和高校课程设计中反复使用的经典二分类任务热词里搜基于机器学习的企业员工离职因素分析与预测研究机器学习课程设计选题都能找到类似项目。它的业务逻辑是根据员工的工龄、薪资、绩效、工作满意度、加班情况等特征预测该员工是否会离职离职1留在0。为什么选这个因为它具备真实业务场景的三个典型特征特征类型丰富既有数值型年龄、月收入、工龄又有分类型部门、职位、婚姻状况。存在明显的类别不平衡问题离职人数通常远少于在职人数这在真实业务中极其常见。模型结果可以直接落地预测离职概率后HR部门可以做针对性干预。从一个入门者的视角看处理这份数据学到的技能几乎可以平移到任何二分类业务场景风控中的违约预测、营销中的用户流失预测、医疗中的疾病风险预测等等。1.2 环境搭建和数据集加载我假设你已经装好了Python 3.8以上版本。如果还没装Scikit-learn直接执行pip install scikit-learn pandas matplotlib seaborn建议顺手装上Jupyter Notebook或者直接用VSCode后面看中间结果方便很多。我实际工作里用的是Jupyter因为数据处理过程要反反复复查看中间变量交互式环境效率高得多。下面用代码生成一份模拟数据结构和真实员工数据保持一致import numpy as np import pandas as pd from sklearn.model_selection import train_test_split # 设置随机种子保证实验结果可复现 np.random.seed(42) n_samples 2000 data pd.DataFrame({ 年龄: np.random.randint(22, 55, n_samples), 工龄: np.random.randint(0, 15, n_samples), 月收入: np.random.randint(3000, 18000, n_samples), 绩效评分: np.random.randint(1, 5, n_samples), 满意度: np.round(np.random.uniform(0.1, 0.99, n_samples), 2), 加班: np.random.choice([是, 否], n_samples, p[0.4, 0.6]), 部门: np.random.choice([技术, 销售, 人事, 财务, 市场], n_samples), 工龄与收入比: np.round(np.random.uniform(0.3, 2.8, n_samples), 2) }) # 构造离职标签低满意度、低收入、加班的员工离职概率更高 leave_prob 0.2 (5 - data[绩效评分]) * 0.06 (0.6 - data[满意度]) * 0.5 leave_prob np.clip(leave_prob, 0.05, 0.95) data[离职] (np.random.rand(n_samples) leave_prob).astype(int) print(data.shape) print(data[离职].value_counts(normalizeTrue))这段模拟代码背后是有业务逻辑的我把离职概率设计成和绩效、满意度、加班强相关这样后面模型训练时能学出有意义的规律。你如果用的是真实数据集前几步就是pd.read_csv()读入文件先看看有几行几列、缺失值多少、目标变量分布如何逻辑完全一致。2. 数据预处理决定模型上限的关键环节2.1 解决类别不平衡问题的两种思路先看刚才那个value_counts(normalizeTrue)的输出。我跑过的结果里离职率大约是22%在职率78%。表面上看这不算特别极端但如果你不加任何处理直接训练模型很容易走向一个极端——把所有样本都预测成在职准确率照样能到78%。这个现象背后的原因很简单大多数分类算法都在最小化整体误差少数类样本对误差的贡献太小算法懒得去学它的规律。处理类别不平衡有两条主流技术路线数据层面过采样复制少数类样本如SMOTE算法、欠采样丢弃部分多数类样本。入门阶段我建议先理解思路不要一上来就用SMOTE。算法层面在模型里调整类别权重参数。Scikit-learn里几乎所有分类器都有class_weightbalanced这个选项它会让算法自动给少数类样本更高的惩罚权重。我的建议是先不处理训练一个基线模型看效果如果明显偏向多数类再加上class_weightbalanced。这样你能直观感受到不平衡问题的影响而不是盲目套用方法。2.2 分类型特征的编码绝不要直接塞进模型这是新手最容易踩的一个坑。我们的数据里有加班和部门两列是字符串机器学习模型只认数字所以必须转换。对于二分类别加班是/否用LabelEncoder就行。对于多分类别部门有5个值如果直接用1、2、3、4、5这样的整数编码模型会把这个特征当成有序的连续变量——这意味着财务和销售之间的数值距离被强行赋予了含义这完全是误导。正确做法是使用独热编码One-Hot Encoding把1列部门拆成5列每列用0或1表示是否属于该部门。Scikit-learn提供了现成工具from sklearn.preprocessing import OneHotEncoder # 对分类型特征进行独热编码 categorical_cols [加班, 部门] encoder OneHotEncoder(sparse_outputFalse, handle_unknownignore) encoded encoder.fit_transform(data[categorical_cols]) # 拼回到原数据 encoded_df pd.DataFrame(encoded, columnsencoder.get_feature_names_out(categorical_cols)) data_encoded pd.concat([data.drop(columnscategorical_cols), encoded_df], axis1)这里我想多解释一个细节handle_unknownignore这个参数。如果训练集里部门只有5类但未来预测时来了一个运营部Encoder不会报错而是把该列全部置为0。这在模型上线阶段非常实用能避免因为新类别导致程序崩溃。2.3 特征缩放逻辑回归需要决策树无所谓如果把数值型特征直接喂给逻辑回归问题来了月收入从3000到18000满意度从0.1到0.99年龄从22到55。逻辑回归在学习时是基于特征的加权和特征量纲差异大会让优化过程非常不稳定甚至导致某些特征被模型忽略。这时候就需要标准化Standardization让每个特征变成均值为0、标准差为1的分布。数学上很简单原始值 - 均值 / 标准差。Scikit-learn的StandardScaler一句话搞定from sklearn.preprocessing import StandardScaler scaler StandardScaler() numeric_cols [年龄, 工龄, 月收入, 绩效评分, 满意度, 工龄与收入比] data_encoded[numeric_cols] scaler.fit_transform(data_encoded[numeric_cols])注意虽然我现在就在数据全集上做了标准化但更规范的做法是先划分训练集和测试集然后在训练集上fit再transform在测试集上只transform。为什么因为测试集模拟的是未来新来的数据你不应该用整批数据的均值去标准化它——否则就发生了数据泄漏Data Leakage测试结果会虚高上线后性能就没那么好了。这一点我放在第4章结合数据集划分再细说。2.4 为什么必须划分训练集和测试集有些同学拿到数据就想全量训练然后报告准确率95%这是入门阶段最容易犯的严重错误。你想想如果期末考试的题目和平时作业完全一样你能考满分但那能说明你掌握了知识吗机器学习也一样如果拿训练过的数据再去评估模型理论上可以把答案背下来过拟合这个虚高的分数完全没有参考价值。正确做法是把数据分成三份训练集用来学习参数、验证集用来调参、测试集用来最终评估。入门阶段可以先分成训练集和测试集比例为7:3或8:2from sklearn.model_selection import train_test_split X data_encoded.drop(columns[离职]) y data_encoded[离职] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy )这里又一个关键参数是stratifyy它的作用是让训练集和测试集中离职与在职的比例和原始数据保持一致。如果不设置这个参数随机划分很可能让测试集里恰好没有几个离职样本那模型的评估指标就失真了。3. 模型构建从逻辑回归这个万能基线起步3.1 为什么第一个模型必须是逻辑回归很多初学者上来就想玩随机森林、XGBoost让我说这是走弯路。在业务实战中逻辑回归永远是第一个该尝试的模型。原因有三训练速度快几秒钟内就出结果。结果可解释性强。训练完能直接看到每个特征的权重系数正负号和大小都有业务含义。它是一个很好的基准线。如果后来你用更复杂的模型却不比逻辑回归好多少那说明你的时间和算力花得没必要。逻辑回归的数学本质是用Sigmoid函数把线性回归的输出压缩到0和1之间输出值可以被直接解释为属于正类的概率。这个概率输出非常宝贵业务方如果要的是哪些人风险高你直接按概率排序就行。训练它的代码是我觉得整个Scikit-learn里面最优雅的一段from sklearn.linear_model import LogisticRegression model LogisticRegression(max_iter1000, class_weightbalanced, random_state42) model.fit(X_train, y_train) # 查看各特征的权重系数 feature_importance pd.Series(model.coef_[0], indexX_train.columns).sort_values() print(feature_importance.head(10))class_weightbalanced就是我前面提到的自动调整类别权重。跑完之后看一看feature_importance你会立刻发现哪些特征在推动离职概率上升哪些特征在压制离职概率。这个可解释性在向业务方汇报时价值极高——老板问你凭什么说这个人要离职你能把原因拆给他听。3.2 从逻辑回归扩展到树模型家族逻辑回归的局限也很明显它只能学线性决策边界。现实中特征和标签的关系往往不是简单线性这时候就需要树模型出马。决策树的核心思想是通过不断问是/否问题把数据切分到最纯的子集中。它不需要特征缩放天然处理非线性关系而且同样可解释你可以真的画出一棵树来看决策路径。缺点是单棵树很容易过拟合深度稍微大一点就背训练集去了。所以在实际项目里单棵决策树很少直接用我们用它的集成版本——随机森林。随机森林的本质是训练很多棵在数据子集和特征子集上随机化的树最后投票决定结果。它相当于一个委员会单个人可能犯错但一群人投票的结果通常稳得多。from sklearn.ensemble import RandomForestClassifier rf_model RandomForestClassifier( n_estimators200, max_depth10, min_samples_leaf5, class_weightbalanced, random_state42 ) rf_model.fit(X_train, y_train)n_estimators200表示200棵树max_depth10限制每棵树的深度防止过拟合min_samples_leaf5要求叶子节点至少5个样本。这些参数不是随便拍的我先跑了几个实验对比得出的组合。关于怎么科学地调参我在第5章会专门讲。3.3 用XGBoost进一步提升模型上限我注意到你给的热词里有xgboost二分类模型这个确实值得提一下。XGBoost是梯度提升树的代表作和随机森林最大的区别在于随机森林是并行地训练多棵独立树而XGBoost是串行地训练树每棵新树都去拟合前面所有树的残差。这种策略让它对数据的拟合能力更强也长期霸榜各类结构化数据竞赛。在Scikit-learn生态里用XGBoost很简单装好xgboost库之后接口风格完全兼容from xgboost import XGBClassifier xgb_model XGBClassifier( learning_rate0.1, max_depth5, n_estimators300, subsample0.8, colsample_bytree0.8, random_state42 ) xgb_model.fit(X_train, y_train)我建议入门者把XGBoost当作进阶增强器来用——先跑通逻辑回归和随机森林理解分类模型的基本流程后再引入它。因为XGBoost的超参数比前两者多一截如果一上来就全上你会分不清是模型结构起的作用还是调参起的作用。4. 模型评估准确率高不代表模型真的好4.1 混淆矩阵分类模型评估的体检报告训练完模型最忌讳只盯着准确率看。我见过太多新人拿着一份准确率95%的结果兴高采烈结果发现模型把所有离职员工都预测成留在等于白做。正确的第一步是看混淆矩阵Confusion Matrix。它把预测结果分成四个格子预测在职预测离职实际在职真负类TN假正类FP实际离职假负类FN真正类TP从这个表衍生出几个核心指标精确率Precision预测为离职的人中真正离职的比例。公式是 TP/(TPFP)。这个指标回答的是我说你要离职准不准。召回率Recall实际离职的人中被成功预测出来的比例。公式是 TP/(TPFN)。它回答的是真正该离职的人我抓住了多少。F1分数精确率和召回率的调和平均两者都高时F1才高。在员工离职这个场景里我更看重召回率——如果一个人真的要离职我没预测出来假负类公司可能错过了挽留窗口反过来如果我预测错了假正类最多是多做一次谈心成本低得多。这就是为什么光看准确率会误导人一个保守的模型把所有样本都判为在职准确率78%但召回率是0这个模型毫无价值。from sklearn.metrics import classification_report from sklearn.metrics import confusion_matrix train_pred model.predict(X_train) test_pred model.predict(X_test) print(训练集表现) print(classification_report(y_train, train_pred)) print(测试集表现) print(classification_report(y_test, test_pred))注意我故意同时打印了训练集和测试集的评估结果。两相对比你能一眼看出模型是否过拟合如果训练集F1很高但测试集大幅下降说明泛化能力差。4.2 ROC曲线和AUC衡量模型排序能力除了分类报告ROC曲线Receiver Operating Characteristic curve也是分类模型评估的标准工具。它的本质是不断改变判断阈值计算每个阈值下的真正率召回率和假正率画成一条曲线。曲线越靠近左上角模型性能越好。而AUC就是曲线下方的面积范围从0.5纯随机瞎猜到1.0完美。为什么有了F1还要看AUC因为F1是在某个固定阈值下算的而AUC不依赖阈值衡量的是模型的排序能力——它有这个区分好坏的内在能力。逻辑回归输出的概率你可以把阈值设成0.3也可以设成0.7不同阈值下精确率和召回率此消彼长但AUC不变。from sklearn.metrics import roc_auc_score y_prob model.predict_proba(X_test)[:, 1] auc roc_auc_score(y_test, y_prob) print(f逻辑回归测试集AUC: {auc:.4f})我的经验习惯是分类问题先看AUCAUC过0.8说明模型有可用价值然后再根据业务需求选择合适的判断阈值。临床试验里如果你需要召回更多离职员工就把阈值调低即使牺牲一些精确率。4.3 交叉验证让模型评估结果更可信单次划分训练集和测试集有一个隐患如果这次划分的测试集恰好比较简单或比较难评估结果就会偏高或偏低波动很大。为了得到更稳定的评估可以用K折交叉验证。思路是这样的把数据分成K份常用5或10每次拿其中1份当验证集其余K-1份当训练集轮流做K次最后取平均结果。这样每个样本都参与过验证评估分数更可靠。from sklearn.model_selection import cross_val_score scores cross_val_score(model, X_train, y_train, cv5, scoringroc_auc) print(f交叉验证AUC: {scores.mean():.4f} (/- {scores.std():.4f}))这行代码让我想起来很多课程里讲交叉验证时都是一带而过但实际项目里它是防运气好的重要手段。你调参时如果只依赖单次划分的结果很容易把模型调到刚好适配这一次测试集的状态换个数据集就崩。5. 超参数调优从手动尝试到自动搜索5.1 决策树/随机森林要调的关键参数模型训练完别急着交差。超参数的选择直接影响模型性能而且每个模型需要重点关注的参数并不相同。以随机森林为例我建议按这个顺序调n_estimators树的数量。太少容易欠拟合太多训练时间和内存开销变大而且提升幅度会越来越小。通常100到300之间就够。max_depth限制树深度防止过拟合。可以先不设让树自由生长观察过拟合程度再逐步收窄。min_samples_leaf叶子节点最小样本数。它比max_depth更好用因为它在限制模型复杂度的同时还保持平滑。max_features每棵树随机选择的特征数量一般默认是特征总数的平方根。一个很实用的土办法是用默认参数训练一次打印训练集和测试集F1看差距。如果训练集远高于测试集就是过拟合该加min_samples_leaf和max_depth如果两者都低就是欠拟合该增加模型复杂度。5.2 GridSearchCV系统化的参数搜索工具手动试参数不仅慢而且容易漏掉好的组合。Scikit-learn提供了GridSearchCV帮你把指定的几组超参数全部跑一遍自动选出最优解。它的内部逻辑就是穷举加交叉验证比如max_depth取[5, 10, 15]min_samples_leaf取[2, 5, 10]总共跑3×39组参数每组做5折交叉验证等于训练了45次模型。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200, 300], max_depth: [5, 10, 15], min_samples_leaf: [2, 5, 10] } grid_search GridSearchCV( RandomForestClassifier(class_weightbalanced, random_state42), param_grid, cv5, scoringroc_auc, n_jobs-1 ) grid_search.fit(X_train, y_train) print(f最佳参数: {grid_search.best_params_}) print(f最佳AUC: {grid_search.best_score_:.4f})n_jobs-1表示用满所有CPU核心。我个人建议新手先用一份较小的参数网格跑通流程看看每个参数的组合分数差异再适当扩大范围。一次跑满全部参数时间成本和算力成本都大没必要。还有一点要提醒GridSearchCV在内部已经做了交叉验证所以你用它调参时不需要手动再留验证集直接把训练集丢进去就行。调出最优参数后再用独立的测试集做最终评估。6. 常见问题与排查技巧实录6.1 数据泄漏问题标准化和编码的隐形陷阱我前面提过数据泄漏这是所有入门项目里最容易被忽略的错误。最典型的场景就是先用全量的均值做标准化再划分训练测试集。这样做的问题在于测试集的信息均值已经被训练过程偷窥到了模型在测试集上的表现是被虚增的。正确姿势是严格按照这个顺序先划分数据集然后在训练集上fit标准化器再transform训练集本身和测试集。同样的逻辑也适用于特征选择——如果你要用SelectKBest之类的特征选择方法必须在训练集上fit不能在全量数据上先选完特征再切分。6.2 类别不平衡导致预测结果一边倒如果你建模后查看测试集的预测标签发现几乎全是0在职没有1离职不需要慌这是不平衡数据集的典型症状。排查顺序是确认y_train的分布是否和原始数据一致stratify参数是否设置。检查模型是否设置了class_weightbalanced。如果仍然一边倒可以尝试在数据层面做欠采样或过采样理解它们对分布的影响。实在不行就用predict_proba输出的概率来排序而不是依赖predict的二值结果。概率排序本身对不平衡没有那么敏感更容易找出可能离职的重点人群。6.3 特征编码时ColumnTransformer的正确用法最后分享一个让我早期踩过不少坑的点手动做编码和标准化时代码一多就容易出错而且容易忘了某些列应该走哪个pipeline。如果你想把流程规范化用Scikit-learn的ColumnTransformer把不同列的预处理步骤统一管起来from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline preprocessor ColumnTransformer( transformers[ (num, StandardScaler(), numeric_cols), (cat, OneHotEncoder(handle_unknownignore), categorical_cols) ] ) full_pipeline Pipeline(steps[ (preprocessor, preprocessor), (classifier, RandomForestClassifier(class_weightbalanced, random_state42)) ]) full_pipeline.fit(X_train, y_train)用Pipeline把预处理和模型打包成一个整体不仅代码更整洁更重要的是当你后续要做交叉验证时交叉验证的每一折都会重新执行一遍预处理流程天然避免数据泄漏。这是我从一个多走了半年弯路的教训里总结出来的——最初我都是手动先处理完再切分后面发现模型上线后效果总不如测试集排查了几天才意识到是数据泄漏在捣鬼。如果让我给刚入门的朋友一个建议不要想着一步到位上XGBoost也不用纠结要不要学深度学习。把Scikit-learn这套流程吃透——数据预处理、构建基线模型、交叉验证评估、调参优化、再评估——你就能解决结构化数据场景下绝大多数分类问题。这套方法论是可迁移的换数据集、换业务流程不变变的只是数据和参数。我实际带过的人里凡是老老实实把这条链路跑通两三遍的后面自己面对新项目时基本都很有底气了。
返回列表