
简介面向人工智能教育应用开发者、Python机器学习初学者及高校教务管理人员的学业预警系统完整项目基于Python实现学生成绩、出勤、作业等多维度数据的采集清洗、整合建模、风险预测与预警推送全流程。压缩包共530个文件以Java后台服务、JavaScript前端交互、CSS与HTML页面样式架构、SQL数据库脚本及XML配置为主要类型附带图片、日志和字体等静态资源整个包体仅3.95MB结构紧凑便于部署调试。目前已有350人学习或浏览内含完整示例项目覆盖数据处理脚本、模型训练代码及预警推送框架并自带前端界面资源方便参考与二次开发。借助该项目可快速掌握数据预处理、机器学习建模与Web系统集成技能理解学业预警系统的工程化实现思路为教育领域的智能分析与主动干预提供可复用模板。1. 学业预警系统到底是什么不是一个模型而是一套“把人找出来”的规则引擎把几千名学生的成绩、考勤、作业提交记录汇总到一起算出一个需要重点关注的高危名单这件事听起来像人工智能大作业实际上是高校教务场景里最典型的落地题。我做过这类学业预警系统核心难点不在于训练多深的模型而在于把预警规则讲清楚挂科超过两门、绩点跌破警戒线、缺勤率持续走高、成绩一学期比一学期下滑这四类学生的危急程度完全不同处理方式也不一样。系统要解决的是辅导员和教务老师喊了多年的痛点——数据散落在教务系统、Excel 和考勤 App 里等人力发现时学生已经挂了三门。这套方案适合人工智能课程设计、毕业设计也适合想自建预警能力的信息中心用 Python 加规则引擎就能跑起来把漏报率压下来才是这题真正值钱的部分。2. 学业预警系统的数据底座从一张成绩表拆成五张核心业务表2.1 系统分层与技术选型为什么是 Flask 加 SQLite先讲整体结构。一个可交付的学业预警系统通常分四层数据接入层负责把教务导出的 CSV、Excel、手工填的考勤表收进来规则计算层按照绩点、挂科数、缺勤率、成绩趋势四个维度算预警分可视化层给辅导员提供全校视角的大屏和名单导出通知层把预警结果推送给对应角色。技术选型上我一般不用 Hadoop、Spark 那一套重型框架。学业预警的数据量级就是几千个学生、每学期几十万条成绩记录一台普通服务器甚至一台笔记本都能扛住。后端用 Flask 提供接口数据库用 SQLite 起步等并发上来了再平滑迁移到 MySQL全程不用改业务代码因为 SQLAlchemy 的 ORM 层把数据库差异挡掉了。有个容易被忽略的点这套系统必须能离线部署学生成绩属于敏感数据常见做法是放在教务内网服务器上不依赖任何外部大模型 API离线也能完整跑通。四个模块之间通过一张预警记录表衔接规则引擎只负责写表可视化层和通知层都从这张表读数据。这个设计的好处是规则调整后不需要改前端重新算一遍写库即可属于典型的“算写分离”后续维护成本低很多。2.2 五张核心业务表建表 SQL 与字段设计成绩、考勤这些原始数据不会只用来做一次预警学期评优、学分清查都要复用所以我把表拆成学生表、课程表、成绩表、考勤表和预警记录表五张而不是把所有信息塞进一张宽表。建表脚本如下字段命名和类型都按实际跑过的经验给出-- 学生基础信息表 CREATE TABLE student ( student_id TEXT PRIMARY KEY, -- 学号统一按字符串存避免前导0丢失 name TEXT NOT NULL, grade INTEGER, -- 年级如 2023 college TEXT, -- 学院 major TEXT, -- 专业 class_name TEXT, -- 班级 enrollment_date TEXT -- 入学日期统一 YYYY-MM-DD ); -- 课程表 CREATE TABLE course ( course_id TEXT PRIMARY KEY, course_name TEXT NOT NULL, credit REAL NOT NULL, -- 学分用于加权绩点计算 semester TEXT NOT NULL, -- 开课学期如 2023-2024-1 course_type TEXT -- 必修/选修/公共课 ); -- 成绩表 CREATE TABLE score ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, course_id TEXT NOT NULL, score_value REAL, -- 百分制成绩 score_grade TEXT, -- 优秀/良好/及格/不及格 is_makeup INTEGER DEFAULT 0, -- 是否补考成绩 record_time TEXT, UNIQUE(student_id, course_id, semester) ); -- 考勤表 CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, semester TEXT NOT NULL, total_classes INTEGER, -- 应出勤课时 absent_classes INTEGER, -- 缺勤课时 late_count INTEGER DEFAULT 0, UNIQUE(student_id, semester) ); -- 预警记录表核心输出表 CREATE TABLE warning_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, semester TEXT NOT NULL, rule_id TEXT NOT NULL, -- 触发规则编号如 R01 warning_level TEXT NOT NULL, -- red/orange/yellow warning_score REAL, -- 综合预警分 score_snapshot TEXT, -- 计算时的成绩快照JSON格式 status TEXT DEFAULT pending,-- pending/confirmed/closed created_at TEXT, UNIQUE(student_id, semester, rule_id) );说几个关键设计理由。score 表里加了 UNIQUE(student_id, course_id, semester) 约束防止数据接入时重复执行导致同一门课出现多条成绩warning_record 表必须存 score_snapshot 而不是只存学号和等级因为预警规则会调参规则一改历史记录的判定依据就丢了留一份当时成绩的快照才能回溯“当初为什么给他红色预警”。考勤表按学期聚合而不是按每次课一条记录因为大多数教务系统的考勤流水是离散的按学生和学期汇总后查询性能最好计算规则时也只需要读一行记录。2.3 数据接入的清洗约定先把字符串变数字再做规则判定实际拿到的教务导出数据没有一份是干净的。“缺考”“缓考”“作弊”这些文本会混进成绩列数字里偶尔带着换行符学号被 Excel 改成科学计数法更是家常便饭。数据接入层不能贪快直接入库我在 Flask 里写一个清洗管线先按字段映射表把中文表头转成英文字段名再做统一类型转换。清洗时有两个约定我一直坚持。第一个约定是成绩、缺勤率这些数值字段入库前统一用 float() 强转转不成功的单独放进 error_log 表而不是直接丢弃因为“缺考”和“零分”在预警规则里语义完全不同丢数据比保留脏数据更危险。第二个约定是日期字段统一转成 YYYY-MM-DD 格式后面算学期跨度、比对成绩趋势时才能直接排序我见过太多因为 2024/1/1 和 2024-01-01 混排导致排序错乱的项目这种坑一旦数据量上来很难追查。3. 预警规则引擎阈值判定、加权评分与机器学习兜底3.1 四类预警规则与阈值怎么定不翻车规则引擎是整个系统的决策核心它不是越复杂越好。我常用的规则体系分四类每一类对应教务老师熟知的典型画像。规则编号规则条件预警等级设计理由R01累计挂科数 2红挂科两门是多数高校学业警示的最低线R02平均学分绩点 2.0红绩点跌破 2.0 意味着学位证有风险R03缺勤率 25%橙出勤异常是成绩下滑的先行指标R04连续两学期成绩下滑且幅度超过 15 分黄捕捉“温水煮青蛙”式的退步阈值不能拍脑袋定最可靠的办法是从学校历史数据里找分布。比如 R02 的 2.0先统计全体学生的绩点分布看 2.0 以下占多少比例如果低于 5%说明这批学生确实是少数值得单独关注如果占比 30%说明生源整体偏弱阈值要降到 1.8 或者引入条件预警否则名单太长辅导员根本处理不过来。阈值本质上是一个风险容忍度的旋钮不是算出来的常数。3.2 规则引擎 Python 实现从 DataFrame 到预警名单规则的执行我用一个纯 Python 模块实现不依赖复杂框架。输入是清洗后的成绩表、考勤表和学生表输出是预警记录。下面这段代码是核心判定逻辑可以直接抄进项目里跑通第一版import pandas as pd import json from datetime import datetime def calculate_gpa(scores_df): 按学分加权计算平均绩点采用常见的百分制转绩点算法 total_credit 0.0 total_points 0.0 for _, row in scores_df.iterrows(): credit row[credit] score row[score_value] total_credit credit if score 90: total_points credit * 4.0 elif score 80: total_points credit * 3.0 elif score 70: total_points credit * 2.0 elif score 60: total_points credit * 1.0 # 不及格计 0 分 return total_points / total_credit if total_credit 0 else 0.0 def evaluate_student(student_id, semester, scores_df, att_df): 对单个学生执行四类规则返回预警记录列表 # 筛选该生当前学期及之前的成绩 cur_scores scores_df[scores_df[student_id] student_id] cur_scores cur_scores[cur_scores[semester] semester] fail_count len(cur_scores[cur_scores[score_value] 60]) gpa calculate_gpa(cur_scores) # 考勤数据按学期聚合 att att_df[(att_df[student_id] student_id) (att_df[semester] semester)] absent_rate 0.0 if len(att) 0 and att.iloc[0][total_classes] 0: absent_rate att.iloc[0][absent_classes] / att.iloc[0][total_classes] records [] if fail_count 2: records.append({rule_id: R01, warning_level: red, reason: f累计挂科 {fail_count} 门}) if gpa 2.0: records.append({rule_id: R02, warning_level: red, reason: f平均绩点 {gpa:.2f} 低于 2.0}) if absent_rate 0.25: records.append({rule_id: R03, warning_level: orange, reason: f缺勤率 {absent_rate:.1%} 超过 25%}) # R04 需要对比两个学期平均分仅当当前不是第一学期才执行 semesters sorted(cur_scores[semester].unique()) if len(semesters) 2: last_sem semesters[-1] prev_sem semesters[-2] last_avg cur_scores[cur_scores[semester] last_sem][score_value].mean() prev_avg cur_scores[cur_scores[semester] prev_sem][score_value].mean() if prev_avg - last_avg 15: records.append({rule_id: R04, warning_level: yellow, reason: f平均分从 {prev_avg:.1f} 降至 {last_avg:.1f}}) return records这段代码要注意三个地方。第一filter 条件用 semester 当前学期是因为计算累计挂科数时不能只看到现在这学期学生大二挂的课到大三依然影响学业状态第二R04 的成绩趋势对比取的是两个学期的平均分差而不是单科成绩因为不同学期课程难度不一样单科对比没有意义第三absent_rate 判断用了严格大于 0.25而不是大于等于这样缺勤恰好四分之一的学生不会被误伤边界取值通常配合同一条规则反复调。这个引擎跑完后会得到规则记录列表再写入 warning_record 表。3.3 为什么还要加一层机器学习模型降低误报也防“人工智能偏见”规则引擎的缺点是边界生硬绩点 2.01 的学生和 1.99 的学生只差 0.02预警结果却完全不同。所以我习惯在规则引擎之外加一个机器学习兜底层用逻辑回归对已经标记过的历史数据做概率预估把概率在 0.4 到 0.6 之间的“灰名单”标出来让辅导员人工复核。特征不用多四到六个足够前学期绩点、本学期平均分、缺勤率、挂科数、作业提交率。训练数据从历史预警记录里倒推生成有预警标记为正样本没有则为负样本。这里要特别提一句模型预测结果只作为参考排序不直接替代规则因为一旦模型给出“这个学生不会挂科”的错误判断又没人复核责任归属会很麻烦。模型的价值不是做决策而是帮规则引擎发现它漏掉的组合模式比如“平时作业全交但期末突然崩盘”这类单看阈值发现不了的情况。做这一层时最容易翻车的是样本不平衡——预警学生通常只占全校 5% 左右负样本是正样本的二十倍。我一般对正样本做 SMOTE 过采样或者直接用 class_weightbalanced 让逻辑回归自动调权重。评估指标不要看准确率要看召回率因为漏报一个高危学生的代价远大于多标记一个待复核学生。模型的特征重要性也要定期打印出来看一眼防止模型学到某个学期某门课太难这种假相关这种“人工智能偏见”在学业数据里很常见靠规则引擎反而更透明。4. 可视化大屏与通知推送让辅导员三秒定位高危学生4.1 ECharts 大屏四个图表组件与配置项规则引擎算完的数据如果不能直观呈现系统价值就打了一半折扣。大屏我选用 ECharts因为它是纯前端方案不需要额外安装服务离线环境照样渲染而且对辅导员这种非技术用户来说一眼能看懂比功能炫酷更重要。大屏通常放四个视图全校预警等级分布饼图、各学院预警人数排行柱状图、近四个学期预警数量趋势折线图、红色预警学生明细表格。饼图的核心配置是半径和标签我习惯把红色预警的扇区单独标亮不用默认配色// 预警等级分布饼图核心配置 option { tooltip: { trigger: item }, series: [{ type: pie, radius: [35%, 65%], // 内半径35%外半径65%做环形图 itemStyle: { borderRadius: 6, borderColor: #fff, borderWidth: 2 }, label: { formatter: {b}\n{c}人 ({d}%) }, data: [ { value: redCount, name: 红色预警, itemStyle: { color: #d4322c } }, { value: orangeCount, name: 橙色预警, itemStyle: { color: #ff7d00 } }, { value: yellowCount, name: 黄色预警, itemStyle: { color: #f9c700 } } ] }] };这里 radius 用百分比形式可以让图表跟着容器自适应缩放避免不同分辨率的大屏显示器下图形变形。tooltip 的 formatter 用了模板字符串鼠标悬停时直接显示人数和占比比默认提示多一层信息量。明细表我用 ECharts 的 grid 组件配合滚动条固定表头红色预警按预警分降序排这样辅导员打开大屏的第一眼就能看到最该处理的人。趋势折线图要注意一个数据细节学期字段是字符串排序要用自定义排序函数而不是默认字典序否则“2024-2025-1”会排在“2023-2024-2”前面。我通常在后端先把学期转成数值序号前端只负责按序号取数不在 ECharts 里处理日期逻辑。全套图表的数据接口我统一返回 {code:0, data:{...}} 格式前端只认 data 字段后续扩展图表不用改接口约定。4.2 预警通知推送邮件合发与 webhook 的取舍名单算出来之后最怕的事情是辅导员不知道。预警系统的最后一公里是把结果推出去我这里的方案是优先级最高的红色预警走 SMTP 邮件附带被预警学生的完整成绩快照橙色和黄色预警走企业微信机器人 webhook推送简短摘要。邮件适合正式留痕webhook 适合快速触达两者互补。SMTP 发邮件有个性能坑几百封邮件逐封发送会触发服务商限流而且发信时间极长。正确做法是合发——每个辅导员收一封汇总邮件里面包含他所带班级的红色预警名单。核心代码如下import smtplib from email.mime.text import MIMEText from email.header import Header def send_warning_email(advisor_email, warning_records): 给辅导员发送一份汇总预警邮件warning_records 为记录列表 # 在 HTML 表格里按等级排序逐行展示学生信息 rows for r in sorted(warning_records, keylambda x: x[warning_level]): rows ( ftrtd{r[student_id]}/tdtd{r[name]}/td ftd{r[warning_level]}/tdtd{r[reason]}/td/tr ) html_content f htmlbody h3学业预警通知 ({len(warning_records)} 人)/h3 table border1 cellpadding6 trth学号/thth姓名/thth等级/thth原因/th/tr {rows} /table /body/html msg MIMEText(html_content, html, utf-8) msg[From] Header(学业预警系统, utf-8) msg[To] Header(advisor_email, utf-8) msg[Subject] Header(f红色预警 {len(warning_records)} 人请及时处理, utf-8) with smtplib.SMTP(smtp.example.edu.cn, 587) as server: server.starttls() server.login(alertexample.edu.cn, your_password) server.sendmail(alertexample.edu.cn, [advisor_email], msg.as_string())代码里的 From 和 To 都用 Header 包装了 utf-8 编码否则中文姓名在部分邮件客户端里显示乱码SMTP 连接用 starttls 加密传输成绩单走明文邮件不合适内网邮件服务器也要开 TLS。发信账号密码不要硬编码在代码里存环境变量或者单独的配置文件否则代码一旦传到公共仓库学生数据就跟着泄露了。webhook 推送的逻辑更简单拼一个 JSON 发给机器人接口内容里只放学号和预警等级不放成绩明细防止聊天记录里泄露敏感数据。推送频率也要控制我通常在每天晚上八点统一推一次而不是实时触发因为辅导员不可能一天二十四小时盯着消息固定时间推送更容易形成工作节奏。5. 学业预警系统常见问题与排错五个把项目整破防的坑做了几个版本的学业预警系统后我发现翻车点高度集中在数据处理和工程细节上而不是算法本身。下面五条是踩过之后写进自检清单的坑每一条都按现象、原因、解决三个步骤拆开适合直接对照排错。坑一平时分和期末成绩直接相加算出来的预警名单和班主任印象完全对不上。现象是系统标红的学生里有几个平时作业全交、实验课从不缺勤的“乖学生”班主任看了名单直摇头。原因是教务导出的成绩表里平时成绩是三十分制期末成绩是百分制程序直接用 score_value 平时分 期末分存库平时分权重被放大了三倍多。解决方法是接入时统一量纲先判断每门课成绩字段的最大值如果小于等于 50 就按比例放大到百分制再做加权汇总。我一般在清洗管线里加一个 normalize_score 函数所有成绩先过一遍再决定是否参与绩点计算。坑二预警记录不更新改完规则重跑一遍反而生成了一堆重复数据。现象是第一次跑出了 200 条记录调整阈值后重跑变成 400 条老记录还在库里。原因是 warning_record 表的唯一约束只建在了 id 上没有约束“同一学生同一学期同一规则只能有一条记录”。解决方法是把 UNIQUE(student_id, semester, rule_id) 加上写入方式改成 SQLite 的 INSERT OR REPLACE或者先查重再更新。重算逻辑一定要设计成幂等的不管跑多少次结果都一样这是规则引擎和普通数据分析脚本最大的差别。坑三学生名单导出到 Excel 后学号显示成科学计数法。现象是辅导员把名单导入学校系统时学号后四位变成了 0000几百人没法匹配。原因是 Excel 默认把超过 11 位的数字当成浮点数处理学号这种文本本质被转了类型。解决方法是导出接口里把学号列强制写成文本格式OpenPyXL 写入时设置 number_format 或者导出 CSV 时在学号前加 \t 制表符。这个问题在“人工智能应用与安全工程师”视角下其实是数据完整性事故学号一旦错位整个预警链路就断了。坑四成绩表里“缺考”被当成 0 分拉低了班级平均分导致大量误报。现象是有个班期末出现 20% 橙色预警排查发现是一门公选课有三十几个学生缺考缺考在教务系统里记的是文本“缺考”清洗时被 float() 转成了 0.0。原因是缺考和真正考了零分在预警语义里不同缺考可能代表学生弃修零分代表知识掌握为零两者对应的干预手段完全不同。解决方法是清洗时把“缺考”“缓考”“作弊”映射成单独的标记字段不参与平均分和绩点计算但单独统计缺考科目数作为一条独立规则参与预警。处理完这个之后预警名单一下子就变得贴合实际了。坑五期末高峰期批量推送邮件被服务器退信辅导员一封都没收到。现象是系统日志显示发送成功但辅导员邮箱里空空如也。原因是发送端一次性给几百个辅导员发信内网 SMTP 服务器把它判定为垃圾邮件直接静默丢弃或者前面几封发送太慢导致连接超时。解决方法是改用合发策略把同一个学院的预警名单合并成几封邮件发送间隔加 time.sleep(1) 限速最关键的是先给测试邮箱发一封确认能收到再批量执行。我现在的习惯是每次批量发送前先送一封带时间戳的测试邮件收到后再跑正式任务这已经是发通知类系统的默认流程了。6. 从“能跑”到学期可用三个进阶校验与一个收尾习惯系统能算出名单只是第一步真正让老师愿意长期用的是结果可信。这里分享三个我每次上线前必做的校验。第一个是用混淆矩阵校准阈值。拿去年的历史数据回测把预警结果和学生实际期末表现对比记录准确率和召回率。召回率偏低的规则说明阈值太严漏掉了本该预警的人准确率过低的规则说明条件太宽把大量不该预警的学生卷了进来。按这个思路把 R02 的绩点阈值从 2.0 调到 1.8召回率可能从 62% 涨到 85%同时保持准确率不崩这就是阈值调参的科学依据。第二个是把所有规则参数外置成 JSON 配置文件。规则引擎代码里不写任何魔法数字阈值、学期、课程类型过滤条件全部从 config.json 读取改阈值不用改代码重新部署。配置加上版本号每次调整留一份历史记录这样期末复盘时能说清楚“这个学期预警标准比上学期严在哪”而不是凭记忆拍脑袋。第三个是日期和学期的统一约定。所有表的时间字段一律 YYYY-MM-DD所有学期字段一律 YYYY-YYYY-N这是我在排错生涯里吃过最多亏的地方。代码里任何地方出现 datetime.now() 之前都要想一想这条记录应该用当前时间还是学期结束时间预警系统的统计口径必须和时间点绑定否则学期交替那几天数据会莫名漂移。最后说一个我个人的习惯每次跑完预警名单我都会随机抽十个学生人工翻一遍他们的成绩单和考勤记录核对预警原因是否和真实情况一致。这个动作不花多少时间但能发现数据清洗埋下的隐性错误——比如某门课学分配错导致绩点算偏这类问题在统计指标上根本看不出来。人工智能项目实践做到最后拼的不是模型多先进而是细节经不经得起抽查。希望帮到你。本文还有配套的精品资源点击获取