☰
基于Python的高考志愿推荐系统:位次法冲稳保算法与Streamlit实现
2026/10/3 5:19:45 网站建设 项目流程

简介:这是一套基于Python开发的高考志愿推荐系统完整项目资料,面向计算机相关专业的毕业设计、课程设计学生以及需要项目实战练手的开发者,帮助解决志愿填报场景下院校与专业智能匹配、分数线参考等实际问题。压缩包共127个文件,约4.51MB,以74个py源码文件为核心,辅以11个html页面、5个js脚本、4个css样式及bootstrap相关资源构成前端界面,另有5个csv分数线与评分数据集、2个sqlite3数据库文件、2个sql文件用于数据存储,并包含png、jpg界面截图与md说明文档,结构完整、层次清晰。项目源码经过严格测试,可直接运行参考,并在此基础上延伸扩展。目前已有95人学习下载。读者可获得完整可运行的推荐系统源码、配套项目文档、数据库与数据集、界面截图,便于快速理解推荐逻辑与前后端交互流程,高效完成毕设或课设任务。

1. 高考志愿推荐系统到底在解决什么问题:从一张志愿表说起

每年六月末,总有亲戚朋友把孩子的分数和一分一段表甩过来,问「这个分能上什么学校」。手工翻大厚本、对位次、查三年录取线,一个下午只能排两三所学校,还容易漏掉「大小年」波动。基于 Python 开发的高考志愿推荐系统,本质就是把这件事拆成三步:把历年录取数据清洗成结构化表,用位次而不是分数做匹配,再按冲稳保三档给出候选清单。它适合计算机毕业设计选题、课程设计交作业,也适合真想给家里孩子做一份参考工具的人。这篇笔记不讲空概念,直接按「数据怎么来、算法怎么算、界面怎么搭、坑在哪」走一遍,源码和项目文档的落地思路也会一并说清。你照着做,能跑出一个可演示、可写论文、可继续扩展的系统。

2. 数据从哪来、怎么洗:志愿推荐系统的地基

2.1 数据源选型和字段设计

志愿推荐系统的成败,八成在数据。常见做法是拿本省考试院公布的一分一段表、院校专业组投档线、招生计划三张表。一分一段表给的是「分数—累计人数」,投档线给的是「院校专业组—最低分—最低位次」,招生计划给的是「院校专业组—专业—计划数—学费—选科要求」。这三张表用「年份+省份+科类」做联合主键就能串起来。

字段设计上,我一般会保留这些列:year、province、subject_type(物理/历史或文理)、batch、school_code、school_name、major_group、major_name、min_score、min_rank、plan_num、tuition、subject_req。其中min_rank是核心,因为分数年年变,位次相对稳定。选科要求用字符串存,比如「物理+化学」,后面做过滤时按包含关系判断。

提示:不同省份的批次名称差异很大,比如「本科批」「本科一批」「特殊类型招生控制线」,建议单独建一张batch_alias映射表,别在代码里写死。

2.2 用 pandas 做清洗和位次换算

原始数据多半是 Excel 或 PDF 转出来的,列名不统一、有空行、有合并单元格。下面这段清洗脚本是我常用的骨架,处理「分数转位次」和「缺失值填充」两个高频问题。

import pandas as pd import numpy as np # 读取一分一段表,假设列为:分数、本段人数、累计人数 score_rank = pd.read_excel("score_rank_2024.xlsx") score_rank.columns = ["score", "seg_num", "cum_num"] # 位次取累计人数,分数从高到低排序 score_rank = score_rank.sort_values("score", ascending=False).reset_index(drop=True) def score_to_rank(score): """把分数映射成全省位次,找不到精确分就取最接近的低一分位次""" row = score_rank[score_rank["score"] == score] if not row.empty: return int(row.iloc[0]["cum_num"]) lower = score_rank[score_rank["score"] < score] if lower.empty: return int(score_rank.iloc[-1]["cum_num"]) return int(lower.iloc[0]["cum_num"]) # 读取投档线表 admit = pd.read_excel("admit_2024.xlsx") admit.columns = ["school_code", "school_name", "major_group", "min_score", "plan_num", "subject_req"] # 缺失的最低分用同院校专业组往年均值填充,这里简化为中位数 admit["min_score"] = admit.groupby("school_name")["min_score"] \ .transform(lambda x: x.fillna(x.median())) # 换算位次 admit["min_rank"] = admit["min_score"].apply(score_to_rank) # 选科要求统一成集合,方便后续过滤 admit["subject_req"] = admit["subject_req"].fillna("不限") admit.to_csv("admit_clean.csv", index=False, encoding="utf-8-sig") print(admit.head())

