ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python的高校学业预警系统:从数据清洗到实时干预的完整实现

基于Python的高校学业预警系统:从数据清洗到实时干预的完整实现 简介这是一套面向高校毕业设计、课程设计与毕业论文场景的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(成绩, ascendingFalse) df df.drop_duplicates(subset[学号, 课程号, 学期], keepfirst) # 标记是否及格 df[is_fail] df[成绩] 60 return dfdrop_duplicates之前先按成绩降序排列keepfirst就能保住最高分。这一步的逻辑是重修刷分的情况很常见如果不去重一个学生同一门课会被算两次挂科门数直接翻倍预警就失真了。第二步按学号聚合出指标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(nameabsent_count) # 合并 metrics score_agg.merge(attend_agg, on学号, howleft) 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_existsreplace, indexFalse) 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 11 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(host0.0.0.0, port5000)/api/calculate是 POST 接口每次调用会重新跑一遍完整流程并覆盖结果表。/api/warnings支持按院系和学号过滤dept参数通过子查询关联学生信息表拿到院系下的所有学号。参数上注意host0.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_namejwgl) conn cx_Oracle.connect(userreadonly, password***, dsndsn) 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(ascendingFalse) bar Bar() bar.add_xaxis(dept_counts.index.tolist()) bar.add_yaxis(风险人数, dept_counts.values.tolist()) bar.set_global_opts( title_optsopts.TitleOpts(title各院系学业风险人数), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate30)) ) return bar.render_embed()groupby(院系)[学号].nunique()统计每个院系去重后的风险学生数rotate30防止院系名称太长重叠。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_asciiFalse。新版 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 到辅导员群里使用率立刻上来了。技术方案再漂亮不如贴着使用者的习惯走。希望帮到你。本文还有配套的精品资源点击获取
返回列表