资讯动态

基于Django与智能算法的高考志愿推荐系统设计与实战

发布时间:2026/10/27 15:04:18 来源:尧图企业网站定制
简介基于Django和智能算法的高考志愿填报推荐系统源码包面向高考生、家长以及Django开发者用于解决志愿填报信息冗余、决策困难的问题。系统采用MVC架构Django负责后台数据处理与业务逻辑前端借助Templates模板、CSS与JavaScript实现交互并将历年录取数据、考生分数、兴趣偏好等纳入推荐模型。包内共631个文件包含93个Python核心源码、34个HTML页面、90个JavaScript脚本、29个CSV数据集、40个JSON配置以及xlsx/docx说明文档、ttf字体、模型权重压缩包7z/zip/rar等总大小约85.91MB可完整支撑项目部署与二次开发。目前已有41人学习下载。资源中除Django工程代码外还提供了数据预处理脚本、算法调优记录、BERT相关模型文件与笔记文档适合想深入理解智能推荐算法、学习完整Web项目结构的开发者也适合考生家庭了解系统逻辑后理性使用。1. 高考志愿推荐系统是什么数据驱动的方案不是替代专家一个考生拿着 600 分和全省位次面对上百所学校、上千个专业要在几天内排出顺序。传统的做法是翻书、问老师、凭经验估而“基于 Django 和智能算法的高考志愿填报推荐系统”要做的就是把这件事拆成一组可计算的问题考生画像是什么、录取数据长什么样、怎么在“冲一冲、稳一稳、保一保”之间找到平衡。它不是一个拍脑袋的“智能算命”工具而是一个把位次、选科、地域偏好和院校历年录取数据对齐的匹配引擎。这套系统适合两类人一是做毕业设计或课设、需要完整技术栈的学生开发者二是想给学校或教育机构做志愿填报辅助工具的从业者。前者关心代码能不能跑通后者关心推荐结果靠不靠谱——这篇笔记会把两条路都走一遍。2. 建模先行为什么位次比分数更值钱等效分怎么算2.1 用位次而不是分数一张一分一段表把两年数据拉平如果你直接把今年的 600 分拿去比去年的 600 分结果会非常离谱。每年考题难度、考生人数、招生计划都在变分数本身不是一个稳定尺度。真正稳定的是位次——全省排名。600 分在题难的年份可能是全省第 8000 名在题简单的年份可能排到第 15000 名两者能报的学校完全不同。所以推荐系统的第一个核心逻辑是考生输入分数和位次系统用位次去对齐历史录取数据。具体做法是维护一张“等效分”换算表。假设考生今年位次是 8000 名系统去去年的“一分一段表”里找位次最接近 8000 的那一行它对应的分数就是考生的“去年等效分”。有了等效分历史录取数据才可以直接参与计算。def calculate_equivalent_score(current_rank: int, history_rank_table: list[dict]) - float: 根据今年位次在历史一分一段表中找到等效分。 history_rank_table: [{score: 612, rank: 7500}, {score: 610, rank: 8200}, ...] # 按位次绝对值差排序取最接近的一行 closest min(history_rank_table, keylambda row: abs(row[rank] - current_rank)) return closest[score]这段代码的逻辑很直接min配合key按“位次差的绝对值”找最接近的记录。如果一分一段表的粒度不够细比如每 3 分一档还可以做线性插值但按我的经验直接用最接近位次的行精度已经能满足志愿填报的场景——因为录取位次本身就是一个范围不是精确值。2.2 画像字段设计把选科、地区、专业意向变成可计算的标签考生画像不只是“分数 位次”。新高考模式下选科组合直接决定能报哪些专业。物理类考生报不了要求“历史”的专业没选化学的报不了绝大多数化工、临床医学类专业。如果系统不校验选科推荐结果里会出现大量无效志愿这是新手最容易忽视的地方。我一般会在画像里维护这几类字段考试年份、省份、首选科目物理/历史、再选科目列表化学/生物/政治/地理、位次、分数以及一个“意向标签”列表——比如想去的城市、想读的学科门类、能接受的学费区间。城市和学科门类就是后面做内容匹配的标签提前结构化存好省得推荐阶段再分词。from django.db import models class StudentProfile(models.Model): SUBJECT_CHOICES [(physics, 物理), (history, 历史)] exam_year models.IntegerField(verbose_name高考年份) province models.CharField(max_length32, verbose_name省份) first_subject models.CharField(max_length16, choicesSUBJECT_CHOICES, verbose_name首选科目) electives models.JSONField(defaultlist, verbose_name再选科目) rank models.IntegerField(verbose_name全省位次) score models.IntegerField(verbose_name高考分数) preferred_cities models.JSONField(defaultlist, verbose_name意向城市如[杭州,成都]) preferred_majors models.JSONField(defaultlist, verbose_name意向专业 fee_limit models.IntegerField(default100000, verbose_name可接受最高学费(元/年))这个表的设计原则是所有会影响推荐结果的约束都单独建字段不要塞进一个 JSON 里让推荐引擎自己猜。electives和preferred_cities用 JSONField 是因为它们数量不固定但查询时如果用 ORM 的contains去做精确匹配会吃亏推荐阶段我会把它们取出到内存里做过滤而不是在 SQL 层硬碰。2.3 数据源和更新策略录取数据的口径决定推荐结果上限推荐系统的算法再花哨数据不准就是零。院校专业的录取数据来源一般有两个口径省级招生考试院公布的“院校投档线”只有学校最低位次以及部分省份公布的“专业录取数据”每个专业的最低分和最低位次。能拿到专业级数据推荐粒度才够细只有院校级数据就只能做到“这个学校能冲/能稳”给不出具体专业建议。我处理这些数据时有一个习惯每一行录取记录都带batch本科批/提前批/专科批、school_type公办/民办/中外合作、fee学费字段。原因很现实——提前批和普通批的录取位次相差很大中外合作办学专业分数低但学费高三种口径混在一个表里推荐结果会失真。数据定期更新每次导入新一年的录取数据后用DataFrame做一次重复项和异常值检查比如最低位次为 0、分数为空、专业名称带乱码的记录直接剔除。import pandas as pd def clean_admission_data(raw_csv_path: str) - pd.DataFrame: df pd.read_csv(raw_csv_path) # 必填字段不能为空 df df.dropna(subset[school_name, major_name, min_rank]) # 位次为 0 或者负数是脏数据 df df[(df[min_rank] 0) (df[min_rank] 800000)] return df这段清洗代码的要点是“宁可少一条数据不要错一条数据”。脏数据流入推荐阶段后轻则推荐出一个根本录取不了的学校重则让整个列表的梯度全部乱掉。后续所有召回和排序都建立在这张干净的底表上所以我会在导入流程里加一步校验日志把每次清洗掉的行数打出来方便追溯。3. 智能算法选型协同过滤落不了地基于内容的混合方案才是正解3.1 协同过滤在志愿填报场景的两个致命伤很多做过电影推荐系统的人容易把协同过滤直接搬到志愿填报上。协同过滤的本质是“和你相似的人喜欢什么你也可能喜欢什么”它需要大量历史行为数据——谁把哪个学校放进了志愿表最后去了哪。但高考志愿填报恰恰没有这种数据每年全国考生上千万但一个考生只填几十个志愿而且这是“人生只有一次”的决策不存在反复浏览、收藏、评分的行为链路。数据稀疏到这种程度协同过滤算出来的相似度基本是噪声。第二个致命伤是冷启动。一个新上线的系统没有历史志愿数据协同过滤连“相似用户”都找不到只能退化成随机推荐。而基于内容的推荐完全没有这个问题它不需要用户行为只需要考生画像和院校专业特征两边都有标签就能算相似度。所以我给这类系统的建议很直接以基于内容的匹配为主规则分层做辅助不要碰协同过滤。这也是很多做过“基于 spring boot 的大学生就业推荐系统”的同行踩过的同一个坑——就业场景同样是低频决策行为数据一样稀疏。3.2 基于内容的匹配给院校、专业打标签再做加权相似度基于内容的推荐核心是把院校和专业变成一张标签向量表。院校的标签可以包括所在城市、城市等级一线/新一线/二线、院校类型综合/理工/师范/医学、学科评估档次A/B/C、是否是 985/211/双一流。专业的标签可以包括所属学科门类工学/理学/医学/经管、就业方向、适合的选科组合。考生画像在 2.2 里已经建好了推荐阶段就是把考生对城市的偏好、对学科门类的偏好和院校专业标签做加权匹配。这里不需要上深度学习模型用最朴素的加权相似度就行。我的经验是位次区间能不能录取至少要占 60% 的权重标签匹配度占 30%剩下 10% 留给学费和院校声望之类的调节项。先把硬条件卡死再在硬条件里挑喜欢的这个顺序不能反。def content_score(profile: StudentProfile, school: dict, major: dict) - float: 基于内容匹配的加权打分返回 0~100 的匹配度。 school 和 major 是已经从数据库取出的标签字典。 score 0.0 # 位次区间命中最核心的硬条件占 60 分 if school[min_rank] profile.rank school[max_rank]: score 60 # 城市偏好命中意向城市列表里有一个匹配就算占 20 分 if school[city] in profile.preferred_cities: score 20 # 专业方向匹配意向专业和当前专业同属一个学科门类占 15 分 if major[category] in profile.preferred_majors: score 15 # 学费可接受占 5 分 if school[fee] profile.fee_limit: score 5 return score这套打分逻辑故意写得直白因为它要的就是可解释性。用户问“为什么推荐这个学校”你能直接说出“位次在录取区间内 城市是成都 学费能接受”比一个黑匣子模型算出的 0.87 分有说服力得多。3.3 冲稳保分层召回-排序两段式结构里的系数怎么调“冲稳保”是高考志愿填报的核心方法论推荐系统必须把它内化成一条明确规则。设定考生位次为 r常见的分档逻辑是冲 录取位次区间在[r, r*0.8]往年录取位次比考生靠前 0~20%的学校稳 [r, r*1.2]保 [r*1.2, r*1.5]。这个比例不是绝对的各省考生密度和院校数量不同需要微调但作为默认值是靠谱的。工程实现上我一般用两段式结构。第一段“召回”按批次、选科硬条件、冲稳保位次区间用 ORM 查询快速选出候选池第二段“排序”对候选池里的每一条记录跑content_score按分数排序最后按冲稳保三档各取若干个名额拼成一个推荐列表。这样设计的好处是召回阶段可以放心用数据库索引加速排序阶段的数据量小了Python 里逐条算分数也不会慢。def recall_candidates(profile: StudentProfile) - list[dict]: 第一段按硬条件从数据库召回候选院校专业。 按位次区间分三档召回每一档取前 200 条。 base_qs AdmissionRecord.objects.filter( batch本科批, provinceprofile.province, exam_yearprofile.exam_year - 1, # 用上一年数据做参照 subject_requirement__containsprofile.first_subject, ) candidates [] # 冲往年录取位次在 [rank, rank*0.8] 之间 for label, rank_min, rank_max in [ (冲, int(profile.rank * 0.8), profile.rank), (稳, profile.rank, int(profile.rank * 1.2)), (保, int(profile.rank * 1.2), int(profile.rank * 1.5)), ]: qs base_qs.filter( min_rank__lterank_max, max_rank__gterank_min, ).order_by(min_rank)[:200] for item in qs: candidates.append({**item.to_dict(), level: label}) return candidates这段代码里最需要注意的坑是rank_min和rank_max的方向。在志愿填报里“位次越小越好”全省第 5000 名比第 10000 名强。所以“冲”是往年录取位次更小的学校区间是[r*0.8, r]“保”是往年录取位次更大的学校区间是[r*1.2, r*1.5]。方向一搞反推荐列表就是反的。4. Django 代码落地三张核心表、一个推荐引擎和两个管理命令4.1 模型设计考生画像表、院校专业表、录取数据表怎么建Django 项目里最怕的是把所有字段塞进一张大表。志愿填报系统我一般分成三张核心表StudentProfile用户画像、SchoolMajor院校专业基本信息、AdmissionRecord历年录取数据。画像表在 2.2 里已经给了这里重点说后两张表。SchoolMajor存静态属性学校名称、所在城市、院校类型、办学性质、学费、选科要求、专业名称、学科门类。AdmissionRecord存动态数据年份、批次、最低分、最低位次、平均位次、招生人数。为什么拆开因为同一个专业在不同年份的录取位次年年变动态部分单独放一张表更新数据时只动AdmissionRecord不动学校专业基础信息。class SchoolMajor(models.Model): school_name models.CharField(max_length128, db_indexTrue) city models.CharField(max_length32) school_type models.CharField(max_length16) # 综合/理工/师范/医学 major_name models.CharField(max_length128) major_category models.CharField(max_length32) # 工学/理学/医学/经管 subject_requirement models.JSONField(defaultlist) # 选科要求如[物理,化学] fee models.IntegerField(default5000) class AdmissionRecord(models.Model): school_major models.ForeignKey(SchoolMajor, on_deletemodels.CASCADE, related_namerecords) exam_year models.IntegerField(db_indexTrue) batch models.CharField(max_length16) # 本科批/提前批/专科批 min_score models.IntegerField() min_rank models.IntegerField(db_indexTrue) max_rank models.IntegerField() enroll_count models.IntegerField(default0) class Meta: # 最常用的查询维度年份 位次范围 indexes [models.Index(fields[exam_year, min_rank, max_rank])]索引一定要加在exam_year min_rank max_rank这个组合上因为推荐引擎的召回查询就是拿年份和一个位次区间去过滤。不加这个索引录取数据量到几万条以上时推荐接口会明显变慢。4.2 推荐引擎写在哪独立模块 管理命令跑批新手常见的做法是把推荐逻辑直接写进视图函数用户请求一次就现场算一次。数据量小的时候没问题但真实场景里一个专业就有 5 年录取记录一个省有几千个专业现场算一遍就是几万次相似度计算接口响应直接卡死。我把推荐引擎写成独立模块recommend/engine.py用 Django 管理命令触发跑批结果落库前端只读结果表。from django.core.management.base import BaseCommand from recommend.engine import build_recommendations class Command(BaseCommand): help 为指定考生生成志愿推荐列表并落库 def add_arguments(self, parser): parser.add_argument(--profile-id, typeint, requiredTrue) parser.add_argument(--year, typeint, default2026) def handle(self, *args, **options): profile_id options[profile_id] year options[year] count build_recommendations(profile_id, year) self.stdout.write(f生成 {count} 条推荐结果)python manage.py run_recommend --profile-id1 --year2026推荐结果写入一张Recommendation表包含profile外键、school_major外键、match_score、level冲/稳/保、rank排序字段和created_at。用户在前端查看时永远不需要重新计算只需要查这张表。这也是 Django 项目实战新手最容易忽略的一点推荐类系统的计算密度远高于普通 CRUD跑批落库是标配不是优化项。4.3 查询优化和缓存一次推荐请求背后要查多少次库排名结果落库之后读接口的查询很轻但仍有两个细节要注意。第一个是用select_related一次性把SchoolMajor的外键信息带出来避免循环里逐条查外键第二个是推荐结果按level分组展示时用values或annotate在数据库层做分组不要把几百条结果取到 Python 里再手动分组。def get_recommendation_list(profile_id: int) - dict[str, list]: 按 冲/稳/保 三档分组返回推荐结果。 select_related 一次性查出外键关联的院校专业信息。 qs Recommendation.objects.filter(profile_idprofile_id) \ .select_related(school_major) \ .order_by(rank) grouped {冲: [], 稳: [], 保: []} for rec in qs: grouped[rec.level].append({ school: rec.school_major.school_name, major: rec.school_major.major_name, score: rec.match_score, city: rec.school_major.city, }) return groupedselect_related是这里的关键——它会在 SQL 层用 JOIN 把外键表的数据一次性查出来而不是等你在循环里访问rec.school_major时才发第二条 SQL。如果你在日志里看到推荐列表接口有几十条重复的查询语句基本就是漏了这一步。4.4 接口和页面把推荐结果展示成一个可交互的志愿表Django 端我习惯提供两个接口一个是生成推荐的跑批入口管理命令已经覆盖接口可以省另一个是查询推荐结果的 JSON 接口给前端表格用。用 Django REST Framework 写一个只读的ViewSet或者直接JsonResponse返回grouped字典都行——项目里没有复杂的权限和嵌套序列化需求时没必要为了用 DRF 而用。前端展示上最实用的形态是一张“志愿表”样式的表格左边是冲稳保三档标签每一档下面罗列院校和专业点击某一行的“查看详情”展开显示该专业近三年的最低位次和录取人数。后端在返回grouped之前顺带把每一条推荐结果的近三年位次趋势一起查出来减少前端二次请求。def _attach_rank_trend(rec: dict, school_major_id: int) - dict: records AdmissionRecord.objects.filter( school_major_idschool_major_id ).order_by(-exam_year)[:3] rec[rank_trend] [(r.exam_year, r.min_rank) for r in records] return rec这段函数在 4.3 的循环里对每条结果调用一次产生 N1 查询。如果推荐结果有几十条建议加一层prefetch_related或直接在get_recommendation_list里一次性查出所有关联的AdmissionRecord再在内存里做分组。数据量可控时后者的写法更省事数据量大时必须上Prefetch对象。5. 避坑指南录取数据、批次规则和推荐结果里的五处常见翻车坑一位次区间方向搞反“冲”成了“保”第一次跑推荐列表时发现标签为“冲”的学校全是往年录取位次比考生低一大截的学校也就是所谓的“保底校”。查了代码之后定位到 3.3 里的区间设置冲的rank_max写成了profile.rank / 0.8导致冲的区间变成了位次更大的方向。解决方法是把区间参数抽成配置项写单元测试覆盖三个区间的边界值而不是靠肉眼检查结果。位次方向上记住“数字越小越强冲是找更强的学校”。坑二选科要求没有校验历史类考生拿到了物理类专业系统上线后收到的第一个用户反馈是选“历史”的考生被推荐了计算机科学与技术。原因是召回阶段只过滤了batch和province漏掉了subject_requirement字段。Django 的JSONField存[物理, 化学]这类列表ORM 里用contains做包含匹配很容易误判。解决方式是画像里的first_subject和electives先转成 Python 的set在召回前做一次内存过滤如果数据量大就把选科要求拆成require_first_subject和require_elective_subject两个独立字段用精确匹配。坑三早期录取数据混入提前批和中外合作办学推荐列表出现“分数倒挂”有段时间推荐结果里频繁出现一组位次很低但学费极高的“名校”。排查发现是数据导入时把提前批如军校、公费师范生和中外合作办学专业混进了普通本科批。提前批录取位次往往低于普通批中外合作办学学费高但分数低。解决方式是在AdmissionRecord表里把batch和school_type做成必填字段清洗逻辑里增加规则校验——batch为空或非法值直接丢弃推荐引擎只认batch本科批且school_type为“公办/民办”的记录杜绝“收费高昂的低分捡漏”出现在常规志愿表里。坑四推荐列表梯度不合理全是一个档次的学校第一次用基于内容的评分排序时发现生成的列表里 80% 都是“稳”档学校“冲”和“保”几乎为空。原因是content_score里的 60 分位次命中权重把“位次落在区间内”设成了硬条件而大多数学校往年的位次区间偏宽容导致它们全落在同一个区间里。解决方式是召回和排序分工召回阶段按位次区间划分三档排序阶段不在位次上重复加权而是用院校声望、城市偏好、专业热度这些“软条件”在档内排序。冲稳保的比例由召回阶段的分档数量决定排序不会跨档调换。坑五缓存键没带年份用户查到了上一届的推荐结果有一版代码用了cache.set(frecommend_{profile_id}, result)做结果缓存结果新一届录取数据导入后老用户刷新页面看到的还是旧推荐。原因很简单缓存键里没有带exam_year而推荐结果最核心的输入就是年份。解决方式是缓存键一律拼上年份frecommend_{profile_id}_{year}数据更新时按年份批量清理对应的缓存键。这个坑单看代码很难发现上线后第一次更新数据就会踩到属于典型的“数据更新导致的黑匣子问题”。6. 效果验证用历史数据回测推荐精度两个指标看清系统水平推荐系统做完之后最怕的是“跑通了但不知道准不准”。最直接的验证方法是回测拿去年的考生画像喂给今天这个系统用今年已经公布的录取结果来判断去年的推荐准不准。这里用两个指标就够了第一个是档位命中率推荐为“冲”的学校里有多少所今年的录取位次真的高于考生位次第二个是整体满意度推荐列表里的学校有多少所今年实际录取位次落在考生的冲稳保区间内。def evaluate(profile, recommendation_result, actual_admission_data): hit 0 for rec in recommendation_result: school_id rec[school_major_id] actual actual_admission_data.get(school_id) if actual and actual[min_rank] profile.rank: # 实际录取位次更靠前冲成功了 hit 1 total len(recommendation_result) return {hit_rate: hit / total, total: total}第一次跑回测时我的命中率只有四成。查数据发现是前一年的“本科批”数据里混了一部分征集志愿的记录把位次区间拉宽了。清洗之后命中率提高到了六成以上。除了回测还要做一次和人工经验的对比找三个做了五年志愿填报的老师对同一个考生各出一份推荐清单然后看系统推荐和他们重叠多少。重叠率低于三成说明系统的标签权重设置和现实决策逻辑有偏差优先调content_score里的城市和专业权重。最后说一个进阶技巧把“服从调剂”和“专业组”概念纳入排序。新高考实行院校专业组投档同一个学校不同专业组的录取线相差很大。如果数据能区分专业组推荐引擎应该在组内做专业排序而不是把整个学校的专业混在一起排。这个改造不难但能把系统从“能用”推到“能真正上线给用户填志愿用”的水平。我自己在做这类系统时养成的习惯是先跑通最小闭环再盯着回测指标做迭代别一上来就堆算法。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