简介:适用于机器学习初学者和教育数据分析人员,这份压缩包聚焦某高校在线平台的学生行为数据,利用到课率、预习率、习题正确率与综合成绩构建决策树分类模型,将学生划分为优秀、良好、差三个等级,并以千余条真实记录完成训练与预测,便于平台后续使用者提前识别学业风险、开展教学预警。压缩包共含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−∑_{i=1}^{k} p_i²
entropy对应信息熵:
H(D)=−∑_{i=1}^{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', encoding='utf-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', encoding='utf-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(errors='coerce')把非法值变成NaN。第二,决定缺失行怎么处理。学生行为数据里偶尔会出现某位学生缺考导致数据为空,直接丢弃比胡乱填充更干净,因为千余条样本丢几行不影响模型稳定。
for col in ['到课率', '预习率', '习题正确率', '综合成绩']: df[col] = pd.to_numeric(df[col], errors='coerce') df = df.dropna(subset=['到课率', '预习率', '习题正确率', '综合成绩']) print(df.shape)errors='coerce'会把无法解析的值替换为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_size=0.2, random_state=42, stratify=y ) model = DecisionTreeClassifier(max_depth=4, min_samples_leaf=5, random_state=42) model.fit(X_train, y_train) print('训练集准确率:', model.score(X_train, y_train)) print('测试集准确率:', model.score(X_test, y_test))max_depth=4限制树的最大深度,min_samples_leaf=5要求叶子节点至少含5个样本。如果完全不限制这两个参数,树会长到每个叶子都极其纯净,训练集准确率接近1,测试集却明显下降,这是典型的过拟合表现。random_state固定是为了让复现结果一致,改到不同随机种子,树的形态和评估分数都可能有变化。
train_test_split的test_size设成0.2,表示五分之一的样本留作验证。stratify=y让等级比例在切分前后保持一致,比如"良好"在总数据中占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_names=feature_cols, class_names=['优秀', '良好', '差'], filled=True, rounded=True, fontsize=9 ) plt.savefig('decision_tree.png', dpi=150, bbox_inches='tight') plt.show()filled=True让节点颜色随多数类变化,一眼能看出哪些分支集中着"差"类学生。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_state=42), param_grid, cv=5, scoring='f1_weighted', n_jobs=-1 ) search.fit(X, y) print('最优参数:', search.best_params_) print('交叉验证F1:', search.best_score_)scoring设成f1_weighted而不是accuracy,是因为三个类别样本量不同,用accuracy会让搜索过程偏向多数类。cv=5表示五折交叉验证,每个参数组合都训练五棵小树,数据量小所以完全没有压力。n_jobs=-1让并行计算占满所有CPU核心。搜索结果如果给出max_depth=4到6、min_samples_leaf=3到5,属于正常区间。
如果最优max_depth是None,要小心。None表示树可以一直长到叶子节点足够纯为止,这种模型在训练集上往往接近满分,测试集会差一截。真正业务落地的树很少需要超过六层,超过六层后规则复杂到没法向教学管理解释,也很难说是稳定规律。
4.3 随机种子和复现问题
决策树训练表面上是确定性算法,但只要特征值存在并列,sklearn会按列顺序决定优先分裂哪个特征,数据集的行顺序不同也会导致树的形态变化。同一个code.py,在不同机器上运行,结果可能有细微差别。为了在报告里稳定复现,凡是涉及随机性的地方都要固定seed:train_test_split传random_state,模型构造函数传random_state,网格搜索也要传。这一步被很多人忽略,实验跑完发现两次结果对不上,最后定位到是种子没固定,耽误的时间比调参还多。
5. 决策树项目避坑:五个我替你先踩过的坑
5.1 坑:中文CSV读取直接报UnicodeDecodeError
现象:执行pd.read_csv时报错,提示"'utf-8' codec can't decode byte...",或者列名显示成乱码。
原因:CSV文件大多是用Excel或WPS保存的,编码是GBK或GB2312,Python默认用UTF-8解码中文内容就会失败。这不是文件损坏,而是编码不匹配。
解决:优先尝试pd.read_csv('student_data.csv', encoding='gbk')。如果仍然报错,用文本编辑器把CSV另存为UTF-8 with BOM格式再读取。读取后检查列名,如果列名前出现不可见字符,说明存在BOM,用df.columns = df.columns.str.replace('\ufeff', '')清理。复制脚本时要注意引号不要被word编辑器替换成中文引号,这种低级报错往往最迷惑。
5.2 坑:成绩边界被pd.cut默认规则挪动
现象:数据里有一条综合成绩正好等于80分,人工判断应属于"优秀",但value_counts结果里这条落进了"良好"。
原因:pd.cut默认right=True,也就是区间左开右闭。bins=[0,60,80,100]、labels=['差','良好','优秀']时,80被划入(60,80],而不是[80,100]。
解决:不用pd.cut的默认行为,改用第2章里的np.select,把优秀条件明确写成>=80。若坚持用cut,加上include_lowest=True调节,并逐一确认边界归属。边界问题发生在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 failed",CSV解出来只有半截。
原因:压缩包在传播过程中常被二次处理过。有一种情况是"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', encoding='gbk') 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', index=False)预测等级直接给出三类标签,差类概率给出该学生被模型判为差生的置信度。用list(model.classes_).index('差')定位类别列的下标,避免被classes_顺序坑到,因为类别的排列有可能是按字母序或训练数据出现顺序。导出成Excel后可以直接交给辅导员做二次筛选,不需要人工去数据库反复查。
6.2 用概率而不是硬分类来定预警名单
一个实际经验:不要只把预测为"差"的学生放进预警名单,而是把差类概率超过0.5的全部列出来。决策树叶子节点的预测概率本质上就是该类别在叶子样本中的占比,它能反映置信度。有些学生被预测为差,概率只有0.52,说明模型内部也有分歧,这部分人需要关注但优先级不一定最高。真正实用的预警名单应该是一个按差类概率降序排列的连续表,而不是非黑即白的0/1分类,这样有限的教学资源能优先投给最确定的样本。
6.3 每个学期结束后重新训练一次
模型不能一个版本用到底。新学期的学生行为分布会变,线上平台也可能调整统计口径,导致特征含义漂移。常见做法是每个学期结束时,把新得到的综合成绩并入训练集,重新跑一次网格搜索,对比新旧模型在测试集上的f1分数。如果差异不大就沿用老模型,差异明显就换新模型。成绩分界保持80和60不变,行为特征定义不变,预警规则跨学期才可解释。
这个项目第一次复现时我犯过一个低级错误:直接拿默认参数训练,训练集f1极高,测试集f1却很难看,当时没有先做分层抽样就急着看混淆矩阵,浪费了不少时间。后来临时上线一版模型,又发现预警名单里集中出现一批"预习率很高但成绩差"的学生,细查后才明白是平台在假期开放了回看功能,预习率统计口径被充值了。从那以后,每次训练完模型,我都会把预警名单随机抽十个人和任课老师核对一遍,确认规则看起来像人该有的判断,而不是模型自己发挥。希望帮到你。
本文还有配套的精品资源,点击获取