ARTICLE DETAIL

资讯详情

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

基于Python的高校历年招生分数分析与可视化系统实现

基于Python的高校历年招生分数分析与可视化系统实现 简介一套面向高校毕业设计/课程设计场景的Python数据挖掘与可视化系统基于Flask Web框架构建提供可运行的高校历年招生分数研究工具。系统采用网络爬虫从高考网抓取各大高校历年录取分数线完成数据清洗与存储并借助ECharts展示录取分数线分布、录取人数与批次分布、历史趋势、专业模糊搜索及专业分析等。压缩包共46个文件约78.47MB核心包含Python源码、前端JS/CSS/HTML页面、JSON数据、SQLite数据库及演示视频目录按模板、静态资源、核心代码分层便于快速定位与二次开发。目前已有133人浏览学习。从中可完整了解从爬虫策略、字段设计、数据处理到图表渲染的全流程实现路径获得可运行代码、操作录屏、数据文件和排错参考适合作为毕业设计或课程设计的完整范本。1. 想用「基于Python的高校历年招生分数研究与可视化系统」做毕设先看清它的骨架基于Python的高校历年招生分数研究与可视化系统这种题目每年都有一批学生选。乍看是个数据分析项目实际是一款“数据分析 机器学习 Flask Web”三合一的毕业设计先收集各省考试院历年投档数据用 pandas 清洗再用机器学习模型预测下一年的录取位次和分数最后把趋势图和预测结果放到网页上展示。相比纯爬虫或单模型调参这个题目把完整流程走通了演示效果好答辩也有得讲。适合会用 Python 基础语法、想低成本接触机器学习和数据分析的人。交付内容通常就是源代码和演示视频真正值得花时间的不是剪视频而是把数据和模型口径讲明白。2. 先拆需求再写代码招生分数系统的表结构、技术选型与模型落点拿到题目先别急着装依赖。标题拆开是三件事历年招生分数、机器学习数据分析、flask web分别对应数据层、模型层和展示层。我一般会先列出系统模块数据清洗、院校趋势分析、分数预测、可视化看板、演示视频支撑。后面所有代码都围绕这几件事展开不会跑偏。2.1 让数据表先回答“哪所学校的什么人群在什么批次被录取”数据是这个项目的命根子。字段设计决定你后面能做多深。常见做法是维护一张宽表把多所学校、多个省份、多年数据放在一起字段类型说明school_idINT院校编号用于页面路由关联school_nameVARCHAR院校名称provinceVARCHAR录取省份yearINT录取年份batch_nameVARCHAR本科一批 / 本科二批 / 本科批class_typeVARCHAR理科 / 文科 / 物理类 / 历史类major_nameVARCHAR专业或专业组名称min_scoreFLOAT当年最低投档分min_rankINT当年最低位次plan_numINT招生计划数province_lineFLOAT当年该省该科类省控线这里最容易被忽略的是min_rank。分数会随试题难度和考生整体水平波动位次才是跨年份可比的锚点。很多学生只盯着最低分做趋势最后发现某年整体分数都高误判成学校变热了。province_line同样重要它用来算“线差”也就是最低分比省控线高多少分这个特征在后面的模型里非常有用。数据来源方面常见做法是整理各省教育考试院每年公布的投档情况统计表和学校招生网的历年分数页。手工整理成 CSV 或 Excel 就行。不建议把大量时间花在写爬虫上一是很多考试院页面反爬且结构每年都变二是老师更想看到数据处理和建模能力答辩时说“数据来自官网公告整理后存为结构化 CSV”已经足够。拿到 CSV 后先跑df.info()看各列空值再跑df.groupby([school_name, province, year]).size()检查是否有重复记录重复数据会让模型把同一年当成两个样本学。2.2 用 Flask Web 而不是 Django单机演示场景“轻”是优势标题里带着 flask web但很多人会纠结选 Flask 还是 Django。我的选择很简单单机演示、前后端不分离、项目规模小Flask 是最快路径。Django 自带 admin、ORM、Auth对毕业设计来说偏重目录结构也复杂。Flask 核心就三样东西Flask(__name__)创建应用、app.route定义路由、render_template返回页面再配一个 JSON 接口给前端图表取数展示层就齐了。对比项FlaskDjango上手成本低适合毕设偏高适合大型系统内置后台无根据需要自己写admin 内置模板Jinja2Django 模板路由写法装饰器URL conf本项目适合度非常适合偏重Flask 内置开发服务器跑 localhost 演示足够。项目文件组织也不用过度设计一个常见最小结构是这样的app.py templates/ index.html school_detail.html static/ css/style.css js/echarts.min.js data/ admission_scores.csv rank_score_2025.csv models/ predictor.pyapp.py只放路由和全局初始化数据处理和模型训练放进models/predictor.py页面模板放在 templates 下。第一天就把结构按这个搭好后面扩容不痛苦。2.3 机器学习在项目里到底学什么预测位次而不是预测分数很多机器学习入门教程喜欢拿鸢尾花和房价举例但招生分数这个场景更贴近真实业务。录取结果由全体考生的志愿填报行为决定本质是一个博弈系统可预测的部分有限。所以要做的是把目标从“分数”换成“位次”或“线差”。常见方案分三档。第一档是基线模型用线性回归预测“线差”也就是最低分减省控线数据少但解释性强。第二档是主模型用随机森林回归预测“位次”可以捕捉扩招计划数、年份趋势和批次变化的非线性交互。第三档不推荐上来就用 LSTM、Transformer因为一校一省一科一年只有一个样本五年数据才五个点序列模型很容易把噪声当规律答辩也说不清楚。这里的关键流程是先训练位次模型得到下一年的预测位次再用当年一分一段表把位次映射回预测分数。为什么绕一圈位次跨年份可比一分一段表保存了当年分数分布两者结合既符合预测模型又落回实际的排名体系。直接回归“分数”等于把不同年份的试卷难度混在一起做出来谁都不信。3. 把整套流程跑起来数据清洗、模型训练与 Flask 页面可视化这一章按数据流顺序给最小可复现代码。假设原始数据是data/admission_scores.csv字段就是第 2 章的字段编码可能是 UTF-8 或 GBK。先安装依赖pip install pandas scikit-learn flask。这套环境基本是 python 数据分析项目的标配。3.1 第一步清洗历年数据把文本数字和缺省值变成能训练的 DataFrameimport pandas as pd df pd.read_csv(data/admission_scores.csv, encodingutf-8-sig) # 把“最低分”等列从文本 / “650”强制转成数值 df[min_score] pd.to_numeric(df[min_score], errorscoerce) df[min_rank] pd.to_numeric(df[min_rank], errorscoerce) df[province_line] pd.to_numeric(df[province_line], errorscoerce) df[year] pd.to_numeric(df[year], errorscoerce) # 缺最低分或缺位次的记录对建模没有价值直接剔除 df df.dropna(subset[min_score, min_rank]) # 同一学校同年同专业如果出现重复保留第一条 df df.drop_duplicates(subset[school_id, province, year, major_name]) # 线差最低分与当年省控线的差距后续可作为预测特征或目标 df[score_diff] df[min_score] - df[province_line] # 筛出一所学校便于单校建模 school_df df[(df[school_id] 1) (df[province] 河南)].copy() school_df school_df.sort_values(year) print(school_df[[year, min_score, min_rank, score_diff]].tail(10))逻辑说明pd.to_numeric(..., errorscoerce)会把“600”“约 580”这类文本置为 NaN错过之后统一dropna就不会在训练时报类型错误。这里的drop_duplicates防止同一条投档数据因为导出过程重复出现。score_diff是构造出来的核心特征它把不同年份的分数线波动剥离了一部分。参数说明读取中文 CSV 优先用encodingutf-8-sig兼容 Excel 导出的 BOM 头如果打印出来中文乱码再试encodinggbk这是 excel 老版本最常见的编码问题。另外如果某年缺min_rank但不缺min_score不要急着删行。常见做法是先尝试用同省位次表按分数反查补一个近似位次补不了就不进训练集但保留在可视化里这样趋势图不至于断掉。3.2 第二步训练预测模型用线差和位次代替裸分数回归from sklearn.ensemble import RandomForestRegressor from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error train school_df[school_df[year] 2024] test school_df[school_df[year] 2024] # 注意用相对年份而不是绝对年份 base_year train[year].min() train[year_idx] train[year] - base_year test[year_idx] test[year] - base_year features [year_idx, plan_num, province_line, score_diff] X_train, y_train train[features], train[min_rank] X_test, y_test test[features], test[min_rank] baseline LinearRegression() rf RandomForestRegressor(n_estimators300, max_depth4, random_state42) baseline.fit(X_train, y_train) rf.fit(X_train, y_train) for name, model in [(线性回归, baseline), (随机森林, rf)]: pred model.predict(X_test) print(name, MAE:, mean_absolute_error(y_test, pred))逻辑说明这里没有用train_test_split随机切分因为招生数据是明显的时间序列随机切会把“过去”和“未来”混在一起模型等于偷看了答案。按年份切分才符合真实场景用 2015 至 2023 年预测 2024 年。参数说明max_depth4是要故意限制随机森林复杂度。一所学校一个省一个科类的有效训练样本通常只有 6 到 10 条树太深很快就把每条样本都背下来了。n_estimators300在这个小数据量下不是关键参数300 比 100 更稳但再往上提升很有限。year_idx用相对年份而不是绝对年份是避免随机森林对未出现的年份做暴力外推。用预测位次还原分数的代码# 用预测位次去一分一段表反查预测分数 rank_score pd.read_csv(data/rank_score_2025.csv) rank_score rank_score.sort_values(rank) pred_rank int(round(rf.predict(X_test)[0])) idx rank_score[rank].searchsorted(pred_rank) pred_score rank_score.iloc[min(idx, len(rank_score) - 1)][score] print(预测位次:, pred_rank, 预测分数:, pred_score)说明searchsorted返回第一个大于等于pred_rank的位置再用min(idx, len(rank_score)-1)卡住上界防止预测位次超过表范围导致越界。如果预测位次超出位次表范围说明模型输出异常后面需要用业务规则拦截。3.3 第三步Flask 提供 JSON 接口前端用 ECharts 渲染趋势# app.py from flask import Flask, jsonify, render_template import pandas as pd app Flask(__name__) score_df pd.read_csv(data/admission_scores.csv, encodingutf-8-sig) app.route(/) def index(): return render_template(index.html) app.route(/api/school/int:school_id/province/class_type) def api_school(school_id, province, class_type): part score_df[ (score_df[school_id] school_id) (score_df[province] province) (score_df[class_type] class_type) ] part part.sort_values(year) result part.groupby(year, as_indexFalse).agg( min_score(min_score, min), min_rank(min_rank, min), plan_num(plan_num, sum), ) return jsonify(result.to_dict(orientrecords)) if __name__ __main__: # 正式演示时关掉 debug避免录屏到一半自动重载 app.run(host127.0.0.1, port5000)逻辑说明路由里带school_id、province、class_type三个参数比只传一个 school_id 更合理。前端选学校后必须把省份和科类一起传给接口后端才知道按哪个口径取数。聚合时min_score取最小代表“该校当年最低投档分”plan_num求和代表该校当年在省内总计划数。前端模板templates/index.html!DOCTYPE html html langzh head meta charsetUTF-8 title招生分数分析/title script src{{ url_for(static, filenamejs/echarts.min.js) }}/script /head body div idtrend stylewidth: 90%; height: 400px;/div script const schoolId 1, province 河南, classType 理科; fetch(/api/school/${schoolId}/${province}/${classType}) .then(r r.json()) .then(data { const chart echarts.init(document.getElementById(trend)); chart.setOption({ title: { text: 历年最低投档分趋势 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.map(d d.year) }, yAxis: [ { type: value, name: 最低分 }, { type: value, name: 最低位次 } ], series: [ { name: 最低分, type: line, data: data.map(d d.min_score) }, { name: 最低位次, type: line, yAxisIndex: 1, data: data.map(d d.min_rank) } ] }); }); /script /body /html两点要注意。第一ECharts 的 JS 文件建议下载后放进static/js/echarts.min.js不要从公网 CDN 引。离线也能跑演示视频不会因为现场断网白屏。第二分数和位次数量级差异很大一个在五六百一个可能到几万必须用双 Y 轴分开。上面的yAxisIndex: 1就是把位次绑到右侧轴否则位次曲线会被压成一条直线。4. 招生分数项目最容易翻车的 5 个地方现象、原因与处理这类项目翻车往往不在模型而在数据口径、页面加载和演示录像。我建议排查顺序也固定下来先看数据形状是否符合预期再看接口返回的 JSON 是否正常最后才怀疑前端图表。按这个顺序能省下大量时间。4.1 跨省跨科混训模型把“省份差异”当成了趋势现象把一所学校所有省份的数据丢进同一个模型预测下一年最低分结果河南考生 560 分能上、山东却要 640 分整体误差大得没法看。原因不同省份试卷难度、省控线、考生人数都不一样裸分数压根不在同一个坐标系。模型把“省份平均分差异”当成趋势学进去了而不是学学校本身的热度变化。解决特征必须带province和class_type并按这两个字段分组训练。数据不够时只保留连续 5 年以上记录的分组。分组模型数量多就写个循环分别训练分别预测最后把预测结果汇总回页面。这个改造不复杂但能把误差降一大截。4.2 断档年份把模型带偏预测值连续两年走低现象某校 2020 年最低位次从 1.2 万突然掉到 3.5 万模型预测 2021 和 2022 年都明显偏低和真实录取结果对不上。原因招生里的“大小年”很常见。头年断档次年大量考生涌入录取位次往往强力回升。线性回归对极端值敏感随机森林稍好但断档样本占训练集比例大时照样会把整体预测拉偏。解决先画箱线图看min_rank分布把明显低于历史区间的年份标出来。确认当年是否扩招、是否新增冷门专业把异常原因写进数据说明。训练时可以选择剔除或降权这个断档样本但可视化里保留它。页面加一列“异常年说明”答辩时主动讲出来反而是亮点。4.3 页面空白ECharts 请求不到根因在浏览器 Console现象Flask 服务能启动首页也打开了但图表区域一片空白刷新几次偶尔能出来。原因前端 JS 报错。常见两类一是直接用file://双击打开了 HTMLfetch 接口请求跨协议失败二是模板里的静态文件路径不对echarts.min.js加载失败导致echarts is not defined。解决页面必须走http://127.0.0.1:5000访问不要双击 HTML。打开浏览器开发者工具 Console 看第一条报错是 404 还是 ReferenceError。404 就检查 Flask 项目的 static 目录是不是叫static文件名大小写是否一致ReferenceError 就说明 ECharts 没加载。做过 flask web 开发实战的人遇到页面空白第一反应就应该是 Console。4.4 演示视频录到一半Flask 自动刷新了现象录制演示视频时一切正常中途保存了一次代码文件Flask 开发服务器自动重启页面白屏几秒这段也录进视频里很难看。原因app.run(debugTrue)默认开启 reloader保存代码文件会触发自动重载。解决录制演示视频前把启动参数改成app.run(debugFalse)或者写成app.run(debugTrue, use_reloaderFalse)。血泪经验我第一次录这个项目的演示视频就是没关 debug十分钟的片子中间卡了一道白屏只能重录。录之前先完整跑一遍流程确认不依赖热重载再正式开录。4.5 数据口径不一致“理科”和“物理类”被当成同一批数据现象页面里同一所学校 2022 年最低分 5852023 年突然 620趋势线出现夸张的 V 形老师看一眼就觉得数据不可信。原因老高考省份把考生分文科理科新高考省份改成了历史类和物理类批次也从一本二本合并成本科批。两个口径的分数没有直接可比性。解决class_type保留原始口径不要统一改成“理科”。趋势图可以分段显示2020 到 2022 年显示“理科”2023 年之后显示“物理类”图例里注明口径。如果只能画一条线就只取口径一致的那几年或者加“口径变更”备注。千万别为了凑年份把两段硬接在一起这是数据可视化项目里最容易被问倒的地方。5. 答辩前要做的三件小事模型验证、预测口径与演示顺序项目能跑通只完成一半毕业设计答辩要的是人能听懂。下面三件事花一个下午就能做完但能明显提升说服力。5.1 用 MAE 和命中区间双重验证预测结果mae mean_absolute_error(test_y, pred) hit_rate (abs(pred - test_y) / test_y 0.1).mean() print(位次平均绝对误差:, mae, 10%区间命中率:, hit_rate)位次预测的 MAE 如果是 800意思是预测位次和实际位次平均差 800 名。对招生场景答辩老师更容易听懂的是命中率预测落在实际位次正负 10% 范围内的比例。不要只盯 R2训练样本只有十几条R2 很容易碰巧很高或很低不够稳定。5.2 预测结果加业务约束if pred_score 0: pred_score None if pred_score province_line: pred_score province_line # 录取最低分不应低于省控线线性回归外推时可能出现负数位次或低于省控线的荒谬分数。模型输出后要加业务规则兜底超出历年区间的预测不显示“精确值”改显示“参考区间”。这个细节看起来简单但能体现你考虑过真实录取规则。5.3 演示顺序先数据后模型再页面建议录屏顺序是先打开首页展示学校筛选和历年趋势图再切到数据说明页讲口径然后回到模型代码展示按年份切分和 MAE最后回到页面点击“预测下一年”亮出预测结果。收尾用一句业务结论比如“该校在河南的录取位次连续三年上升预计 2025 年最低位次在 1.4 万左右对应分数约 612 分上下”。录屏前把系统字体调大关闭 debug 模式录音前安静三秒。我第一次录演示视频开着 debug保存代码导致服务自动重载后来所有正式录制前都会先跑一遍debugFalse。希望这些经验能帮到你。本文还有配套的精品资源点击获取
返回列表