ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

决策树预测学生表现:可解释预警模型与实战源码解析

决策树预测学生表现:可解释预警模型与实战源码解析 简介适用于机器学习初学者和教育数据分析人员这份压缩包聚焦某高校在线平台的学生行为数据利用到课率、预习率、习题正确率与综合成绩构建决策树分类模型将学生划分为优秀、良好、差三个等级并以千余条真实记录完成训练与预测便于平台后续使用者提前识别学业风险、开展教学预警。压缩包共含5个文件包括可直接运行的Python建模脚本、CSV数据集、Word分析文档、Markdown说明和License授权文件整体大小约456KB结构清晰适合快速运行与对照学习。目前已有757人学习下载是入门决策树分类、理解特征筛选与模型评估流程的不错样本。通过源码和配套文档读者可以完整走通数据清洗、训练集与测试集划分、决策树构建、可视化展示及准确率评估等步骤也能从中借鉴在线教育场景下的学业预警方案为实际项目提供可复用的思路。1. 决策树预测学生表现自带数据与代码的可解释预警模型学生行为数据的预测场景里最尴尬的不是模型不准而是模型给出的结论没人敢用。决策树算法在网课平台的学生行为分析中天然有优势数据量千余条、特征只有到课率、预习率、习题正确率用树结构把成绩划分为优秀80分-100分、良好60分-79分、差0分-59分三类训练完成后看到的是一棵能读懂的树不是黑匣子。这个zip包里提供了code.py源码、student_data.csv数据和说明文档适合课程设计、期末项目以及教管场景中需要预警差生但又要解释依据的真实需求。你能拿到的不是概念介绍而是一套能直接跑起来的基线。2. 决策树的分类逻辑信息熵、基尼系数与三类标签的划分标准2.1 为什么决策树适合这类行为数据学生行为数据分析是机器学习里很典型的小规模表格任务也是数据分析类课程中常见的综合实践题目。这类数据一般不超过几千行特征以数值型比例为主决策树在这种规模下优势非常明显不需要特征缩放不需要做复杂的交叉特征只要数据完整一棵树的训练时间是以秒计的。换成逻辑回归虽然也能做三分类但需要先检查特征间的多重共线性输出的是概率和系数教务老师很难直接用换成SVM准确率可能更好看但核函数和gamma的调参成本高调完之后模型解释起来更费劲在必须向使用者交代为什么把这个学生列为预警对象的场景里站不住脚。决策树还有两个容易被忽略的优点。第一个是能自然处理非线性关系到课率从90%降到70%和从40%降到20%对成绩的影响幅度完全不同决策树通过多次阈值切分会自动逼近这种阶梯式关系不需要人工构造多项式特征。第二个是抗缺失能力相对强虽然训练时sklearn要求数据里没有NaN但预测阶段如果新数据某列缺失树模型会沿着当前分支中样本数较多的方向继续走不会像线性模型那样直接算出一个奇怪结果。可解释性才是这个项目真正卖点。训练完成后把树画出来分支规则一目了然比如到课率≤72.5且习题正确率≤61.3的学生落入差类。这种规则和日常教学经验能对上管理者才会信。决策树的精度不一定是最高的但它能把误差摆到台面上被人审查这对学生预警场景比追求0.01的准确率更重要。2.2 熵和基尼系数分裂时到底在算什么决策树的每个节点都在做同一件事找一个特征和一个阈值把当前样本集切成两份让切完之后的不纯度下降最多。sklearn里DecisionTreeClassifier的criterion参数控制不纯度的计算方式gini或entropy。gini对应基尼系数Gini(D)1−∑_{i1}^{k} p_i²entropy对应信息熵H(D)−∑_{i1}^{k} p_i log₂ p_i其中p_i是当前节点中第i类的占比k是类别数这里就是3。假设根节点有1000条样本三类占比分别是440、360、200那么基尼系数约等于1−(0.44²0.36²0.20²)0.6368熵约等于1.52。假如按某个阈值切分后左节点和右节点内部都比父节点更纯那么加权不纯度就会明显下降这个分裂就是有效的。决策树遍历所有特征的所有取值找到能让不纯度下降最多的切分点然后一层一层重复这个过程。实际使用gini还是entropy并没有绝对标准。gini计算更快entropy对不纯度变化更敏感在几千条样本、三个类别的场景下两者产生的树往往高度相似。我一般默认用gini调参时把这两个放入网格搜索里对比。学生行为数据三分类的区分度并不微妙gini和entropy的差距通常不构成瓶颈不需要在选指标上过度纠结。2.3 特征含义与等级标签的构造原始CSV里主要的四列字段是到课率、预习率、习题正确率、综合成绩。到课率是实际出勤次数与应出勤次数的比值预习率是平台内预习任务完成比例习题正确率是随堂练习的正确比例综合成绩则是最终评价也是我们要预测的目标变量。为了把它从连续值变成分类任务需要按摘要标准切分80到100为优秀60到79为良好0到59为差。边界看起来简单但有个细节值得注意。pd.cut默认是左开右闭区间bins[0,60,80,100]会把正好80分的样本划入良好这和文档描述80分-100分优秀不一致。我一般不用pd.cut直接用np.select写规则代码读起来也更贴近业务口径import numpy as np import pandas as pd df pd.read_csv(student_data.csv, encodingutf-8) df[等级] np.select( condlist[ df[综合成绩] 80, # 优秀区间 df[综合成绩] 60, # 良好区间 df[综合成绩] 60 # 差区间 ], choicelist[优秀, 良好, 差], default差 ) print(df[等级].value_counts())这段代码把综合成绩从连续值转成三分类标签。condlist里的三个布尔条件是从宽到窄写的np.select会从上到下匹配第一个为真的条件所以一个样本只会落到一个等级。default差是安全兜底防止缺失值或非法字符出现时没有标签可赋。这里还要注意三列特征的完整性和类型。到课率、预习率、习题正确率建议统一为浮点数。如果CSV里出现85%这类带百分号的字符串需要先剥离百分号再转float否则pd.to_numeric会把整列变成字符串类型模型训练时直接报错。这个预处理步骤看起来简单但往往是新手第一次跑源码时翻车的起点。3. 跑通code.py从CSV读取到决策树可视化的完整步骤3.1 准备运行环境与文件位置拿到zip后的第一步不是打开代码研读而是确认三件事Python版本、依赖库、数据文件路径。这个项目的核心代码是code.py数据是student_data.csv如果直接把两个文件放在桌面然后双击运行大概率会报No such file or directory——因为脚本当前工作目录不在桌面上。我习惯把文件和代码放进同一个项目文件夹然后在命令行里cd到该目录再运行。依赖安装只需要下面几个库pip install pandas scikit-learn matplotlibscikit-learn提供DecisionTreeClassifier和评估函数pandas负责读取CSV和分组统计matplotlib用于把决策树画成图片。如果不需要导出Graphviz格式的dot文件就不必安装graphviz这样能省掉不少系统环境上的麻烦。如果zip里有requirements.txt也可以直接用pip install -r requirements.txt一键装齐。3.2 读取CSV并做初步清洗import pandas as pd df pd.read_csv(student_data.csv, encodingutf-8) print(df.shape) print(df.dtypes) print(df.head())读取后先打印shape、dtypes和前五行。如果列名是中文但出现乱码多半是编码不是UTF-8把encoding参数改成gbk或gb2312再试。如果是Excel导出的CSV文件头可能带BOM字符列名里会出现不可见的\ufeff用df.columns df.columns.str.replace(\ufeff, )清理。拿到数据后重点清洗两件事。第一把到课率、预习率、习题正确率、综合成绩全部统一成数值类型用pd.to_numeric(errorscoerce)把非法值变成NaN。第二决定缺失行怎么处理。学生行为数据里偶尔会出现某位学生缺考导致数据为空直接丢弃比胡乱填充更干净因为千余条样本丢几行不影响模型稳定。for col in [到课率, 预习率, 习题正确率, 综合成绩]: df[col] pd.to_numeric(df[col], errorscoerce) df df.dropna(subset[到课率, 预习率, 习题正确率, 综合成绩]) print(df.shape)errorscoerce会把无法解析的值替换为NaN这样底层格式问题不会让train直接崩溃。dropna指定了必要的四个字段只要任一项缺失就删除该行。这个策略适合数据量充足的情况如果样本只有几百条则要考虑用均值或中位数填充。3.3 切分训练集和测试集训练决策树标签构造好之后特征矩阵只保留三个行为特征标签列就是前面生成的等级。切分时用stratify能保证三个类别在训练集和测试集中的比例与原始数据一致这是分类任务中最容易被漏掉的参数。from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier feature_cols [到课率, 预习率, 习题正确率] X df[feature_cols].astype(float) y df[等级] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) model DecisionTreeClassifier(max_depth4, min_samples_leaf5, random_state42) model.fit(X_train, y_train) print(训练集准确率:, model.score(X_train, y_train)) print(测试集准确率:, model.score(X_test, y_test))max_depth4限制树的最大深度min_samples_leaf5要求叶子节点至少含5个样本。如果完全不限制这两个参数树会长到每个叶子都极其纯净训练集准确率接近1测试集却明显下降这是典型的过拟合表现。random_state固定是为了让复现结果一致改到不同随机种子树的形态和评估分数都可能有变化。train_test_split的test_size设成0.2表示五分之一的样本留作验证。stratifyy让等级比例在切分前后保持一致比如良好在总数据中占50%切分后的训练集和测试集也各自约占50%这样评估结果不会因为抽样偏差产生误导。完成训练后可以用一行命令验证最简单的预测效果python code.py如果项目里code.py是一个完整可执行脚本运行后会直接输出准确率或树图保存信息。实际工程中我建议把模型训练和可视化拆成两个脚本训练脚本负责调参与输出指标绘图脚本只加载已保存的模型改动可视化样式时不需要重新训练。3.4 把决策树画出来模型训练结束后先把树图画出来看看哪些特征被用作根节点与二三层分裂再进入指标评估。plot_tree是sklearn内置函数不需要额外安装graphviz用matplotlib即可保存PNG。import matplotlib.pyplot as plt from sklearn.tree import plot_tree plt.figure(figsize(18, 12)) plot_tree( model, feature_namesfeature_cols, class_names[优秀, 良好, 差], filledTrue, roundedTrue, fontsize9 ) plt.savefig(decision_tree.png, dpi150, bbox_inchestight) plt.show()filledTrue让节点颜色随多数类变化一眼能看出哪些分支集中着差类学生。class_names的顺序必须与模型内部类别顺序一致最稳妥的办法是画图前打印model.classes_确认而不是靠记忆。树图里每个节点包含条件、样本数、类别分布和类别占比这些信息后续做预警说明时可以直接引用。4. 模型评估与参数调优准确率不是唯一的及格线4.1 分类报告和混淆矩阵只看准确率对三分类问题是不够的。假设数据里良好占比一半模型把所有样本都预测成良好准确率也能到50%但这个模型毫无用处。学生行为预警更关心漏报——一个真实属于差类的学生被预测成良好就不会进入教师关注名单。所以评估阶段要重点看差类的召回率。from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay y_pred model.predict(X_test) print(classification_report(y_test, y_pred, target_names[优秀, 良好, 差])) cm confusion_matrix(y_test, y_pred) ConfusionMatrixDisplay(cm, display_labels[优秀, 良好, 差]).plot() plt.title(混淆矩阵) plt.show()classification_report为每个类别给出precision、recall、f1-score。对差这一类recall表示真实为差的学生中有多少被模型找出来precision表示模型标为差的学生里真正差的比例。预警场景优先保recall宁可误报也不要漏报因为误报最多是浪费一次谈话时间漏报则可能让一个学生持续掉队。混淆矩阵用来定位具体错配方向。常见情况是模型把差预测成良好或者把优秀误判成良好。如果错配集中在相邻等级说明特征和能力表现是连续变化的结果可以接受如果出现差到优秀的跨级别误判多半是数据里存在异常记录需要回头检查原始CSV。4.2 用网格搜索找合适的树参数决策树常用参数有四个criterion、max_depth、min_samples_split、min_samples_leaf。它们的作用分别是不纯度计算方式、树的最大深度、内部节点继续分裂的最少样本数、叶子节点的最小样本数。手调这些参数往往凭感觉我习惯直接上GridSearchCV做小范围搜索。这个项目数据量小整个搜索几秒钟就能跑完。from sklearn.model_selection import GridSearchCV param_grid { criterion: [gini, entropy], max_depth: [3, 4, 5, 6, 8, None], min_samples_leaf: [1, 3, 5, 10] } search GridSearchCV( DecisionTreeClassifier(random_state42), param_grid, cv5, scoringf1_weighted, n_jobs-1 ) search.fit(X, y) print(最优参数:, search.best_params_) print(交叉验证F1:, search.best_score_)scoring设成f1_weighted而不是accuracy是因为三个类别样本量不同用accuracy会让搜索过程偏向多数类。cv5表示五折交叉验证每个参数组合都训练五棵小树数据量小所以完全没有压力。n_jobs-1让并行计算占满所有CPU核心。搜索结果如果给出max_depth4到6、min_samples_leaf3到5属于正常区间。如果最优max_depth是None要小心。None表示树可以一直长到叶子节点足够纯为止这种模型在训练集上往往接近满分测试集会差一截。真正业务落地的树很少需要超过六层超过六层后规则复杂到没法向教学管理解释也很难说是稳定规律。4.3 随机种子和复现问题决策树训练表面上是确定性算法但只要特征值存在并列sklearn会按列顺序决定优先分裂哪个特征数据集的行顺序不同也会导致树的形态变化。同一个code.py在不同机器上运行结果可能有细微差别。为了在报告里稳定复现凡是涉及随机性的地方都要固定seedtrain_test_split传random_state模型构造函数传random_state网格搜索也要传。这一步被很多人忽略实验跑完发现两次结果对不上最后定位到是种子没固定耽误的时间比调参还多。5. 决策树项目避坑五个我替你先踩过的坑5.1 坑中文CSV读取直接报UnicodeDecodeError现象执行pd.read_csv时报错提示utf-8 codec cant decode byte...或者列名显示成乱码。原因CSV文件大多是用Excel或WPS保存的编码是GBK或GB2312Python默认用UTF-8解码中文内容就会失败。这不是文件损坏而是编码不匹配。解决优先尝试pd.read_csv(student_data.csv, encodinggbk)。如果仍然报错用文本编辑器把CSV另存为UTF-8 with BOM格式再读取。读取后检查列名如果列名前出现不可见字符说明存在BOM用df.columns df.columns.str.replace(\ufeff, )清理。复制脚本时要注意引号不要被word编辑器替换成中文引号这种低级报错往往最迷惑。5.2 坑成绩边界被pd.cut默认规则挪动现象数据里有一条综合成绩正好等于80分人工判断应属于优秀但value_counts结果里这条落进了良好。原因pd.cut默认rightTrue也就是区间左开右闭。bins[0,60,80,100]、labels[差,良好,优秀]时80被划入(60,80]而不是[80,100]。解决不用pd.cut的默认行为改用第2章里的np.select把优秀条件明确写成80。若坚持用cut加上include_lowestTrue调节并逐一确认边界归属。边界问题发生在80分刚好卡线的少数样本上但一个差生被误判成优秀在预警名单里造成的影响会被放大。5.3 坑树干得太深导致测试集准确率低现象用默认DecisionTreeClassifier()训练训练集f1接近0.99测试集f1只有0.75左右预测结果和人工判断对不上。原因决策树在无限制情况下会把每个叶子切到几乎纯净训练数据里的噪声也被当作规律学了进去这就是过拟合。学生行为数据里本身存在大量例外情况比如到头率高但成绩低的学生树越深越容易把这类特例单独成叶却无法泛化。解决网格搜索里限制max_depth和min_samples_leaf至少把max_depth限制在4到6再比较。另一个实用判断标准是画出来的树深度如果超过6层业务上已经很难解释。树开始变复杂之前先看训练集与测试集的f1差距差距超过0.2就说明模型记忆了噪声。5.4 坑新学期的CSV少了一列predict直接抛异常现象用训练好的模型去预测新一批学生报错Number of features of the model must match the input特征数量对不上。原因训练时输入了三个特征predict时新DataFrame因为列名不完整或顺序不同被拼成了不同数量的列。sklearn不认列名只认位置少一个列就相当于少一个维度。解决预测前统一做列对齐先建立特征列表再按列表从新数据取列。如果新数据缺少某列直接报警而不是用填充值糊弄过去required [到课率, 预习率, 习题正确率] assert set(required).issubset(new_data.columns), 新数据缺少必需特征 X_new new_data[required].astype(float)一段断言就能避免模型在缺少维度的状态下偷跑结果。决策树虽然对缺失值有一定宽容度但特征数量不一致时绝对不能让数据带病进入predict否则输出的预警名单没有意义。5.5 坑zip文件解压时报加密或CRC失败现象下载的压缩包双击解压时提示需要密码或解压到某个文件时报CRC failedCSV解出来只有半截。原因压缩包在传播过程中常被二次处理过。有一种情况是zip伪加密zip头部的通用标志位第0位被置为1看似加密实际文件内容并没有真正加密另一种情况是压缩包不完整或分卷缺失导致CRC校验失败。解决先用Python查看压缩包内文件的标志位import zipfile z zipfile.ZipFile(基于决策树算法的学生学习行为预测数据分析.zip) for info in z.infolist(): print(info.filename, hex(info.flag_bits))如果flag_bits最低位是1但解压时不需要密码或任意密码都能解出内容基本可以判定文件本身没有真加密只是zip头被做了改动。处理方向是清理伪加密标记把通用标志位第0位置回0后重新解压。常见压缩工具都能直接打开这类文件导入后正常导出即可。如果确实遇到CRC失败优先重新下载整个压缩包不要只替换其中一个文件因为分卷之间的校验相互依赖。6. 更进一步把训练好的模型变成新学期预警名单6.1 持久化和批量预测训练完成后把模型和特征列表一起保存到pkl文件下次直接加载使用不需要重新训练。这在工程里是基础操作但这个源码包的README里通常不会写太多细节。import joblib joblib.dump({model: model, features: feature_cols}, student_tree.pkl)加载后对新数据做列对齐、预测、导出三步loaded joblib.load(student_tree.pkl) model loaded[model] new_data pd.read_csv(new_semester_students.csv, encodinggbk) X_new new_data[loaded[features]].astype(float) new_data[预测等级] model.predict(X_new) new_data[差类概率] model.predict_proba(X_new)[:, list(model.classes_).index(差)] new_data[[学号, 预测等级, 差类概率]].to_excel(预警名单.xlsx, indexFalse)预测等级直接给出三类标签差类概率给出该学生被模型判为差生的置信度。用list(model.classes_).index(差)定位类别列的下标避免被classes_顺序坑到因为类别的排列有可能是按字母序或训练数据出现顺序。导出成Excel后可以直接交给辅导员做二次筛选不需要人工去数据库反复查。6.2 用概率而不是硬分类来定预警名单一个实际经验不要只把预测为差的学生放进预警名单而是把差类概率超过0.5的全部列出来。决策树叶子节点的预测概率本质上就是该类别在叶子样本中的占比它能反映置信度。有些学生被预测为差概率只有0.52说明模型内部也有分歧这部分人需要关注但优先级不一定最高。真正实用的预警名单应该是一个按差类概率降序排列的连续表而不是非黑即白的0/1分类这样有限的教学资源能优先投给最确定的样本。6.3 每个学期结束后重新训练一次模型不能一个版本用到底。新学期的学生行为分布会变线上平台也可能调整统计口径导致特征含义漂移。常见做法是每个学期结束时把新得到的综合成绩并入训练集重新跑一次网格搜索对比新旧模型在测试集上的f1分数。如果差异不大就沿用老模型差异明显就换新模型。成绩分界保持80和60不变行为特征定义不变预警规则跨学期才可解释。这个项目第一次复现时我犯过一个低级错误直接拿默认参数训练训练集f1极高测试集f1却很难看当时没有先做分层抽样就急着看混淆矩阵浪费了不少时间。后来临时上线一版模型又发现预警名单里集中出现一批预习率很高但成绩差的学生细查后才明白是平台在假期开放了回看功能预习率统计口径被充值了。从那以后每次训练完模型我都会把预警名单随机抽十个人和任课老师核对一遍确认规则看起来像人该有的判断而不是模型自己发挥。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进