基于Django和K-means的校园美食推荐系统构建与优化

发布时间:2026/10/20 10:07:30

基于Django和K-means的校园美食推荐系统构建与优化 简介一份基于Django与K-means算法的校园美食推荐系统完整项目资料面向需要完成毕业设计或课程设计的计算机专业学生解决推荐系统开发、论文撰写与部署演示等痛点。资源共220个文件约14.52MB以Python后端34个py、Vue前端38个vue、JavaScript脚本32个js及大量图片素材为主并附带Word格式万字论文、PPT、bat一键部署脚本和录制讲解视频便于快速运行和理解。系统围绕用户管理、美食展示、K-means推荐算法等模块展开内容涵盖绪论、相关理论、系统架构与数据库设计、算法实现与优化、系统测试等章节提供从环境配置到部署验证的完整链路。目前已有62人学习下载适合作为算法应用类毕业设计的参考蓝本或实训教学案例。1. 校园美食推荐为什么要用 K-means它不猜你喜欢什么而是先找到和你口味相近的那群人拿到“django基于k-means算法的校园美食推荐系统”这个标题时我第一个反应不是去看算法而是想这个场景为什么要用聚类而不是协同过滤或者深度学习。校园美食推荐有个非常典型的特点用户群体高度同质作息时间、消费区间、口味偏好都有明显的聚集性——早八党、夜宵党、重辣党、清淡党本质上就是几簇人。K-means不关心某一个用户下一秒想吃什么它只负责把口味相近的人分到一组然后在这一组里找公认好吃的菜推荐出去。这套逻辑在校园场景里比“猜你喜欢”更稳。项目本身是一个 Django 后端工程用 K-means 对用户行为做聚类再基于簇内偏好生成推荐列表适合正在做毕业设计、课设或者想低成本验证“推荐系统 聚类”思路的开发者参考。后面我把这套系统的数据建模、聚类代码、推荐逻辑和踩坑点完整拆开讲给的是可以直接照跑的方案。2. 先搭数据地基Django 模型、三张核心表与行为矩阵的生成2.1 用户-菜品-评分建模为什么不用单一评分表先想清楚一个问题校园美食推荐系统的输入是什么K-means 聚类不是凭空聚的它需要每个用户有一个可以被量化的行为特征向量。这个向量从哪来——从用户对菜品的评分、下单次数、口味标签里来。很多同学拿到这个标题直接建一张表存评分跑 K-means 时发现特征不够用这是第一个翻车点。我一般会建三张核心表用户表、菜品表、评分/行为表。用户表不存推荐结果只存基础信息菜品表存菜名、档口、分类、标签评分表是关键它不能只存一个分数最好把“评分值、下单次数、最近一次消费时间”都记下来。原因是 K-means 的特征向量里评分均值代表口味偏好强度下单次数代表消费频次这两个维度在聚类时区分度非常高。# models.py 核心表设计 from django.db import models class User(models.Model): student_id models.CharField(max_length20, uniqueTrue) campus models.CharField(max_length50, default主校区) created_at models.DateTimeField(auto_now_addTrue) class Dish(models.Model): name models.CharField(max_length100) stall models.CharField(max_length100) # 档口 category models.CharField(max_length50) # 川菜/粤菜/小吃 tags models.JSONField(defaultlist, blankTrue) # [辣, 下饭] avg_rating models.FloatField(default0.0) class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) dish models.ForeignKey(Dish, on_deletemodels.CASCADE) score models.FloatField() # 用户评分1-5 order_count models.IntegerField(default1) # 下单次数 last_order_at models.DateTimeField(nullTrue) class Meta: unique_together (user, dish)注意评分表里我加了unique_together同一个用户对同一个菜品只能有一条行为记录每次下单就更新order_count和last_order_at而不是插入新行。这样建特征矩阵时不会出现重复行导致聚合结果翻车。score存原始评分后续会做归一化这里不需要提前处理。实际项目里很多开发者把“评分”表做成“订单”表通过订单明细反推用户偏好也能跑通但聚合 SQL 会复杂很多。我建议还是保留独立的评分表因为聚类前要做的是“用户 × 菜品”的透视表评分表直接pivot就能出来省掉一层关联查询。2.2 菜品标签与档口信息聚类结果的“可解释性”全靠它们K-means 聚类完成后每个簇会得到一个中心点但光看中心点没法告诉用户“你们这组人喜欢吃啥”。要让推荐结果能落地必须把菜品标签和档口信息挂进去。比如聚出来的第 2 簇中心点显示“麻婆豆腐、水煮鱼”权重很高那这一簇的特征就可以打上“重辣偏好”推荐时优先推这个簇内高评分菜品的同标签菜。菜品表的tags字段别用简单逗号分隔存字符串后续查标签权重时会写得很难受。我用JSONField存[辣, 下饭, 食堂二楼]这种结构聚类后做解释时直接遍历即可。这里有个小习惯标签尽量控制在 3 到 5 个之间太少了区分度不够太多了后面做标签权重统计时数据稀疏。# 给菜品写标签的辅助函数 def get_dish_feature_vector(dish, ratings_by_user): # 返回这道菜在一批用户行为下的特征评分均值、被点次数、标签命中数 scores [r.score for r in ratings_by_user[dish.id]] return { avg_score: sum(scores) / len(scores) if scores else 0, order_count: sum(r.order_count for r in ratings_by_user[dish.id]), tag_hits: len([t for t in dish.tags if t in [辣, 清淡, 夜宵]]) }这是我自己写数据脚本时常用的辅助函数它不直接进训练而是用来生成“菜品维度的特征缓存”推荐阶段需要解释“为什么推这个菜”时直接读缓存。2.3 数据准备脚本从数据库到 K-means 能吃的矩阵Django 的 ORM 很适合直接操作数据库但把数据变成 K-means 能吃的numpy矩阵需要写一个独立的准备脚本。核心是三步取用户列表 → 取每个用户的菜品评分列表 → 按“用户 × 菜品”维度填入矩阵。不能直接在 Django 视图里写这段耗时太长我会用management command跑离线任务。# management/commands/prepare_matrix.py import pandas as pd from django.core.management.base import BaseCommand from apps.recommend.models import Rating class Command(BaseCommand): def handle(self, *args, **options): rows Rating.objects.values(user_id, dish_id, score, order_count) df pd.DataFrame.from_records(rows) # 用户 × 菜品透视值为评分和下单次数的加权组合 pivot_score df.pivot_table(indexuser_id, columnsdish_id, valuesscore, aggfuncmean).fillna(0) pivot_count df.pivot_table(indexuser_id, columnsdish_id, valuesorder_count, aggfuncsum).fillna(0) # 加权评分是偏好次数是频率两者都重要 matrix pivot_score.values * 0.7 pivot_count.values * 0.3 pd.DataFrame(matrix).to_csv(data/rating_matrix.csv, indexFalse) self.stdout.write(fmatrix saved, shape{matrix.shape})参数说明评分权重0.7、次数权重0.3不是写死的要看你业务里评分稀疏还是订单稀疏。如果校园用户很少打分基本靠点单那要调成0.3/0.7如果评分数据充足可以0.8/0.2。这个矩阵就是后面 K-means 唯一的输入。3. K-means 聚类落地K值怎么定、特征为什么必须标准化、模型怎么存3.1 标准化不看这一步 K-means 会全部被“点餐次数”带偏K-means 算的是欧氏距离特征量纲不同会让距离被大数值维度主导。比如评分是 1 到 5点餐次数可能是 0 到 50直接算距离的话点餐次数的影响会是评分的十倍以上。聚类结果就是“一周点十次的人”被聚在一起而不是“口味相似的人”聚在一起。这个坑几乎所有第一次用 K-means 做推荐的人都会踩。标准做法是用StandardScaler对矩阵做标准化。注意不能用MinMaxScaler因为 K-means 的均值中心在极端值下会被拖偏StandardScaler把数据变成均值为 0、方差为 1 的分布聚类中心更稳定。# cluster_users.py 标准化与K-means训练 from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans import pandas as pd df pd.read_csv(data/rating_matrix.csv) # 只对有行为的列标准化全0列留着会让距离计算没意义 active_cols df.columns[df.sum(axis0) 0] scaler StandardScaler() X_scaled scaler.fit_transform(df[active_cols]) # 先固定K值跑一次后面会讲怎么选K kmeans KMeans(n_clusters5, random_state42, n_init10) kmeans.fit(X_scaled) # 把簇标签写回用户表便于推荐时查询 df[cluster] kmeans.labels_ df.to_csv(data/user_clusters.csv)逻辑说明先过滤全 0 列是因为很多菜品只有极少数人吃过保留全 0 列会让这些菜在距离计算中变成“无信号噪声”。n_init10是让 K-means 从 10 个不同的初始中心开始跑选最优结果避免一次随机初始化陷入局部最优。random_state42固定随机种子保证每次结果可复现。调参重点n_init在 sklearn 1.4 之后默认变成 10老版本默认是 1直接用老版本跑出来的聚类会很不稳定每次结果都变这是很多同学说“聚类结果玄学”的主要原因。3.2 选 K 值肘部法则加轮廓系数别只信一个K-means 要先指定 K 值。校园美食场景 K 值通常在 3 到 8 之间但具体取几需要验证。只看“损失函数越小越好”会得到“K 等于用户数”的荒谬结果因为每个用户自己一簇当然损失最小。我一般同时看两个指标inertia簇内平方和的肘部拐点和silhouette_score轮廓系数的最大值。from sklearn.metrics import silhouette_score K_range range(2, 11) inertia_list [] silhouette_list [] for k in K_range: km KMeans(n_clustersk, random_state42, n_init10) km.fit(X_scaled) inertia_list.append(km.inertia_) silhouette_list.append(silhouette_score(X_scaled, km.labels_)) # 快速找肘部计算相邻K的inertia下降率 drops [inertia_list[i] - inertia_list[i1] for i in range(len(inertia_list)-1)] max_drop_idx drops.index(max(drops)) 1 print(建议K值(肘部):, K_range[max_drop_idx]) print(轮廓系数最高K:, K_range[silhouette_list.index(max(silhouette_list))])说明两个指标经常不一致肘部看的是“再增加簇数收益锐减”的位置轮廓系数看的是“簇内紧密、簇间分离”的程度。我的实际处理是先取肘部位置的 K再看该 K 的轮廓系数是否大于 0.25。小于 0.25 说明数据本身就没有明显的簇结构这时我会回到特征工程找问题而不是硬调 K。3.3 聚类不是一次性的训练产物落盘与后续复用聚类结果必须在训练后落盘推荐接口不能每次都重新跑 K-means一是耗时二是每次结果不一致会导致用户今天看到的推荐明天就变。我习惯把三个产物存下来标准化器、K-means 模型、用户簇标签表。前两个用joblib存簇标签直接写进 Django 模型字段。# 存模型 标准化器 import joblib joblib.dump(kmeans, models/kmeans.pkl) joblib.dump(scaler, models/scaler.pkl) # cluster字段需要提前加到User模型 # 这里用ORM批量写入 from apps.recommend.models import User cluster_map df[cluster].to_dict() # user_id - label for uid, label in cluster_map.items(): User.objects.filter(pkuid).update(clusterlabel)注意cluster字段我建议直接加在 User 表上虽然有点“非规范化”但推荐接口读起来就是一条主键查询比关联一张簇表快一个量级。如果你要保留每次聚类的历史结果做分析再单独建一张UserClusterHistory表存快照当前活跃簇放 User 表。4. 把聚类结果变成推荐列表推荐逻辑、Django 视图与冷启动兜底4.1 推荐核心逻辑先簇内 TopN再簇间交叉聚类只是中间产物用户打开页面时看到的是推荐列表。我的推荐策略分两层第一层是“簇内推荐”取该用户所在簇内评分最高的 N 个菜第二层是“簇间交叉推荐”从相邻簇里挑几个口碑菜做补充。为什么加第二层因为纯簇内推荐会让用户永远只看到自己熟悉的口味时间长了系统没有惊喜度转化反而下降。# services/recommend.py 推荐核心逻辑 from django.db.models import Avg from apps.recommend.models import Dish, Rating, User def recommend_for_user(user, top_n10): cluster user.cluster # 簇内用户的评分聚合 user_ids User.objects.filter(clustercluster).values_list(pk, flatTrue) qs (Rating.objects.filter(user_id__inuser_ids) .values(dish_id) .annotate(avg_scoreAvg(score)) .order_by(-avg_score)[:top_n]) base_dish_ids [item[dish_id] for item in qs] # 簇间交叉从相邻簇补充 neighbor_clusters get_neighbor_clusters(cluster, k2) supplement_ids get_top_dishes_by_clusters(neighbor_clusters, top_n3) final_ids base_dish_ids [i for i in supplement_ids if i not in base_dish_ids] return list(Dish.objects.filter(pk__infinal_ids))get_neighbor_clusters的实现很简单把每个簇的聚类中心算出来再算当前簇中心和其他簇中心的欧氏距离取最近的 2 个簇作为交叉来源。这样交叉推荐不是随机的而是从“口味相邻”的簇里找新鲜感。4.2 Django 视图与序列化接口返回给前端什么结构推荐接口返回的数据结构需要包含菜品基础信息和推荐理由。理由字段很重要哪怕只是“和你口味相似的人也喜欢”也能显著提升用户点击率。前端拿这套结构直接渲染卡片不需要再查表。# views.py 推荐接口 from rest_framework.decorators import api_view from rest_framework.response import Response api_view([GET]) def recommend(request, user_id): user User.objects.get(pkuser_id) dishes recommend_for_user(user, top_n10) data [{ id: d.id, name: d.name, stall: d.stall, category: d.category, avg_rating: d.avg_rating, reason: 和你口味相似的同学推荐 } for d in dishes] return Response({code: 0, data: data})接口设计上我故意不做认证只传user_id这样开发和测试阶段方便直接curl验证。生产环境再接入JWT或 Django session。返回结构里的reason字段可以后期扩展成真正的解释文本比如“第 2 簇的同学 314 人其中 68% 喜欢重辣口味”这就要在菜品标签和簇特征关联表里做文章了。4.3 冷启动兜底新用户没有行为数据怎么办K-means 的硬伤是对新用户完全失效——没有历史行为没法判断属于哪个簇。我的做法是给新用户设置一个“默认簇”该簇由全部用户的全局聚类中心代表推荐全局热门菜。同时做一道关卡用户前三次下单之后立即用增量数据把用户划入最近的已有簇中心而不是重新训练模型。def assign_new_user(user, recent_ratings): # 把新用户行为转成和训练时一致的特征向量 features build_user_feature(user, recent_ratings) scaler joblib.load(models/scaler.pkl) kmeans joblib.load(models/kmeans.pkl) X scaler.transform(features.reshape(1, -1)) cluster kmeans.predict(X)[0] User.objects.filter(pkuser.pk).update(clustercluster) return cluster这个方案能保证新用户不需要等批量聚类也能在几次交互后进入正轨。5. 避坑手册K-means 做美食推荐的 5 个踩坑现场5.1 现象聚类结果全是按“点餐次数”分的口味完全没体现原因特征矩阵里评分和下单次数量纲差异过大K-means 的欧氏距离被次数维度主导。我见过某同学的项目里聚类出来的“第 1 簇”全是每周点十次外卖的人“第 2 簇”全是点两次的人和口味毫无关系。解决必须标准化。StandardScaler标准化的不只是数据分布的收敛更重要的是让两个维度在距离计算中地位平等。还可以把下单次数做log1p变换再标准化防止极端值把中心点拉飞。5.2 现象每次跑出来的簇都不一样上周叫“重辣组”这周变成“主食组”原因K-means 依赖随机初始化n_init参数过小或没有固定random_state。校园美食数据量不算大初始中心选的不同收敛结果天差地别。解决固定random_state42n_init10。如果加了这两个参数结果还不稳定说明 K 值本身不合适或者特征区分度太低。这时候不要调参回去加特征。5.3 现象新菜品永远进不了推荐列表聚类模型压根没见过它原因K-means 模型训练时菜品维度已经固定新菜的评分列在原始矩阵里不存在模型在推理时无法给它任何权重。这算是模型的“黑匣子”效应带来的结构性问题不是调参能解的。解决在训练周期我推荐每周一次里重新生成特征矩阵并重训模型。同时单独维护一张“新品榜”聚类推荐里空出 10%-20% 的位置给新品避免模型永远推荐老菜。5.4 现象推荐接口响应时间突增到 2 秒数据库被 ORDER BY 拖垮原因直接在视图里对簇内所有用户关联聚合数据量大了以后ORDER BY avg_score DESC没有索引可用。校园数据几千用户还能顶住上万就开始明显变慢。解决离线把每个簇的菜品 TopN 缓存起来推荐接口直接读缓存。用 Django 的cache框架簇-菜品列表的缓存键设为cluster_top_{cluster_id}过期时间设为 6 小时定时任务负责刷新。缓存没命中时才执行重聚合查询。from django.core.cache import cache def get_cluster_top(cluster_id, top_n10): cache_key fcluster_top_{cluster_id} result cache.get(cache_key) if result is not None: return result # 执行聚合查询并写入缓存有效期6小时 dish_ids aggregate_cluster_top(cluster_id, top_n) cache.set(cache_key, dish_ids, timeout6 * 60 * 60) return dish_ids5.5 现象离线评估准确率很高用户反馈却没变化甚至不点原因K-means 推荐的准确率衡量的是“簇内用户对推荐菜品的真实评分”但用户打开页面时看到的卡片、价格、档口距离都影响点击。准确率再高推一个 1 公里外的档口菜用户也不会去。解决在推荐列表排序阶段叠加“档口距离”和“营业状态”权重这是工程侧问题不是算法侧问题。具体做法先用 K-means 粗筛出候选集约 30 个菜再用加权打分公式排序比如final_score avg_rating * 0.5 nearby_score * 0.3 match_cluster * 0.2nearby_score根据用户当前所在宿舍楼到档口的距离计算match_cluster表示候选菜是否来自用户所在簇是则给 1否则给 0.5。这一步调完点击率通常会有肉眼可见的提升。6. 进阶技巧把 K-means 做成每周自动重训的 Django 命令项目跑稳之后另一件值得做的事是让聚类自动更新。校园美食的消费偏好不是一成不变的毕业季旧用户离开开学季新用户涌入夏天和冬天的口味也有变化。做成定时命令的好处是不用人肉触发推荐系统每周自己“复习”一次。# management/commands/retrain_cluster.py from django.core.management.base import BaseCommand from apps.recommend.services import run_cluster_pipeline class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument(--k, typeint, default5, help聚类数量) parser.add_argument(--min-ratings, typeint, default10, help行为少于该值的用户不参与聚类) def handle(self, *args, **options): run_cluster_pipeline( koptions[k], min_ratingsoptions[min_ratings], save_modelTrue, update_user_clusterTrue ) self.stdout.write(cluster retrained and cached refreshed)配合系统定时任务或 CI 定时器每周执行一次。执行后记得清掉 4.1 节里的cluster_top_*缓存否则旧缓存会继续给用户推上一个周期的菜。我踩过一次改完模型忘了清缓存线上显示的推荐列表还是两周前的别提多尴尬。最后一个习惯是每次重训后把模型效果快照存一份和上一轮的聚类结果做对比。比如把每个簇的用户数、平均评分、覆盖菜品数记到一张历史表里跑三个月后你能直接看出哪段时间推荐质量在下降是不是该加特征、改 K 值了。这个习惯帮我避免了很多次“为什么效果变差”的盲目排查也推荐给你。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/20 10:02:27