逻辑说明:score_to_rank是核心函数,它把「分数」翻译成「位次」,这是整个推荐系统能跨年份比较的前提。参数上,score_rank必须按分数降序排列,cum_num才是累计人数;如果原始表是升序,要先翻转。fillna(x.median())是一种保守填充,真实项目里更稳妥的做法是用同院校专业组近三年均值,但毕业设计阶段中位数足够演示。

参数怎么改:如果你做的是新高考「专业组」模式,major_group这一列必须保留,因为投档是按专业组投的,不是按单个专业。subject_req的匹配逻辑后面会讲,这里先统一成字符串。

2.3 数据入库和增量更新

清洗完的数据建议进 SQLite,轻量、免安装、方便打包进毕业设计。建表语句如下:

CREATE TABLE admit_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, year INTEGER NOT NULL, province TEXT NOT NULL, subject_type TEXT NOT NULL, school_code TEXT, school_name TEXT, major_group TEXT, major_name TEXT, min_score INTEGER, min_rank INTEGER, plan_num INTEGER, tuition INTEGER, subject_req TEXT ); CREATE INDEX idx_rank ON admit_history(min_rank); CREATE INDEX idx_school ON admit_history(school_name);

idx_rank这个索引很关键,因为推荐查询本质是「找位次在某个区间内的记录」,没有索引,几万条数据还能忍,几十万条就会明显卡顿。增量更新时,按year+province+subject_type先删后插,避免重复。常见做法是写一个update_data.py,每年六月后手动跑一次。

3. 推荐算法怎么算:冲稳保三档的位次区间法

3.1 为什么用位次法而不是线差法

线差法是「我的分数减批次线」对比「院校最低分减批次线」,优点是直观,缺点是批次线波动大时误差明显。位次法直接用全省排名对比院校最低位次,稳定性更好。举个具体例子:某考生位次 20000,某院校去年最低位次 18000,那今年大概率够不着;如果最低位次 23000,那属于稳。冲稳保的区间我一般这样设:

档位位次区间(考生位次为 R)含义
冲R × 0.7 ~ R × 0.9有一定风险,可尝试
稳R × 0.9 ~ R × 1.1匹配度高,重点参考
保R × 1.1 ~ R × 1.4兜底,确保有学上

这个系数不是拍脑袋,是多年实操里比较稳的经验值。冲的系数太小(比如 0.5)会推荐一堆完全够不着的学校,保的系数太大又会浪费分数。

3.2 核心推荐函数和选科过滤

下面这段是推荐引擎的主逻辑,输入考生位次和选科,输出三档候选。

import sqlite3 import pandas as pd def recommend(rank, subject_type, subjects, province, year=2024): """ rank: 考生位次 subject_type: 物理/历史 subjects: 考生选科列表,如 ['物理', '化学', '生物'] """ conn = sqlite3.connect("gaokao.db") query = """ SELECT school_name, major_group, major_name, min_rank, min_score, plan_num, subject_req FROM admit_history WHERE province = ? AND subject_type = ? AND year = ? """ df = pd.read_sql(query, conn, params=(province, subject_type, year)) conn.close() # 选科过滤:要求是「不限」或考生选科包含要求科目 def match_subject(req): if req == "不限": return True need = set(req.replace("+", " ").split()) return need.issubset(set(subjects)) df = df[df["subject_req"].apply(match_subject)] # 按位次划分三档 chong = df[(df["min_rank"] >= rank * 0.7) & (df["min_rank"] < rank * 0.9)] wen = df[(df["min_rank"] >= rank * 0.9) & (df["min_rank"] <= rank * 1.1)] bao = df[(df["min_rank"] > rank * 1.1) & (df["min_rank"] <= rank * 1.4)] # 每档按位次升序(越靠前越难),取前 20 条 result = { "冲": chong.sort_values("min_rank").head(20), "稳": wen.sort_values("min_rank").head(20), "保": bao.sort_values("min_rank").head(20), } return result res = recommend(20000, "物理", ["物理", "化学", "生物"], "河南") for k, v in res.items(): print(f"=== {k} ===") print(v[["school_name", "major_group", "min_rank"]].to_string(index=False))

