基于Python的高校学业预警系统:数据建模、规则引擎与Web展示
2026/9/23 13:58:54 网站建设 项目流程

简介:这是一份基于Python Django框架与MySQL数据库的高校学生学业预警系统毕业设计资料包,适用于计算机相关专业学生完成毕设或学习Web系统开发。系统涵盖管理员端菜单模块、预警分析、学生信息与成绩管理、用户管理,以及学生端个人信息、学习计划等核心功能,能够对学业数据进行实时监控与异常预警。压缩包共323个文件,除Python源码外,还包含HTML/CSS/JS前端页面、pyc编译文件、SQL数据库脚本及doc/docx开发文档,整体大小约10.53MB,目录结构清晰,便于快速定位与二次开发。目前已有53人学习下载。资料附带数据库文件与完整开发文档,涵盖系统安装配置、模块说明及使用指南,项目已测试可正常运行,适合需要快速上手或参考完整项目流程的毕业设计者。

1. 高校学业预警系统值不值得做:先看清这题到底在考什么

一个学期结束,辅导员手上最头疼的名单不是奖学金名单,而是挂科名单。学业预警系统本质上是把这个“人工翻成绩单、逐个算挂科和绩点”的过程变成一套可复用的Python程序:数据从教务系统或者Excel里来,程序按规则算出哪些学生需要提醒、提醒到什么程度,再把结果用Web页面展示给辅导员和管理员。这套系统真正难的地方从来不在页面好不好看,而在三条数据口径能不能对齐——教务处那边的成绩口径、辅导员手里的名单口径、学生自己感知的挂科情况,稍微差一点,预警名单就废了。

做这个方向是有价值的。毕业后如果你想走数据方向,这套系统能拿来讲你和业务方对齐口径的经历;如果你走开发方向,它也是一个完整的增删改查加统计展示的项目,够你写进简历。这篇笔记按我实际做过的方案,把需求拆分、数据库建模、规则引擎、Web展示和踩过的坑整个走一遍,照着搭一套能用、经得起答辩问的完整系统。

2. 把需求翻译成系统:从教务数据到预警名单的数据流转

2.1 预警系统的核心业务:先搞清楚谁在用、用什么数据

学业预警系统放在高校里一般服务三类人。辅导员要的是“这个学期我们班谁挂科了、挂了几门、累计学分差多少”;学院教务员要的是“哪个专业、哪个年级的挂科率在上涨,需不需要干预”;学生本人要的是“我现在离学业警示线还有多远”。一套系统如果只做了其中一个视角,答辩时很容易被问住。所以数据模型设计时就要让这三个视角都查得到。

数据来源通常是两个。一个是教务系统的成绩库,但一般情况下学生不可能拿到教务库的直连权限,毕设里最常见的是从教务处导出的Excel成绩单,或者用你自己造的一套仿真成绩数据。另一个是培养方案,它决定了课程属于必修还是选修、对应多少学分,这是算累计挂科学分的基础。没有培养方案,你只能说“挂了两门课”,说不清“累计挂科学分已经到8分”,后者才是学校做退学警示的依据。

整个系统的数据流按顺序是:成绩Excel导入、清洗落库、按学期和学号组织成绩、预警引擎跑规则、生成预警记录、Web端展示和导出。后面所有章节都绕着这条线走,先看数据库怎么建。

2.2 数据库表结构设计与Python建模:学号为什么必须用字符串

学业预警系统的表数量不多,按最小可用方案四张表就够:学生表、课程表、成绩表、预警记录表。我用SQLAlchemy写了一套简化模型,跑在SQLite上。SQLite对毕设来说是性价比最高的选择,文件即数据库,演示时直接拷走就能跑,不用装MySQL服务。

