
简介这是面向Python初学者与机器学习入门者的学生成绩预测项目基于Pandas、NumPy和Scikit-Learn等工具实现。资源以记录学生国籍、年级、举手次数、出勤次数等信息的CSV数据为基础完整展示数据清洗、特征选择、特征缩放等预处理流程并利用决策树、支持向量机、随机森林等多种分类器进行建模配合混淆矩阵和可视化图表直观呈现预测效果可用于探究影响学业表现的关键因素。压缩包内含三个文件Python实现脚本、CSV格式数据集和Markdown说明文档整体大小仅八KB体积轻巧便于快速下载与本地运行。目前已有九百六十六人学习或下载项目结构清晰代码与数据分离读者可对照说明文档逐行理解每个环节也能替换成自己的数据完成类似分类任务。通过这份资料能掌握从数据预处理到模型调优的完整思路对入门机器学习分类问题很有帮助。1. 用机器学习预测学生成绩表现先判断这是回归题还是分类题“学生成绩预测”放到真实教务场景里往往不是单一问题。有人想预估期末分数这是回归问题有人想提前圈出“挂科高风险”名单这是分类问题。用 python 做这件事并不神秘本质就是拿历史成绩、考勤、作业完成度这类特征去训练一个模型再对还没出分的学生做推理。适合正在做机器学习入门、想完整走一遍应用流程的从业者也适合手里有一批成绩表、想用数据做学情干预的老师。开始之前先把环境捋顺我建议直接用 pycharm 或 vscode 配好 python 环境然后安装 pandas、scikit-learn后面所有代码都在这套环境上跑。2. 数据准备与特征工程把成绩表变成长度一致的特征矩阵2.1 拿到数据先做三件事看分布、定标签、查缺失成绩数据的原始形态通常是一张宽表行是学生列是性别、缺勤次数、家长学历、平时成绩、期中成绩、期末成绩。第一步不是急着建模而是先用 pandas 把这张表读进来看一眼每列的量纲和缺失情况。我一般会写这样几行import pandas as pd # 有真实数据时用 CSV 路径替换没有就先跑通流程再换全量数据 # df pd.read_csv(student_scores.csv) df pd.DataFrame({ study_time: [2, 1, 4, 3, 2, 5], # 每周学习小时数 absences: [0, 4, 2, 8, 1, 0], # 缺勤次数 parent_edu: [3, 2, 4, 1, 3, 4], # 家长受教育等级 1~4 midterm: [78, 52, 88, 60, 74, 92], # 期中成绩 final_score: [81, 55, 90, 63, 72, 95], # 期末成绩回归的标签 pass: [1, 0, 1, 1, 1, 1] # 是否及格分类的标签 }) print(数据形状, df.shape) print(df.head()) print(每列缺失值数量) print(df.isna().sum()) print(describe 统计) print(df.describe().T)这段代码做了三件事看行数列数判断表是否干净逐个字段查缺失值缺得多的列后期要么补要么丢用describe()看分数分布是否集中在某个区间。study_time这类特征量纲小midterm和final_score量纲大后续如果做线性模型还得考虑标准化。标签的选择也要在这时定清楚如果目标是从学期中段预测期末midterm是拿不到的特征里就不能放如果想做纯历史成绩复盘midterm反而能明显提升精度。这个取舍直接决定模型上线后能不能用是成绩预测里第一个要拍板的事情。2.2 类别特征怎么编码LabelEncoder 和 OneHot 的取舍学生数据里总有几个文本列比如性别、家庭住址、家长职业。python 的机器学习算法不吃字符串必须转成数字。常见做法有两种把类别直接映射成 0/1/2/3或者拆成多个 0/1 列。很多人图省事一律LabelEncoder但对“家长职业”这种没有大小关系的类别0/1/2 的数字大小会给模型传递不存在的排序含义这属于把语义搞歪了。from sklearn.preprocessing import LabelEncoder, OneHotEncoder import pandas as pd # 示例性别和住址拆成独热列 df_demo pd.DataFrame({ gender: [M, F, F, M], address: [U, U, R, R], study_time: [2, 1, 4, 3] }) df_encoded pd.get_dummies(df_demo, columns[gender, address], dtypeint) print(df_encoded) print(列数, df_encoded.shape[1]) # 家长受教育程度有明确等级顺序可以直接映射数值 edu_map {小学: 1, 初中: 2, 高中: 3, 大学: 4} df_demo[parent_edu_level] df_demo[address].map({U: 2, R: 1})这里的取舍原则是没有顺序的类别用OneHot有顺序的等级用数字映射。get_dummies的dtypeint参数把独热列变成 0/1 整数避免 pandas 默认转成布尔值给 sklearn 带来兼容问题。对成绩预测这类样本量通常只有几百到几千的小数据集独热后列数暴涨的风险不大但如果你有“家长职业”这种几十个取值的列建议先用value_counts()看看频次把低频类别合并成一列“其他”。这一步做得越细后面调模型时越省心。2.3 切分数据为什么成绩预测要慎用随机打乱机器学习教科书写的是train_test_split随机切分但成绩预测有自己的特殊性如果同一个学生的多次考试记录同时落进训练集和测试集模型等于提前看到了答案。这类问题在员工离职预测、信用评分里同样存在统称信息泄露。对学生成绩而言比较稳的切分方式是按学生 ID 去重后切分或者严格按时间切分用上一学期的数据训练用本学期数据验证。from sklearn.model_selection import train_test_split X df_encoded.drop(columns[study_time]) # 演示用正式建模时特征要另选 y_reg df[final_score] # 回归标签 y_cls df[pass] # 分类标签 X_train, X_test, y_train, y_test train_test_split( X, y_cls, test_size0.3, random_state42, stratifyy_cls ) print(训练集样本数, len(X_train)) print(测试集样本数, len(X_test)) print(训练集是否及格比例, y_train.mean()) print(测试集是否及格比例, y_test.mean())stratifyy_cls是关键参数它让训练集和测试集里的“及格/挂科”比例保持一致。成绩预测里的分类标签往往不平衡挂科学生可能只占 10%如果随机切分测试集里可能一个挂科样本都没有后面的召回率计算就完全没有意义。我在实际项目里还会再打一层保险先按学生 ID 分组再用GroupShuffleSplit切分保证同一个学生的所有记录不会被拆到两侧。这个细节在公开数据集上不明显但拿到真实的年级成绩单时几乎每张表里都有同一个学生补考、重修的多条记录。3. 建模对比回归和分类各跑一个基线用同一套数据说话3.1 回归基线线性回归 vs 随机森林R2 和 MAE 怎么读在正式开始调参之前我会先跑两个最朴素的模型线性回归和随机森林。前者能让你看到特征和成绩之间的线性关系有多强后者能暴露非线性规律。用同一份训练集和测试集对比不给任何调参跑出来的数字就是后续所有优化的起点。from sklearn.ensemble import RandomForestRegressor from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, r2_score from sklearn.model_selection import train_test_split X df[[study_time, absences, parent_edu, midterm]] y df[final_score] # 注意如果做真实预测midterm 应被剔除此处仅为对比演示 X_reg_train, X_reg_test, y_reg_train, y_reg_test train_test_split( X, y, test_size0.3, random_state42 ) models { 线性回归: LinearRegression(), 随机森林: RandomForestRegressor(n_estimators100, random_state42) } for name, model in models.items(): model.fit(X_reg_train, y_reg_train) pred model.predict(X_reg_test) print(f{name}: MAE {mean_absolute_error(y_reg_test, pred):.2f}, R2 {r2_score(y_reg_test, pred):.2f})MAE是平均绝对误差直接理解成“预测成绩和真实成绩平均差多少分”对教务场景最直观。R2是模型解释了多少成绩方差学生成绩受太多偶然因素影响R2 在 0.3 到 0.5 之间就已经有实用价值不必追求 0.9 以上。跑完这组对比你会看到两个现象线性回归的 R2 通常比随机森林低但随机森林的 MAE 有时反而更大原因是树模型在个别极端分数上错得离谱。两个模型一起看比单看一个指标更能判断数据里到底有没有可挖掘的信号。3.2 分类基线逻辑回归 vs 梯度提升预测挂科风险回归任务预测具体分数分类任务直接判断会不会挂科。挂科预测在很多学校比分数预测更受欢迎因为它的结论能变成“预警名单”。这里的标签是把期末成绩按 60 分切成 0/1再跑逻辑回归和梯度提升分类器。分类模型的评估不能只看准确率挂科样本少的时候一个“全部预测为及格”的模型也能拿到 85% 准确率但没有任何管理价值。from sklearn.ensemble import GradientBoostingClassifier from sklearn.linear_model import LogisticRegression from sklearn.metrics import confusion_matrix, recall_score from sklearn.model_selection import train_test_split X_cls df[[study_time, absences, parent_edu, midterm]] y_cls df[pass] X_cls_train, X_cls_test, y_cls_train, y_cls_test train_test_split( X_cls, y_cls, test_size0.3, random_state42, stratifyy_cls ) models { 逻辑回归: LogisticRegression(max_iter1000), 梯度提升: GradientBoostingClassifier(random_state42) } for name, model in models.items(): model.fit(X_cls_train, y_cls_train) pred model.predict(X_cls_test) tn, fp, fn, tp confusion_matrix(y_cls_test, pred).ravel() recall recall_score(y_cls_test, pred) accuracy (tp tn) / (tn fp fn tp) print(f{name}: 准确率 {accuracy:.2f}, 挂科召回率 {recall:.2f}, 预测挂科人数 {tp fp})这段代码的关键是confusion_matrix拆包出来的fn漏报的挂科生和tp抓对的挂科生。recall_score算的是“真实挂科的学生里模型抓出了多少比例”。对老师来说漏掉一个挂科生比误报一个正常学生代价更大所以我会优先看召回率。如果两个模型的召回率都不到 0.5说明特征里的信号不足要么补数据要么放弃做高精度预警只做粗粒度分层。3.3 为什么先跑基线而不是直接上复杂模型很多新手拿到成绩数据直接上 XGBoost容易掉进一个陷阱模型很复杂但解释不了也没法判断数据本身值不值得建模。先跑线性回归和随机森林这两个基线本质是给数据集做一次体检。线性回归系数告诉你哪些特征有统计意义随机森林的重要性排序告诉你哪些特征有预测力两者结合就能判断下一步是补特征还是换模型。我见过一个案例班主任提供了考勤、作业、课堂表现十几列数据线性回归 R2 只有 0.15随机森林却到 0.4说明数据里存在明显的非线性交互后来换成梯度提升才真正可用。基线模型不是终点但它是判断数据含金量的标尺。4. 常见问题与避坑成绩预测最容易翻车的 5 个环节4.1 数据泄露把当期考试成绩当成特征模型漂亮但上线即废现象训练时 R2 高达 0.97测试集表现也很好等到新学期真正预测时模型给出的结果和实际差距巨大基本不能用。原因建模时把“期中考试成绩”放进了特征而期中考试和期末考试的关联本来就极强模型学到的是“期中高则期末高”的捷径。到了真正预测的场景新学生的期中成绩还不存在特征缺失模型只能胡乱猜测。这在成绩预测里是最隐蔽也最致命的问题。解决做特征清单时明确区分“预测时点之前能拿到的数据”和“预测时点之后才产生的数据”。比如在学期第 8 周做预警就只能用前 8 周的数据不能把第 12 周的作业成绩放进去。代码层面做个白名单过滤# 假设当前是学期第 6 周只能使用这之前产生的字段 usable_features [col for col in df.columns if col not in [midterm, final_score, week12_homework]] X df[usable_features]usable_features这个列表就是你的数据口径它比任何模型参数都重要。后续每次加特征都要先问一句这个字段在预测当天拿得到吗拿不到就不能进模型。4.2 类别不平衡挂科样本太少模型无脑预测“通过”现象准确率 85%看起来不错但打开混淆矩阵发现挂科学生一个都没预测出来全部被判成了“及格”。原因真实班级里挂科比例往往只有 5% 到 15%极端的类别不平衡会让模型觉得“全预测为及格”的损失最小算法根本学不到挂科样本的规律。解决先算标签比例再用class_weight或者在评估指标里加重对少数类的关注。print(挂科样本占比, df[pass].mean()) from sklearn.linear_model import LogisticRegression model_balanced LogisticRegression(max_iter1000, class_weightbalanced) model_balanced.fit(X_cls_train, y_cls_train)class_weightbalanced会自动给少数类更高的权重让模型不敢忽视挂科样本。如果数据量够大也可以考虑 SMOTE 过采样但成绩数据通常只有几百条过采样容易制造重复样本我用下来感觉class_weight更稳。评估时不要只看准确率把recall_score和precision_score一起打出来宁可多捞几个误报的也不要漏掉真挂科的。4.3 过拟合R2 冲到 0.98实际上是模型在背答案现象训练集上预测误差近乎为零测试集上误差马上放大几倍模型换一批数据就失效。原因学生成绩数据样本量小随机森林、梯度提升这类树模型如果不限制深度会把每个训练样本的细节都记住包括噪声。样本越少越容易过拟合。解决用交叉验证代替单次切分同时给树模型加上深度和叶子节点限制。from sklearn.model_selection import cross_val_score from sklearn.ensemble import RandomForestRegressor model RandomForestRegressor( n_estimators200, max_depth4, # 限制树深度 min_samples_leaf5, # 叶子节点最少样本数 random_state42 ) scores cross_val_score(model, X, y, cv5, scoringneg_mean_absolute_error) print(5 折 MAE, -scores.mean().round(2))max_depth4和min_samples_leaf5是成绩建模里两个非常实用的参数。前者控制树的复杂度后者强制叶子节点有足够样本量防止模型为了一个学生单独分出一个分支。交叉验证的意义在于用多份数据轮流做验证成绩模型跑出来的分数如果每折差异巨大说明模型对某批特定样本过度敏感这个问题不解决调参没有意义。4.4 分数分布偏斜期末成绩都挤在 70 到 90 分回归模型拉不开差距现象MAE 很小只有 3 分左右但预测结果全都落在均值附近高分段和低分段完全预测不出来。原因期末成绩的方差很小学生差距本来就不大回归模型在压缩的分数区间里很难学到区分度。更糟的是如果成绩整体偏高模型只要预测“大家都及格”就能拿很高的准确率但这对教学管理没有任何帮助。解决把回归目标改成分档分类或者对目标做变换。比如划分“优秀/良好/及格/不及格”四个等级df[score_level] pd.cut( df[final_score], bins[0, 60, 70, 85, 100], labels[不及格, 及格, 良好, 优秀] ) print(df[score_level].value_counts())pd.cut把连续分数映射成有序分类标签模型从“猜分数”变成“猜区间”对分数集中分布的数据往往更稳定。如果必须预测连续分数可以对标签做np.sqrt或np.log1p变换把两端的差异拉大预测完再变换回来。这里要记得变换后的 MAE 不能直接和原始分数 MAE 对比需要先做逆变换再计算。4.5 线性模型漏掉交互效应缺勤和学习时间不是独立影响成绩现象线性回归的残差图出现明显的曲线形状或者模型在低缺勤高学习时间的学生身上系统性低估。原因学生成绩的特征之间经常存在交互效应。缺勤次数少但学习时间短的学生和缺勤多但学习时间长的学生期末表现可能有完全不同而线性回归默认每个特征是独立起作用的。解决一种方法是直接换成树模型随机森林和梯度提升天然能捕捉交互另一种是在线性模型里手动加交互项。df[absence_study_interact] df[absences] * df[study_time]这行代码把缺勤次数和学习时间相乘生成一个交互特征放进线性回归后模型就能识别“缺勤多但学习时间也长”这种组合的独特影响。我在实际做成绩预测时更偏向加这种可解释的交互项而不是直接上一堆高阶多项式因为给班主任解释时“缺勤多但补课多的学生如何如何”比一个抽象的高次项要好懂得多。5. 特征重要性与结果解释让模型输出变成能汇报的学情结论5.1 用 permutation_importance 看哪些特征真正在起作用树模型自带feature_importances_属性但这个值对高基数特征和高度相关的特征有偏向有时候会把一个没实际意义的特征排到前面。置换重要性permutation importance是更可靠的替代方案把某一列数据随机打乱再观察模型误差上升多少上升越多说明这个特征越重要。这个逻辑很直观也容易讲给非技术背景的老师听。from sklearn.inspection import permutation_importance rf_model RandomForestRegressor(n_estimators200, max_depth4, random_state42) rf_model.fit(X_reg_train, y_reg_train) result permutation_importance( rf_model, X_reg_test, y_reg_test, n_repeats10, random_state42 ) importance_df pd.DataFrame({ 特征: X_reg_train.columns, 重要性: result.importances_mean, 标准差: result.importances_std }).sort_values(重要性, ascendingFalse) print(importance_df)n_repeats10表示每列打乱 10 次取平均值数值越大越稳定。注意这里X_reg_test里有midterm如果做了数据口径的剔除测试集也要保持同样的列。跑完这段你通常会发现缺勤次数和学习时间的重要性排名比预想的高而家长学历这类社会背景特征的重要性会低一些。这个结论对后续干预最有价值提醒老师能改变的行为特征比不能改变的背景特征更值得关注。5.2 把预测结果导回 DataFrame形成可汇报的学情分析表模型跑完不是终点预测结果要变成教务能直接用的表格。我会把学生编号、真实成绩、预测成绩和误差放在同一张表里按预测风险从高到低排序再写入 Excel 发给班主任。python 写 Excel 用to_excel就够了不需要额外配置复杂的库。result_df pd.DataFrame({ student_id: range(1, len(y_reg_test) 1), 真实成绩: y_reg_test.values, 预测成绩: pred.round(1), 误差: (y_reg_test.values - pred).round(1) }) result_df[风险等级] pd.cut( result_df[预测成绩], bins[0, 60, 75, 100], labels[高风险, 中风险, 低风险] ) result_df result_df.sort_values(预测成绩, ascendingTrue) result_df.to_excel(student_risk_report.xlsx, indexFalse) print(result_df.head(10))这里我把预测分低于 60 的标记为高风险60 到 75 是中风险75 以上是低风险然后按预测成绩升序排列。班主任拿到这张表后只需要看最上面十几行那就是下个月需要重点关注的名单。排序 分档这两个动作比单纯给出一列预测分数实用得多。如果你担心误报还可以把误差列加进来凡是预测成绩在 60 分边缘波动58 到 62 之间的学生单独标记出来这些人适合做更细致的二次评估。5.3 给教学管理者的汇报模板数字要说成“建议”而不是“判词”做成绩预测项目最容易忽略的是结果解释的边界。模型告诉你某学生预测成绩 57 分你不能直接写“该生预计挂科”因为模型有误差而且学生状态会变化。我一般会在汇报表格里加一列“关注建议”用规则引擎把预测结果翻译成动作def suggest_action(row): if row[风险等级] 高风险: return 建议每周单独辅导重点检查缺勤原因 elif row[风险等级] 中风险: return 建议纳入小组学习关注作业完成度 else: return 保持当前学习节奏 result_df[关注建议] result_df.apply(suggest_action, axis1)这段代码不涉及任何模型魔法但它决定了业务方愿不愿意用你的结果。模型输出的是概率和分数班主任需要的是下一步做什么。成绩预测项目里算法只占一半工作量另一半是把预测结果翻译成教务能执行的动作。真正投产时你还要在汇报里注明模型的适用边界比如“模型基于上学期数据训练对转学生和复读生可能失效”这句话能省去后面很多解释成本。6. 进阶用 LightGBM 处理非线性和类别特征并做一次上线前验证当特征列数变多、样本量上千后我会把模型换成 LightGBM。它对缺省值有内置处理也支持直接传入类别特征省掉一部分编码工作。参数上我习惯先固定learning_rate0.05再调num_leaves和min_child_samples。import lightgbm as lgb lgb_model lgb.LGBMClassifier( learning_rate0.05, num_leaves16, min_child_samples20, n_estimators300, random_state42 ) lgb_model.fit(X_cls_train, y_cls_train, feature_namelist(X_cls_train.columns))num_leaves16是限制叶子数上限避免在几百条学生数据上长出过深的分支min_child_samples20保证每个叶子节点至少有 20 个样本和随机森林的min_samples_leaf作用一致。这两个参数先定下来再调learning_rate和n_estimators的组合一般来说不会翻车。上线前最重要的验证不是看测试集指标而是做一次“时间线外推”用上学期数据训练用本学期已出的成绩做验证。我最早做成绩预测时把期中分数直接放进特征R2 跑出 0.97沾沾自喜了半个月结果新学期第一次预测就全面翻车。后来才明白预测这件事最怕的不是模型不够复杂而是特征口径里混进了“未来信息”。现在每做一版模型我都会先列一个特征时间线每个字段标注“第几周可获取”超时的数据一律不用。模型指标可以慢慢优化这个数据口径一旦错了后面所有工作都要推翻重来。希望这个坑能帮你省掉几个月的返工时间也祝你的成绩预警项目真正落地到班主任的办公桌上。本文还有配套的精品资源点击获取