机器学习预测学生成绩:分类模型与数据清洗全流程解析

发布时间:2026/10/3 6:09:25
机器学习预测学生成绩:分类模型与数据清洗全流程解析
简介这份压缩包是一个完整的学生成绩预测机器学习小项目适合初步接触数据挖掘与监督学习的开发者用来理解分类建模流程。项目基于Python与Pandas、Scikit-Learn等库围绕一个包含学生国籍、年级、举手次数、出勤情况等属性的CSV数据集展开使用决策树、支持向量机、随机森林等分类器对比预测影响成绩的关键因素并通过混淆矩阵与图表辅助评估模型效果。除模型构建与评估外还涉及数据清洗、特征选择、特征缩放等预处理步骤以及探索性数据分析内容可帮助读者完整感知一个入门级ML项目的推进脉络。包内共3个文件包括可运行的Python脚本、原始CSV数据以及说明文档整体仅8KB结构简单适合快速上手。目前已有966人学习选它可作为入门级实战参考既能直接查看完整代码与数据格式也能复现数据预处理、模型调优与可视化流程对理解分类模型在真实教育数据上的应用很有帮助。1. 机器学习预测学生成绩这个开源包到底能做什么做教育数据分析和机器学习入门的人多半会遇到同一个困惑网上教程一堆但真正能跑通、数据完整、还带可视化结果的项目并不多。这个名为 StudentPerformancePrediction-ML 的项目就是少见的能直接落地的那种——它用 Python 和 Pandas 读取一份包含学生国籍、年级、举手次数、出勤率、学习时长的 CSV 数据集然后用决策树、支持向量机、随机森林等分类器去预测“哪些因素在真正影响学生成绩”最后用图表和混淆矩阵把结果展示出来。对新手来说它的价值在于提供了一个完整可复现的机器学习流程从数据清洗、特征选择、特征缩放到模型训练、交叉验证、网格搜索调参每一步都有对应代码。对熟手来说它是一份值得下载的参考模板拿到后可以替换成自己的数据集快速做一轮分类对比实验。整个项目基于 Python 的 Pandas、NumPy、Scikit-Learn结构清晰压缩包里包含 AI-Data.csv 数据文件、Project.py 主脚本和 README 说明文档解压就能跑。2. 读懂数据才能定模型AI-Data.csv 里到底存了什么2.1 数据集的字段构成与含义这个项目的核心数据源是 AI-Data.csv不是那种动辄几十万行的工业级数据而是一份适合课堂实验和教育分析的中小规模数据集。从项目摘要来看它记录的是不同国籍、不同年级学生的行为与背景信息包括举手次数、出勤人数、学习时数等所谓的 SOE 决定因素。这里先解释一下 SOE 这个概念。在教育数据挖掘里SOE 通常指 Student Observable Engagement即学生可观察的参与度指标。和考试成绩这类结果变量不同SOE 特征描述的是学生在学习过程中的行为表现——课堂上举手的频率、出勤的情况、课后花在学习上的时间。这些指标的优势在于它们可以在学期中持续采集不像期末成绩那样要等考试结束才知道结果。用这类特征做预测本质上是在回答一个问题学生平时的课堂参与行为能在多大程度上提前预示他的成绩水平。文件里除了这些行为特征还应该包含学生的个人信息、家庭背景和学校信息。从字段类型上看大致可以分成三类类别型特征比如国籍、年级、性别这类数据需要编码后才能喂给模型数值型特征比如举手次数、出勤次数、学习时数这类数据量纲不同有的接近 0有的可能是几百直接丢给 SVM 这类模型会出问题目标变量也就是最终的成绩等级或成绩区间这是模型要预测的标签列。拿到 CSV 文件后第一步不要急着建模先把它读进来看看数据长什么样。我一般会用 Pandas 先做一个快速体检确认行数、列数、是否有缺失值、各字段的类型分布。import pandas as pd df pd.read_csv(AI-Data.csv) print(df.shape) print(df.info()) print(df.head(10)) # 检查缺失值和重复项 print(df.isnull().sum()) print(df.duplicated().sum())这段代码做的事情很基础但很关键。df.shape 告诉你数据有几行几列如果列数和 README 里描述的不一致说明文件可能被改过或者版本不对。df.info() 会输出每一列的数据类型和非空值数量一眼就能看出哪列有缺失、哪列被读成了字符串。df.isnull().sum() 精确统计缺失值df.duplicated().sum() 统计完全重复的行。2.2 数据清洗和预处理的常见做法读进来之后马上要面对的就是数据质量问题。这个数据集整体比较规范但教学用的 CSV 文件往往隐藏着几个典型陷阱类别列里可能有前后空格比如 USA 和 USA 会被当成两种国籍数值列里可能存在除数为零产生的异常值目标列可能分布极度不均衡比如成绩好的样本占了 90%那模型全猜好成绩也能有 90% 准确率意义不大。针对这些问题我按照教育数据挖掘项目最常见的流程来处理。第一步是清洗把空格、大小写不一致、错误占位符统一掉第二步是特征编码把类别型变量转成数值第三步是特征缩放让所有数值特征处在同一个量纲下。# 去除字符串列首尾空格统一大小写 for col in df.select_dtypes(include[object]).columns: df[col] df[col].str.strip().str.lower() # 处理缺失值数值列用中位数填充类别列用众数填充 num_cols df.select_dtypes(include[int64, float64]).columns cat_cols df.select_dtypes(include[object]).columns for col in num_cols: df[col] df[col].fillna(df[col].median()) for col in cat_cols: df[col] df[col].fillna(df[col].mode()[0]) # 查看目标变量分布 print(df[GradeClass].value_counts())这里有几个选择背后的原因。数值列用中位数而不是均值填充是因为中位数对异常值更鲁棒类别列用众数填充本质上是用“大多数人是什么就补什么”的朴素策略对教学项目来说够用。填充完后马上看目标列 GradeClass 的分布如果某个类别的占比超过 85%后文做模型评估时就别只看 accuracy要看精确率和召回率这点在避坑章节会展开说。数据清洗完成后还需要做标签编码和特征缩放。推荐用 Scikit-Learn 里的 LabelEncoder 处理类别标签用 StandardScaler 处理数值特征。需要特别注意的是顺序问题先划分训练集和测试集再在训练集上 fit 缩放器最后用同一个缩放器 transform 测试集。如果先对全量数据做缩放再切分会造成数据泄漏测试集的信息提前进入了训练过程跑出来的准确率会虚高。from sklearn.preprocessing import LabelEncoder, StandardScaler from sklearn.model_selection import train_test_split # 对类别型特征做标签编码 le_dict {} for col in cat_cols: le LabelEncoder() df[col] le.fit_transform(df[col]) le_dict[col] le # 分离特征和标签 X df.drop(columns[GradeClass]) y df[GradeClass] # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 特征缩放先fit训练集再transform测试集 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)这段代码里 train_test_split 的 stratifyy 参数容易被忽略它能让切分后的训练集和测试集里各类别占比和原始数据一致。对于成绩等级这种类别不平衡的目标列加上这个参数能显著降低评估时的偶然性。random_state42 固定随机种子保证每次跑出来的结果一致方便复现实验。3. 先看数据再建模用可视化理解成绩的核心影响因素3.1 相关性分析与特征筛选机器学习项目里最常见的翻车点不是模型选得不对而是没看数据就急着调参。这个项目提供了一套完整的 EDA探索性数据分析代码包括图表和混淆矩阵这些可视化工具不只是为了把结果画得好看它们承担着两个实际任务找出与成绩强相关的特征以及发现特征之间的共线性问题。使用 Pandas 自带的相关系数矩阵可以快速定位哪些特征和目标列的关系最密切。这里说的相关系数默认是皮尔逊相关系数适合检测线性关系取值在 -1 到 1 之间。如果某个特征和成绩的相关系数绝对值在 0.3 以上说明有一定预测价值如果多个特征彼此之间的相关系数超过 0.8则存在共线性会干扰 SVM 这类模型的权重解释。import matplotlib.pyplot as plt import seaborn as sns # 计算相关系数矩阵 corr_matrix df.corr() print(corr_matrix[GradeClass].sort_values(ascendingFalse)) # 绘制热力图 plt.figure(figsize(12, 10)) sns.heatmap(corr_matrix, annotTrue, cmapcoolwarm, fmt.2f) plt.title(Feature Correlation Heatmap) plt.show() # 绘制目标变量分布直方图 plt.figure(figsize(8, 5)) df[GradeClass].value_counts().sort_index().plot(kindbar) plt.xlabel(Grade Class) plt.ylabel(Count) plt.title(Grade Class Distribution) plt.show()corr_matrix[GradeClass].sort_values() 会把所有特征按与成绩的相关程度从高到低排序这是做特征筛选最直接的依据。热力图的作用是看特征之间的两两相关性如果两块深色区域集中在同一行说明这几个特征信息冗余可以考虑只保留其中一个。成绩等级的柱状图则用于确认类别分布是否均衡这个图直接决定了后文评估指标的选择。3.2 特征重要性排序与降维思维在相关性分析之外还可以借助树模型输出特征重要性来交叉验证结论。随机森林和决策树天然支持 feature_importances_ 属性它会给出每个特征在树分裂中被选中的比例比单纯看相关系数更接近非线性关系。需要注意的是不要把特征重要性当作唯一依据它可能会因为树的随机性产生抖动建议结合相关系数一起看。一个常见的做法是先跑一个未调参的随机森林把特征重要性打印出来再和相关系数排序对比。如果某个特征在两份排序里都靠后比如排名垫底的几项可以考虑删掉它们降低模型复杂度。这个项目本身规模不大特征数量有限没必要做 PCA 降维但理解这个过程可以帮你应对后续自己的数据集。from sklearn.ensemble import RandomForestClassifier # 先用默认参数的随机森林看特征重要性 rf_temp RandomForestClassifier(n_estimators100, random_state42) rf_temp.fit(X_train_scaled, y_train) # 按重要性排序输出 importance pd.DataFrame({ feature: X.columns, importance: rf_temp.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance) # 绘制横向条形图 plt.figure(figsize(10, 6)) sns.barplot(dataimportance, ximportance, yfeature) plt.title(Feature Importance from Random Forest) plt.show()跑完这段代码你会得到一张特征重要性排序表。对于这个数据集一个典型的结果是举手次数和出勤率排名靠前家庭背景类特征排名靠后。这说明什么说明学生的课堂参与行为对成绩的预测力比单纯的背景信息更强。这是个有教育意义的结论也是这个项目最值得拿出来讨论的点。需要注意的是 n_estimators100 意味着用了 100 棵决策树如果训练时间太长可以降到 50效果差距通常不明显。4. 多模型对比与调参决策树、SVM、随机森林谁更准4.1 三种分类器的选型理由与适用边界这个项目的核心实验部分是用多种分类器做横向对比。摘要里明确提到了决策树、支持向量机、随机森林这三种模型恰好代表了机器学习算法里三种不同的思路。决策树的核心逻辑是递归划分特征空间每个节点选一个特征和一个切分阈值把样本分成两类逐步细分直到叶子节点足够纯净。它的最大优势是解释性极强生成的树可以直接用图展示教育场景里的人能一眼看懂“举手次数超过 30 的学生更可能拿 A”这样的规则。缺点是单棵树容易过拟合训练集上准确率很高测试集上就掉下来。支持向量机走的是另一条路。它试图找到一个超平面让不同类别的样本间隔最大化。对于成绩预测这种可能是线性不可分的问题SVM 靠核函数把数据映射到高维空间再找超平面默认的 RBF 核能处理大多数非线性关系。它的优点是泛化能力强样本量不大时表现稳定缺点是对特征缩放极其敏感这就是第 2 章先做 StandardScaler 的原因。随机森林是决策树的集成版本同时训练多棵树每棵树用不同的随机子集最终投票决定类别。它在准确率和鲁棒性上通常优于单棵决策树几乎不需要精细调参就能拿到不错的结果是这类项目的默认保险选项。代价是失去了单棵树的直观可解释性。4.2 完整训练与评估流程在这个项目里Project.py 做的事情就是把上面三个模型依次训练、评估、可视化对比。我用一个统一的标准流程来跑先定义模型字典再逐个训练和预测最后汇总指标。这样代码结构清晰后续加新模型也只需要往字典里加一项。from sklearn.tree import DecisionTreeClassifier from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, classification_report, confusion_matrix # 定义统一训练的评价函数 def train_and_eval(model, model_name, X_tr, X_te, y_tr, y_te): model.fit(X_tr, y_tr) y_pred model.predict(X_te) acc accuracy_score(y_te, y_pred) print(f {model_name} ) print(fAccuracy: {acc:.4f}) print(classification_report(y_te, y_pred)) cm confusion_matrix(y_te, y_pred) plt.figure(figsize(6, 5)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.title(f{model_name} - Confusion Matrix) plt.xlabel(Predicted) plt.ylabel(Actual) plt.show() return model, acc # 初始化三个模型 models { DecisionTree: DecisionTreeClassifier(random_state42), SVM: SVC(random_state42), RandomForest: RandomForestClassifier(n_estimators100, random_state42) } results {} for name, model in models.items(): trained_model, acc train_and_eval( model, name, X_train_scaled, X_test_scaled, y_train, y_test ) results[name] acc print(results)这段代码是项目的核心所在。train_and_eval 函数把训练、预测、输出分类报告、绘制混淆矩阵封装在一起避免每个模型写一遍重复代码。classification_report 会同时输出每个类别的精确率、召回率、F1 分数当成绩类别不均衡时这份报告比单独看 accuracy 可靠得多。混淆矩阵的每一行代表实际类别每一列代表预测类别对角线越深说明分对的样本越多。4.3 用网格搜索和交叉验证做模型调优默认参数的模型只是基线不调参就下结论容易误判模型能力。这个小结要讲清楚两件事网格搜索是什么以及交叉验证为什么必须和网格搜索搭配使用。网格搜索的意思是穷举一组超参数组合比如决策树的 max_depth 取 3、5、7SVM 的 C 取 0.1、1、10每组组合都训练并评估一次挑出分数最高的那组参数。交叉验证则是把训练集再切成 K 份轮流拿一份做验证其余做训练最后取 K 次结果的平均值作为这组参数的得分。网格搜索里内置了交叉验证功能就是 GridSearchCV其中的 CV 指的就是它。from sklearn.model_selection import GridSearchCV # 决策树调参限制深度和最小样本数来防过拟合 tree_params { max_depth: [3, 5, 7, None], min_samples_split: [2, 5, 10], min_samples_leaf: [1, 2, 4] } tree_grid GridSearchCV( DecisionTreeClassifier(random_state42), tree_params, cv5, scoringaccuracy, n_jobs-1 ) tree_grid.fit(X_train_scaled, y_train) print(Best tree params:, tree_grid.best_params_) print(Best tree score:, tree_grid.best_score_) # SVM调参C控制正则化强度gamma控制核函数的影响半径 svm_params { C: [0.1, 1, 10, 100], gamma: [0.01, 0.1, 1], kernel: [rbf] } svm_grid GridSearchCV( SVC(random_state42), svm_params, cv5, scoringaccuracy, n_jobs-1 ) svm_grid.fit(X_train_scaled, y_train) print(Best SVM params:, svm_grid.best_params_) print(Best SVM score:, svm_grid.best_score_)这里有几个参数值得单独说明。决策树的 max_depth 设置为 None 意味着树可以无限生长在小数据集上很容易过拟合所以给一组有限值让网格搜索自己找最优。min_samples_split 和 min_samples_leaf 是限制节点继续分裂的阈值值越大树越保守泛化能力通常越好。SVM 的 C 是误分类惩罚系数C 越大越不肯容忍错误容易过拟合C 越小则决策边界越平缓容错性越高。gamma 只在用 RBF 核时生效它控制单个样本的影响半径gamma 过大会让模型只认训练样本附近的小块区域过拟合风险很高。网格搜索跑完后还要做一步关键操作用最优参数重新在完整训练集上训练再在测试集上验证一次。GridSearchCV 内部在调参过程中只用了训练集和验证集测试集始终没参与。如果你直接把 grid_search.best_estimator_ 拿去做测试集预测是没问题的但代码里重训一遍会更清晰。# 用网格搜索拿到的最优参数重建模型 best_svm SVC(**svm_grid.best_params_, random_state42) best_svm.fit(X_train_scaled, y_train) y_pred_best best_svm.predict(X_test_scaled) print(SVM after tuning accuracy:, accuracy_score(y_test, y_pred_best)) print(classification_report(y_test, y_pred_best))5. 避坑指南评估陷阱、数据泄漏与特征工程误区5.1 只看准确率结果被不均衡数据绑架现象模型在测试集上的准确率有 0.9看起来很理想但看混淆矩阵发现模型几乎把所有样本都预测成了同一个类别某个成绩等级的召回率是 0。原因数据集里该类别样本占了绝对多数模型靠“无脑猜多数类”就能刷出高准确率。解决不要只看 accuracy必须同时看每个类别的精确率、召回率和 F1 分数必要时改用 macro avg 或 weighted avg 作为评判依据。另一个有效手段是使用 class_weightbalanced让算法自动提高少数类的惩罚权重。5.2 先缩放全量数据再切分造成数据泄漏现象训练集和测试集分数异常高但换一个新数据集重新实验性能掉得厉害。原因在划分数据集之前就对所有数据做了标准化StandardScaler 在计算均值和方差时已经偷看了测试集的统计信息模型训练时相当于提前知道了测试集的一部分信息。解决严格按照先 fit 训练集、再 transform 测试集两步走或者用 Pipeline 统一封装。from sklearn.pipeline import Pipeline # 用Pipeline把标准化的fit和transform顺序固定下来 svm_pipeline Pipeline([ (scaler, StandardScaler()), (svm, SVC()) ])5.3 类别特征编码不一致训练和预测结果对不上现象模型训练时一切正常部署或重新跑预测时报错说特征数量不匹配。原因训练集和测试集分开做 LabelEncoder 时测试集里出现了训练集中没见过的类别或者两个编码器对同一个类别的映射编号不同。解决特征编码必须在数据划分之前完成或者在划分后用 fit_transform 处理训练集、用 transform 处理测试集确保测试集完全复用训练集的映射关系。5.4 网格搜索不设随机种子每次结果都不一样现象用相同的数据和参数跑两次 GridSearchCVbest_params_ 输出不同的结果。原因决策树和随机森林内部有随机过程SVM 在部分实现里也涉及随机初始化网格搜索的交叉验证切分如果不固定随机种子每次得到的子集组合就不同。解决GridSearchCV 里设置 cv 的随机种子比如用 StratifiedKFold(n_splits5, shuffleTrue, random_state42)同时模型内部也固定 random_state。5.5 混淆矩阵热力图不标数字看不出分错方向现象图很漂亮但信息量低只知道预测错了不知道是哪个类别被误分成了哪个类别。原因sns.heatmap 默认只显示颜色不显示格子里的具体数值肉眼很难精确判断。解决设置 annotTrue 并在 fmt 里指定格式fmtd 表示显示整数。还要注意坐标轴标注xticklabels 和 yticklabels 用真实的成绩等级名称不要用数字序号。6. 模型验证的最后一公里混淆矩阵和 ROC 曲线的正确打开方式模型训练完、调完参只是整个流程的一半。最后一件事是确认模型真的可用这时候只看测试集的准确率是不够的。我习惯的做法是画两张图交叉验证结论一张是带真实标签名称的混淆矩阵另一张是 ROC 曲线。混淆矩阵解决的是“哪些类别容易混淆”的问题ROC 曲线解决的是“模型在多大置信度下能区分正负类”的问题。from sklearn.metrics import roc_curve, auc from sklearn.preprocessing import label_binarize # 先拿到最优模型的预测结果 y_scores best_svm.decision_function(X_test_scaled) # 如果是多分类需要把标签二值化 classes sorted(y_test.unique()) y_test_bin label_binarize(y_test, classesclasses) # 为每个类别计算ROC曲线 plt.figure(figsize(10, 8)) for i, cls in enumerate(classes): if y_test_bin.shape[1] 1: fpr, tpr, _ roc_curve(y_test_bin, y_scores) else: fpr, tpr, _ roc_curve(y_test_bin[:, i], y_scores[:, i]) roc_auc auc(fpr, tpr) plt.plot(fpr, tpr, labelfClass {cls} (AUC {roc_auc:.2f})) plt.plot([0, 1], [0, 1], k--, labelRandom Guess) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.title(ROC Curves for SVM) plt.legend() plt.show()这段代码有几个细节值得注意。roc_curve 的输入是测试集的真实标签和模型的预测分数而不是预测类别因为分数保留了排序信息能算出不同阈值下的真正例率和假正例率。多分类问题要用 label_binarize 把标签拆成多个二元问题每个类别单独画一条 ROC 曲线。SVM 的 decision_function 方法返回的是样本到决策边界的距离值越大属于当前类别的置信度越高决策树和随机森林则用 predict_proba 取正类的概率列。ROC 曲线下的面积 AUC 是一个直觉性很强的指标0.5 相当于随机猜1.0 是完美分类。正常情况下AUC 在 0.8 以上说明模型有实际区分能力0.7 到 0.8 属于勉强可用。如果所有类别的 AUC 都接近 0.5你要反过来检查前面的特征工程问题不在模型而在数据。另外还有一个常见陷阱多分类的 AUC 会分别给每个类别算一条曲线如果类别不平衡严重少数类的曲线可能很抖这是样本量不足导致的正常现象不必过度解读。从数据清洗、特征分析、模型对比到最后的验证整个流程跑下来你会发现这个项目真正的价值不在于某一个模型有多准而在于它把机器学习项目从数据到结论的完整链路串了起来。我自己跑过的教育数据项目里最深刻的教训是永远不要跳过 EDA 直接调参——我曾在某个数据集上没看相关性矩阵就硬调 SVM调出来的参数在训练集上表现完美换了数据就崩后来才意识到两个特征相关性高达 0.95模型把冗余信息当成了规律。从那以后我每次拿到新数据集都强制先跑一遍相关系数矩阵和特征重要性排序。数据不撒谎模型才会老实。希望这份拆解能帮你把项目跑通少走我走过的弯路。本文还有配套的精品资源点击获取