from sqlalchemy import create_engine, Column, Integer, String, Float, ForeignKey from sqlalchemy.orm import declarative_base Base = declarative_base() class Student(Base): __tablename__ = 'students' student_no = Column(String(20), primary_key=True) # 学号 name = Column(String(50), nullable=False) major = Column(String(50)) # 专业 grade = Column(String(4)) # 年级,如 2021 class_name = Column(String(50)) # 行政班 class Course(Base): __tablename__ = 'courses' course_code = Column(String(20), primary_key=True) course_name = Column(String(100), nullable=False) credit = Column(Float) # 学分 is_compulsory = Column(String(1), default='1') # 是否必修,1-是 0-否 class Score(Base): __tablename__ = 'scores' id = Column(Integer, primary_key=True, autoincrement=True) student_no = Column(String(20), ForeignKey('students.student_no'), index=True) course_code = Column(String(20), ForeignKey('courses.course_code')) semester = Column(String(10)) # 例如 2023-2024-1 score = Column(Float) # 百分制成绩,缺考记0 is_retake = Column(String(1), default='0') # 1-重修成绩 0-正常成绩 class WarningRecord(Base): __tablename__ = 'warning_records' id = Column(Integer, primary_key=True, autoincrement=True) student_no = Column(String(20), ForeignKey('students.student_no'), index=True) semester = Column(String(10)) # 触发预警的学期 level = Column(String(10)) # yellow / orange / red reason = Column(String(255)) # 触发规则描述,例如:挂科2门,累计挂科学分6分 is_processed = Column(String(1), default='0') # 辅导员是否已处理

这里最想强调的一点:学号不要用Integer,必须用String。真实学号短的七八位、长的十位出头,Excel里一旦被处理成数字,前导零直接消失,超过15位还会变科学计数法。虽然学号长度不至于触发15位精度问题,但前面有0的话,转成数字再转回来就回不去了。这个坑在后面的导入环节还会再出现。

成绩表里is_retake字段很多人会忽略。一个学生补考过了,成绩库里可能留着一条原始的挂科记录和一条新的通过记录,如果你不去区分,挂科门数会被重复统计,预警名单就会多出一批不该出现的人。先把这个标记留好,规则引擎里再决定取哪条。

2.3 选Flask还是Django:毕设场景下的框架选型逻辑

学业预警系统的交互密度很低,核心操作就是导入成绩、跑预警、看列表、看统计图。对于这种场景,Flask和Django都能做,但我的建议是Flask。理由不是Django不好,而是Django自带Admin后台、ORM、Migration这些能力对毕设来说大部分用不上,反而增加理解负担。

Flask的核心优势是路由和视图函数的结构非常直观,一个@app.route装饰器对应一个页面或接口,答辩时你好讲,老师也好顺着问。Django的MTV分层在项目大了是优势,但一个预警系统的代码量撑不起这个复杂度,硬套反而是负担。

Python环境配置就按最常规的来:虚拟环境建完后用pip安装flask、sqlalchemy、pandas、openpyxl、scikit-learn这几个包就够了。开发调试在PyCharm或VS Code里都行,重点是虚拟环境要选对解释器,不然经常出现“命令行能跑、IDE里报No module named flask”的尴尬情况。装包时国内用户记得把pip源切成清华或阿里镜像,不然在默认源上下载pandas那几十兆文件能等得人想摔电脑。

3. 用Python实现预警引擎:规则阈值、触发逻辑与名单生成

3.1 预警维度怎么选:挂科、绩点、缺勤三个方向

预警规则不要拍脑袋定,要能说出依据。高校里常见的学业预警维度有三个:成绩维度、学分维度、考勤维度。成绩维度指单科挂科或学期平均绩点低于某条线;学分维度指累计未获学分达到一定值;考勤维度指出勤率低于某个比例,一般归到学生管理而不是教务系统里,但可以在系统里预留字段。

在毕设论文里,规则要以表格形式写清楚,这是我建议的默认参数:

预警等级触发条件(默认值)响应方式
黄色预警单学期挂科1门系统记录,辅导员谈话提醒
橙色预警单学期挂科2门,或累计挂科学分≥6通知学生本人并约谈
红色预警单学期挂科≥3门,或累计挂科学分≥12,或连续两学期橙色预警通知家长,学院介入

这些数字不是乱写的,挂科学分阈值参考的是很多高校“累计挂科超过20学分做退学警示”的底线,预警线要设置在比底线更早触发的位置,才有干预意义。你拿到具体学校的数据后,这些参数值全部可以做成配置项,别写死在代码里。

3.2 用pandas实现成绩统计:一次算清挂科门数和加权绩点

确定规则后,引擎实现我用pandas。有人会问为什么不用纯SQL,理由是成绩数据大概率来自Excel,反正都要用pandas清洗一遍,不如统计也一起在DataFrame里做了,逻辑更直观。

