简介基于决策树算法的学生学习行为预测与数据分析完整项目包面向数据挖掘初学者、高校师生及教学管理人员用于从行为数据中挖掘规律并实现教学预警。资源共5个文件涵盖Python实现源码、Word分析文档、Markdown说明、CSV数据集及License授权说明压缩包仅456KB轻量易用。当前已有757人下载学习。数据基于某学校网上平台千余条真实行为记录包括到课率、预习率、习题正确率与综合成绩决策树模型按综合成绩将学生划分为优秀80-100分、良好60-79分、差0-59分三类便于后续使用者输入行为指标预测学业状况及时发现学习风险。配套Word与Markdown文档详细解读了数据预处理、特征选择、决策树构建及结果分析全过程源码保留完整训练与预测逻辑CSV数据表可直接用于复现实验适合课程设计、毕业设计或机器学习入门实践也很适合作为教学预警系统的参考实现。1. 决策树算法做学生学习行为预测为什么这个题目值得复现一个压缩包丢到手里里面装的是“基于决策树算法的学生学习行为预测数据分析”这其实是一个典型的校园数据挖掘课题也是你在大学生数据分析技术技能大赛、毕业设计、课程项目里最容易遇到的组合题。它要解决的核心问题并不复杂把学生的学习行为数据出勤、作业提交、在线学习时长、考试成绩等整理成结构化表格用决策树算法训练一个分类模型预测学生最终的学习结果——及格、挂科、优秀或者是辍学风险。这类项目真正的价值不在于算法本身有多前沿而在于它把数据分析的完整链路走了一遍数据清洗、特征工程、模型训练、参数调优、结果解释。决策树在这个场景里是性价比最高的选择因为它可解释性强你能直接看到“出勤率低于60% 作业提交率低于70% → 高风险挂科”这种规则教育管理者可以拿着规则去干预而不是面对一个黑匣子。同时它对数据分布没有严格要求学习行为数据里往往有大量离散特征和缺失值决策树处理这些都很自然。这篇笔记我按自己做过这类项目的顺序来讲拿到数据先做什么、决策树怎么选型、参数怎么调、哪些坑最容易翻车以及最后怎么把模型结果变成真正有用的分析结论。新手可以照步骤走熟手可以直接跳到参数和排查部分。2. 从原始数据到建模表学习行为数据清洗与特征工程2.1 原始数据长什么样学习行为数据的常见字段与观测口径拿到这个项目先别急着跑模型先弄清楚数据里有什么。常见的学生学习行为数据集通常包含几类字段学生基本信息学号、性别、专业、年级、课程参与行为出勤次数、迟到次数、请假次数、作业与测验作业提交率、作业平均分、测验次数、测验平均分、在线学习行为平台登录次数、视频观看时长、讨论区发帖数、历史成绩前期课程绩点、补考次数以及最终标签是否挂科/成绩等级。这些字段的观测口径需要特别留意出勤率是“实际出勤/应出勤”还是“缺勤次数”在线学习时长是“累计登录时长”还是“有效学习时长”不同口径直接决定特征含义。我一般会先建立一个字段字典表把每一列的含义、类型、单位、取值区间列清楚否则后面做特征解释时很容易说出错误结论。数据缺失也是这类项目的大概率事件学生休学导致的整段行为缺失、系统漏采导致某门课数据为空、手工导入产生的乱码。第一步永远是读取数据后立刻做shape、dtypes、describe、isnull统计把这个结果存下来作为后续所有处理的基线。2.2 用Pandas完成第一轮清洗缺失值、重复值与异常值处理import pandas as pd import numpy as np # 读取原始数据先不假设编码常见的有utf-8和gbk df pd.read_csv(student_behavior.csv, encodingutf-8) print(原始数据形状:, df.shape) print(缺失值统计:\n, df.isnull().sum()) print(重复行数:, df.duplicated().sum()) # 删除完全重复的行 df df.drop_duplicates().reset_index(dropTrue) # 处理缺失值按列缺失率分三级处理 missing_rate df.isnull().sum() / len(df) drop_cols missing_rate[missing_rate 0.5].index.tolist() # 缺失超过50%的列直接删 fill_cols missing_rate[(missing_rate 0) (missing_rate 0.5)].index.tolist() df df.drop(columnsdrop_cols) print(删除缺失率超50%的列:, drop_cols) # 数值列用中位数填充离散列用众数填充 num_cols df.select_dtypes(include[np.number]).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]) # 异常值处理以成绩字段为例用IQR缩尾而不是直接删除 def winsorize(s, lower0.01, upper0.99): q_low s.quantile(lower) q_high s.quantile(upper) return s.clip(q_low, q_high) df[final_score] winsorize(df[final_score]) print(清洗后数据形状:, df.shape)这段代码的决策逻辑是缺失率超过50%的列携带的信息量已经不足以支撑建模删掉比强行填充更安全数值型列的缺失用中位数填充比均值更抗异常值影响离散列用众数填充保持分布形态。成绩类字段的异常值用分位数缩尾而不是直接删除因为一个学生总成绩特别高或特别低本身就是有意义的信号但极端输入错误需要压制。需要注意fillna(df[col].median())对偏态分布很友好但如果某列缺失率在30%到50%之间可以创建一个“是否缺失”的附加列因为缺失这个事实本身可能暗示行为异常。比如“在线学习时长缺失”可能意味着该学生压根没登录过平台这个信息比填充后的数值更有价值。2.3 特征构造从原始行为里挖出有预测力的新特征原始字段直接丢进决策树也能跑但效果一般。这个项目里真正有区分度的特征往往是构造出来的出勤率 实际出勤次数 / 应出勤总次数比绝对次数更稳定可以跨课程比较。作业提交率 已提交次数 / 布置总次数低提交率与挂科的相关性通常比作业分数更强因为没交作业意味着学习过程断裂。学习规律性 登录天数 / 登录总次数同样的登录次数分散在30天和集中在3天代表完全不同的学习习惯。成绩波动 测验成绩标准差波动大的学生状态不稳定风险偏高。拖沓指数 平均提交时间与截止时间的差总是卡点提交的学生学习节奏偏被动。# 构造新特征 df[attendance_rate] df[actual_attendance] / df[expected_attendance] df[homework_submit_rate] df[homework_submitted] / df[homework_total] df[regularity] df[login_days] / df[login_total] df[score_std] df.groupby(student_id)[quiz_score].transform(std) df[submission_lag] df[deadline_diff].clip(lower0) # 处理除零问题分母为0时填充为0 df[attendance_rate] df[attendance_rate].replace([np.inf, -np.inf], 0) df[homework_submit_rate] df[homework_submit_rate].replace([np.inf, -np.inf], 0) # 构造标签final_score 60 为 pass否则为 fail # 也可以做三分类优秀85、及格60-84、不及格60 df[label] np.where(df[final_score] 60, 1, 0)除零处理是血泪教训某门课没有布置作业时homework_submitted / homework_total会产生inf如果不处理决策树在分裂时会把这个inf当成一个独立的特殊值导致分支毫无意义。transform(std)是分组计算但仍然保留每行粒度数据的关键技巧不会改变DataFrame的行数。特征构造完之后做一次相关性检查对两两相关系数超过0.8的特征做取舍决策树虽然不像线性回归那样害怕多重共线性但冗余特征会分散分裂的注意力让树变得臃肿。2.4 数据划分训练集、验证集与测试集的正确切法from sklearn.model_selection import train_test_split, StratifiedKFold feature_cols [attendance_rate, homework_submit_rate, regularity, score_std, submission_lag, login_total, quiz_mean] X df[feature_cols] y df[label] # 先切出测试集保证测试集完全不参与调参 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 训练集内部用分层K折做交叉验证 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) print(训练集样本数:, X_train.shape[0], 测试集样本数:, X_test.shape[0])stratifyy的作用是保证切分后训练集和测试集中正负样本比例与原始数据一致。学习行为数据里挂科的学生通常只占15%到25%如果不分层切分测试集可能分到极少数挂科样本评估出的准确率虚高但实际毫无参考价值。random_state42固定随机种子保证每次运行得到相同结果这对调试和复现极其重要。3. 决策树在学生行为预测上的核心构建从特征选择到剪枝策略3.1 决策树三大算法怎么选ID3、C4.5还是CART决策树算法的核心逻辑是递归地选择最优特征进行分裂让分裂后子节点的数据“纯度”尽可能高。“纯度”的度量方式决定了算法形态。ID3用信息增益偏好取值多的特征C4.5用信息增益率对ID3的偏好做了矫正CART用基尼系数同时支持分类和回归。在学生行为预测场景里最常用的是CART树原因很实际sklearn的DecisionTreeClassifier原生实现的就是CART而且CART是二叉树分裂逻辑更清晰生成的规则更容易解释。ID3和C4.5更多出现在教科书和论文的对比实验里你在实际项目中基本不会手写它们——除非你的课程设计要求必须从零实现那就另当別论。信息增益和基尼系数在大多数情况下给出的分裂结果很接近真正影响模型性能的是树的最大深度和分裂阈值而不是算法本身。3.2 用sklearn构建第一个决策树模型最小可行代码from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import classification_report, confusion_matrix, accuracy_score # 先跑一个默认参数的决策树作为基线 clf DecisionTreeClassifier(random_state42) clf.fit(X_train, y_train) y_pred clf.predict(X_test) print(准确率:, accuracy_score(y_test, y_pred)) print(分类报告:\n, classification_report(y_test, y_pred)) # 混淆矩阵看漏报率和误报率 tn, fp, fn, tp confusion_matrix(y_test, y_pred).ravel() print(f混淆矩阵 — TP: {tp}, FP: {fp}, FN: {fn}, TN: {tn})默认参数的决策树会一直生长到每个叶子节点都“纯”为止这在训练集上表现很好但在测试集上很容易过拟合。上面这段代码是基线模型作用不是做预测而是给你一个“至少要超过这个水平”的参照。有一个关键细节confusion_matrix的返回值顺序在sklearn不同版本间有过调整用ravel()拆包时四个值的顺序是[tn, fp, fn, tp]而不是[tp, fp, fn, tn]。如果项目里既有旧版本又有新版本环境最好用labels参数显式指定否则后续算精确率和召回率会完全对调这个错我犯过一次排查了整整一下午。3.3 剪枝策略限制树生长的四个手段决策树过拟合的典型表现是树越来越深训练集准确率接近100%但测试集表现持续下降。剪枝是控制复杂度的核心手段scikit-learn给出了四个常用旋钮max_depth限制树的最大深度。学生行为数据通常特征在10个左右深度控制在3到6之间就能覆盖绝大多数有效分裂。min_samples_split内部节点再分裂所需的最小样本数。设得太小比如2会让树捕捉到噪声一般建议20到50起步。min_samples_leaf叶子节点最少样本数。这个参数对最终效果影响最直接设得越大模型越保守泛化越好。max_leaf_nodes限制最大叶子节点数。适合对树的大小有明确上限要求的场景。from sklearn.model_selection import GridSearchCV # 用网格搜索 交叉验证来确定合理的剪枝参数 param_grid { max_depth: [3, 4, 5, 6, 7, None], min_samples_split: [10, 20, 30, 50], min_samples_leaf: [5, 10, 20, 30], criterion: [gini, entropy] } grid GridSearchCV( DecisionTreeClassifier(random_state42), param_grid, cvskf, # 使用前面建好的分层K折 scoringf1, # 类别不平衡时用f1比accuracy更合理 n_jobs-1 ) grid.fit(X_train, y_train) print(最优参数:, grid.best_params_) print(最优得分:, grid.best_score_) # 用最优参数重建模型 best_clf grid.best_estimator_scoringf1的选择值得说明如果挂科学生只占20%一个什么都不预测、把所有人都判为“不挂科”的模型准确率也有80%但这个模型毫无价值。F1分数同时考虑了精确率和召回率能把这种“偏科”模型打回原形。网格搜索的开销在特征数不多时完全可控但如果数据集很大建议把min_samples_split和min_samples_leaf的候选值放宽max_depth用[3,4,5,6]就够24组参数组合乘以5折交叉验证计算量已经不小。3.4 训练完成后如何观察决策树的真实行为from sklearn.tree import export_text, plot_tree import matplotlib.pyplot as plt # 文本形式的规则输出适合快速阅读 tree_rules export_text(best_clf, feature_namesfeature_cols, max_depth4) print(tree_rules) # 可视化树结构 plt.figure(figsize(20, 10)) plot_tree(best_clf, filledTrue, feature_namesfeature_cols, class_names[pass, fail], roundedTrue, fontsize10) plt.savefig(decision_tree.png, dpi150, bbox_inchestight)export_text的输出形式是缩进的if-else规则比如“若出勤率0.62且作业提交率0.58则判为fail”这正是这个项目“可解释性”价值的体现。plot_tree画出来的图如果树太深会糊成一团所以展示的时候建议限制max_depth或者只画剪枝后的树。看树的时候重点观察两个位置根节点选的是哪个特征——这通常就是对学生学习结果影响最大的行为指标靠近根部的分裂阈值是多少——这定义了最需要干预的行为红线。比如根节点是attendance_rate阈值为0.63那就意味着出勤率低于63%的学生群体挂科概率显著升高这个数字可以直接拿给辅导员做预警参考。4. 把握类别不平衡与特征重要性模型评估和调参分析4.1 类别不平衡对决策树的影响与对策学习行为数据里“挂科”或“辍学”永远是少数类。当少数类占比低于20%时决策树会倾向把所有样本都分到多数类因为这样整体的“不纯度”降低最快。对抗不平衡的手段按优先级排列先尝试class_weightbalanced其次是调整决策阈值最后才考虑过采样或欠采样。# 方案一class_weight平衡 clf_balanced DecisionTreeClassifier( max_depth4, min_samples_leaf10, class_weightbalanced, random_state42 ) clf_balanced.fit(X_train, y_train) # 方案二不调模型直接调决策阈值 # 拿到预测概率自己设定阈值 y_prob best_clf.predict_proba(X_test)[:, 1] y_pred_custom (y_prob 0.3).astype(int) # 默认是0.5这里降到0.3提高召回率 # 对比两个方案的效果 from sklearn.metrics import f1_score, recall_score, precision_score print(默认阈值 F1:, f1_score(y_test, best_clf.predict(X_test))) print(0.3阈值 F1:, f1_score(y_test, y_pred_custom)) print(0.3阈值 召回率:, recall_score(y_test, y_pred_custom))class_weightbalanced的本质是在计算基尼系数时给少数类样本更高的权重让树的分裂更照顾少数类。调整阈值则是在模型输出层面操作把预测为“挂科”的概率门槛从0.5降到0.3相当于放出去更多预警召回率上升但误报也会增加。在学生预警场景里误报的代价是找学生谈话、多关注一下漏报的代价是学生真的挂科甚至辍学宁可比阈值偏低一点。实际项目里我发现两者叠加效果最好先用class_weightbalanced训练再在验证集上画Precision-Recall曲线选PR曲线拐点对应的阈值而不是盲目用0.3或0.5。4.2 特征重要性分析出勤率为什么排在第一位import pandas as pd importance pd.DataFrame({ feature: feature_cols, importance: best_clf.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance) # 可视化 ax importance.plot.barh(xfeature, yimportance, figsize(8, 5), legendFalse) ax.set_xlabel(Feature Importance) plt.tight_layout() plt.savefig(feature_importance.png, dpi150)特征重要性反映的是该特征在所有分裂中被选中的总贡献度它是一个相对值。通常在这个项目里出勤率和作业提交率的importance会显著高于其他特征这与教育学常识一致行为投入比智力因素更能预测学业结果。这个结果的最大用途有两个一是给论文或项目报告提供核心论据用数据说明“干预出勤比干预成绩更有效”二是做特征瘦身把importance低于0.01的特征删除重新训练模型看性能是否下降。如果下降在可接受范围内就说明这些特征可以被安全丢弃模型更简洁、解释更容易。需要提醒的是决策树的特征重要性存在偏差——它偏好数值型特征和取值多的特征所以解释时不要绝对化可以结合排列重要性permutation importance做交叉验证。4.3 模型对比决策树与其他常见分类算法的边界决策树在学生学习行为预测这个场景里绝不是唯一选择但它的优势在特定条件下非常突出。和逻辑回归相比决策树不需要特征标准化不用假设特征线性关系能自动捕捉交互作用。和随机森林、XGBoost相比决策树准确率通常低于集成模型但可解释性碾压后者。一个常见做法是先跑决策树拿到基线和规则再跑随机森林看提升空间。如果随机森林只提升了两三个点的F1那说明数据里的信号本来就有限应该回去做特征工程而不是堆模型复杂度。如果提升超过十个点说明单棵树的方差太大数据噪声多这时可以考虑用随机森林作为最终模型但需要配合SHAP值来解释预测逻辑保证可解释性不丢失。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators500, max_depth6, min_samples_leaf10, class_weightbalanced, random_state42 ) rf.fit(X_train, y_train) print(随机森林 F1:, f1_score(y_test, rf.predict(X_test))) print(决策树 F1:, f1_score(y_test, y_pred_custom))如果两个F1接近我会毫不犹豫选择单棵决策树作为交付方案因为教育场景的使用者需要“看得懂的规则”而不是一个能讲故事但没人敢依赖的黑匣子。如果随机森林显著胜出那就用随机森林但报告里要加上特征重要性和部分依赖图把“黑匣子”的解释工作补上。4.4 阈值分析方法Precision-Recall曲线画出风险拦截边界from sklearn.metrics import precision_recall_curve import matplotlib.pyplot as plt y_prob_all best_clf.predict_proba(X_test)[:, 1] precision, recall, thresholds precision_recall_curve(y_test, y_prob_all) # 找到F1最大的阈值 f1_scores 2 * (precision * recall) / (precision recall 1e-9) best_idx np.argmax(f1_scores) best_threshold thresholds[best_idx] print(f最佳阈值: {best_threshold:.3f}, 对应F1: {f1_scores[best_idx]:.3f}) # 画出PR曲线 plt.figure(figsize(8, 6)) plt.plot(recall, precision, marker.) plt.xlabel(Recall) plt.ylabel(Precision) plt.axvline(xrecall[best_idx], colorr, linestyle--, alpha0.5) plt.axhline(yprecision[best_idx], colorr, linestyle--, alpha0.5) plt.tight_layout() plt.savefig(pr_curve.png, dpi150)PR曲线比ROC曲线更适合类别不平衡场景。ROC曲线对多数类占比变化不敏感看似优秀的结果可能是假象PR曲线直接反映“预警之后有多少是真正的高风险学生”这才是业务方关心的。选阈值的一般思路如果干预资源充足比如辅导员人手多阈值向低端挪宁可误报如果干预资源有限阈值向高端挪优先保证预警精准度。5. 学生行为预测项目的常见坑与排查记录5.1 数据泄露测试集混进了不该有的信息现象测试集上准确率高达98%F1接近完美模型表现好得不真实。原因构建特征时用到了final_score衍生出来的列或者在做缺失值填充时用了全量数据的统计量比如用包含测试集的均值去填充训练集的缺失值。这个叫做data leakage是这类项目里最隐蔽、也最致命的问题。解决检查特征列表里有没有包含标签信息的字段所有填充和标准化操作必须在train_test_split之后做并且用fit_transform只作用于训练集、transform作用于测试集。我的做法是写完特征工程代码后逐列列出特征来源凡是直接或间接来自结果变量的列一律删除。5.2 GridSearchCV选择的最优参数不稳定现象网格搜索每次运行得到的最优max_depth和min_samples_leaf都不一样有时差很多。原因特征量少、样本量不大时决策树模型本身方差较高K折交叉验证的不同随机划分会带来明显波动。GridSearchCV返回的是“在这个划分下最好”的参数不一定是全局最优。解决把随机种子固定保证至少能复现采用多次交叉验证取平均的方式比如用RepeatedStratifiedKFold重复5次、每次5折综合25个得分来选参数而不是单次5折。代价是训练时间增加5倍但换来的是参数选择的稳定性。5.3 树深度超过10层可解释性沦为纸上谈兵现象export_text输出的规则长到十几层根本没法读画出来的图占好几页A4纸业务方看完直接放弃使用。原因网格搜索只看F1没有把“树是否可解释”作为约束条件。默认参数下树长到几十层F1可能是高了但这个模型在实际场景中价值大打折扣。解决给搜索空间增加硬约束——max_depth上限设为6min_samples_leaf下限设为10。在模型性能和可解释性之间这类项目应优先保证可解释性哪怕F1降三个点也可以接受。可以跟业务方这样解释“你得到一个能看懂的规律比一个精确但没人敢用的数字更有价值。”5.4 中文列名和路径导致绘图和读取报错现象plt.savefig保存图片时文件名包含中文直接报错读取CSV时列名是中文导致feature_names传参失败。原因matplotlib和sklearn的中文兼容问题。文件路径含中文时部分版本的savefig可能无法写入特征列名含中文时export_text和plot_tree的显示会乱码或报字体缺失。解决统一约定项目内所有文件名、列名都用英文小写加下划线在数据读取阶段完成一次列名映射原始中文列名存为字典备份。涉及绘图时用plt.rcParams[font.sans-serif] [SimHei]设置字体如果环境里没有中文字体就干脆放弃中文显示改用英文标签报告里再翻译解释。5.5 类别不平衡被忽略导致模型“全票通过”现象准确率88%但点开混淆矩阵发现预测结果里一个“挂科”都没有。原因建模时只用了准确率作为指标没有看分类报告和混淆矩阵。挂科学生只占12%全预测为“不挂科”就拿到了88%的准确率。解决任何分类任务跑完都强制自己查看classification_report、confusion_matrix、f1_score三个输出。准确率只作为参考模型好坏的决策指标用F1或AUC。上面的第4.1小节的class_weightbalanced就是应对这个坑的常规手段。6. 从决策树到可交付的学习预警系统三个进阶技巧6.1 用决策树规则生成一份学生风险名单模型训练完最终交付物不应该只是一堆指标而是一份业务方可以直接使用的风险名单。做法是把全部学生数据输入best_clf.predict_proba拿到每个人的风险概率按阈值分组输出高风险、中风险、低风险名单。# 生成风险预警名单 df_all pd.concat([X, y], axis1) df_all[risk_prob] best_clf.predict_proba(X)[:, 1] df_all[risk_level] pd.cut( df_all[risk_prob], bins[0, 0.3, 0.7, 1.0], labels[低风险, 中风险, 高风险] ) # 输出高风险学生名单 high_risk df_all[df_all[risk_level] 高风险].sort_values(risk_prob, ascendingFalse) high_risk.to_csv(high_risk_students.csv, indexFalse, encodingutf-8-sig) print(高风险学生数量:, len(high_risk))encodingutf-8-sig是因为Excel直接打开UTF-8编码的CSV会乱码加BOM头可以保证用Excel打开时中文正常。这份名单可以直接交给教务或辅导员按风险概率从高到低排列优先干预排名靠前的学生。6.2 把单棵决策树升级为随机森林并保持可解释性如果随机森林确实比单棵决策树F1高出一截也不必放弃可解释性需求。可以用permutation_importance计算全局特征重要性再用SHAP的TreeExplainer计算单个样本的预测解释。from sklearn.inspection import permutation_importance import shap # 排列重要性比默认feature_importances更可靠 perm_importance permutation_importance(rf, X_test, y_test, n_repeats10, random_state42) for i, col in enumerate(feature_cols): print(f{col}: {perm_importance.importances_mean[i]:.4f}) # SHAP值解释单个学生的风险来源 explainer shap.TreeExplainer(rf) shap_values explainer.shap_values(X_test[:100]) shap.summary_plot(shap_values, X_test[:100], feature_namesfeature_cols)SHAP值为每个样本的每个特征贡献值做分解能告诉你“这个学生被判为高风险主要因为出勤率只有0.42其次是作业提交率0.51”。这是决策树不可替代的解释性天花板。不过SHAP计算时间会随样本量增长数据很大时先抽样再解释。6.3 把训练好的模型持久化方便系统集成import joblib # 保存模型、特征列名、阈值等完整配置 model_config { model: best_clf, feature_columns: feature_cols, threshold: best_threshold, label_map: {0: 正常, 1: 挂科风险} } joblib.dump(model_config, student_risk_model.pkl) print(模型已保存文件大小:, round(len(model_config[model].__repr__()) / 1024, 2), KB)模型保存时别只存一个pkl把特征列表、阈值、标签映射一起打包成字典。这样下次加载时不会出现“特征列顺序变了但模型的期望没变”的低级错误。加载模型做预测时传入的数据必须和训练时的特征列完全一致顺序不一致会出结果但没有意义。我自己的习惯是每次建模迭代都在模型字典里加一个train_date: 2025-xx-xx字段所有尝试过的参数组合和对应F1也单独存一份CSV。项目结束回头看时这份记录比模型本身更能帮你理清“为什么最终选了这组参数”。如果你正在做类似课题建议把决策树当作基线、把随机森林当作上限参照用可解释的规则作为最终交付。决策树模型在这个场景下最大的价值是可以“自杀式验证”——让业务方挑几个学期末结果已知的学生来校验树规则是否可靠。如果业务方拿着你给的规则对历史数据做了复核那这个项目的价值才算真正落地。希望这些经验能帮你少踩几个坑。本文还有配套的精品资源点击获取