ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

学生成绩预测实战:用Python实现分类模型与特征工程

2026/9/11 21:44:40 拓冰建站 浏览量
学生成绩预测实战:用Python实现分类模型与特征工程 简介面向初学者的机器学习实战项目用Python实现学生成绩预测与影响因子分析。项目以CSV数据集为基础涵盖国籍、年级、举手次数、出勤率、学习时数等多元特征完整演示数据清洗、特征选择与缩放、决策树/支持向量机/随机森林等多模型构建并结合交叉验证与网格搜索进行调优输出图表和混淆矩阵辅助结果解读。压缩包共3个文件包含Python主脚本、CSV数据文件及README说明文档整体仅8KB轻量易用。资料已吸引966人学习下载适合希望快速上手分类建模、理解模型评估全流程的Python学习者与数据科学初学者。通过该资源可掌握从数据预处理到模型调参的实际操作思路并可直接复用代码扩展至其他学业表现预测场景。1. 学生成绩预测这个任务很多人一开始就选错了模型期中考试之后教务处的老师最想知道一件事期末谁有挂科风险。表面上看给一批学生算出“期末大概考多少分”再把名单按分数排出来就能解决于是新手拿到“学生成绩预测”这个题目第一反应就是把 G1、G2、出勤率、作业完成度这些特征丢给线性回归或者 MLP。但实际交付的时候你会发现学校要的从来不是一个精确到小数点的分数而是一份“该重点盯谁”的名单。把回归问题收敛成分类问题预测效果和业务可用性反而会翻一个台阶。这个标题里最需要先想清楚的不是“哪个机器学习算法更厉害”而是这个任务到底在回答什么问题。本文后面的内容都会围绕一个核心思路展开用 Python 实现一套从数据处理、特征工程到模型调参的学生成绩预测管线并且把验证方式选对让结果真正经得起跨班级、跨学期的考验。适合刚学完 sklearn 基础、正在找项目练手的人也适合需要把模型交付给教务系统的算法工程师。2. 先做一个能跑通的基线随机森林与逻辑回归的 Python 实现2.1 为什么不是一上来就做深度学习学生成绩数据有个明显特征样本量小通常一个年级只有几百到两三千条记录特征数量在十几到几十之间而且大量变量是类别型的比如家长职业、上学路途远近、是否参加过补习班。这种形态的表格型数据深度学习很难发挥优势反而容易在小样本上过拟合。梯度提升树和带正则的逻辑回归才是这个场景下投入产出比最高的选择。再往深一层说成绩预测本质上是多因子影响下的模式识别特征之间有很多交互项。比如“成绩下滑”比“绝对分数低”更有预警意义这个信息要靠前两次考试成绩的差值来表达模型没法凭空从原始字段里自动合成这种东西。所以这类任务的提升重点在特征工程不在模型复杂度。2.2 基线代码与 AUC 口径这里用常见的 UCI Student Performance 数据集作为示例。数据里 G1、G2 是前两次考试的成绩G3 是期末成绩特征是学生背景和在校表现。要用机器学习预测成绩表现第一版代码关键是把“及格线”这个口径定下来然后把验证方式组好。import pandas as pd import numpy as np from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier # 载入数据注意这个数据集的分隔符是分号 df pd.read_csv(student-mat.csv, sep;) # 构造二分类标签G3 10 视为及格这是 UCI 数据集上常用的切分口径 df[pass] (df[G3] 10).astype(int) # 预测时输入里不能出现 G3 本身但 G1/G2 可以保留 X df.drop(columns[G3, pass]) y df[pass] # 自动区分数值列和类别列 cat_cols X.select_dtypes(object).columns.tolist() num_cols X.select_dtypes(np.number).columns.tolist() # 类别特征用 one-hot数值特征做标准化 preprocessor ColumnTransformer([ (num, StandardScaler(), num_cols), (cat, OneHotEncoder(handle_unknownignore), cat_cols), ]) # 逻辑回归基线max_iter 调大避免收敛警告 lr Pipeline([ (pre, preprocessor), (clf, LogisticRegression(max_iter1000)), ]) # 分层 5 折交叉验证保证每一折里及格/不及格比例一致 cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) lr_scores cross_val_score(lr, X, y, cvcv, scoringroc_auc) print(LogisticRegression AUC: {:.3f} ± {:.3f}.format(lr_scores.mean(), lr_scores.std())) # 随机森林基线限制树的数量以便快速跑通 rf Pipeline([ (pre, preprocessor), (clf, RandomForestClassifier(n_estimators200, random_state42)), ]) rf_scores cross_val_score(rf, X, y, cvcv, scoringroc_auc) print(RandomForest AUC: {:.3f} ± {:.3f}.format(rf_scores.mean(), rf_scores.std()))这段代码里的门道主要在三个地方用roc_auc而不是accuracy作为评分指标。学生成绩数据里及格和不及格往往不平衡某所学校可能 80% 的学生都能及格这时候准确率会被多数类带跑模型看似分得很准实际上对不及格群体完全无感。AUC 看的是排序能力对不平衡更稳健。StratifiedKFold保证每一折的及格率与整体一致。如果像普通KFold那样随机切分某些折里可能只有一两个不及格样本模型训练起来完全没有区分度。输出里要同时看均值和标准差。AUC 均值代表模型的整体水平标准差代表稳定性。如果均值 0.90 但标准差 0.15那模型大概率在某个折里表现崩了这时候更可能是数据切分或特征泄漏的问题而不是模型本身不行。2.3 数据集切分和标签口径的参数说明再回到“预测”这个词本身。预测意味着模型见到的时间点早于它要预测的那个时间点。所以严格的做法是如果要用 G1、G2 的成绩去预测期末 G3那么训练集里的 G1/G2 和验证集里的 G1/G2 应该是两次真实的考试数据而不是把同一次考试数据随机切出来的两份。这里会有一个很多入门项目都踩过的坑同一个学期内G1、G2、G3 是时间上先后发生的随机切分相当于让模型“看到”了未来的信息分布AUC 会虚高。跨学期做时间切分才是真实场景。这个原则落实到参数上就是cv层的shuffleTrue可以让同分布数据更稳定但如果要做严谨的性能估计应该手动构造一个按时间排序的训练/验证切分比如用前 80% 学号的成绩做训练后 20% 做验证。下面的是更贴近业务场景的做法。# 假设 df 已按时间顺序排列直接按行号切分 train_size int(len(df) * 0.8) train_df df.iloc[:train_size] test_df df.iloc[train_size:] X_train train_df.drop(columns[G3, pass]) y_train train_df[pass] X_test test_df.drop(columns[G3, pass]) y_test test_df[pass] lr_ts Pipeline([ (pre, preprocessor), (clf, LogisticRegression(max_iter1000)), ]) lr_ts.fit(X_train, y_train) test_auc roc_auc_score(y_test, lr_ts.predict_proba(X_test)[:, 1]) print(Temporal split AUC: {:.3f}.format(test_auc))注意这里roc_auc_score需要从sklearn.metrics手动导入上面的代码片段依赖它。时间切分得到的 AUC 通常会比随机折低一些这很正常它更接近模型下学期真正上线时的水平。切分方式意义适合场景StratifiedKFold每折保持类别比例特征筛选、算法选型的快速对比时间顺序切分严格模拟未来数据最终模型上线前的最后评估3. 特征工程让机器学习模型看到成绩之外的规律3.1 先排雷最容易出现的标签泄漏特征工程做得越多泄漏的风险越大。成绩预测场景里最常见的泄漏是把“最终结果”的某种编码混进了特征。比如有些学生记录里有一个字段叫“是否获得助学金”如果这个字段是在期末后根据成绩评定的它就不能进入模型。另一个高频泄漏点是构造特征时用到了整张表的统计量比如用全年级的 G3 均值去填充缺失值这等于把未来信息写进了训练集。排查泄漏的办法很简单把每个特征按“它是否在学生参加期末考试之前就能拿到”这个标准重新过一遍。拿不准的字段直接丢掉成绩预测任务的特征来源应当是考勤、作业、先前考试成绩、家庭背景这些确定先于预测时点存在的变量。这比多调出一个百分点 AUC 重要得多因为泄漏会让模型在换一个学期时完全失灵。3.2 三个值得手工构造的成绩特征第一版模型跑通之后接下来值得花时间的不是换算法而是做特征。在成绩数据上有三个特征几乎每次都能带来明显的 AUC 提升历史成绩均值把 G1 和 G2 取平均代表学生的稳定水平。它比单独的 G1 或 G2 更抗噪声。成绩波动幅度G2 减 G1 的差值。差值为正代表进步为负代表退步绝对值大代表不稳定。其实更合理的是归一化后的差值去掉不同科目难度的量纲影响这一点做多科目预测时要特别注意。零分标记如果 G1 或 G2 中有任何一次成绩是 0这种极端值往往代表缺考或重大异常直接作为一个布尔特征喂给模型。树模型对 0 值本身不敏感但显式标记出来能让分裂动作更直接。这种手工特征的意义在于机器学习模型做的是“分段切割”它擅长发现 x 某个阈值时 y 的概率显著变化但不擅长理解“G2 比 G1 低 3 分”这个语义。把语义翻译成显式特征就是在帮算法减负。3.3 One-Hot 编码不是唯一选择高基数类别变量用 Target Encoding学生成绩数据里家庭背景相关的字段很多比如父亲职业、母亲职业、家庭住址。这类变量有几个明显特征类别数量多、某些类别样本极少、类别本身带有强先验信息。如果对此无脑用 One-Hot会出现两个后果一是稀疏矩阵维度暴增几十个类别的字段会变成几十列二是低频类别在训练集中出现次数太少One-Hot 后几乎学不到东西。常见做法是改用 Target Encoding也就是用该类别下历史学生的平均及格率去替换原始类别值。这个编码方式能更高效地捕捉类别与目标变量的相关性。但要注意Target Encoding 本身有泄漏风险必须在交叉验证的每一折内单独计算编码值否则模型直接看到该学生的历史结果AUC 会虚标到接近 1。下面是一个可以直接抄的写法from sklearn.model_selection import KFold from sklearn.preprocessing import LabelEncoder def target_encode(train, test, col, target, k5): 在训练集上分 fold 计算 target encoding再映射到测试集。 参数 k 是 fold 数量用来控制编码的泛化性。 train train.copy() test test.copy() # 记录全局平均值用来处理测试集中出现的未见类别 global_mean train[target].mean() # 也可以测试 KFold 每折的随机性 kf KFold(n_splitsk, shuffleTrue, random_state42) train_enc np.zeros(train.shape[0]) test_enc np.zeros(test.shape[0]) for tr_idx, va_idx in kf.split(train): tr train.iloc[tr_idx] va train.iloc[va_idx] # 当前折内计算该类别的平均目标值 category_mean tr.groupby(col)[target].transform(mean) # 用折内均值填空缺失 temp tr[[col]].copy() temp[category_mean] category_mean va_map temp.drop_duplicates(col).set_index(col)[category_mean] # 验证集和测试集都用训练折内的统计量来转换 train_enc[va_idx] va[col].map(va_map).fillna(global_mean) # 测试集的编码值累积到 test_enc取多折均值 test_enc test[col].map(va_map).fillna(global_mean).values / k train[col _te] train_enc test[col _te] test_enc return train, test这段代码本质上是把一个高基数类别变量换成连续值。每个参数都直接影响模型结果k控制 fold 数量。k 越大每折用于计算编码的样本越少噪声越大k 越小编码越平滑但可能丢失类别差异。一般取 5 到 10。global_mean是全局回落参数。凡是测试集里出现训练集没见过的类别时都用全局均值兜底避免出现缺失值。对测试集的编码用的是训练折内计算的映射不是在测试集上重新计算否则测试集信息会被混入编码过程。一个容易忽视的细节是groupby(col).transform(mean)得到的结果与行对齐是自动完成的不需要额外合并。验证集和测试集转换时如果用.map()遇到编码表中不存在的类别会返回 NaN正好被fillna(global_mean)接住这是一个比较稳妥的写死逻辑。3.4 用随机森林看特征重要度锁定该保留哪些变量在特征工程做完一轮后下一步是看哪些特征真正起了作用避免把维度越堆越高。随机森林训练完成后可以直接打印特征重要度rf_model Pipeline([ (pre, preprocessor), (clf, RandomForestClassifier(n_estimators200, random_state42)), ]) rf_model.fit(X_train, y_train) # 只有随机森林部分才能取特征重要性 rf_clf rf_model.named_steps[clf] # 与预处理后的特征名一一对应 cat_encoder rf_model.named_steps[pre].named_transformers_[cat] cat_names cat_encoder.get_feature_names_out(cat_cols) all_feature_names num_cols list(cat_names) importance pd.Series(rf_clf.feature_importances_, indexall_feature_names) print(importance.sort_values(ascendingFalse).head(10))特征类型预期重要度排序G2 成绩数值高因为它距离 G3 最近G1 成绩数值同样高但贡献低于 G2历史均值手工构造替代性变量排在 G2 之后家长职业 target encoding类别中等取决于学校分层情况零分标记布尔低但稳定偶尔会进前五特征重要度的读数逻辑是这样的如果模型里同一信息源的特征超过两个比如同时有 G1、G2、均值、波动幅度那么重要度会被这几个特征分走看起来每个都不高但不能因此删掉其中某一个否则模型会丢失该信息源的总量。更合理的方式是拿重要度做筛选把排名靠后且业务解释困难的字段直接剔除。4. 模型再进一步从随机森林换到梯度提升树4.1 为什么这个任务里 GBDT 比随机森林更有优势随机森林的基学习器是深度较浅的树通过随机抽样特征来降低每棵树之间的相关性从而降低方差。它在特征维度不高、样本量中等的场景下表现不错但当特征里同时存在数值型、经过 target encoding 后的类别型、以及若干业务构造的交叉特征时梯度提升树的优势会显现出来。核心原因是 GBDT 每一棵树都在拟合前一棵树的残差对特征之间的非线性交互建模更彻底。以学生成绩预测为例特征“家长职业”经过 target encoding 后已经变成连续值它与“成绩波动幅度”之间可能存在交互关系例如特定职业家庭的波动幅度对结果的影响更大这种组合在随机森林里靠随机特征选择也能偶尔捕捉到但概率不高。梯度提升树通过逐步迭代能够在误差较大的区域分配更多注意力对这类交互项的建模更主动。4.2 LightGBM 的常用优化参数直接从可跑的代码开始下面的代码使用lgb.LGBMClassifier配合RandomizedSearchCV快速找一组可用的参数组合。其实核心参数不多搜索时最值得关注的就三个深度相关的量。import lightgbm as lgb from sklearn.model_selection import RandomizedSearchCV from scipy.stats import randint, uniform lgb_model lgb.LGBMClassifier( objectivebinary, metricauc, random_state42, verbosity-1 ) param_dist { n_estimators: randint(200, 800), learning_rate: uniform(0.02, 0.18), # 均匀分布在 [0.02, 0.2] num_leaves: randint(15, 63), max_depth: randint(3, 8), min_child_samples: randint(20, 100), subsample: uniform(0.7, 0.3), # 均匀分布在 [0.7, 1.0] colsample_bytree: uniform(0.7, 0.3), } random_search RandomizedSearchCV( estimatorlgb_model, param_distributionsparam_dist, n_iter30, scoringroc_auc, cvStratifiedKFold(n_splits5, shuffleTrue, random_state42), n_jobs-1, verbose1, random_state42, ) random_search.fit(X_train, y_train) print(Best params:, random_search.best_params_) print(Best AUC:, random_search.best_score_)这里值得说明的参数有n_estimators树的数量。注意不要一上来就设 3000配合learning_rate看学习率低则需要的树更多搜索范围不要拉得太宽。小数据场景 200 到 500 棵已经足够。num_leavesLightGBM 特有的叶子节点数。调大能提高模型容量但也更容易过拟合。经验值是max_depth与num_leaves要配套调整深度大、叶子少会浪费容量。min_child_samples叶子节点里最少需要的样本数。这个参数在成绩预测这种小数据集上非常关键默认 20 可能太小导致叶子节点被单个异常学生带偏。搜索范围设在 20 到 100实际使用时观察最终模型在验证集上的波动。subsample和colsample_bytree行采样和列采样相当于给模型加随机性能有效防止树与树之间高度相关。小数据时subsample不要设到 0.6 以下否则每棵树看到的样本太少了。用RandomizedSearchCV而不是GridSearchCV是不想在参数组合数上失控。6 个参数、每种 4 个候选如果做网格搜索就是 4096 组每组跑 5 折总共两万次训练完全没必要。随机搜索 30 组在这个规模上已经能覆盖参数空间中的有效区域。4.3 类别不平衡处理不只靠 class_weight如果数据里及格学生远多于不及格学生比如 9:1光靠scale_pos_weight不足以让模型在少数类上学到足够信息。常见做法是按正负样本比例设置scale_pos_weight然后再观察验证集上不同阈值下的表现。# 让模型的负样本权重变高惩罚漏掉的不及格学生 scale_pos_weight (y_train 0).sum() / (y_train 1).sum() # 训练时传入该参数签名为 scale_pos_weight final_model lgb.LGBMClassifier( objectivebinary, metricauc, scale_pos_weightscale_pos_weight, **random_search.best_params_ )很多人在这一步会犯的错误是模型预测输出的是概率然后把 0.5 当成固定阈值去判断及格/不及格。实际上当类别不均衡时0.5 这个阈值并不反映业务最优切分点。正确做法是找到训练集上精确率和召回率平衡的点或者直接按业务要求来如果目标是盯住“必挂”学生那就把阈值调低比如 0.3宁可多预警几个也不能漏掉真正会挂科的人。这个阈值调整不能放在交叉验证之前做否则阈值本身也变成了一个泄漏源。先完成模型训练再单独在验证集上测试不同阈值的表现选一个业务可接受的点。5. 上板前的检查清单泄漏、阈值与重训节奏到这一章模型已经能从数据里学出“谁可能成绩下滑”的规律但离真正能用还差最后一步。这步不是加更多特征而是做一轮严格的模型体检。检查项做法判定标准标签泄漏逐一核对每个特征在预测时间点是否可知任何可疑字段都丢弃时间切分验证用第一个学期的数据训练第二个学期做验证AUC 下降不超过随机切分结果的 15%阈值合理性在时间切分的验证集上画 PR 曲线阈值下精确率和召回率的乘积最大特征共线性计算数值特征间的相关系数大于 0.9 的特征只保留业务意义更强的一个跨学期稳定性比较两个学期的特征均值均值变幅超过两倍标准差的特征要警惕概念漂移重训节奏上如果学校是每个学期一考那至少一个学期重训一次。不要试图用去年的模型直接跑今年的数据考试难度、生源结构都在变成绩分布整体上移时同样的特征值对应及格概率的语义就变了。最简单的监测方式是每次考完一批数据就对比新数据在该模型上的平均预估分数与真实成绩偏移量超过设定值就触发重训。阈值方面还有一个容易被忽略的细节预测的目标若是“给老师一份重点关注名单”那模型应当输出概率加完再列表格而不是只输出布尔标签。把概率排名展示出来让老师看到“该生处于及格线边缘”比单纯一个“预警”更有决策价值。按概率从低到高排序后取前 N 名作为重点关注对象比固定阈值更适应不同班级的情况。把这份检查表贴在你的训练脚本上方每次跑新数据时逐项过一遍比在模型上多刷出一两个百分点的收益更实用。本文还有配套的精品资源点击获取