基于Django与数据挖掘的高考志愿推荐系统设计实践
发布时间:2026/10/1 5:15:02来源:尧图网络
每年高考出分后的几天我家里就像开了个小型咨询台。亲戚朋友拿着分数截图来问“这个志愿到底怎么填”我发现把几十个学校的历年分数线摊在Excel里比对根本处理不了位次波动、选科限制、招生计划变化这些交叉变量。后来我决定用Django写一个基于数据挖掘的高考志愿推荐系统让历史录取数据自己“说话”。这篇文章把整个项目的设计与实现完整记录下来包括数据模型、推荐算法、异步推送以及我在实际开发中踩过的坑。适合正在学Django想做实战项目或者想了解推荐系统在垂直领域落地流程的同学。1. 从“查分数线”到“推志愿”这个系统要解决的核心问题1.1 传统报考方式的最大瓶颈传统报考依赖三样东西上一年的分数线、某几本志愿填报参考书、周围人的经验。这三个信息源单独看都有问题。只参考一年数据完全忽略录取分数线的年度波动直接用分数对比又会因为每年试卷难度、考生整体水平不同而产生系统性偏差而周围人的经验往往停留在几年前院校的热门程度早就变了。实际上比分数更稳定的是“位次”——也就是考生在全省范围内的排名。举个例子A校去年最低分600今年考生考了605看起来能上。可去年600分在全省对应的是12000名今年605分可能对应的是15000名位次反而往后倒退了3000名。所以用分数匹配院校本质上是个失真过程。但要逐个院校、逐专业去对比近三年位次、招生计划变化人工根本忙不过来更别说还要叠加城市偏好、选科要求、专业兴趣这些个性化维度。这正是数据挖掘的用武之地从海量历史录取数据里把规律自动找出来再结合考生特征输出推荐结果。1.2 系统边界推荐不是“算命”设计这个系统之前我先给自己定了一条原则推荐只是参考不是决定。数据挖掘模型再漂亮也只能反映历史统计规律没法预测某所学校今年的“大小年”更没法预知哪个专业会突然爆冷或爆热。所以系统输出的不是“你就报这个学校”而是一组带概率的“冲、稳、保”三档选项。每个推荐项都要能说清楚“为什么推荐”。比如“该校近三年最低位次在8000到10000之间你的位次是9500录取概率约45%属于冲一冲的范畴”这就是一条可解释的推荐。带上依据用户才会信任结果也更愿意根据自己的风险偏好做调整。项目的后端逻辑、前端展示都围绕这个可解释原则来设计。这样也避免了推荐系统变成黑箱减少使用过程中的误导风险。2. 数据体系搭建没有干净数据算法全是空中楼阁2.1 数据采集范围与合规性处理高考志愿推荐依赖的数据主要有三块院校分省分专业的历年录取数据、招生计划数据、每年的一分一段表。录取数据里最关键的字段是年份、省份、院校代码、专业组/专业、选科要求、招生计划数、最低分、最低位次、平均分等。采集的时候要注意合规问题。我坚持只使用官方发布的公开数据爬虫请求控制在合理频率内不影响对方服务器正常服务。为了演示和学习也可以用模拟数据替代根据自己的经验构造一批带噪声的院校录取记录格式和真实数据保持一致但这部分要和真实数据分目录存放防止混淆。数据入库前还需要校验比如最低位次不能为0录取最低分不能高于平均分等这些基础规则能挡住大部分脏数据。2.2 清洗、归一化与特征构造拿到的原始数据通常乱到让人头大缺失值、重复记录、字段格式不一致是家常便饭。我常用pandas先做一轮清洗import pandas as pd df pd.read_csv(admission_data.csv) df df.drop_duplicates(subset[year, province, university_code, major_code]) df df.dropna(subset[min_score, min_rank]) df df[df[min_rank] 0] df[rank_percentile] df[min_rank] / total_candidates位次是最重要的参照量但我们不能直接拿原始位次跨省份、跨年份比较所以需要归一化处理比如把位次除以当年该省考生总数得到“位次百分位”数值越小代表排名越靠前。再进一步构造三个实用特征位次波动系数近三年最低位次的标准差除以均值衡量某校某专业的录取稳定性。招生计划变化率当年计划数相比前一年的增降比例计划缩减通常意味着竞争加剧。热度指数用招生计划变化率和录取位次变化率复合计算用来识别前一年“录爆”或“遇冷”的院校。这些特征最终都会存入特征宽表供推荐算法直接读取。2.3 Django数据模型设计实战数据模型是Django后端的地基。我划分了四个核心模型院校University、专业Major、历年录取记录AdmissionRecord、推荐结果RecommendationResult。下面是一个简化版from django.db import models class University(models.Model): code models.CharField(max_length10, uniqueTrue) name models.CharField(max_length100) province models.CharField(max_length50) level models.CharField(max_length20) # 985/211/双一流/普通 class Major(models.Model): code models.CharField(max_length10) name models.CharField(max_length100) category models.CharField(max_length50) class AdmissionRecord(models.Model): university models.ForeignKey(University, on_deletemodels.PROTECT) major models.ForeignKey(Major, on_deletemodels.PROTECT) year models.IntegerField() province models.CharField(max_length50) plan_count models.IntegerField() min_score models.IntegerField() min_rank models.IntegerField() avg_score models.IntegerField() class RecommendationResult(models.Model): student models.ForeignKey(users.StudentProfile, on_deletemodels.CASCADE) university models.ForeignKey(University, on_deletemodels.CASCADE) major models.ForeignKey(Major, on_deletemodels.CASCADE) group_type models.CharField(max_length10) # 冲/稳/保 probability models.FloatField() reason models.TextField() created_at models.DateTimeField(auto_now_addTrue)外键级联策略要格外小心。AdmissionRecord是历史数据属于“一旦丢失很难找回”的资料所以用了PROTECT而不是CASCADE——如果误删关联院校数据库会拒绝执行并提示存在引用记录。RecommendationResult属于过程数据学生画像删除后同步删除推荐结果用CASCADE更合理。模型设计好之后按常规执行makemigrations和migrate就能建表。3. 推荐引擎的算法选型与融合策略3.1 基于位次与计划数构建“冲稳保”梯度这就是整套推荐系统的地基算法目标是把海量院校按照考生位次划分成“冲、稳、保”三个档位。通常录取概率大于0.8视为“稳”0.4到0.8之间视为“冲”小于0.4则建议作为“保底”。概率计算不能只用一年数据要汇总近三年的历史录取位次。import numpy as np def grade_probability(user_rank, history_ranks): mu np.mean(history_ranks) sigma np.std(history_ranks) if sigma 0: sigma max(100, mu * 0.01) z (user_rank - mu) / sigma prob 1 / (1 np.exp(1.2 * z)) return prob这里需要说明一个容易混淆的点位次的数值越小代表全省排名越高。所以如果用户位次小于历史平均位次z是负数概率会偏高反之z越大概率越低。随后用计划数变化率对概率做修正如果当年招生计划比往年平均增加就把概率稍微上调公式可以写成adjusted_prob prob * (1 0.3 * plan_change_rate)。之所以用logistic函数而不是简单的线性比例是因为录取概率不是线性的位次越接近录取线微小位次变化对结果影响越大logistic曲线的“S”形特征更贴近现实。3.2 用K-Means给考生分层后做协同过滤单个位次模型可以解决“能不能上”的问题但没法解决“该选什么专业方向”的问题。于是我加了一路基于考生画像的聚类协同过滤。先把考生特征整理成向量位次百分位、选科组合编码、城市偏好编码、兴趣方向编码等。使用scikit-learn做标准化和K-Means聚类from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(features) model KMeans(n_clusters5, random_state42) features[cluster] model.fit_predict(X_scaled)聚类数用肘部法则确定分别计算K3到K12的轮廓系数或畸变值选拐点。同一个簇里面的考生在分数水平和偏好上更相似因此可以用“和你在同一簇、且近两年成功录取的考生”作为相似用户再对相似用户的录取院校做加权统计生成推荐候选。这一步可以有效补充位次模型没有覆盖到的专业兴趣和城市偏好。3.3 基于Apriori的院校-专业关联规则挖掘数据挖掘里很经典的一个方法就是关联规则挖潜。应用到高考场景里我会把“同一位考生被某校某专业录取”看作一个事务事务里面的项是“院校代码-专业代码”组合。用Apriori算法找频繁项集找出哪些院校专业经常被同一类考生共同选择。使用mlxtend库可以快速跑通from mlxtend.frequent_patterns import apriori, association_rules frequent_itemsets apriori(onehot_df, min_support0.05, use_colnamesTrue) rules association_rules(frequent_itemsets, metricconfidence, min_threshold0.6)关联规则能产出一些让人意外的结论比如“高分段、偏好东部城市的考生往往同时在关注计算机类和电子信息类院校专业”。这类规则不适合直接作为决策依据但非常适合做“补充召回”当考生明确想学计算机时系统会把关联规则中经常同时出现的电子信息类院校专业也纳入提前批候选扩大推荐视野。3.4 多路召回与加权融合排序上面三条路线各有偏重位次模型偏“稳”聚类协同过滤偏“兴趣”关联规则偏“探索”。单独使用任何一路都会太偏科所以最终采用多路召回加加权融合。final_score 0.5 * admission_prob 0.3 * user_similarity 0.2 * rule_supportadmission_prob来自位次模型user_similarity来自聚类协同过滤的相似度得分rule_support来自关联规则提升度或支持度。权重可以考虑用户风险偏好进行调整保守型用户提高admission_prob的权重冒险型用户提高user_similarity权重。计算完成后按final_score排序再按“冲、稳、保”三档分组每组返回前十名。这里的权重初值是我根据离线测试结果调的实际部署后还应该做小范围A/B测试再迭代。4. Django工程化实现ORM、WebSocket与后台管理4.1 项目结构与核心App划分从零开始创建项目时我采用了按业务域拆分的结构django-admin startproject gaokao_recommend python manage.py startapp users python manage.py startapp schools python manage.py startapp recommendation python manage.py startapp dashboardusers负责考生画像和登录注册schools维护院校、专业、录取记录数据recommendation承载推荐算法和推荐结果接口dashboard处理页面展示和数据可视化。App拆得清晰后期加功能就不会牵一发动全身。4.2 批量查询与对象删除的优化细节Django ORM用好了很顺手但还是有几个地方特别容易踩坑。第一是N1查询。推荐结果列表页如果循环读取每条记录关联的院校和专业每次循环都会发SQL页面会很慢。解决方法是使用select_relatedrecords RecommendationResult.objects.filter(studentstudent) \ .select_related(university, major)第二是对象删除。我们经常会清理过期的推荐结果比如数据重新计算后旧记录已经失去时效性直接执行QuerySet批量删除是最有效率的RecommendationResult.objects.filter(created_at__ltexpire_time).delete()但删除院校时要特别小心AdmissionRecord上设置了PROTECT外键所以误删院校会直接抛ProtectedError这正是我们要的结果。再有一个容易被忽略的性能点生成推荐结果时如果一条条save()几千条数据要等得人心态崩掉。正确做法是构造对象列表后用bulk_create批量写入objs [ RecommendationResult(...) for each_candidate in final_result ] RecommendationResult.objects.bulk_create(objs, batch_size500)实测效率至少提升一个数量级。4.3 用Django Channels实现推荐过程实时推送推荐计算不是一蹴而就的它要先跑位次模型、聚类模型、关联规则再合并排序整个过程可能持续几秒甚至几十秒。如果用户在页面上干等着体验会很差。我选择用Django Channels建立WebSocket通道把后台计算进度实时推送到前端。这也是我在开发这个项目时觉得最“活”的一部分。首先在settings中配置ChannelsINSTALLED_APPS [ daphne, channels, ... ] ASGI_APPLICATION gaokao_recommend.asgi.application CHANNEL_LAYERS { default: { BACKEND: channels_redis.core.RedisChannelLayer, CONFIG: {hosts: [(127.0.0.1, 6379)]}, } }然后写一个异步Consumerimport json from channels.generic.websocket import AsyncWebsocketConsumer class RecommendationConsumer(AsyncWebsocketConsumer): async def connect(self): self.task_id self.scope[url_route][kwargs][task_id] self.group_name ftask_{self.task_id} await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() async def receive(self, text_data): pass async def task_progress(self, event): await self.send(text_datajson.dumps(event[data]))前端建立连接后后台在算法流程的每个阶段执行完就通过channel_layer.group_send往该任务组推送进度。用户能看到“正在计算位次梯度”“正在加载考生画像”“推荐完成”这样实时的动态体验感完全不一样。相比前端定时轮询WebSocket只在有事件时推送也更省服务器资源。4.4 用Django Unfold提升后台管理体验后台管理是运营人员日常处理院校数据的地方Django自带的admin功能齐全但界面确实朴素。我尝试用Django Unfold做了美化直接把它放到admin前面即可INSTALLED_APPS [ unfold, django.contrib.admin, ... ]Unfold提供侧边栏折叠、暗色主题、卡片式列表等能力。配合list_filter和search_fields字段管理员可以快速按照年份、省分、院校层级筛选录取记录极大减少了我在演示系统时来回切换后台页面的次数。5. 性能优化、测试与部署的实操记录5.1 耗时计算任务交给Celery异步队列推荐算法不能放在视图函数里同步执行否则浏览器请求会被卡住几十秒。我把推荐计算封装成Celery任务发布到队列后立刻返回task_id进度通过Channels推送给前端。Celery配置如下from celery import Celery app Celery(gaokao_recommend) app.config_from_object(django.conf:settings, namespaceCELERY) app.autodiscover_tasks()推荐计算任务可以定义成这样app.task def run_recommendation(student_id, params): result recommendation_pipeline(student_id, params) progress_update.delay(student_id, 100, 推荐完成, result_idresult.id)针对于耗时较长的“全量更新模型参数”场景我会配合django-celery-beat做成定时任务比如在每天早上低峰期重新计算热门院校的特征数据避免推荐接口现场做重计算。5.2 针对重复查询的Redis缓存策略高考志愿填报有一个很高频的场景同一个省份、相近位次、相似选科组合的考生查询结果几乎是相同的。如果每次都重新跑一整套算法流程服务器压力很大。所以我在Django缓存里按参数组合生成key命中缓存直接返回from django.core.cache import cache def get_recommendations(params): key frecommend:{params.province}:{params.rank}:{params.subject}:{params.city_pref} result cache.get(key) if result: return result result recommendation_pipeline(params) cache.set(key, result, timeout60 * 60 * 24) return result缓存时间设置24小时是因为录取数据一天内几乎不会变化。当管理员手动修改了录取记录以后我会在后台保存动作里清理对应的缓存key防止用户看到旧结果。实际运营中也遇到过缓存穿透的问题大量用户用极端参数查询每次都绕开缓存打到了数据库和算法层。解决办法是给命中不到结果的情况也做短暂空缓存比如缓存5分钟。5.3 接口测试与推荐结果的验证方法推荐算法没有标准答案但我可以用过去两年的真实录取数据做离线验证把去年录取结果当作“标准答案”跑今年的模拟考生数据看系统推荐的“冲稳保”名单里是否包含这名考生最后实际被录取的学校。这个指标叫召回率能直观反映推荐覆盖度。除此之外接口层面的自动化测试也不能少。比如测试冲稳保分档的边界条件import pytest from django.test import Client def test_recommendation_api(): client Client() resp client.get( /api/recommend/, {rank: 10000, province: sample, subject: physics}, ) assert resp.status_code 200 data resp.json() assert stable in data assert len(data[stable]) 10这类测试写多了以后即使后续调整算法也不会担心改坏接口。推荐结果本身也要做数据完整性校验每次生成推荐记录时probability必须在0到1之间未完成计算的记录不允许出现在接口返回中。5.4 Nginx反向代理与ASGI部署Channels应用必须用ASGI服务器跑。我选择Daphne启动Django再用Nginx做反向代理。核心Nginx配置如下upstream daphne { server 127.0.0.1:8001; } upstream django_http { server 127.0.0.1:8000; } server { listen 80; server_name your_domain; location /static/ { alias /var/www/gaokao_recommend/static/; } location /ws/ { proxy_pass http://daphne; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; } location / { proxy_pass http://django_http; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }部署时几个安全问题必须注意关闭DEBUG、配置严格SECRET_KEY、设置ALLOWED_HOSTS数据库也不要暴露公网端口。Celery Worker和Daphne进程都要用systemd或supervisor守护避免意外退出后服务不可用。6. 从项目里长出来的经验踩坑与建议6.1 数据量不大时别盲目上复杂模型刚开发这个系统时我一度想把K-Means、Apriori、协同过滤全部堆上去觉得这才显得“数据挖掘”。结果数据集只有几千条记录时关联规则支持度极低聚类结果也非常不稳定。后来我把重心收回到位次加概率的统计模型上用户反馈准确率反而明显上升。数据挖掘不是模型越复杂越好模型复杂度必须匹配数据量。当记录量达到几万条以上再逐步引入协同过滤和关联规则才是有意义的迭代路径。6.2 推荐结果的可解释性比精度更重要考生和家长不会盲信一个从黑箱里蹦出来的分数。系统上线初期推荐结果只显示“该校录取概率62%”很多用户留言问“凭什么”。后来我在每个推荐卡片下加上“依据”近三年位次波动范围、招生计划变化、学生兴趣匹配度等信息用户的信任度一下子高了。工程实现上并不复杂只要在生成推荐结果时把每个候选者的特征和档位判断过程写入reason字段。可解释性终究是这类决策辅助系统的基石。6.3 新手如何顺着这个项目练Django如果你正准备学Django这个项目很适合作为完整练手路线第一步先把admin后台配好手动录入几所院校和录取记录练习Django的ORM增删改查。第二步写一个最简单的接口按位次从数据库里筛出院校列表了解视图和路由的配合。第三步把算法逻辑拆成独立的pipeline函数推荐结果写入数据库练习模型字段设计。第四步引入Celery和Redis把耗时任务移到异步队列感受一下系统性能的质变。第五步再加WebSocket实时推送让页面“活”起来。这样一步一步推进每一步都能看到实际产出不会在某个复杂的算法环节卡死。这个系统也可以作为毕业设计选题因为数据采集、数据清洗、特征工程、推荐算法、Web后端、异步推送、部署维护这些环节都被覆盖到了而且每个环节都有清晰的业务目标不是为技术而技术。最后再分享一个小技巧在开发这类推荐系统时一定要把“临时调试用代码”和“正式算法逻辑”严格分开。我一开始把很多调试用的print和matplotlib绘图混在算法脚本里结果迁移成Celery任务时踩了不少坑。后来专门建了一个debug_scripts目录只负责调试和可视化正式推荐流程保持干净可测试。养成这个习惯后续维护项目的体验会好很多。
网站建设高端定制企业官网