丙午年大年初七晨听歌:从人日习俗到一份专属歌单

丙午年大年初七的清晨,我醒来时天刚蒙蒙亮。窗外没有车流声,也没有邻居的响动,整座城市像是还没上线。我泡了一杯热茶,戴上耳机,在手机里滑到一份提前一晚排好的歌单。标题叫"丙午年大年初七晨听歌"&#xf…

2026/10/20 10:02:27

TIA博途V19用户角色与安全设置:构建产线权限管理

简介:TIA博途V19的访问控制与用户角色设置,是很多S7-1500项目工程师容易卡壳的环节。资源聚焦V19中S7-1500 V3.1 CPU的安全设置,系统讲解从禁用访问控制、启用访问控制,到用户与角色分配、传统访问等级密码登录的完整流程&#xf…

2026/10/20 14:19:16

多智能体协作翻车实录:32倍耗时13倍Token换来准确率下降

1. 从一次翻车的多智能体协作实验说起去年下半年,我接手了一个内部知识库问答系统的重构任务。需求本身不算复杂:把散落在多个文档仓库里的技术资料整合起来,做一个能回答"某个接口怎么调用""某个配置项默认值是多少"这类…

2026/10/20 14:19:16

多播与广播编程实战:从原理到代码排障

我最早接触多播和广播,是在调一个局域网内多台设备的同步程序时被逼出来的。当时用单播一条条发指令,设备一多,数据包就像早高峰的地铁,挤在一条链路上,延迟和丢包全都来了。后来换成广播,一台发、全网收&a…