逻辑说明:match_subject处理选科要求,need.issubset(set(subjects))表示院校要求的科目考生都选了。参数上,rank * 0.7这些系数可以按省份竞争激烈程度微调,竞争激烈的省份冲的区间可以再收窄。head(20)是防止结果太多,实际展示时可以分页。

注意:如果某院校专业组今年招生计划数骤减,位次会大幅前移,单纯用去年位次会误判。稳妥做法是结合plan_num变化做修正,计划减少超过 30% 的,位次上浮 10% 再比较。

3.3 排序和去重:让结果更可读

推荐结果里经常出现同一学校多个专业组,或者同一专业组多个专业。展示时我一般按「院校层次(985/211/双一流)」再排一次,这个信息可以单独建一张school_tag表。去重逻辑是:同一school_name + major_group只保留位次最接近考生位次的那条,避免刷屏。

def dedup_by_group(df, rank): df = df.copy() df["distance"] = (df["min_rank"] - rank).abs() df = df.sort_values("distance").drop_duplicates( subset=["school_name", "major_group"], keep="first") return df.drop(columns=["distance"])

distance是考生位次和院校最低位次的绝对差,越小越匹配。这个函数在三档里都可以套用,让每档结果更干净。

4. 界面和项目文档怎么搭:让毕业设计能演示

4.1 用 Streamlit 快速搭一个可交互界面

毕业设计答辩时,老师最想看的是「能点、能输、能出结果」。Streamlit 是最省事的方案,几十行代码就能出一个网页界面,不用写前端。

import streamlit as st from recommend import recommend st.set_page_config(page_title="高考志愿推荐系统", layout="wide") st.title("高考志愿推荐系统") col1, col2, col3 = st.columns(3) with col1: rank = st.number_input("全省位次", min_value=1, max_value=500000, value=20000) with col2: subject_type = st.selectbox("科类", ["物理", "历史"]) with col3: province = st.text_input("省份", value="河南") subjects = st.multiselect("选科", ["物理", "化学", "生物", "政治", "地理", "历史"], default=["物理", "化学", "生物"]) if st.button("开始推荐"): res = recommend(rank, subject_type, subjects, province) for level in ["冲", "稳", "保"]: st.subheader(f"{level}档") st.dataframe(res[level][["school_name", "major_group", "major_name", "min_rank", "plan_num"]])

逻辑说明:st.number_input拿位次,st.multiselect拿选科,st.button触发推荐。st.dataframe直接渲染 pandas 表格,支持排序和滚动。参数上,min_value和max_value按本省考生总数设,别设太小导致输不进去。

运行方式:streamlit run app.py,浏览器会自动打开。这个界面足够答辩演示,截图放进项目文档也好看。

4.2 项目文档该写哪几块

项目文档不是把代码贴一遍,而是让看的人能复现。我一般按这几块写:需求分析(用户是谁、解决什么)、系统设计(数据流图、模块划分)、数据库设计(表结构、字段说明)、核心算法(位次法、冲稳保区间)、界面截图(首页、推荐结果、数据管理)、测试用例(输入位次 20000 输出什么)、部署说明(Python 版本、依赖包、启动命令)。其中数据库设计和核心算法是重点,老师大概率会问。

依赖包用requirements.txt固定版本,常见的是pandas、streamlit、openpyxl、sqlite3(标准库不用写)。Python 版本建议 3.9 以上,太老的版本 pandas 行为有差异。

提示:界面截图至少放三张——输入页、推荐结果页、数据管理页。截图里别出现真实考生姓名和身份证号,用测试数据。

