ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python大数据下K-means聚类校园美食推荐系统从0到1实战

Python大数据下K-means聚类校园美食推荐系统从0到1实战 去年帮学弟做本科毕设题目是“Python大数据背景下基于K-means算法的校园美食推荐系统”。接到题目的时候第一反应是“K-means加推荐往上叠个可视化页面就算齐活”结果真正从数据采集一路做到最终演示前后花了两个多月把特征工程、无监督聚类、前后端联调完整走了一遍。这篇文章把整个系统从0到1的实现思路、关键代码和踩坑记录完整整理出来给正在做毕业设计、课程设计或者想入门推荐系统但不想一上来就啃协同过滤的朋友提供一个能落地、能演示、还能在答辩时讲出深度和细节的完整参考方案。1. 项目全貌与核心设计思路1.1 为什么做校园美食推荐场景决定了方案选型校园里吃饭这件事是真的需要推荐。我在前期调研的时候粗略统计过一个中等规模的校园食堂加周边档口通常有十几个窗口每个窗口每天轮换十几道菜学生每天面对的是上百种选择再加上高峰期排队时间各不相同选择成本其实非常高。和电商、短视频这类场景相比校园美食推荐有一个显著特点选择池有限、更新节奏快、用户没有打分习惯。这也直接决定了不能照搬大厂那套推荐架构。我最初考虑过几种主流方案包括基于物品的协同过滤、基于用户的协同过滤甚至想过用关联规则做“买了A的人还买了B”。但我的项目背景里只有食堂档口的脱敏订单数据和一份口味偏好问卷订单数据只能覆盖几周用户之间重叠购买的菜品也很少。强行做协同过滤矩阵稀疏到没法看推荐效果基本靠猜。反而是K-means聚类这条路对数据要求友好而且结果可解释性极强非常适合“按口味分人群再推荐”这个逻辑。从用户真实需求来看校园美食推荐解决的不是“猜你喜欢”这种伪需求而是“在已知的有限选择里帮你快速决策”。目标用户是每天纠结吃什么的在校生核心痛点是选择焦虑和信息不透明技术手段则是先给用户做画像分组让同一类口味的用户能共享彼此的挖掘结果这比针对单个人做建模要稳定得多。1.2 为什么选K-means而不是协同过滤一次选型对比做推荐系统很多人一听“推荐”两个字就想到协同过滤这其实是个惯性误区。协同过滤确实经典但它在校园食堂这个特定场景里存在先天不足。我花了两个下午调surprise库跑SVD结果交叉验证的RMSE高得离谱后来果断放弃才彻底转向聚类方案。两类方案的对比我整理成了表格做答辩PPT的时候可以直接引用。对比维度协同过滤K-means聚类推荐数据要求需要大量用户-物品评分记录只需要用户特征向量冷启动能力新用户无行为数据基本瘫痪问卷或注册信息即可完成初始推荐实现成本矩阵分解、相似度计算、调参复杂聚类加簇内统计链路简单直观可解释性推荐理由是黑盒很难说明白可以明确告诉用户你属于哪类口味人群数据稀疏容忍度稀疏矩阵下效果急剧下降聚类对特征完整性依赖更强但对评分稀疏不敏感论文与答辩友好度公式多但业务故事弱业务逻辑清晰、可视化丰富、能讲出完整故事选K-means还有一个很实际的原因数据现实。校园里几乎没有成熟的评分数据问卷也只是模糊的口味偏好而非精确打分。与其花大量精力去构造一个虚假的评分矩阵不如直接面向用户特征建模。聚类本质上是在对“人”做分组分完组之后的推荐就变成了组内的统计排序每一步都经得起追问。1.3 整体架构与数据流向轻量但完整的闭环整个系统的架构我刻意选择了轻量方案数据存SQLite后端用Flask前端是单个HTML页面加ECharts算法部分用scikit-learn。这套组合不需要重型分布式框架一台普通笔记本就能全部跑起来但对毕设来说链路完整度和逻辑闭环才是最重要的。数据流向大致是这样用户端通过问卷填写口味偏好同时采集食堂档口的脱敏订单记录所有数据进入Python数据处理层经过pandas清洗和特征工程生成结构化的用户特征表随后送入K-means模型做聚类训练训练完成后Flask推荐接口接收一个用户特征向量预测该用户所属的簇再基于簇内的菜品热度统计生成推荐列表前端页面负责展示推荐结果、用户画像雷达图和各窗口的热度排行。需要提一句的是我虽然在毕设里没有上Spark那套重工具但在架构设计上保留了清晰的替换边界。数据处理层和算法建模层的代码完全解耦数据量如果真的涨到百万级聚类部分可以直接换成Spark MLlib中的KMeans实现接口不动底层随便换。这一点在答辩时是加分项说明你不是只会调库而是思考过扩展路径的。2. 数据收集与预处理推荐系统的“原材料”工程2.1 数据源怎么设计问卷星加脱敏订单的组合拳数据是推荐系统的根我在这个项目里花了接近一半的时间在数据上。很多人在毕设里直接去网上爬数据但校园食堂这个垂直场景公开数据集几乎没有自己造数据又显得不严谨。我的做法是双轨采集一部分用问卷星制作校园饮食偏好问卷在食堂门口和班级群发放收集用户的口味标签、价格接受度、常去窗口等个人信息另一部分向学校后勤申请了食堂档口近四周的脱敏订单数据订单记录不包含姓名学号只有用户编号、菜品编号、购买时间、价格、份数等字段。问卷设计是门学问问得太细没人愿意填问得太粗又没有建模价值。我最终只保留了六个维度辣度接受度、甜度偏好、咸淡偏好、主食偏好、价格区间、忌口情况。每个维度用“清淡”“微辣”“中辣”“重辣”这样的离散选项描述方便直接做成类别特征。字段设计上我最终的数据库一共三张表用户表、菜品表、订单表具体如下。表名字段说明useruser_id, gender, grade, spicy_level, sweet_level, taste_type, price_range用户基础信息与口味偏好dishdish_id, dish_name, window_name, category, price, tags菜品信息tags是口味标签的逗号分隔串orderorder_id, user_id, dish_id, order_time, quantity, amount脱敏后的点餐记录问卷回收了418份其中有效问卷382份订单数据整理后约8600条覆盖6个窗口、53道菜品。这个数据量对小系统来说完全够用了做聚类不追求大追求的是特征维度的合理覆盖。2.2 特征工程用户画像到底怎么“算”出来K-means聚类吃的不是原始问卷而是向量化的特征。特征工程这一步直接决定了聚类的质量我在这个环节里踩过不少坑最典型的是刚开始把口味偏好直接做One-Hot编码导致维度膨胀而且特征之间没有距离感。后来调整为“用户-菜品偏好强度矩阵”的思路每个用户用一组数值型特征描述。具体的特征向量设计我最终敲定七个维度辣度指数、甜度指数、咸鲜指数、价格敏感度、面食偏好度、饭类偏好度、特色窗口偏好度。辣度这些指数来自问卷选项的映射比如“不辣0、微辣1、中辣2、重辣3”这是有序类别用数值映射比One-Hot合理。价格敏感度来自选择的价位区间加上订单平均价的反向归一。面食偏好度和饭类偏好度则直接统计用户订单里两类菜品的占比。这里有一个关键操作特征标准化。因为辣度指数的取值范围是0到3价格敏感度可能是0到1的浮点数而偏好度占比也是0到1如果不做标准化价格和辣度对欧式距离的贡献会失衡。我用的sklearn里面的MinMaxScaler把每一列都压到0到1区间。这个操作虽然简单但很多人会忽略直接拿原始特征跑聚类结果就是所有类别几乎只由“辣度”这一列决定其他特征全部失效。import pandas as pd from sklearn.preprocessing import MinMaxScaler feature_cols [spicy_level, sweet_level, price_sensitivity, noodle_pref, rice_pref, special_window_pref] scaler MinMaxScaler() user_feat_scaled scaler.fit_transform(user_feat[feature_cols]) print(user_feat_scaled.shape) # 输出大概是 (382, 6)2.3 数据清洗的几个关键细节脏数据远比想象中多数据清洗是同样的脏活累活但做得干不干净直接影响后面所有的分析可信度。我在清洗阶段处理了三类典型问题。第一类是“同菜不同名”比如“红烧肉”和“红烧肉套餐”在订单里被当成两个菜但实际是同一道主食。我最后靠菜品名称做了一次模糊匹配加人工核对把53道菜规整成了47道。第二类是缺失值问卷里有一部分人辣度选项没选我用该字段的中位数做了填充而不是删行因为样本量本身就不大删行太可惜。第三类是异常值有个用户的订单记录里出现了单次购买20份米饭的记录明显是数据录入错误或团体代买这类记录直接剔除。清洗完成后还有一个步骤不容忽略聚合统计。把订单表按用户分组统计每个用户对每个窗口的消费次数、平均客单、高峰时段等信息。这一步其实是在构建行为特征和问卷偏好特征互为补充。比如问卷里有人填了“很辣”但订单里全是麻辣烫窗口的微辣记录这种矛盾信息在聚类时会被视为噪声但恰恰是这种噪声让聚类结果更贴近真实规律因为人的自述偏好和实际行为往往有偏差聚类能自动把两者折中。清洗和聚合的代码不算复杂但顺序很重要必须先清洗再聚合否则异常值会被带入统计值里。我当时就因为先聚合后清洗导致有个用户的窗口偏好被代买的20份饭严重拉偏排查了很久才定位到问题根源。3. K-means聚类实战从数学原理到代码落地3.1 K-means原理用“分堆”的逻辑理解无监督学习K-means这个名字看起来很吓人其实逻辑就是“把一堆人按相似度分成K堆”。它做的事情可以拆成四步随机挑K个点作为初始聚类中心计算每个样本到K个中心的距离把样本归到最近的那个中心重新计算每一簇的中心点也就是簇内所有点的均值重复后两步直到中心点不再变化或者变化幅度小于阈值。算法要优化的目标叫簇内误差平方和也就是SSE。每个样本点和它所属聚类中心的距离平方加起来SSE越小说明簇内样本越紧聚类效果越好。但这里有一个天然矛盾K越大簇越紧密SSE自然越小K特别大的时候每个点自己就是一个簇SSE直接变0但聚类也就没有意义了。所以找K的过程本质是在找“业务上可解释、数学上能接受”的那个平衡点。在校园美食场景里K值的选择我一开始是用经验硬拍的直接选了3结果是三类人群的边界非常模糊辣度维度区分出来了但价格维度和窗口偏好完全没有体现。后来改用“肘部法则”加“轮廓系数”双重校验才让结果变得可解释。3.2 核心代码与关键参数K值确定全流程K值选取我用的是一个可视化配合量化的组合方法。肘部法则会画出“K值-SSE”曲线找曲线由陡变缓的那个拐点轮廓系数则计算每个样本与自身簇内以及最近邻簇的相似度取值范围在-1到1之间越接近1说明聚类越合理。两个方法都集成在下面这段代码里。import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score sse [] silhouette_scores [] K_range range(2, 11) for k in K_range: kmeans KMeans(n_clustersk, initk-means, n_init10, random_state42) labels kmeans.fit_predict(user_feat_scaled) sse.append(kmeans.inertia_) sil silhouette_score(user_feat_scaled, labels) silhouette_scores.append(sil) plt.subplot(1, 2, 1) plt.plot(K_range, sse, markero) plt.xlabel(K) plt.ylabel(SSE) plt.subplot(1, 2, 2) plt.plot(K_range, silhouette_scores, markers) plt.xlabel(K) plt.ylabel(Silhouette Score) plt.show()我最终跑出来的结果SSE曲线在K等于5之后下降明显变缓而轮廓系数在K等于5时达到0.53的峰值两个指标指向一致于是我把K最终定为5。这里有一个实操心得K值不是越大越好更不是越小越好必须结合业务可解释性来判断。5类人群对应下来分别是“重辣实惠党”“清淡养生党”“甜口尝鲜党”“面食爱好者”和“均衡大众党”每一类都能讲出清晰的校园人物画像这就是好的聚类结果。聚类模型的保存也很重要训练好了直接拿joblib.dump把模型和标准化器一起保存后面Flask接口直接加载不用每次请求都重新训练。我因为一开始没保存标准化器导致接口里传入的新数据维度对不上白排查了一晚上。3.3 聚类结果分析如何读懂每一簇用户画像聚类跑完只是第一步真正值钱的是解读结果。我写了一段代码输出每个簇的中心向量再配合簇内样本的统计分布来分析。中心向量就是标准化后每个簇的均值这个值表示了该簇人群在每一个特征维度上的平均水平比如第0簇的辣度指数中心是0.87而第2簇的辣度中心只有0.12区别一目了然。从业务角度解读第0簇人群的特点是辣度指数高、价格敏感度低、饭类偏好度高基本都是男生喜欢去麻辣香锅和烤鱼饭窗口这类人群推荐时就要优先推重口味的饭类套餐。第1簇人的特点是咸鲜指数高、甜度低、价格敏感度高经常选大众餐窗口的基本款属于食堂里的“性价比军师”。每一簇我都在代码里生成了画像名称并且存到了数据库里方便后续推荐接口按名调用。我还做了一张雷达图把每个簇的中心向量可视化出来。答辩的时候这张图比任何代码都有说服力因为评委看到图上五个多边形形状各异一眼就能理解聚类的意义。可视化的实现用的是ECharts在Python里把中心向量转成JSON传给前端即可。4. 推荐引擎与系统落地实现4.1 基于聚类的推荐逻辑先看人再看菜聚类部分完成之后推荐引擎的逻辑就变得非常清晰了。整个推荐过程分两步先把用户分到对应的簇再在簇内统计菜品热度生成推荐列表。新用户进来的时候系统会先引导填写一张迷你问卷字段和训练时的特征严格保持一致然后把问卷内容转换成同样的特征向量经过同样的MinMaxScaler标准化送入训练好的KMeans模型用predict方法得到所属簇ID。这个流程走完用户画像就算建成了不依赖任何历史行为记录天然解决了冷启动问题。推荐排序用的不是简单的簇内“谁被点得最多”而是给了一个综合热度评分。这个评分函数我设计成三个因素的加权簇内消费频次占比、菜品全局流行度、价格匹配度。举个例子假设当前用户属于“重辣实惠党”簇那么一份25元的豪华麻辣香锅在簇内很受欢迎但因为单价比该簇人群的平均接受价位高出太多价格匹配度就会拉低它的综合分最终排到第4名而不是第1名而一份15元的重辣鸡腿饭则会拿到最高分排在推荐第一位。推荐列表还会做一个简单的多样性处理防止出现前十名全是同一个窗口菜品的情况。我对窗口做了分组每个窗口最多出现三道菜保证推荐结果在品种上有覆盖。这个细节是我自己加的但效果很好用户主观体验明显提升。4.2 后端接口实现Flask轻量级部署后端我用Flask写了两个核心接口一个用于提交用户特征并返回推荐列表另一个用于返回各簇画像数据和窗口热度统计。代码结构清楚一个app.py文件加一个model_loader.py就够用。模型加载的代码比较简单关键是要一次性把标准化器和KMeans模型都加载到内存里避免每次请求重复读文件。推荐接口的核心逻辑就是把请求里的JSON数据转成特征向量标准化预测簇然后查数据库按热度规则排序。from flask import Flask, request, jsonify import joblib import pandas as pd app Flask(__name__) scaler joblib.load(./models/minmax_scaler.pkl) kmeans joblib.load(./models/kmeans_model.pkl) feature_cols [spicy_level, sweet_level, price_sensitivity, noodle_pref, rice_pref, special_window_pref] app.route(/api/recommend, methods[POST]) def recommend(): data request.get_json() user_vector [[data.get(col, 0.0) for col in feature_cols]] scaled_vector scaler.transform(user_vector) cluster_id int(kmeans.predict(scaled_vector)[0]) dishes get_cluster_top_dishes(cluster_id, top_n10) return jsonify({ cluster_id: cluster_id, dishes: dishes }) def get_cluster_top_dishes(cluster_id, top_n10): # 伪代码从数据库按热度公式排序并限制同名窗口数量 return query_by_heat_score(cluster_id, top_n)这里有一个容易踩的坑新用户提交的特征如果缺失某个字段直接用data.get(col, 0.0)兜底虽然不会报错但会把缺失项当作最低偏好处理影响聚类结果归属。正规做法是在前端就做必填校验后端再做一遍完整性校验缺字段直接返回错误提示而不是默默给默认值。4.3 前端与可视化展示让推荐结果看得见前端我没有用复杂框架一个单页HTML加ECharts和原生JavaScript就够演示了。页面左侧是推荐菜品卡片列表包括菜品名、窗口名、价格和推荐理由右侧是用户画像雷达图和六大窗口热度柱状图。整体看起来已经是一个像模像样的产品雏形了。ECharts的雷达图直接接收从后端拿到的聚类中心向量每个簇的颜色不同当前用户所属的簇会高亮显示。窗口热度柱状图则是查询过去一个月各窗口的订单量按天聚合之后展示在时间轴上能明显看出午餐高峰和晚餐高峰的窗口热度差异。这块数据全部来自推荐接口下方的统计接口前后端联调的时候我顺手做了一个定时刷新功能演示的时候数据是动态跳动的比静态页面有感染力的多。5. 常见问题排查与踩坑实录5.1 聚类效果差劲K值只是表象特征设计才是根因我在项目中期遇到过一次严重的聚类失效无论K取多少两类人群之间几乎看不出差异。排查到最后发现问题根本不在K值而在特征维度里混入了一个噪音字段——用户填写的“常去食堂编号”。这个字段看起来和饮食偏好无关但归一化之后它偏偏方差特别大直接主导了欧式距离的计算把人群按照宿舍远近而不是口味切开了。这个教训非常典型。K-means聚类是“对输入特征的聚类”特征里有什么聚类结果就长什么样。如果特征都是强相关的聚类就变成单维度划分。我后来把特征之间的相关性矩阵打印出来逐对检查去掉了常去食堂、性别等与口味关联度弱的字段聚类结果立刻变得可解释。做这类项目一定不要偷懒特征工程阶段宁可多花几天梳理数据也不要急着跑模型。5.2 数据稀疏怎么办降维和聚合是两条腿校园订单数据天然存在稀疏问题很多用户只点过两三次饭统计出来的窗口偏好和菜品偏好几乎全是零。这种情况下一味增加行为特征只会让向量更空。我的处理方法是聚合降维把菜品级特征聚合到窗口级用四类主食偏好替代四十多道菜的逐个偏好。面食、饭类、麻辣烫、炸鸡这四个大类本身就和食堂窗口强相关聚合后特征更稳定。聚合还有一个额外的好处模型的泛化能力变强了。用菜品级特征训练时新菜品上线后因为没有历史数据直接无法推荐聚合到窗口级后新菜只要落入已有窗口就能参与推荐排序推荐系统的可扩展性大幅提升。这个经验后来被我写进了答辩总结相当于直接回答了“系统对新品冷启动如何处理”这个高频问题。5.3 冷启动与新品问题问卷设计和兜底策略冷启动在真实的推荐系统里是绕不开的课题。我的系统通过问卷设计规避了新用户无行为数据的问题但问卷本身的设计也必须仔细推敲。如果问卷问题设计得和建模特征不一致比如问卷里问“你早餐吃什么”但建模用的是晚餐数据那预测结果就会错位。确保问卷在录入特征前经过和训练数据完全相同的变换流程是第一要务。对于新菜品我采用的策略是“全局热门池兜底”。一道新菜进入菜品表后先被分配到对应窗口如果它所在窗口的历史热度高这道菜会以“新品推荐”的身份进入推荐列表的后半段等积累了足量的簇内消费数据之后再参与热度排序。这能保证系统永远有探索新品的机制不会被困在历史热门里。5.4 推荐效果怎么评估离线指标加上问卷回访推荐系统做完不是“能跑就行”还需要一个评估方案来证明系统效果。我在离线评估上采用了两个常用指标精确率PrecisionN和召回率RecallN。做法是把用户近两周的订单按时间分成前七天后七天前七天数据用来聚类和生成推荐后七天实际购买记录作为真实标签看看推荐列表里有多少菜是用户后七天真的买过的。我跑出来的结果是Precision5在0.21左右Recall5在0.38左右。这个数值对比电商推荐动辄百分之几的精确率已经不错了但更重要的是我做了在线问卷回访给参与测试的32名学生发放推荐结果让他们给推荐满意度打分最终平均分4.2分满分5分超过一半的用户认为“推荐结果里有至少一道菜是我平时会点的”。两种评估方式结合比单纯堆离线指标更有说服力。最后再分享一个我感触很深的点这类系统真正难的不是算法而是数据链路和特征设计的闭环。K-means本身的代码半小时就能写完但为了让它产出有价值的聚类结果前面的问卷设计、清洗规则、特征取舍、K值校验、结果解读每一步都在消耗精力。如果你也要做类似的推荐项目我的建议是不要急着写算法先把数据摸透、把业务逻辑想清楚算法调用反而是整个项目里最顺滑的一环。这个系统后续其实还能继续扩展比如把窗口排队时长加入推荐权重、用用户对推荐结果的点击反馈做特征迭代甚至引入时间维度做用餐时段差异化推荐这些都是你可以继续深挖的方向。
返回列表