import pandas as pd from sqlalchemy import create_engine engine = create_engine('sqlite:///warning_system.db') # 关联学生、课程、成绩三张表,一次性取全量数据 sql = """ SELECT s.student_no, s.major, s.grade, sc.semester, sc.course_code, sc.score, sc.is_retake, c.credit, c.is_compulsory FROM scores sc JOIN students s ON s.student_no = sc.student_no JOIN courses c ON c.course_code = sc.course_code """ df = pd.read_sql(sql, engine) # 先处理补考/重修:同一门课只取该学期该生最新一条成绩 df = df.sort_values('is_retake').drop_duplicates( subset=['student_no', 'course_code', 'semester'], keep='last' ) # 标记挂科 df['is_fail'] = (df['score'] < 60).astype(int)

排序后按学号加课程加学期去重,保留最后一条,这样重修通过的学生不会因为历史挂科记录被重复预警。这种写法在真实数据里也站得住,因为教务系统导出时重修记录一般排列在原始记录之后。

接下来统计挂科门数和挂科学分。这里有一个细节要注意:挂科学分应该只累加必修课,选修课挂科一般不影响毕业学分要求,但这个规则不同学校不一样,我做成一个开关。累计方法上,先筛出挂科记录再按学号分组求和,比在groupby里用lambda安全得多——后者很容易踩索引不对齐的坑。

# 只保留必修课挂科记录用于累计挂科学分 fail_df = df[(df['is_fail'] == 1) & (df['is_compulsory'] == '1')] fail_stats = fail_df.groupby('student_no').agg( fail_count=('course_code', 'count'), fail_credit=('credit', 'sum') ).reset_index() # 统计每个学生本学期各科的成绩和学分,用于算加权平均绩点 gpa_df = df[df['is_retake'] == '0'].copy() # 绩点计算不算重修 def score_to_gpa(score): if score >= 90: return 4.0 elif score >= 85: return 3.7 elif score >= 82: return 3.3 elif score >= 78: return 3.0 elif score >= 75: return 2.7 elif score >= 72: return 2.3 elif score >= 68: return 2.0 elif score >= 64: return 1.5 elif score >= 60: return 1.0 return 0.0 gpa_df['gpa'] = gpa_df['score'].apply(score_to_gpa) gpa_df['weighted'] = gpa_df['gpa'] * gpa_df['credit'] gpa_stats = gpa_df.groupby('student_no').apply( lambda g: pd.Series({ 'gpa_avg': g['weighted'].sum() / g['credit'].sum(), 'term_fail': (g['score'] < 60).sum() }) ).reset_index()

score_to_gpa函数用的4.0制是简化版,实际学校有百分制加权和等级制绩点两种算法,你只需要替换这一个函数就能适配。groupby之后reset_index是为了把学号从索引里放回列,不然后面按学号merge时会多一层结构,新手经常在这里翻车。

把这几个统计结果按学号合并,就得到预警判定所需的全部特征。原则上我建议统计和判定分开写函数,统计函数返回DataFrame,判定函数接收DataFrame返回预警等级。答辩老师问“如果我换个绩点算法,需要改哪里”的时候,你就能理直气壮地说只改score_to_gpa这一个函数。

3.3 预警等级判定:规则可配置,预警记录可追溯

统计完成之后,判定逻辑反而是最简单的,就是把阈值规则翻译成代码。但我建议预警记录表里必须写入触发原因,格式是“2023-2024-1学期挂科3门,累计挂科学分14分”,这条reason字段在导出通知单和答辩展示时都是关键素材。

def judge_warning(row): """根据统计结果判定预警等级,只依赖行内字段""" if row['fail_count'] >= 3 or row['fail_credit'] >= 12: return 'red' if row['fail_count'] >= 2 or row['fail_credit'] >= 6 or row['gpa_avg'] < 2.0: return 'orange' if row['term_fail'] >= 1: return 'yellow' return None # 合并统计结果 result = fail_stats.merge(gpa_stats, on='student_no', how='outer') result = result.fillna({'fail_count': 0, 'fail_credit': 0, 'term_fail': 0}) # 判定并生成预警记录 result['level'] = result.apply(judge_warning, axis=1) warn_records = result[result['level'].notnull()].copy() warn_records['reason'] = ( warn_records['semester'] + '挂科' + warn_records['term_fail'].astype(int).astype(str) + '门' )

rule函数放在独立模块里,不要和视图函数混在一起。因为你后续很可能要调整阈值,比如某学院觉得单学期挂3门才算严重,只需要改judge_warning里的常量,不需要动任何其他代码。