2026/10/20 14:19:16

AI芯片选型指南:从TOPS参数到功耗生态的实用过滤方法

年初有位朋友做边缘计算设备选型,一开始列了个 AI 芯片候选清单,拢共 57 款,觉得已经覆盖得很全面了。结果调研越做越深,发现同一个厂家在不同产品线、不同代际、甚至不同封装下都会分出好几款,最后这份清单硬生生整理…

2026/10/20 14:19:16

iOS网络

-1001 就是 ‌请求超时‌(NSURLErrorTimedOut),App 在规定时间内没等到服务器响应。‌‌ NSURLConnection同步,异步与SSL http://qzc770707.blog.163.com/blog/static/3408275320105249566560/ 通常在iPhone里用NSURLConnection做…

2026/10/20 14:14:15

VXLAN生产落地三大核心:VNI规划、BGP EVPN时序、隧道韧性设计

1. 为什么“VXLAN详解(三)”这个标题本身就在传递关键信号看到“VXLAN详解(三)”,有经验的网络工程师第一反应不是去点开看内容,而是下意识地翻出前两篇——这已经不是单纯的技术文档编号,而是一…

2026/10/19 18:06:35

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/20 1:05:37

单片机C运行时从启动到多任务:中断安全与共享资源冲突

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

GCC编译四步详解:预处理、编译、汇编、链接实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

FIW线是什么?FEP注塑线与普通氟塑料线的区别及应用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