基于Python的学业预警系统:从数据清洗到Flask接口的完整实战

发布时间:2026/9/23 12:08:53
基于Python的学业预警系统:从数据清洗到Flask接口的完整实战 简介这份Python项目源码面向高校教务管理者、辅导员以及计算机相关专业的课程设计学习者核心目标是借助数据分析提前识别学业困难学生降低辍学风险。系统围绕数据采集与整合、基于逻辑回归或随机森林等算法的风险评估模型、实时监控、预警通知、可视化界面与报告生成等模块展开并兼顾数据安全与隐私保护技术栈覆盖Python后端、MySQL等数据库、前端页面及scikit-learn等机器学习库。压缩包为zip格式整体约10.71MB上游未提供文件总数与类型明细可结合描述判断包含后端逻辑、数据处理脚本与前端资源等。目前已有105人学习下载适合作为课程设计参考或二次开发基础。读者可从中获取完整的项目结构、风险评估建模思路、预警规则设计与报告生成逻辑便于快速理解学业预警系统的实现路径并迁移到自己的课题中。1. 学业预警系统到底在预警什么从一张挂科名单说起每学期期末教务老师手里都会多出一份让人头疼的名单高数挂了三成大物又挂了两成还有几个学生连续两学期绩点跌破 1.5。问题是这份名单是事后才出现的——等到成绩录入系统学生已经错过了补救窗口。学业预警系统要干的事就是把这个时间点往前挪用学生已有的成绩、出勤、选课、作业提交等数据在学期中甚至学期初就给出「这个学生有风险」的判断让辅导员和导师能提前介入。这个标题里的「python项目」不是装饰词。学业预警系统的核心是数据处理和模型计算Python 生态里的 pandas、scikit-learn、Flask 正好覆盖了从数据清洗到模型训练再到接口暴露的全链路所以绝大多数高校教务口的自研预警工具都选 Python 做主力语言。适合读这篇的人有三类想拿一个完整项目练手的 Python 学习者、需要给学院搭一套轻量预警工具的开发人员、以及想知道「预警分数到底怎么算出来」的教务管理者。下面按「数据怎么来 → 特征怎么造 → 模型怎么训 → 系统怎么跑 → 坑在哪」的顺序拆开讲。2. 数据从哪来教务系统导出的三张表怎么变成建模输入2.1 先搞清楚预警系统的最小数据闭环很多人一上来就想接教务系统的数据库结果卡在权限和接口上。我一般建议先用导出文件跑通闭环常见做法是从教务系统导出三张 CSV学生基本信息表、成绩表、选课记录表。这三张表构成了最小可用数据集不需要实时接口也能算出预警结果。学生基本信息表通常包含学号、姓名、专业、年级、生源地等字段成绩表包含学号、课程号、课程名、学分、绩点、成绩、学期选课记录表包含学号、课程号、选课时间、是否退课。三张表通过学号关联就能拼出一个学生一个学期的完整学业画像。提示导出时注意编码问题。教务系统导出的 CSV 经常是 GBK 编码直接pd.read_csv会报 UnicodeDecodeError后面会讲怎么处理。2.2 用 pandas 做三表关联和缺失值处理拿到 CSV 后第一步不是建模是清洗。下面这段代码做三件事读入三张表、统一编码、按学号合并。import pandas as pd # 教务系统导出文件常见 GBK 编码先试 utf-8 再回退 def read_csv_safe(path): try: return pd.read_csv(path, encodingutf-8) except UnicodeDecodeError: return pd.read_csv(path, encodinggbk) stu read_csv_safe(student_info.csv) # 学号,姓名,专业,年级 score read_csv_safe(score.csv) # 学号,课程号,学分,绩点,成绩,学期 course read_csv_safe(course_select.csv) # 学号,课程号,选课时间,是否退课 # 成绩表里可能有空成绩缓考、缺考先标记再决定是否剔除 score[成绩] pd.to_numeric(score[成绩], errorscoerce) score[缺考标记] score[成绩].isna().astype(int) # 按学号聚合出每个学生每学期的统计特征 semester_stat score.groupby([学号, 学期]).agg( 课程数(课程号, count), 平均绩点(绩点, mean), 最低绩点(绩点, min), 挂科数(绩点, lambda x: (x 1.0).sum()), 缺考数(缺考标记, sum) ).reset_index() # 和基本信息表合并 df semester_stat.merge(stu, on学号, howleft) print(df.head())逻辑说明read_csv_safe解决编码翻车问题pd.to_numeric把成绩列里混入的「缓考」「缺考」等文本转成 NaN再用缺考标记单独记录避免直接丢数据导致样本偏差。groupby按学号和学期聚合产出的是「一个学生一个学期一行」的建模宽表。参数说明挂科数的阈值我设的是绩点小于 1.0不同学校绩点算法不同有的用百分制 60 分有的用 5 分制 1.0这个阈值必须按本校教务规则改。howleft保证以成绩表为主基本信息缺失的学生不会被静默丢弃而是留出 NaN 让你后续排查。2.3 特征工程把「历史表现」变成「未来风险」的输入预警模型要预测的是「下一个学期会不会挂科」所以特征必须来自当前学期及之前标签来自下一学期。这是最容易搞错的地方——如果把当前学期的挂科数直接当特征去预测当前学期那就是数据泄露模型准确率虚高但上线就废。我一般会构造这几类特征历史平均绩点、历史挂科率、绩点变化趋势本学期减上学期、已修学分进度、是否有退课记录。标签用下一学期是否出现挂科二分类或下一学期绩点区间多分类。# 按学号排序后用 shift 构造「下一学期」标签 df df.sort_values([学号, 学期]) df[下学期挂科] df.groupby(学号)[挂科数].shift(-1) df[下学期挂科] (df[下学期挂科] 0).astype(int) # 趋势特征本学期平均绩点 - 上学期平均绩点 df[绩点变化] df.groupby(学号)[平均绩点].diff() # 去掉没有下一学期记录的最后一行无法打标签 model_df df.dropna(subset[下学期挂科]).copy() model_df model_df.fillna({绩点变化: 0}) # 第一学期没有变化值补 0这里shift(-1)是关键它把「下一学期」的挂科情况挪到当前行这样每一行就是「用当前及历史特征预测下一学期结果」。diff()算绩点变化第一学期结果为 NaN补 0 表示「无历史可比」。做完这一步建模宽表才算干净。3. 模型怎么选怎么训逻辑回归够不够用阈值怎么定3.1 为什么学业预警首选逻辑回归而不是深度学习学业预警的数据量通常不大——一个学院一届几百人几年下来几千条样本特征也就十几个。这种规模下逻辑回归的可解释性优势远大于深度模型的精度优势。辅导员需要知道「这个学生为什么被预警」逻辑回归能直接给出每个特征的系数比如「历史挂科数每增加 1风险 odds 增加 2.3 倍」这种结论能直接写进预警报告。随机森林和 XGBoost 可以作为对比基线但上线我一般还是用逻辑回归或带 L2 正则的逻辑回归。选型理由说清楚样本量小、特征维度低、需要可解释、需要快速迭代阈值——这四个条件同时满足时逻辑回归是最稳的选择。如果学院有上万条样本且特征上百再考虑树模型。3.2 训练脚本与类别不平衡处理学业预警有个天然问题真正挂科的学生是少数可能只占 10% 到 20%直接训练会导致模型偏向预测「不挂科」。常见做法是用class_weightbalanced让模型自动调整权重或者用 SMOTE 过采样。我一般先用 class_weight因为它不改变数据分布解释起来简单。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report, roc_auc_score import pandas as pd feature_cols [平均绩点, 最低绩点, 挂科数, 缺考数, 绩点变化, 课程数] X model_df[feature_cols] y model_df[下学期挂科] # 按时间切分更合理用前几年训练最后一年测试 # 这里简化为随机切分实际项目建议按学期切 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test) clf LogisticRegression(class_weightbalanced, max_iter1000, C1.0) clf.fit(X_train_s, y_train) y_prob clf.predict_proba(X_test_s)[:, 1] print(AUC:, roc_auc_score(y_test, y_prob)) print(classification_report(y_test, (y_prob 0.5).astype(int))) # 输出系数方便写预警解释 coef_df pd.DataFrame({特征: feature_cols, 系数: clf.coef_[0]}) print(coef_df.sort_values(系数, ascendingFalse))逻辑说明StandardScaler对逻辑回归很重要因为绩点范围是 0 到 5课程数是 0 到 15量纲差异会让系数不可比。class_weightbalanced自动按类别频率反比加权。stratifyy保证训练集和测试集的挂科比例一致避免切分偏差。参数说明C1.0是正则化强度的倒数C 越小正则越强。如果发现模型过拟合训练 AUC 远高于测试 AUC把 C 降到 0.1 试试。max_iter1000是防止默认 100 次迭代不收敛的保险数据标准化后一般几百次就收敛。3.3 预警阈值不是 0.5按干预成本反推模型输出的是概率但辅导员需要的是「预不预警」的二值判断。默认 0.5 的阈值在学业预警场景下往往不合适——漏掉一个真正会挂科的学生代价远大于多提醒一个不会挂科的学生。所以阈值应该往低调比如 0.3 甚至 0.25。具体怎么定先画出不同阈值下的召回率和精确率然后问教务老师「你们能接受多少比例的误报」。如果辅导员人力有限只能跟进 50 个人那就把阈值定在能让预警名单正好 50 人的位置。这个逻辑比追求 F1 最大值更贴近实际落地。import numpy as np from sklearn.metrics import precision_recall_curve precision, recall, thresholds precision_recall_curve(y_test, y_prob) # 找到召回率 0.8 时对应的最低阈值 idx np.where(recall 0.8)[0][0] print(建议阈值:, thresholds[idx], 此时精确率:, precision[idx])这段代码帮你找到「召回率至少 80%」的最低阈值保证不漏掉大部分真正有风险的学生同时精确率还能接受。实际部署时这个阈值应该每学期根据辅导员反馈微调。4. 从脚本到系统Flask 接口和预警名单生成怎么接4.1 最小可用的 Flask 预警接口模型训完不能只躺在 Jupyter 里得让教务系统或辅导员能调用。最小方案是用 Flask 暴露一个 POST 接口传入学生特征返回风险概率和预警等级。from flask import Flask, request, jsonify import joblib import numpy as np app Flask(__name__) model joblib.load(warning_model.pkl) # 训练好的逻辑回归 scaler joblib.load(scaler.pkl) # 同一个 StandardScaler FEATURE_ORDER [平均绩点, 最低绩点, 挂科数, 缺考数, 绩点变化, 课程数] app.route(/predict, methods[POST]) def predict(): data request.get_json() # 按训练时的特征顺序组装缺的补 0 features [data.get(f, 0) for f in FEATURE_ORDER] x scaler.transform([features]) prob model.predict_proba(x)[0][1] level 高风险 if prob 0.6 else (中风险 if prob 0.3 else 低风险) return jsonify({学号: data.get(学号), 风险概率: round(prob, 4), 预警等级: level}) if __name__ __main__: app.run(host0.0.0.0, port5000)逻辑说明joblib.load加载训练时保存的模型和 scaler保证推理时的标准化参数和训练时一致——这是最常见的翻车点很多人重新 fit 一个 scaler 导致预测结果完全不对。FEATURE_ORDER强制特征顺序避免字典顺序不一致导致模型输入错位。参数说明host0.0.0.0让接口能被局域网内其他机器访问port5000是 Flask 默认端口。预警等级的分界值 0.6 和 0.3 是我按经验设的实际应该根据第 3 章算出的阈值来定。4.2 批量生成预警名单并导出 Excel辅导员不会一个个调接口他们需要一张 Excel 名单。下面脚本批量读取学生数据调用模型输出带预警等级的名单。import pandas as pd import joblib model joblib.load(warning_model.pkl) scaler joblib.load(scaler.pkl) feature_cols [平均绩点, 最低绩点, 挂科数, 缺考数, 绩点变化, 课程数] current pd.read_csv(current_semester.csv) # 当前学期特征表 X scaler.transform(current[feature_cols]) current[风险概率] model.predict_proba(X)[:, 1] current[预警等级] pd.cut( current[风险概率], bins[0, 0.3, 0.6, 1.0], labels[低风险, 中风险, 高风险] ) # 按风险概率降序高风险在前 result current.sort_values(风险概率, ascendingFalse) result.to_excel(预警名单.xlsx, indexFalse) print(高风险人数:, (result[预警等级] 高风险).sum())逻辑说明pd.cut按概率分箱打等级和接口里的分界值保持一致。sort_values让高风险学生排在前面辅导员打开 Excel 第一眼就能看到最需要关注的人。to_excel直接产出可下发的文件。参数说明bins的分界值必须和接口里的level判断一致否则同一学生在接口和名单里等级不同这是实际项目里被反馈过的问题。current_semester.csv的特征列名必须和训练时完全一致包括中文列名改一个字符就会 KeyError。5. 避坑与排查学业预警系统上线前必须过的五道坎5.1 现象模型 AUC 0.95上线后辅导员说「全是误报」原因数据泄露。最常见的是把「当前学期挂科数」当特征去预测「当前学期是否挂科」或者标签和特征来自同一学期。训练时 AUC 虚高实际预测时模型看到的是未来信息上线就崩。解决严格按时间切分。特征只取当前学期及之前标签取下一学期。用shift(-1)构造标签后检查每一行的特征列是否都来自标签学期之前。我一般会额外加一列特征截止学期和标签学期肉眼确认没有交叉。5.2 现象接口返回的概率全是 0.5 附近没有区分度原因推理时用了新的 StandardScaler或者特征顺序和训练时不一致。逻辑回归对输入尺度敏感scaler 参数不一致会让所有样本的标准化结果偏移概率被压缩到中间。解决训练时用joblib.dump(scaler, scaler.pkl)保存推理时加载同一个文件绝不重新 fit。特征顺序用列表固定组装输入时按列表顺序取值不依赖字典顺序。5.3 现象教务系统导出的 CSV 读进来全是乱码或者学号变成科学计数法原因编码是 GBK 而非 UTF-8学号列被 pandas 推断为数值类型长学号变成1.234e09。解决读入时指定encodinggbk学号列用dtype{学号: str}强制字符串。如果已经读进来变了用pd.read_csv(..., converters{学号: str})重新读。导出 Excel 时也要注意学号列格式否则辅导员看到的学号是错的。5.4 现象预警名单里同一学生出现多次原因成绩表里一个学生一个学期有多条记录多门课聚合时没有按学号学期去重或者 merge 时产生了笛卡尔积。解决聚合后用drop_duplicates(subset[学号, 学期])检查。merge 前确认关联键在右表中唯一如果不唯一先聚合再 merge。我一般会在 merge 后立刻print(df.shape)和print(df[学号].nunique())两者不一致就说明有重复。5.5 现象模型这学期准下学期就不准了原因课程难度、绩点算法、培养方案变了导致特征分布漂移。比如某学期高数换了个给分严的老师整体绩点下降模型会把大量学生误判为高风险。解决每学期重新训练一次模型用最近几年的数据滚动更新。同时监控预警名单的人数比例如果高风险人数突然从 5% 跳到 20%先检查数据分布再信模型。保留一个人工复核环节辅导员可以标记「误报」这些标记数据攒够了就加入下一轮训练。6. 让预警更准的一个技巧用课程难度做特征校准前面所有特征都是学生维度的但学业预警有个被忽略的变量课程本身难度不同。高数和体育的挂科率天然不在一个量级如果模型不知道这一点就会把「选了高数」误判成「学生不努力」。我一般会加一个课程难度校准特征用历史数据算出每门课的挂科率然后对学生当前学期所选课程取平均挂科率作为「课程难度基线」放进模型。# 用历史成绩算每门课的挂科率 course_difficulty score.groupby(课程号).apply( lambda g: (g[绩点] 1.0).mean() ).rename(课程挂科率).reset_index() # 学生当前学期所选课程的难度均值 stu_course score[score[学期] 2024-2025-1][[学号, 课程号]] stu_course stu_course.merge(course_difficulty, on课程号, howleft) stu_difficulty stu_course.groupby(学号)[课程挂科率].mean().rename(课程难度基线) model_df model_df.merge(stu_difficulty, on学号, howleft) model_df[课程难度基线] model_df[课程难度基线].fillna(model_df[课程难度基线].mean())逻辑说明course_difficulty用历史所有学期的成绩算每门课的挂科率这是一个稳定的课程属性。stu_difficulty把学生当前学期所选课程的挂科率取平均得到「这个学生这学期面对的课程有多难」。把它加入特征后模型能区分「绩点低是因为课程难」和「绩点低是因为学生状态差」。参数说明课程挂科率的计算应该排除当前学期只用历史数据否则又引入泄露。fillna用全局均值补缺失保证新生或新课不会因为缺这个特征被丢弃。加入这个特征后我实测 AUC 通常能提升 0.03 到 0.05更重要的是辅导员的误报反馈明显减少——他们最常说的「这门课本来就难」被模型学到了。验证方法加特征前后各跑一次roc_auc_score同时看高风险名单里「选了公认难课」的学生比例是否下降。如果下降说明校准生效。我现在的习惯是每接一个学院的预警需求先花半天算课程难度表再开始调模型——这个顺序比反过来省事得多。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询