简介:这是一套面向高校毕业设计、课程设计与毕业论文场景的Python学业预警系统完整项目源码,适合具备Python与Django基础、希望完成教育管理类实战项目的学生参考。系统围绕学生成绩、出勤、作业等学业数据,构建数据收集、清洗分析、机器学习预警模型与可视化后台,可帮助教师和辅导员提前识别学业风险并采取辅导措施。压缩包共317个文件,约4.13MB,包含27个py源码文件、1个sql数据库脚本、14个html页面及大量css、js、gif、woff2等前端与图标资源,覆盖后端逻辑、模型训练、数据处理与界面展示各环节。目前已有213人学习下载。项目结构完整,涵盖Django后端、Scikit-Learn预测模型与Bootstrap前端,可作为毕业设计选题、课程实践或论文实现的参考方案,便于快速理解教育数据分析和预警机制的落地思路。
1. 学业预警系统:从教务数据到干预动作的最后一公里
每学期期末,教务老师手里都有一份让人头疼的名单:挂科两门以上的、绩点跌破 1.5 的、连续两学期学分修不满的。问题是,这份名单往往在成绩全部录入之后才被整理出来,而这时候学生已经放假回家了,干预窗口早就关了。基于 Python 的高校学生学业预警系统要解决的核心问题,就是把这件事从「事后统计」变成「学期中实时触发」——成绩一录入、考勤一同步、作业一提交,系统就能算出风险等级并推送给辅导员。
这套系统适合两类人:一类是高校信息化部门或教务处的技术岗,需要一套能跑在校园内网、对接现有教务数据库的预警工具;另一类是计算机相关专业的学生,想拿一个真实场景的 Python 项目练手,涉及数据处理、规则引擎、Web 后端和可视化。它不需要多高深的技术栈,但对数据清洗和规则设计的细节要求很高,后面会逐层拆开讲。
2. 预警规则怎么定:从教务原始表到风险标签
2.1 先搞清楚数据从哪来、长什么样
大部分高校的教务系统导出数据无非几张表:学生基本信息表、成绩表、培养方案表、考勤记录表。成绩表通常长这样——学号、课程号、课程名、学分、绩点、成绩、学期。考勤表可能是学号、日期、状态(出勤/迟到/旷课/请假)。这些表直接拿来算预警是不行的,因为存在大量脏数据:重修成绩和正考成绩混在一起、同一门课多次选课记录、学分字段为空、学期格式不统一。
我一般会先写一个数据探查脚本,把每张表的字段类型、空值率、重复行数打出来,确认数据质量再往下走。这一步用 pandas 几行就能搞定:
import pandas as pd def profile_table(df, name): """打印单表的数据质量概览""" print(f"=== {name} ===") print(f"行数: {len(df)}, 列数: {len(df.columns)}") # 空值率 null_rate = df.isnull().mean().round(4) print("空值率:\n", null_rate[null_rate > 0]) # 重复行 dup = df.duplicated().sum() print(f"完全重复行: {dup}") # 每列唯一值数量 print("唯一值数:\n", df.nunique()) # 用法 score_df = pd.read_excel("成绩表.xlsx") profile_table(score_df, "成绩表")这段代码的逻辑很直接:isnull().mean()算出每列空值占比,duplicated().sum()统计完全重复的行。参数上唯一需要注意的是read_excel的dtype参数——学号这类字段如果被 Excel 存成数字,前导零会丢,建议显式指定dtype={'学号': str}。探查完你大概率会发现成绩表里同一学号同一课程有多条记录,这就是重修和正考混在一起了,下一步必须去重。
2.2 规则引擎:把「挂科两门」翻译成可执行逻辑
预警规则听起来简单,但落到代码里要考虑的边界很多。常见的规则维度包括:不及格课程门数、平均绩点、单学期学分获得率、旷课次数、作业提交率。每条规则都要定义阈值、统计周期和触发条件。
我一般用配置化的方式写规则,而不是把 if-else 硬编码在业务逻辑里。这样辅导员想调整阈值时不用改代码:
# rules_config.py RISK_RULES = [ { "name": "不及格课程数超标", "level": "高", "condition": lambda s: s["fail_count"] >= 2, "description": "本学期不及格课程达到2门及以上" }, { "name": "绩点过低", "level": "高", "condition": lambda s: s["gpa"] < 1.5, "description": "本学期平均绩点低于1.5" }, { "name": "学分获得率不足", "level": "中", "condition": lambda s: s["credit_rate"] < 0.6, "description": "已获学分占所选学分比例低于60%" }, { "name": "旷课次数偏多", "level": "中", "condition": lambda s: s["absent_count"] >= 5, "description": "本学期累计旷课5次及以上" }, ]这里用 lambda 表达式把条件抽象出来,s是一个字典,包含每个学生的聚合指标。参数说明:fail_count是不及格门数,gpa是加权平均绩点,credit_rate是获得学分除以所选学分,absent_count是旷课次数。阈值 2、1.5、0.6、5 这些数字不是拍脑袋定的,一般参考学校学籍管理规定里关于学业警告的条款,再结合历史数据做分布分析——比如把过去三年被学业警告的学生指标拉出来,看他们的绩点分布集中在哪个区间,取一个能覆盖大部分真实案例又不至于误报的值。
2.3 从原始表到学生指标:聚合计算的完整链路
有了规则,接下来要把原始成绩表聚合成每个学生一行的指标表。这一步的坑最多,我拆成三步走。
第一步,清洗成绩表。去掉重修标记为「正考」之外的记录,只保留每门课的最高成绩:
def clean_scores(df): """清洗成绩表:去重、保留最高分、统一学期格式""" df = df.copy() # 学号统一为字符串 df["学号"] = df["学号"].astype(str).str.strip() # 只保留有效成绩 df = df[df["成绩"].notna()] # 同一学号同一课程同一学期,保留最高成绩 df = df.sort_values("成绩", ascending=False) df = df.drop_duplicates(subset=["学号", "课程号", "学期"], keep="first") # 标记是否及格 df["is_fail"] = df["成绩"] < 60 return dfdrop_duplicates之前先按成绩降序排列,keep="first"就能保住最高分。这一步的逻辑是:重修刷分的情况很常见,如果不去重,一个学生同一门课会被算两次,挂科门数直接翻倍,预警就失真了。
第二步,按学号聚合出指标:
def build_student_metrics(score_df, attendance_df): """聚合出每个学生的预警指标""" # 成绩维度 score_agg = score_df.groupby("学号").agg( total_courses=("课程号", "nunique"), fail_count=("is_fail", "sum"), total_credit=("学分", "sum"), earned_credit=("学分", lambda x: x[score_df.loc[x.index, "is_fail"] == False].sum()), gpa=("绩点", "mean") ).reset_index() # 学分获得率 score_agg["credit_rate"] = score_agg["earned_credit"] / score_agg["total_credit"] # 考勤维度 attend_agg = attendance_df[attendance_df["状态"] == "旷课"].groupby("学号").size().reset_index(name="absent_count") # 合并 metrics = score_agg.merge(attend_agg, on="学号", how="left") metrics["absent_count"] = metrics["absent_count"].fillna(0) return metrics这里earned_credit的计算用了 lambda,逻辑是只累加is_fail为 False 的行的学分。gpa直接取均值,严格来说应该按学分加权,但很多学校教务系统导出的绩点本身就是加权后的,具体要看数据来源。merge用左连接保证没有考勤记录的学生也不会丢,fillna(0)把缺失的旷课次数补成零。
第三步,套用规则生成预警结果:
def apply_rules(metrics_df, rules): """对每个学生应用所有规则,生成预警标签""" results = [] for _, stu in metrics_df.iterrows(): stu_dict = stu.to_dict() triggered = [] for rule in rules: if rule["condition"](stu_dict): triggered.append({"rule": rule["name"], "level": rule["level"], "desc": rule["description"]}) if triggered: max_level = "高" if any(t["level"] == "高" for t in triggered) else "中" results.append({ "学号": stu["学号"], "风险等级": max_level, "触发规则": "; ".join(t["rule"] for t in triggered), "详情": triggered }) return pd.DataFrame(results)这段代码遍历每个学生,逐条规则判断,把触发的规则收集起来。风险等级取所有触发规则中的最高级——只要有一条「高」就是「高」,否则是「中」。triggered列表保留了每条规则的详情,方便后续推送给辅导员时展示具体原因,而不是只给一个冷冰冰的等级。
3. 系统落地:从脚本到可用的预警服务
3.1 用 Flask 搭一个最小可用的预警接口
规则跑通之后,下一步是让辅导员能查到结果。最轻量的做法是用 Flask 起一个内网服务,提供两个接口:一个触发预警计算,一个按学号或院系查询预警结果。数据库用 SQLite 就够了,校园内网并发不高,没必要上 MySQL。
from flask import Flask, request, jsonify import sqlite3 app = Flask(__name__) DB = "warning.db" def get_db(): conn = sqlite3.connect(DB) conn.row_factory = sqlite3.Row return conn @app.route("/api/calculate", methods=["POST"]) def calculate(): """触发预警计算,结果写入数据库""" from pipeline import run_pipeline # 前面写的清洗+聚合+规则 result_df = run_pipeline() conn = get_db() result_df.to_sql("warning_result", conn, if_exists="replace", index=False) conn.close() return jsonify({"status": "ok", "count": len(result_df)}) @app.route("/api/warnings", methods=["GET"]) def get_warnings(): """按院系或学号查询预警结果""" dept = request.args.get("dept") sid = request.args.get("sid") conn = get_db() sql = "SELECT * FROM warning_result WHERE 1=1" params = [] if dept: sql += " AND 学号 IN (SELECT 学号 FROM student_info WHERE 院系=?)" params.append(dept) if sid: sql += " AND 学号=?" params.append(sid) rows = conn.execute(sql, params).fetchall() conn.close() return jsonify([dict(r) for r in rows]) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)/api/calculate是 POST 接口,每次调用会重新跑一遍完整流程并覆盖结果表。/api/warnings支持按院系和学号过滤,dept参数通过子查询关联学生信息表拿到院系下的所有学号。参数上注意host="0.0.0.0"让服务监听所有网卡,校园内网其他机器才能访问;端口 5000 是 Flask 默认,如果被占用改成 5001 即可。
3.2 对接教务数据源的两种常见方式
实际部署时,数据不会手动导出 Excel。常见做法有两种:一种是直连教务系统的数据库(通常是 Oracle 或 SQL Server),用定时任务拉取增量数据;另一种是教务系统提供 API,通过 HTTP 请求获取。前者更常见,但需要 DBA 开只读账号。
直连数据库的写法:
import cx_Oracle import pandas as pd def fetch_scores_from_oracle(): """从教务 Oracle 库拉取本学期成绩""" dsn = cx_Oracle.makedsn("10.0.0.100", 1521, service_name="jwgl") conn = cx_Oracle.connect(user="readonly", password="***", dsn=dsn) sql = """ SELECT XH AS 学号, KCH AS 课程号, KCM AS 课程名, XF AS 学分, JD AS 绩点, CJ AS 成绩, XQ AS 学期 FROM JW_CJ_TABLE WHERE XQ = :term """ df = pd.read_sql(sql, conn, params={"term": "2024-2025-1"}) conn.close() return dfcx_Oracle.makedsn的三个参数分别是主机、端口、服务名,这些找 DBA 要。SQL 里用绑定变量:term而不是字符串拼接,避免注入也提升查询效率。pd.read_sql直接返回 DataFrame,后续清洗逻辑不用改。
如果教务系统只提供 API,那就用 requests 拉 JSON 再转 DataFrame,逻辑类似,注意处理分页和鉴权 token 过期。
3.3 预警结果的可视化:让辅导员一眼看到重点
辅导员不需要看表格,他们需要的是「哪个院系风险学生最多」「风险等级分布如何」「哪些学生触发了多条规则」。用 pyecharts 或 plotly 生成几个图嵌到页面里就够了。我一般做三个图:院系风险人数柱状图、风险等级饼图、触发规则频次横向条形图。
from pyecharts.charts import Bar, Pie from pyecharts import options as opts def dept_risk_chart(df): """院系风险人数柱状图""" dept_counts = df.groupby("院系")["学号"].nunique().sort_values(ascending=False) bar = Bar() bar.add_xaxis(dept_counts.index.tolist()) bar.add_yaxis("风险人数", dept_counts.values.tolist()) bar.set_global_opts( title_opts=opts.TitleOpts(title="各院系学业风险人数"), xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=30)) ) return bar.render_embed()groupby("院系")["学号"].nunique()统计每个院系去重后的风险学生数,rotate=30防止院系名称太长重叠。render_embed()返回 HTML 字符串,直接塞进 Flask 的模板里。这一步没什么技术难度,但对推动系统落地很关键——领导看到图才知道这事有价值。
4. 避坑与排查:那些跑起来才知道的问题
4.1 成绩表里的重修记录导致挂科数翻倍
现象:某个学生明明只挂了一门课,系统却报出挂科四门,风险等级直接拉满。原因:成绩表里同一门课有正考、补考、重修多条记录,清洗时没有去重,每条不及格记录都被计入fail_count。解决:在清洗阶段按「学号+课程号+学期」去重,只保留最高成绩那条,再去判断是否及格。如果学校允许跨学期重修,去重维度要加上学期,否则会把不同学期的同一门课合并掉。
4.2 学分字段为空导致获得率计算出 NaN
现象:部分学生的credit_rate是 NaN,规则判断时直接跳过,漏掉了本该预警的人。原因:培养方案里有些课程(如军训、社会实践)学分字段在成绩表里是空的,sum()遇到空值返回 0 或 NaN。解决:在聚合前用df["学分"].fillna(0)填充,或者在计算获得率时加一个判断——如果total_credit为 0 就跳过该规则,避免除零。更稳妥的做法是维护一张课程学分对照表,用课程号去关联补全学分。
4.3 考勤数据的时间范围没对齐
现象:系统算出的旷课次数远高于辅导员手工统计的数字。原因:考勤表拉取的是全部历史数据,而预警规则只应该统计本学期。解决:在拉取考勤数据时加学期过滤条件,或者用学期起止日期过滤日期字段。这个坑很隐蔽,因为数据本身没错,错的是范围。我一般会在 pipeline 入口处显式传入term_start和term_end两个参数,所有数据源都按这个范围过滤。
4.4 Flask 接口返回中文乱码
现象:浏览器访问/api/warnings返回的 JSON 里中文显示为\uXXXX。原因:Flask 默认的 JSON 序列化会把非 ASCII 字符转义。解决:在 Flask app 配置里加app.config["JSON_AS_ASCII"] = False,或者用jsonify时指定ensure_ascii=False。新版 Flask 用app.json.ensure_ascii = False。这个不影响功能但影响体验,辅导员看到转义字符会以为系统坏了。
4.5 规则阈值拍脑袋定导致误报率过高
现象:系统上线第一周,预警名单占了全年级 40%,辅导员直接不看了。原因:阈值定得太松,比如「旷课 3 次」就触发预警,但很多学生只是偶尔迟到被记了旷课。解决:先用历史数据做回溯测试——拿过去两年被学业警告的学生数据跑一遍规则,看召回率和准确率。阈值调整到预警名单占比在 5% 到 10% 之间比较合理。另外可以引入「连续两学期触发」才升级为高风险,单学期触发只做提醒,减少误报。
5. 让预警真正起作用:从推送到干预闭环
系统跑通、规则调好之后,最大的挑战不是技术,而是「预警发出去之后有没有人管」。我见过太多系统做完就搁置了,原因是辅导员觉得「你只告诉我谁有风险,但我找他谈什么、怎么谈,你不管」。所以最后一章聊一个具体技巧:把预警结果和干预建议绑定输出。
具体做法是在规则配置里加一个action字段,每条规则触发时附带一条建议动作。比如「不及格课程数超标」对应「建议约谈,了解具体科目困难,联系任课教师」;「旷课次数偏多」对应「建议联系家长,确认是否在外兼职或作息问题」。这些建议不一定要多智能,但能让辅导员拿到名单就知道下一步做什么。
RISK_RULES = [ { "name": "不及格课程数超标", "level": "高", "condition": lambda s: s["fail_count"] >= 2, "action": "建议约谈学生,了解具体科目困难,联系任课教师安排辅导" }, { "name": "旷课次数偏多", "level": "中", "condition": lambda s: s["absent_count"] >= 5, "action": "建议联系家长确认情况,排查是否在外兼职或作息问题" }, ]然后在生成预警结果时把action一起写进去,推送给辅导员的表格里多一列「建议动作」。这个改动很小,但效果很明显——辅导员从「知道谁有问题」变成「知道该做什么」,系统的使用率会高很多。
另一个技巧是加一个「干预记录」表,辅导员约谈后在系统里勾一下「已处理」,下次计算时已经处理过的学生如果指标没有恶化,就不再重复推送。这样避免同一批学生每周都被推一遍,辅导员会烦。干预记录表就三个字段:学号、处理时间、处理人、备注。查询预警结果时左连接这张表,过滤掉已处理的记录。
验证系统是否真的有用,不要看技术指标,看一个数字:从预警发出到辅导员实际约谈的平均天数。如果这个数字在两周以内,说明流程跑通了;如果超过一个月,大概率是推送方式有问题——可能是发邮件没人看,改成企业微信或钉钉机器人推送会好很多。
我自己踩过最大的坑是花了两周做可视化大屏,结果辅导员根本不看,他们只想要一个 Excel 名单。后来我把推送改成每天早上八点自动发一份 Excel 到辅导员群里,使用率立刻上来了。技术方案再漂亮,不如贴着使用者的习惯走。希望帮到你。
本文还有配套的精品资源,点击获取