预警等级逻辑里有一个边界情况要注意:红色和橙色的判定顺序。如果学生挂科4门,他同时满足红色和橙色条件,judge_warning里先判断红色再判断橙色,返回的是红色,覆盖关系靠函数顺序保证。这个顺序建议写注释说明,不然以后改代码的人会把条件顺序调乱,导致预警等级突然降级。

4. 让预警结果可见:Web接口、列表展示与图表设计

4.1 后端接口怎么设计:围绕辅导员的操作路径来

后端不需要做成一堆CRUD接口,先想清楚辅导员打开这个系统后要干嘛。他需要一个总览页看到全学院预警人数和等级分布,需要一张预警名单按班级筛选,需要每个学生的预警详情,还需要能导出Excel。四个页面,对应四个接口,够了。

from flask import Flask, jsonify, request, render_template from sqlalchemy import create_engine import pandas as pd app = Flask(__name__) engine = create_engine('sqlite:///warning_system.db') @app.route('/api/summary') def api_summary(): """总览:返回各预警等级人数和最高频挂科课程TOP5""" summary_sql = """ SELECT level, COUNT(*) AS cnt FROM warning_records WHERE is_processed = '0' GROUP BY level """ level_df = pd.read_sql(summary_sql, engine).to_dict(orient='records') course_sql = """ SELECT c.course_name, SUM(sc.is_fail) AS fail_cnt FROM score_stats sc JOIN courses c ON c.course_code = sc.course_code GROUP BY c.course_name ORDER BY fail_cnt DESC LIMIT 5 """ course_df = pd.read_sql(course_sql, engine).to_dict(orient='records') return jsonify({'levels': level_df, 'top_courses': course_df}) @app.route('/api/warnings') def api_warnings(): """预警名单:支持按班级和专业过滤,返回列表""" major = request.args.get('major', '') class_name = request.args.get('class_name', '') level = request.args.get('level', '') sql = """ SELECT w.id, s.student_no, s.name, s.major, s.class_name, w.semester, w.level, w.reason, w.is_processed FROM warning_records w JOIN students s ON s.student_no = w.student_no WHERE 1 = 1 """ params = [] if major: sql += " AND s.major = ?" params.append(major) if class_name: sql += " AND s.class_name = ?" params.append(class_name) if level: sql += " AND w.level = ?" params.append(level) df = pd.read_sql(sql, engine, params=params) return jsonify(df.to_dict(orient='records')) if __name__ == '__main__': app.run(debug=True, port=5000)

接口返回DataFrame转dict比手写循环构造字典快得多,对预警系统这种数据量级别完全够用。SQL里where 1=1是为了后面拼接条件方便,实际执行时SQLite会把它优化掉,不会拖慢查询。pandas的read_sql是支持params参数传列表的,但要注意参数个数必须和SQL里的问号一一对应,少传一个会报ProgrammingError,这个错误信息经常让人误以为是SQL写错了。

查询性能上,预警记录表加上student_no和semester两个索引后,几万条数据过滤基本都是毫秒级。不用优化到上缓存,那是给自己加戏。

4.2 前端展示用ECharts:两个关键图表就足够

展示层我用ECharts,因为它的图表交互能力强,而且基于JavaScript,不需要后端参与绘图。前端不需要框架,一个HTML页面加CDN引ECharts就够,毕设演示时双击打开都能跑,不需要配Node环境。

预警系统最值得展示的图有两个。第一个是预警等级分布饼图,直接告诉看的人“有多少黄色、橙色、红色”,这是系统存在价值的直观证明。第二个是每个学期挂科率的趋势折线图,能看到整体学业状态在变好还是变差,这对应了系统“预警不是目的,干预才是”的价值。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>学业预警总览</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> </head> <body> <div id="levelChart" style="width: 500px; height: 360px;"></div> <script> fetch('/api/summary') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('levelChart')); chart.setOption({ title: { text: '学期预警等级分布' }, tooltip: { trigger: 'item' }, series: [{ type: 'pie', radius: ['40%', '65%'], data: data.levels.map(item => ({ name: {yellow: '黄色预警', orange: '橙色预警', red: '红色预警'}[item.level], value: item.cnt })) }] }); }); </script> </body> </html>

fetch请求接口拿数据,然后map成ECharts需要的格式。这里唯一容易出问题的是level的英文编码和中文展示名的映射不能丢,不然图上显示的是“yellow”这种原始值。如果你想把图表集成到Flask里,用render_template传渲染好的数据也行,但接口方式更清晰,答辩时还可以顺便演示接口调试。

4.3 把预警名单导出Excel:要考虑辅导员真的会拿去用

