基于Django的高考志愿推荐系统:协同过滤与录取概率预估

发布时间:2026/8/31 17:03:07
基于Django的高考志愿推荐系统:协同过滤与录取概率预估 简介这是一套面向高考生、家长及教育技术开发者的高考志愿填报智能推荐系统源码基于Django框架与数据挖掘、预测优化等智能算法构建聚焦K-12教育阶段升学决策支持解决志愿匹配度低、信息过载、政策理解难等现实痛点。资源包共631个文件含93个Python后端逻辑文件含模型调用与业务路由、90个JavaScript前端交互脚本、34个HTML模板页、29个CSV/Excel录取数据集、14个CSS样式文件及79张院校专业示意图另有BERT微调模型压缩包major_rep_bert_李春澍.7z体现NLP兴趣建模能力整体体积85.91MB。目前已有40人学习下载提供完整可运行的Django项目结构含templates/static/manage.py及sqlite3数据库涵盖用户画像构建、分数位次换算、院校专业多维排序、政策规则引擎等核心模块开箱即用适合Web全栈初学者实践部署也便于算法开发者二次集成推荐模型。1. 项目背景与核心需求拆解1.1 为什么高考志愿填报需要推荐系统做高考志愿填报推荐系统这个项目最初的想法其实很朴素。每年高考出分之后考生和家长面对的是厚厚一摞院校投档线数据、上千所高校、几百个专业要在短短几天内选出几十个志愿组合这个信息处理量非常大。我见过太多考生因为信息不对称导致滑档、退档或者高分低就也见过不少家长花几千块钱找填报机构最后拿到的方案却大同小异。说白了志愿填报本质上就是一个信息筛选和决策匹配的过程而推荐系统恰恰擅长处理这类问题。这个系统的核心任务就是三件事第一帮考生判断自己分数能上什么层次的学校第二在海量院校和专业中筛选出匹配度高的候选组合第三给出可视化的冲稳保推荐列表并解释为什么推荐这些学校。听起来好像不复杂但真正动手做的时候会发现数据清洗、算法选型、推荐结果的可解释性每一个环节都有不少坑。1.2 从“查数据”到“推荐”的需求升级市面上的志愿填报工具很多但大多数停留在“查数据”的层面——输入分数返回所有录取分数线低于该分数的学校列表。这种方式有两个明显问题一是没有考虑位次的波动性直接拿裸分对比往年分数并不科学二是结果列表没有任何优先级考生面对几百条结果依然不知道怎么选。我做的这个系统把逻辑升级到了“推荐”层面。除了基本的分数匹配还会结合考生所在省份、选科组合、院校层次、专业热度、往年录取位次波动等多个维度构建一个综合评分模型。同时引入协同过滤的思路找出往年录取结果与当前考生相似的“学长学姐”参考他们的最终去向作为推荐依据。最终输出的不是一张数据表而是一份有梯度、有解释、有优先级的推荐列表。2. 技术选型与整体架构设计2.1 为什么选择Django而不是其他框架推荐系统类项目用Python写算法是最顺手的所以后端框架我优先考虑Python阵营。前后对比过Flask、FastAPI和Django最终选择了Django。原因有几个。首先是Django自带ORM和Admin后台这对数据管理类系统来说太重要了。志愿填报推荐系统核心就是数据院校信息、专业目录、历年录取分数这类基础数据需要一个能快速录入、批量导入、可视化维护的后台界面。Django Admin可以帮你省下大量后端管理页面的开发时间。我实际开发中光是后台管理这一块就节省了至少一周的工作量。其次是Django在国内的生态成熟度。网上关于Django的教程、社区讨论、第三方库非常丰富。查询相关的用法比如filter、exclude、annotate、select_related、prefetch_related遇到问题随便一搜就有答案。对于个人开发者或者小团队来说开发效率和排障效率是第一位的。第三点是Django内置的认证、会话、CSRF防护等模块对于这个项目后续需要区分普通用户和管理员、保存考生个人信息和志愿草稿的场景来说不需要额外引入太多依赖就能搞定。这里多提一句项目早期的算法原型我用的是FastAPI写的因为异步性能确实好。但到了真正要接业务逻辑、做后台管理、处理用户权限的时候还是Django这套全家桶更省心。如果你们项目本身不是高并发I/O密集型的场景Django的吞吐量完全够用。2.2 推荐算法选型先规则后模型算法这块是项目中我推翻重做次数最多的部分。最初我参照互联网大厂推荐系统的思路直接上了向量召回、深度排序那套架构mind召回、sdm召回这些名词背得滚瓜烂熟模型结构搭起来了embedding也训练了最后发现效果很差。问题出在数据上。大厂推荐系统是建立在海量用户行为数据上的一个视频平台一天就有几亿次点击。但高考志愿填报这个场景一个省份一年也就几十万考生每个考生只有一次填报结果而且这个结果受到政策变化、专业冷热、招生计划变动等大量非结构化因素影响。这种数据背景下纯靠数据驱动的深度模型基本跑不起来很容易过拟合推荐结果反而不如规则模型稳定。最终我采用的是“规则内容协同过滤”的混合推荐策略。先用规则模型做兜底保证推荐结果的合理性和安全性再用内容推荐做精细化排序结合院校层次、专业热度、城市因素等特征打分最后如果有足够的相似考生数据用协同过滤做补充参考。这套方案在数据量不大的情况下表现稳定而且每个推荐结果都能给出明确的解释逻辑用户也更愿意接受。提示做垂直领域的推荐系统不要盲目套用大厂技术栈。数据规模决定了你能用什么模型这是我在这个项目里最大的教训。2.3 系统模块与整体数据流整个系统分成四个核心模块。数据管理模块负责院校、专业、录取分数等基础数据的维护和清洗推荐引擎模块是核心算法部分负责录取概率预估、相似度计算、冲稳保分类Web服务模块基于Django提供界面展示和接口调用用户模块负责考生信息管理和志愿表保存。数据流向是这样的原始数据从公开渠道采集后先进入清洗流程处理缺失值、格式统一、字段映射然后通过Django Admin或批量脚本导入数据库。用户在前端输入自己的分数、位次、选科等信息后Django视图层接收请求调用推荐引擎的服务类引擎读取数据库中的历史数据完成计算生成推荐结构列表再返回到前端展示。这个流程看起来简单但每个环节都有细节要注意。比如历史录取数据会因为招生批次合并而存在断档某些年份的数据格式完全不一样这些都需要在清洗阶段处理掉否则算法算出来的结果就是错的。3. 数据建模与核心算法实现3.1 Django ORM模型设计数据库是推荐系统的基础。好的模型设计能让查询效率翻倍反之则会让代码越来越难维护。我前前后后改了三次数据表结构最终沉淀出这几个核心模型。from django.db import models class School(models.Model): 院校基础信息表 name models.CharField(院校名称, max_length100, uniqueTrue) code models.CharField(院校代码, max_length20, db_indexTrue) province models.CharField(所在省份, max_length50) city models.CharField(所在城市, max_length50) level models.CharField(院校层次, max_length20, choices( (985, 985院校), (211, 211院校), (double_first_class, 双一流), (province_key, 省属重点), (ordinary, 普通本科), )) nature models.CharField(办学性质, max_length20, choices( (public, 公办), (private, 民办), (independent, 独立学院), (joint, 中外合作), )) tags models.JSONField(院校标签, defaultlist) class Major(models.Model): 专业目录表 name models.CharField(专业名称, max_length100) code models.CharField(专业代码, max_length20, db_indexTrue) category models.CharField(专业大类, max_length50) subject_requirements models.JSONField(选科要求, defaultdict) class SchoolMajor(models.Model): 院校专业关系表某个学校招生的某个专业 school models.ForeignKey(School, on_deletemodels.CASCADE, related_namemajors) major models.ForeignKey(Major, on_deletemodels.CASCADE) enrollment_count models.IntegerField(计划招生人数, nullTrue, blankTrue) study_years models.IntegerField(学制, default4) tuition_fee models.IntegerField(学费标准, nullTrue, blankTrue) class ScoreLine(models.Model): 历年录取分数表 school_major models.ForeignKey(SchoolMajor, on_deletemodels.CASCADE, related_namescore_lines) year models.IntegerField(年份, db_indexTrue) province models.CharField(省份, max_length20, db_indexTrue) batch models.CharField(录取批次, max_length20) avg_score models.FloatField(平均分, nullTrue, blankTrue) min_score models.FloatField(最低分) min_rank models.IntegerField(最低位次) avg_rank models.IntegerField(平均位次, nullTrue, blankTrue) class Candidate(models.Model): 考生信息表 name models.CharField(考生姓名, max_length50, nullTrue, blankTrue) province models.CharField(所在省份, max_length20) score models.FloatField(高考分数) rank models.IntegerField(全省位次) subject_selection models.JSONField(选科组合, defaultlist) preferred_city models.JSONField(意向城市, defaultlist) preferred_major models.JSONField(意向专业, defaultlist) class RecommendationResult(models.Model): 推荐结果存储表 candidate models.ForeignKey(Candidate, on_deletemodels.CASCADE, related_namerecommendations) school_major models.ForeignKey(SchoolMajor, on_deletemodels.CASCADE) match_score models.FloatField(匹配度评分) probability models.FloatField(录取概率预估) level models.CharField(推荐级别, max_length10, choices( (chong, 冲), (wen, 稳), (bao, 保), )) reason models.JSONField(推荐理由, defaultdict) created_at models.DateTimeField(生成时间, auto_now_addTrue)几个设计上的关键点。School和Major拆成两张表中间用SchoolMajor关联是因为一个学校招很多专业、一个专业在很多学校开设这是典型的ManyToMany关系。ScoreLine外键指向SchoolMajor而不是直接指向School和Major这样查询某个学校某个专业的历年分数时只需要一次join。Candidate表和RecommendationResult表是一对多关系因为同一个考生在调整意向城市或专业后可能需要重新生成推荐结果保留历史记录方便对比。字段类型方面tags和subject_requirements这类结构不固定、属性比较少的数据直接用JSONField存储比单独建表要省事。但如果后续要做复杂的按标签筛选查询建议还是拆成关联表性能会更好。这个根据实际需求权衡。3.2 院校热度画像与录取概率预估推荐系统最核心的一个指标是录取概率。我用的是“位次法”加“线差法”的组合策略。位次法适用于高分考生因为高分区间考生人数少位次对应关系稳定。线差法适用于中低分考生用考生分数与批次线的差值对比往年数据。录取概率的计算逻辑如下def estimate_admission_probability(score, rank, score_line): 基于位次与线差综合计算录取概率 score_line: ScoreLine对象包含往年min_score, min_rank # 位次比考生位次 / 录取最低位次1 说明考生位次更靠前更有优势 rank_ratio rank / score_line.min_rank # 线差考生分数与省控线的差值这里简化用分数差体现 # score_line.avg_score 和 min_score 提供了参考区间 # 根据位次比换算概率使用sigmoid函数平滑 import math probability 1 / (1 math.exp((rank_ratio - 1) * 3.5)) # 修正逻辑位次比小说明很稳 if rank_ratio 0.7: probability min(probability * 1.2, 0.98) elif rank_ratio 1.3: probability max(probability * 0.6, 0.05) return round(min(probability, 0.99), 4)这里有几个细节。为什么用sigmoid函数而不是直接用位次比的倒数因为位次比在1附近时概率变化非常剧烈但实际录取中位次在最低录取位次附近的考生被录取的概率并没有那么明显的分界线。sigmoid函数的平滑过渡更符合真实情况。3.5这个系数是我用近三年的录取数据反复回测得到的系数越大曲线越陡峭越接近0和1两极分化系数太小则概率区别不明显推荐排序拉不开差距。另外我在实际代码中还加入了多年度数据加权。比如2023年的数据权重0.52022年0.32021年0.2对三年的概率预测做加权平均。因为高校录取位次是有趋势性的可能逐年上涨也可能逐年下降单纯用某一年的数据容易出现偏差。3.3 相似考生协同过滤参考协同过滤的思路在志愿填报场景下可以这样理解找一个和你在同一分数段、来自同一省份、甚至选科组合都类似的考生看看他最后去了哪里这个去向可以作为你的参考。因为在高考录取这个场景下录取结果是客观存在的能反映真实的市场选择情况。实现相似度计算时需要先筛选候选集。不能拿全省考生挨个算相似度计算量太大。我首先用分数区间过滤只保留位次在你前后3%范围内的考生然后再计算相似度。from sklearn.metrics.pairwise import cosine_similarity import numpy as np def find_similar_candidates(candidate, all_candidates, top_n20): 基于分数、选科组合、意向维度计算相似考生 def _candidate_vector(c): # 构造特征向量分数归一化(0-1)、位次归一化、选科向量 score_norm c.score / 750.0 rank_norm 1.0 / (1 c.rank / 10000) subject_vec [] for subject in [physics, chemistry, biology, history, geography, politics]: subject_vec.append(1 if subject in c.subject_selection else 0) return np.array([score_norm, rank_norm] subject_vec) target_vec _candidate_vector(candidate) candidates [] for c in all_candidates: if c.id candidate.id: continue # 位次范围过滤减少计算量 if abs(c.rank - candidate.rank) candidate.rank * 0.03: continue vec _candidate_vector(c) similarity cosine_similarity([target_vec], [vec])[0][0] candidates.append((c, similarity)) candidates.sort(keylambda x: x[1], reverseTrue) return candidates[:top_n]协同过滤在高分段的参考价值更高因为高分段考生样本少每个参考对象都很珍贵。中低分段考生样本多但选择也更多样化这时候协同过滤结果只能作为辅助参考主要决策依据还是规则模型和概率预估。4. 核心功能实战实现4.1 冲稳保三档推荐策略的完整实现冲稳保是志愿填报的核心策略。简单理解就是冲刺的学校录取概率低但值得尝试稳妥的学校录取概率中等保底的学校录取概率很高确保有学上。这个项目最核心的功能就是把这套策略算法化。实现逻辑是遍历考生所在省份的所有院校专业组合调用录取概率预估函数根据概率值把推荐结果分成三档。我设定的档位阈值是经过多次调整后的经验值推荐级别录取概率区间说明冲0.20 - 0.45有机会但风险高适合博一博稳0.46 - 0.75匹配度较高作为主力志愿保0.76 - 0.98大概率录取垫底保障放弃0.20 或 0.98太低没意义太高浪费志愿这个阈值不是拍脑袋定的我实际验证过。概率低于0.2的学校在平行志愿规则下基本不会被投档填了也是浪费志愿名额。概率高于0.98的学校虽然百分百能上但如果出现在前面志愿里会占据宝贵的志愿位次影响后面更好的学校录取。当然这里有个前提是平行志愿的省份如果是顺序志愿的录取规则逻辑会不一样。生产环境的推荐代码如下class RecommendationEngine: def __init__(self, province): self.province province self.year_weights {2023: 0.5, 2022: 0.3, 2021: 0.2} def generate_recommendations(self, candidate, limit_per_level30): 生成完整的冲稳保推荐列表 results [] # 获取候选院校专业列表排除选科不匹配的 school_majors self._get_candidate_school_majors(candidate) for sm in school_majors: score_lines sm.score_lines.filter( provinceself.province, year__in[2021, 2022, 2023] ).order_by(year) if not score_lines: continue # 分年度计算概率后加权 weighted_probability 0 total_weight 0 for sl in score_lines: w self.year_weights.get(sl.year, 0) prob estimate_admission_probability( candidate.score, candidate.rank, sl ) weighted_probability prob * w total_weight w weighted_probability round(weighted_probability / total_weight, 4) # 计算匹配度综合评分热度、意向城市、意向专业 match_score self._calculate_match_score(candidate, sm) level self._classify_level(weighted_probability) if level ! discard: results.append({ school_major: sm, probability: weighted_probability, match_score: match_score, level: level, reason: self._generate_reason(candidate, sm, weighted_probability) }) # 每个档位内按match_score排序 chong sorted([r for r in results if r[level] chong], keylambda x: -x[match_score])[:limit_per_level] wen sorted([r for r in results if r[level] wen], keylambda x: -x[match_score])[:limit_per_level] bao sorted([r for r in results if r[level] bao], keylambda x: -x[match_score])[:limit_per_level] return {chong: chong, wen: wen, bao: bao}在实际开发中这段代码最耗时的部分在遍历所有院校专业组合。一个省份的招生计划可能有上万条记录如果每次都实时计算响应时间会非常长。后来我做了缓存优化把历史数据预计算成位次-概率对照表存到Redis里推荐时直接查表响应时间从原来的3秒降到了200毫秒以内。4.2 推荐理由与结果解释推荐系统一个容易被忽视但极其重要的功能是可解释性。用户不仅想知道推荐结果是什么更想知道为什么被推荐。我最初只返回推荐列表结果测试用户反馈“看着像是随便给的”后来专门花了两天时间实现了推荐理由生成模块。理由生成基于规则模板结合院校特征和概率数据def _generate_reason(self, candidate, sm, probability): sl sm.score_lines.filter( provincecandidate.province, year2023 ).first() reasons [] # 位次角度 if sl: rank_diff sl.min_rank - candidate.rank if rank_diff 0: reasons.append(f往年最低录取位次{sl.min_rank}比你的位次低{rank_diff}名) else: reasons.append(f往年最低录取位次{sl.min_rank}比你的位次高{-rank_diff}名) # 院校层次角度 if sm.school.level 985: reasons.append(985院校) elif sm.school.level 211: reasons.append(211院校) # 专业热度角度 if sm.major.category in [计算机类, 电子信息类]: reasons.append(热门专业) # 意向匹配角度 if candidate.preferred_city and sm.school.city in candidate.preferred_city: reasons.append(符合你的意向城市) if candidate.preferred_major and sm.major.name in candidate.preferred_major: reasons.append(就是你的意向专业) return ; .join(reasons)这样生成的推荐理由就是类似“往年最低录取位次12000比你的位次低2000名211院校符合你的意向城市”这样有实际信息量的描述。用户看了之后既能理解推荐依据也能自己复核数据的合理性信任感增强很多。4.3 Django视图与前端接口对接后端算法跑通之后还需要通过Django的视图层暴露给前端。项目中我用了Django REST Framework提供接口前端用Vue框架展示页面。接口设计遵循了资源化思路主要提供三个接口考生信息提交接口、推荐结果生成接口、志愿表保存接口。# views.py from rest_framework.views import APIView from rest_framework.response import Response class RecommendationView(APIView): def post(self, request): 生成推荐结果 serializer CandidateSerializer(datarequest.data) if not serializer.is_valid(): return Response({code: 400, msg: 参数错误, errors: serializer.errors}) candidate serializer.save() engine RecommendationEngine(candidate.province) recommendations engine.generate_recommendations(candidate) # 保存推荐结果到数据库 for level, items in recommendations.items(): for item in items: RecommendationResult.objects.create( candidatecandidate, school_majoritem[school_major], match_scoreitem[match_score], probabilityitem[probability], levellevel, reasonitem[reason] ) result_data format_recommendations(recommendations) return Response({code: 200, data: result_data})在接口联调过程中我发现一个问题Django默认的ORM查询会有N1问题。比如遍历SchoolMajor对象去拿school信息和major信息时每条记录都会执行一次额外的查询推荐列表返回50条结果就要查上百次数据库。解决办法是查询时就用select_related把关联对象一次性取出来。# 使用select_related优化关联查询 def _get_candidate_school_majors(self, candidate): from django.db.models import Q # 用位次范围粗略过滤后select_related提前取出外键对象 base_qs SchoolMajor.objects.select_related(school, major).filter( school__province__incandidate.preferred_city if candidate.preferred_city else [], ) # 过滤选科要求 major_codes [] for selected in candidate.subject_selection: # 简化逻辑只查包含该选科要求的专业 pass return base_qsprefetch_related也是常用的优化手段适用于多对多关系和外键反向查询。使用场景不同select_related适合外键正向查询一对一、多对一prefetch_related适合反向查询和多对多关系。SQL层面都是join和子查询的区别但Django帮你封装好了用对地方性能提升非常明显。5. 常见问题与排查技巧实录5.1 冷启动问题与推荐兜底策略冷启动是推荐系统绕不开的问题高考志愿填报场景下尤为严重。新开发的系统没有任何用户行为数据协同过滤算法根本没法跑。即使系统运行了一两年历史用户数据规模依然很小覆盖不了所有分数段。这导致高分段用户基本找不到几个相似考生做参考。我的处理方式是分层兜底。协同过滤因为数据不足算不出来时自动降级到纯规则模型直接用位次概率分档。规则模型不依赖用户历史数据只要有院校历年的录取分数线就能跑所以永远不会出现“推荐不了”的情况。等相似考生数据积累到一定量级比如同一个分数段有50个以上样本时才自动启用协同过滤的结果进行加权。还有一种情况是用户输入的意向城市或意向专业非常冷门筛选条件一加候选院校专业组合变成了个位数。这时候我会自动放宽筛选条件在推荐结果中标注“根据你的意向扩展推荐”并附上拓展说明。5.2 数据质量问题的排查与修正这个项目数据清洗的工作量远超我最初的预期。公开渠道拿到的历年录取数据存在各种问题字段缺失、格式混乱、数据重复、不同年份口径不一致。最典型的例子是某个学校在某一年改过名字导致同一条记录在数据库里出现两次导致推荐时概率计算出现重复项。排查数据问题我常用的方法是在Django Admin里做交叉验证写一个定期巡检脚本找出明显异常的数据。比如同一学校同一专业同一省份某一年的最低录取位次比前后两年高出十倍那大概率是当年招生计划缩减或者数据录入错误需要人工核实修正。另外每个省在2024年实行新高考物理类和历史类分开划线有些省份还分本科一批二批合并。不同省份的高考制度差异非常大如果系统要支持多省使用必须建立一套省份配置表保存该省的批次线、选科规则、志愿数量限制等差异参数而不是把这些参数硬编码在算法里。5.3 推荐结果的评估与迭代推荐系统的效果评估不能只看用户满意度还要对推荐列表本身做回测。在开发过程中我设计了一套简单的评估方法拿往年已经发生录取结果的考生数据用他们的分数和位次跑推荐系统然后看真实录取结果在不在推荐列表中以及被归在哪个档位。评估指标指标含义我的项目实测值召回率真实录取结果出现在推荐列表中的比例78.6%冲稳保命中分布真实录取结果落在冲/稳/保各档的比例冲20%稳55%保25%平均推荐数量单次推荐返回的院校专业组合数量85个从评估结果看真实录取结果有超过半数落在稳档这说明推荐算法整体是靠谱的。但仍有接近20%的考生录取结果没被推荐出来进一步分析发现绝大多数是跨专业调剂或者去了一些数据缺失的冷门专业。这个数据也说明一个事推荐系统的输出是辅助参考最终决策权还是在用户手里系统要做的是把不同选择的可能性充分呈现在用户面前。5.4 Django项目部署与常见运行问题项目开发完成后部署到Linux服务器中间遇到的问题也不少。第一个坑是静态文件收集Django在DEBUGFalse模式下需要执行collectstatic命令配置Nginx来处理静态文件否则页面样式全挂。第二个坑是数据库迁移我用的是PostgreSQL本地开发时用的SQLite生产环境切换数据库后在执行migrate时出现过字段类型不兼容的问题。最后解决了方法是导出数据前先检查字段长度尤其是JSONField在两种数据库中的差异。部署时还有一个值得注意的地方是CSRF配置。手机端APP访问接口时不需要CSRF验证但Web前端需要。我给接口做了区分纯API接口用csrf_exempt装饰器跳过校验页面请求保留CSRF中间件避免安全问题。6. 实操中的几点体会项目做到后期我对“推荐系统”这个词的理解比刚开始做的时候深刻了不少。互联网大厂的推荐系统更多是迎合用户兴趣、延长使用时长但高考志愿填报这种决策型场景推荐系统背后的责任完全不同——它影响的是一个考生未来几年甚至更长时间的发展方向。所以在算法设计上我没有追求模型复杂度反而花了大量精力在规则合理性和结果可解释性上确保每一个推荐结果都是有数据支撑、有逻辑可循的。最后分享一个我踩过的坑。系统第一版上线时所有推荐结果都保存在数据库的RecommendationResult表里用户每次查询都会插入几十条记录。当时觉得没什么但后来这个表越来越大影响查询性能。后来改成了只保存用户最终收藏的志愿表临时推荐结果直接返回到前端缓存只有用户确认保存时才落库。既减轻了数据库压力也让推荐列表的更新更灵活。这个项目后续还可以往两个方向扩展。一是引入更细粒度的就业数据把毕业去向、薪资水平纳入推荐评分模型让推荐逻辑更全面二是做一个基于规则的自动问答模块帮用户解读一批学校之间的细微差异。高考政策每年都在调整系统也需要保持迭代更新这本身就是这类数据型产品最有价值也是最需要持续投入的部分。本文还有配套的精品资源点击获取

相关新闻