4.3 源码目录结构建议

一个清晰的目录结构能让项目文档和源码对得上:

gaokao_recommend/ ├── data/ │ ├── raw/ # 原始 Excel │ └── clean/ # 清洗后 CSV ├── db/ │ └── gaokao.db # SQLite 数据库 ├── src/ │ ├── clean_data.py # 清洗脚本 │ ├── build_db.py # 入库脚本 │ ├── recommend.py # 推荐算法 │ └── app.py # Streamlit 界面 ├── docs/ │ └── 项目文档.md ├── requirements.txt └── README.md

clean_data.py和build_db.py分开,是为了让「数据准备」和「算法」解耦,改数据不影响算法。recommend.py里只放纯函数,方便单独测试。

5. 避坑与排查:那些让系统翻车的细节

5.1 位次换算用错累计人数方向

现象:推荐结果全部偏高,冲档里出现一堆明显够不着的学校。原因:一分一段表里「累计人数」是从高分往低分累加的,如果表本身是升序(低分在前),直接取cum_num会得到错误位次。解决:读表后先sort_values("score", ascending=False),确认第一行是最高分,再取累计人数。这个坑我踩过,排查时打印前五行分数和位次就能看出来。

5.2 选科要求字符串匹配漏掉「或」关系

现象:考生选了「物理+化学+生物」,但某些要求「物理或化学」的专业组被过滤掉了。原因:need.issubset(set(subjects))只处理「且」关系,遇到「或」就误判。解决:把选科要求解析成表达式,遇到「或」用并集判断。简单做法是先把「或」拆成多个可选集合,任一满足即可。

5.3 数据库查询没加年份导致跨年混算

现象:推荐结果里同一学校出现多个年份的位次,排序混乱。原因:WHERE条件漏了year,把历年数据全查出来了。解决:查询必须带year,或者用「近三年加权平均位次」时显式按年份分组。这个坑在增量更新后特别容易出,因为表里数据变多了。

5.4 Streamlit 重复运行导致数据库连接泄漏

现象:点几次推荐后程序变卡,控制台报连接数过多。原因:每次点击都sqlite3.connect但没关,或者@st.cache_data用错地方。解决:用with sqlite3.connect(...) as conn或显式conn.close(),查询结果用@st.cache_data缓存,避免重复查库。

5.5 招生计划数变化没做修正

现象:某校去年位次 25000,今年计划减半,系统仍按 25000 推荐为「稳」,实际录取位次可能到 18000。原因:只用了最低位次,没考虑计划数波动。解决:对plan_num同比减少超过 30% 的记录,位次乘以 0.9 再比较;增加超过 30% 的,乘以 1.1。这个修正不复杂,但能明显提升准确度。

6. 进阶技巧:把推荐结果做成可解释的清单

系统能出结果只是第一步,让用户信服还得给出「为什么推荐它」。我一般会在结果里加三列:rank_gap(考生位次与院校最低位次差)、plan_change(计划数同比变化)、tag(院校层次)。rank_gap为正表示考生位次更靠后,属于冲;为负表示更靠前,属于稳或保。这样用户一眼能看懂匹配程度。

另一个技巧是做「专业组内专业排序」。同一专业组里,热门专业和冷门专业录取位次可能差几千名。如果数据里有分专业录取位次,就按位次从高到低排,让用户知道组内哪些专业更稳。没有分专业数据时,至少把major_name列出来,别只显示专业组。

验证方法上,我习惯拿去年真实录取数据做回测:取去年某位次考生,用前年数据跑推荐,看去年实际录取结果是否落在推荐清单里。命中率能到 70% 以上,说明参数合理;低于 50%,就要检查位次换算和选科过滤。这个回测脚本不复杂,但能让你在答辩时底气足很多。

最后说个血泪经验:别在答辩前一天才跑数据。数据清洗和入库至少留两天,因为 Excel 格式问题、编码问题、缺失值问题会轮番出现。我一般提前一周把数据跑通,留出时间调推荐系数和界面细节。希望帮到你。

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

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

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

立即咨询