导出功能是预警系统里被低估的一环。很多毕设做了在线列表就结束了,但实际业务里辅导员需要打印名单去开会、发邮件给家长。没有导出功能的系统,辅导员用两次就弃了。

导出用pandas的to_excel,底层引擎是openpyxl。这里有两个细节:一是文件名不要用固定值,加上当前日期防止覆盖;二是导出的列顺序要按人工习惯排列——学号、姓名、专业、班级、预警等级、触发原因、是否已处理,而不是按数据库字段顺序。

from datetime import datetime from flask import send_file import pandas as pd @app.route('/api/warnings/export') def export_warnings(): """导出当前筛选条件下的预警名单为Excel""" class_name = request.args.get('class_name', '') sql = """ SELECT s.student_no AS 学号, s.name AS 姓名, s.major AS 专业, s.class_name AS 班级, w.level AS 预警等级, w.reason AS 触发原因, w.is_processed AS 是否已处理 FROM warning_records w JOIN students s ON s.student_no = w.student_no """ if class_name: sql += " WHERE s.class_name = ?" df = pd.read_sql(sql, engine, params=[class_name] if class_name else []) # 英文等级转中文,方便直接打印 df['预警等级'] = df['预警等级'].map({ 'yellow': '黄色预警', 'orange': '橙色预警', 'red': '红色预警' }) filename = f"warning_export_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx" df.to_excel(filename, index=False, sheet_name='预警名单') return send_file(filename, as_attachment=True)

Excel输出的列名直接用中文,是因为这份文件最终会给不写代码的人看。如果读者以后自己做的系统要对接其他程序,列名就可以用英文字段,减少编码麻烦。openpyxl引擎写出来的.xlsx文件在WPS和微软Excel里都能正常打开,不用考虑老式.xls的兼容问题。

5. 避坑:成绩导入、口径对齐、环境与导出的四个实战问题

5.1 现象:学号导入后前导零丢失、成绩列全是科学计数法

第一次拿真实教务系统导出的Excel跑的时候,学号列显示成1.23457E+11,成绩列显示成78.00000001,姓名是乱码。原因分两层:一是Excel文件本身把单元格格式设成了数值,二是pandas读取时会把看起来像数字的列自动推断成int64或float64格式。

解决方式是读取时不要让它自动猜类型。用dtype参数强制指定列类型是最省心的做法:

df = pd.read_excel( 'score_export.xlsx', dtype={ '学号': str, # 强制按字符串读,保留前导零 '课程代码': str, }, na_values=['', 'NULL', '缺考'] # 缺考在教务系统里可能是文本 )

这里要注意的是,dtype指定必须在你确认哪些列是编号列之后。如果你用了openpyxl直接读,还可以先打印所有sheet的列名,确认列名匹配再跑批处理。缺考值在很多教务系统里不是0,而是“缺考”两个字或者直接空白,不处理的话会被pandas读成NaN,后续算挂科时NaN小于60的判断是False,这个人就莫名其妙不算挂科了。

5.2 现象:预警结果和教务处人工统计的名单对不上

这是最让人崩溃的坑。同一份成绩单,系统算出挂科5门,教务处那边的名单上写的是挂科3门。我当时逐条核对后发现,问题出在“同一门课程不同学期重修”的情形上:学生第一学期挂科,第二学期重修通过,成绩表里两条记录都在。按我的统计逻辑,这门课计算为通过,但教务处人工统计的口径是“曾经挂过就算累计挂科门数”。

这个问题的本质是口径偏差,不是代码bug。两个口径都有道理:只算当前未通过课程,是“当前学业风险”;算历史挂科次数,是“学习态度评估”。解决方法不是改代码,而是在系统参数里增加一个开关,叫“挂科口径”,可选值为“当前挂科”和“累计挂科”。你要做的是在答辩时明确说出你这个系统选了哪个口径,以及为什么选它。我系统里默认用的是“当前挂科”,因为预警的目的是面向下一步的干预,已经通过了就不需要再警示。

预防这类问题的最好办法是拿一份真实的历史预警名单做对账,哪怕只有几十个学生也行。写一段比对脚本,把系统算出来的人和学校原来发过的人做差集,然后逐个找原因。这个环节做下来,你的系统在业务层面的说服力会上一个台阶。

5.3 现象:代码在自己电脑跑得好好的,换台电脑就报错

典型报错是ModuleNotFoundError: No module named 'pandas',或者flask、sqlalchemy找不到。原因基本都是新机器上没有安装依赖包,或者全局环境里装的是不同版本。

解决方法是写一份requirements.txt,并且在文档里写明安装步骤。生成依赖清单用pip freeze命令,它会把你当前环境里所有包连同版本号导出来。但pip freeze有个问题,它会连一些传递依赖也列进去,比如pandas依赖的numpy。所以更推荐的做法是手动维护一份只包含顶层依赖的文件:

flask>=3.0 pandas>=2.0 sqlalchemy>=2.0 openpyxl>=3.1 scikit-learn>=1.3 urllib3 # 仅当需要requests调接口时

换环境后直接执行pip install -r requirements.txt,五分钟内就能跑起来。另外,数据库文件(.db文件)如果是复制的,要注意别把数据库文件也加入.gitignore,不然换机器后程序能启动但查不到数据,那种问题特别难定位,因为报错信息只会显示“数据库表不存在”。

5.4 现象:导出Excel后打开,学号变成了科学计数法

这个坑在4.3节导出代码里埋了一半。pandas的to_excel会把学号列按字符串写入,但如果学号本身在DataFrame里是int类型,Excel打开时它还是会被识别成数值型,长学号就会显示成科学计数法。

最彻底的解决方法是保证DataFrame里学号列是object类型,也就是字符串。从数据库读出来时SQLAlchemy返回String字段本身没问题,但如果你在做groupby统计后又和别的表merge,学号列的dtype有可能被自动转成int64——因为pandas的merge会自动对齐类型,如果有一边的学号是字符串另一边的学号被读成了整数,它会做隐式转换。

排查技巧是:在导出前打印df.dtypes,盯着学号列看它到底是什么类型。如果不是object,用df['学号'] = df['学号'].astype(str)强制转回来。这个检查动作应该写成习惯,因为它能顺带发现很多数据问题。

6. 从规则预警走向预测预警:让系统具备一点前瞻性

规则预警有一个无法回避的弱点:它只能对已经发生的事情做响应。学生挂科了,系统才知道他有风险。如果能在学期初就预判出哪些人这学期大概率会挂科,提前谈话干预,效果会比事后预警好得多。用scikit-learn里的逻辑回归就能做到这一步,这也是答辩时最能体现技术深度的地方。

特征选取上,用学生前两个学期的数据来预测本学期是否挂科,最有效的三个特征是:前两学期加权平均分、累计挂科门数、前两学期平均绩点。这三个指标都来自预警系统已有的表,不需要额外采集数据,这是选它们的最大理由。

from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 假设 df_features 是合并好的训练数据 # 列:avg_score_prev(前两学期均分) fail_count_prev(累计挂科) gpa_prev(前两学期绩点) # 目标:fail_this_term(本学期是否挂科,1/0) X = df_features[['avg_score_prev', 'fail_count_prev', 'gpa_prev']] y = df_features['fail_this_term'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) model = LogisticRegression(max_iter=1000) model.fit(X_train, y_train) print(classification_report(y_test, model.predict(X_test)))

stratify=y是为了保证正负样本在训练集和测试集中的比例一致。如果你们的正样本(挂科学生)本身就少,不做分层抽样,很可能出现测试集里一个挂科样本都没有的情况,精确率召回率全是0,特别难看。

模型效果上,逻辑回归在这个任务的准确率能做到70%到80%之间就已经够用。因为学业预警不是一个“宁缺毋滥”的场景,漏掉一个真正会挂科的学生比多提醒一个最后没挂科的学生代价大得多。所以重点看召回率,而不是准确率。如果召回率不够高,可以把逻辑回归的判定阈值从默认的0.5往下调,比如调到0.3,让更多风险学生进入预警名单。这个阈值参数建议放到配置文件里,方便答辩时演示它对结果的影响。

最后说一个做这类系统的体会:真正花时间的从来不是写代码,而是理解业务数据长什么样、口径之间怎么对齐。学业预警系统放在高校里是一个很小的应用,但它把数据获取、清洗、规则建模、系统展示、结果验证这个完整链路串了起来。这恰恰是毕业后做数据相关工作最需要的能力。我当年做的时候,大概花了三分之二的时间在找数据和核对规则上,写代码只用了三分之一,这个比例放到真实企业项目里也成立。答辩时把这个过程讲清楚,比任何花哨功能都能打动人。希望这篇笔记能帮你把这个方向做得扎实、做得明白。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询