资讯动态

Python餐饮推荐系统实战:从数据库到协同过滤算法完整实现

发布时间:2026/10/20 6:00:28 来源:尧图企业网站定制
前两周帮一位读者改课程设计他拿来一个特别典型的题目基于Python的个性化餐饮场所推荐平台。打开他的代码数据库三张表、界面只有两个按钮、推荐逻辑写成一坨硬编码。其实这个题目的内核特别适合当综合练手项目——数据表设计、推荐算法、GUI交互、打包部署全都沾得上而且难度刚好卡在能讲清楚和有复杂度之间。这篇文章我就把这个项目从上到下完整拆一遍包含数据库怎么建、推荐算法怎么实现、界面怎么组织、初始化数据怎么写最后附上我实际调程序时踩过的坑。不管你是要做课程设计、毕业设计还是单纯想练一个完整的Python桌面应用照着这套思路走下来至少能省掉一半摸索时间。1. 需求拆解与技术选型从吃什么到推荐平台要拆成哪几块1.1 核心需求三角谁能用、推荐什么、给用户什么反馈先别急着写代码第一步是把业务需求拆清楚。这个平台要解决的事情看起来很直白用户面对一堆餐厅不知道怎么选系统根据他过去的评分习惯把可能合口味的餐厅排到前面。但落到代码层面至少要回答三个问题。第一个问题是用户从哪来。推荐系统必须有用户身份否则系统不知道该为谁推荐。所以用户表是地基用户ID、昵称、口味偏好这些字段都得有。第二个问题是推荐什么。餐饮场所得有基本信息餐厅名称、类别、菜系、价格档位、位置、营业时间、历史评分这些属性决定了推荐结果的信息量。第三个问题更关键——系统怎么知道用户喜欢什么。这里必须有评分记录用户给某家餐厅打了4分系统才有依据去推测他的偏好用户从来没评过分那就是冷启动状态。这三个问题串起来就自然得出数据层的最小集合用户表、餐饮场所表、评分表。再往下想一步平台不能光推荐完就结束要有界面让用户看推荐结果、看餐厅详情、提交评分动作。所以整个项目自然分成四层数据层负责读写SQLite算法层负责根据评分算推荐分界面层负责展示和交互工具层负责初始化模拟数据。在设计之初就要想清楚这个项目最核心的亮点不是界面多炫而是推荐结果有据可循。所有功能最后都要能解释成一句话——系统为什么把这个餐厅推给这个用户。后面算法部分我会展开讲现在先把架子搭起来。1.2 为什么选 Python SQLite tkinter 这套组合这个题目市面上有太多变体有人用MySQL有人做Web版有人用PyQt。我的建议是如果目标是快速跑通、方便答辩演示、并且能把核心逻辑讲明白Python标准库全家桶是最稳的选型。我之前帮人改的方案里就有人用了MySQL结果答辩现场环境没配好数据库连不上程序直接白屏。换SQLite之后彻底不用操心连接串、账号密码、服务没启动这类问题——因为SQLite本质就是一个单文件数据库Python内置sqlite3库直接读写把db文件发给谁都能跑。这对课程设计和毕业设计来说太重要了演示环境永远不可控尽量把外部依赖降到零。GUI选tkinter也是同一个逻辑。其他图形库虽然好看但客户机器上没装依赖就是灾难。tkinter是Python自带的标准库跨平台、启动快、代码量也不大。配合ttk模块可以做到不算丑的界面完全够用于展示项目。下面我把技术栈和理由整理成一张表。技术组件具体方案选型理由语言Python 3.8语法清晰、标准库齐全、适合演示数据库SQLite内置sqlite3零配置单文件、免安装、代码迁移方便GUI框架tkinter ttk标准库自带、跨平台、不需要额外装包数据来源初始化脚本生成模拟数据可控可复现、方便一键重置算法实现纯Python手写不依赖sklearn等库、答辩能讲清楚公式1.3 项目目录与模块划分代码组织上我强烈建议把数据库操作、算法、界面分成三个独立模块不要全塞进一个文件里。哪怕是一个3万行以下的小项目分模块也能省掉大量调试时间。我的目录结构大致是这样restaurant_recommender/ ├── app.py # 程序入口负责启动GUI ├── db.py # 数据库连接、建表、增删改查 ├── recommender.py # 推荐算法偏好加权 协同过滤 ├── gui.py # 桌面界面按钮、列表、详情展示 ├── init_data.py # 生成模拟数据一键重建数据库 └── restaurants.db # SQLite数据库文件运行时生成入口文件app.py只做一件事初始化数据库、创建窗口、进入消息循环。db.py统一管理所有SQL语句界面层永远不要直接拼SQL字符串。recommender.py是纯函数模块输入是数据库里的评分记录输出是推荐餐厅ID列表这样以后换成Web版还能继续复用。ui.py只关心用户操作和界面渲染不掺数据逻辑。模块划分的核心理念是单向依赖界面层调用数据层和算法层的接口算法层调数据层读数据数据层不依赖任何上层模块。这样任何一个模块出问题排查范围都能缩小一大半。2. 数据库结构设计把用户、餐厅和评分串成一张关系网2.1 六张核心表的字段设计数据库设计英文里有个说法叫schema firstcode later中文理解就是先定表结构再写代码。表结构定错了后面改字段成本非常高。这套方案里我设计了六张表其中两张是系统内置字典表四张是业务核心表下面逐一拆解。用户表users是最基础的身份表id、username、password_hash、preferred_category、register_time。password_hash不需要真的做加密演示项目可以存明文但字段名写成hash更规范。preferred_category是用来解决冷启动问题的辅助字段用户注册时如果什么都没评分就先按他选的偏好类别给推荐。场地表restaurants字段设计要够用但不冗余id、name、category中式/西式/火锅/烧烤、cuisine_type川菜/粤菜/日料、price_level1-5表示人均价位、location、rating所有用户评分的平均值、rating_count、description、opening_hours。注意rating这个字段不能每次推荐的时候临时算场景表里冗余一份平均分查询快很多评分新提交时同步更新即可。评分表ratings是推荐算法的输入源头id、user_id、restaurant_id、score、created_at。score建议用1到5的整数刻度小数评分会导致用户打分犹豫数据质量反而下降。外键约束下user_id和restaurant_id分别关联用户表和场地表。这里有个关键点同一用户对同一家餐厅只能有一条评分记录所以user_id restaurant_id要建唯一索引。再有就是推荐会话表recommendationsid、user_id、recommended_ids、algorithm_version、created_at。这个表的作用很多人会忽略——它把当时推荐了什么存下来现场演示或者考核时可以展示历史推荐结果证明系统推荐过什么内容。存算法版本号比如v1偏好加权、v2协同过滤是为了后期对比新旧算法效果属于一个很细节但很加分的字段。分类表和菜系表我直接做成了两张字典表字段就两个id和name。写代码时用硬编码字符串临时拼也能跑但后面做按类别筛选、做统计查询时会很痛苦把类别规范化成字典表反而是省事的选择。2.2 建库建表SQL与关系说明下面给出建表SQL的核心片段完整的会在项目代码里看到。SQLite的语法遵循入门友好路线没有复杂存储过程主键和唯一约束用好就足够了。CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL DEFAULT , preferred_category TEXT DEFAULT , register_time TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS restaurants ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, category TEXT NOT NULL, cuisine_type TEXT NOT NULL, price_level INTEGER DEFAULT 3, location TEXT DEFAULT , rating REAL DEFAULT 4.0, rating_count INTEGER DEFAULT 0, description TEXT DEFAULT , opening_hours TEXT DEFAULT 10:00-22:00 ); CREATE TABLE IF NOT EXISTS ratings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, restaurant_id INTEGER NOT NULL, score INTEGER NOT NULL CHECK (score BETWEEN 1 AND 5), created_at TEXT DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, restaurant_id), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (restaurant_id) REFERENCES restaurants(id) );字段类型选择上有几个经验要提。id一律用INTEGER PRIMARY KEY AUTOINCREMENT这是SQLite的惯例。score用INTEGER加CHECK约束防止非法数值进入表里。rating用REAL类型因为它是平均分会带小数。UNIQUE(user_id, restaurant_id)这个约束一定要加否则同一用户反复评同一家餐厅会生成多条记录推荐算法输入直接乱掉。关于外键还有一个大坑SQLite默认不启用外键约束。这意味着你建了FOREIGN KEY子句但如果没执行PRAGMA foreign_keys ON删除父表记录时子表可能残留孤儿数据。所以每次连接数据库后要第一时间执行这个PRAGMA。我在db.py里封装了一个get_connection函数所有数据库请求都从这一个入口拿连接这个开关就永远不会漏。餐厅的rating和rating_count字段单独说一下。rating代表所有用户评分的算术平均值rating_count是评分人数。推荐算法里不仅要看评分高低还要看评分人数——一个人打了5分的餐厅和一百个人打了4.8分的餐厅显然后者更可信。这两个字段就是算推荐分时的权重参数。2.3 模拟数据的自洽性别让推荐算法拿到空数据数据库表建好之后接下来要到init_data.py里写模拟数据。这里最容易被忽略的问题是数据自洽。什么叫自洽就是说模拟数据要能支撑推荐算法的逻辑链条——用户必须有一定数量的评分记录餐厅必须有合理的分布否则算法跑起来根本看不出推荐效果。我实际生成数据时是按这个比例来的6个用户、80家餐厅、每个用户有8到25条评分记录。其中有一个用户故意做成新用户只有2条评分记录用来演示冷启动兜底逻辑。另一个用户评分集中在川菜和火锅类别高分这样协同过滤能算出跟他兴趣相近的邻居推荐结果才显得有理有据。评分数据也不是随机拍的。我先给每类餐厅设定一个基础分比如川菜基础分4.2奶茶基础分3.8然后在基础分上做正负1.5的随机扰动。这样用户评分的分布符合直觉推荐结果也更容易解释成因为你喜欢川菜所以推荐川菜里的高分店。最后数据量不需要太大。80家餐厅、两百多条评分放在SQLite里连1MB都不到查询秒回。千万别生成几千条数据推荐算法运行慢界面还卡演示体验很差。课程设计的核心是把逻辑跑通不是堆数据量。3. 推荐引擎的两层算法偏好加权打分与相似用户协同过滤3.1 推荐结果要解决的业务问题与评估角度推荐算法是这个项目真正的灵魂。我见过不少学生的实现直接一股脑把餐厅平均分倒序排序就说是推荐系统这玩意儿本质是个排行榜没有任何个性化可言。真正的个性化核心差异在于**同一份餐厅列表不同用户看到的排序必须不同。**排序差异来自不同用户的评分历史他们兴趣偏好不一样系统算出来的推荐分就应该不一样。这个平台的推荐逻辑我拆成了两层。第一层是基于用户历史评分的类别偏好加权逻辑很朴素——用户过去给川菜馆打了高分那川菜馆在推荐时加权分就该高。第二层是基于余弦相似度的用户协同过滤逻辑是找到跟当前用户打分习惯最相似的一批邻居邻居们给过高分的餐厅当前用户也更容易接受。为什么不用基于物品的协同过滤因为餐饮数据规模小用户评分稀疏基于物品的矩阵会有一大堆空值。而且基于用户的逻辑更好解释答辩时讲起来省劲。真让我选我会两层一起做偏好加权负责覆盖大多数常规场景协同过滤负责捕捉跨类别偏好最后把两个分数加权合成。评估推荐效果有个很实用的角度看系统高分推荐里用户有没有实际打过分的记录。推荐前用户没评过分推荐后用户去评分了且分数还不低说明推荐命中。我在推荐记录表和评分表做了关联这个指标可以在需求文档里写一笔。3.2 第一层基于用户历史评分的类别偏好加权先实现第一层。核心思路是这样的把用户所有历史评分按照餐厅所属类别分组计算每个类别的平均评分再把这个类别平均值作为该类餐厅的类别偏好分。某个类别用户打过高分大于3.5分系统就会把该类餐厅排得更靠前。计算公式用伪码写出来就是这样类别偏好分(category) sum(用户在该类别所有评分数值) / 用户在该类别评分次数 候选餐厅推荐分 餐厅平均分 * 0.6 餐厅所在类别偏好分 * 0.4你可能会问为什么两个分数不直接加起来因为量纲必须一致才有得加权。餐厅平均分和类别平均分都是1到5的分数加权出来的推荐分也是1到5排序起来才合理。0.6和0.4这两个权重是我试跑时觉得效果最平衡的组合——既尊重餐厅真实口碑又能体现用户类别偏好。想在答辩时展示调参能力的话可以把这两个权重抽成配置项之后做测试还能对比不同权重对推荐结果的影响。在recommender.py里的实现长这样def calculate_category_preferences(rows): rows: [(restaurant_category, score), ...] category_score_sum {} category_count {} for category, score in rows: category_score_sum[category] category_score_sum.get(category, 0) score category_count[category] category_count.get(category, 0) 1 preferences {} for category, total in category_score_sum.items(): preferences[category] total / category_count[category] return preferences这个函数返回一个字典键是餐厅类别值是该用户对这个类别的平均爱好分。有了这个字典某类餐厅的推荐分就能立刻算出来。3.3 第二层余弦相似度的用户协同过滤第一层对常规场景足够但短板很明显——它假定用户偏好是按类别硬分割的实际上用户的兴趣往往横跨类别。一个爱吃川菜的人也可能因为朋友影响爱上海底捞火锅。这种跨类别的隐性偏好需要找相似用户才能捕捉到。用户相似度的标准算法是余弦相似度先算出两个用户的评分向量然后计算向量夹角的余弦值。直接拿原始分数算会有个问题每个人打分尺度不一样有人习惯打4到5分只说很好有人习惯打1到5分区分度很大。所以先要做中心化处理——用每个分数减去该用户自己的平均分把尺度差异消掉。公式是这样的sim(u, v) sum((score_u - avg_u) * (score_v - avg_v)) / sqrt(sum((score_u - avg_u)^2)) * sqrt(sum((score_v - avg_v)^2))落在 -1 到 1 之间越接近1越说明两人口味接近。核心实现代码def user_similarity(user_ratings, other_ratings, user_avg, other_avg): common_items set(user_ratings.keys()) set(other_ratings.keys()) if not common_items: return 0.0 numerator 0.0 user_sq 0.0 other_sq 0.0 for item in common_items: u_diff user_ratings[item] - user_avg v_diff other_ratings[item] - other_avg numerator u_diff * v_diff user_sq u_diff ** 2 other_sq v_diff ** 2 if user_sq 0 or other_sq 0: return 0.0 return numerator / (math.sqrt(user_sq) * math.sqrt(other_sq))找到相似用户之后怎么推餐厅方法是邻居中评分较高的餐厅且当前用户没评过分则加入候选列表候选餐厅得分用邻居评分的加权平均权重就是相似度。相似度越高邻居的一票越重。这样就把和我口味很像的人喜欢的店变成了推荐结果。注意这个算法对数据量没太高要求但要求用户评分不能太稀疏。我在init_data.py里让每个用户至少有8条评分记录就是保证相似度计算时有足够的重叠项目。如果你的实验数据里两个用户之间共同评分的餐厅太少相似度会趋近0协同过滤直接失效——这是这个算法最重要的调参关注点。3.4 冷启动兜底与推荐主流程编排新用户刚注册一条评分都没有协同过滤和类别偏好逻辑全都算不出来。怎么办这时候就走冷启动兜底策略按餐厅平均分和评分人数综合排序把全站口碑最好的Top餐厅推出去。这是推荐系统里最常见的起点用户评分多了以后再切换到个性化算法。主流程编排在recommender.py里判断逻辑分段走def recommend_for_user(conn, user_id, top_n5): ratings fetch_user_ratings(conn, user_id) if len(ratings) 5: # 冷启动按口碑兜底过滤已评分的餐厅 return recommend_by_popularity(conn, user_id, top_n) # 第一层类别偏好加权 preference_scores calculate_category_scores(conn, user_id, ratings) # 第二层协同过滤补充 cf_scores collaborative_filter_scores(conn, user_id, ratings) # 两阶段得分加权合并 final_scores merge_scores(preference_scores, cf_scores) return sort_candidates(final_scores, top_n)merge_scores里我用0.5的权重等分两类得分当然你也可以按场景调成0.7偏好、0.3协同。我实际测试下来5条和50条评分情况下0.50.5都还挺稳定答辩时改一下权重给导师看结果变化反而显得你对参数有理解。排序后记得用列表推导过滤掉用户已经评过分的餐厅——一个已经打过分的餐厅推荐给用户没有意义用户要么自己已经去吃过要么已经有了判断。4. GUI界面设计把数据库和算法装进一个可操作的桌面程序4.1 界面布局顶部用户区、左侧推荐列表、右侧详情区到了GUI环节很多人会犯一个错误一上来就画按钮画完了不知道怎么跟数据交互。正确顺序是先画布局图、定好界面区域再确定每个控件绑定什么事件和数据来源。这个平台的界面我分成三个大区域。顶部是用户操作区一个下拉框列出所有用户ID和用户名旁边放一个刷新推荐按钮和一个查看历史推荐按钮。中间左侧是推荐列表用ttk.Treeview控件展示推荐结果每行显示排名、餐厅名称、类别、推荐分。中间右侧是详情面板展示当前选中餐厅的完整信息地址、营业时间、人均价格、综合评分、评分人数、简介。底部是评分操作区一个评分滑动条或下拉框1到5一个提交评分按钮还有一个状态栏显示当前用户和算法耗时这个状态栏对演示很加分。为什么推荐列表用Treeview而不是Listbox因为Treeview天然支持多列排序和列宽调节展示推荐分、类别这些字段完全够用Listbox只能显示纯文本行还要手动格式化对齐维护成本高。Treeview还自带选中事件 绑定详情刷新函数非常顺手。布局代码走一个普通网格布局就能拿下。别忘了给关键区域设置固定宽度和权重window.resizable(True, True)加上grid_rowconfigure/columnconfigure让窗口拉伸时组件跟着缩放否则演示换台电脑分辨率一变界面就乱成鸡窝。4.2 核心交互逻辑选择用户、刷新推荐、提交评分界面的核心交互链很长但逻辑闭环选择用户 - 触发加载推荐 - 展示到左侧列表 - 点击某条推荐 - 右侧显示详情 - 下方提交评分 - 刷新推荐结果。这条链路走通项目就完整了。关键事件绑定代码示意class RestaurantApp(tk.Tk): def __init__(self): super().__init__() self.title(个性化餐饮场所推荐平台) self.geometry(1024x680) self.current_user_id None self.current_restaurant_id None self.build_ui() self.load_users() def build_ui(self): # 顶部用户选择区 self.user_combo ttk.Combobox(self, statereadonly) self.user_combo.bind(ComboboxSelected, self.on_user_selected) self.refresh_btn ttk.Button(self, text刷新推荐, commandself.on_refresh) # 左侧推荐列表 self.tree ttk.Treeview(self, columns(rank, name, category, score), showheadings) self.tree.bind(TreeviewSelect, self.on_tree_select) # 底部评分控件 self.score_spin ttk.Spinbox(self, from_1, to5, width5) self.submit_btn ttk.Button(self, text提交评分, commandself.on_submit_rating)on_refresh函数要做的动作拿到当前选择的用户ID调用recommender.recommend_for_user拿到推荐餐厅列表清空Treeview再逐行插入。注意插入时要把推荐分格式化成两位小数否则一串浮点长串会把界面撑变形。on_tree_select函数通过tree.item(item, values)拿到所选行的餐厅ID再查restaurants表把详情填充到右侧面板。评分区还需要一个防重复逻辑提交前先检查ratings表里这个用户和这家餐厅有没有历史记录。有的话应该走更新而不是插入或者提示用户已经评过分。我的实现是直接UPDATE评分可以反复修改这个逻辑更贴近真实App的体验。4.3 界面与数据库、推荐引擎之间的数据流GUI这一层最忌讳的是让界面代码直接操作SQL。我之前看过一个项目所有数据库查询都直接写在按钮回调里排错时要在界面代码里找SQL体验非常糟糕。正确的数据流应该是界面事件 - 调用算法层函数 - 算法层调数据层查询 - 返回结构化的Python对象 - 界面渲染。给个例子比如加载推荐这个动作的调用链应该是def on_refresh(self): user_id self.get_selected_user_id() if user_id is None: messagebox.showwarning(提示, 请先选择用户) return # 调用推荐引擎拿结果不关心SQL长什么样 result recommend_for_user(self.conn, user_id, top_n5) self.render_recommendations(result) self.status_var.set(f当前用户{self.current_user_name}推荐完成共{len(result)}条)界面只负责把result渲染出来。如果推荐引擎内部改成算法v2界面代码一行都不用改——这就是分层设计的好处。另一个细节是连接对象的传递我建议在App初始化时创建一个connection传给所有模块函数不要每次查询都开新连接否则SQLite的并发写锁会频繁蹦出来。界面细节上还有两个小方向值得补一下。一是列表的空状态处理冷启动用户如果一周没评分推荐结果只有兜底5条Treeview里永远给个暂无推荐请先评分的提示比白屏体验好。二是详情面板的字段标签太单调可以加一个模拟星级显示用★★★★☆字符加颜色标记代表餐厅综合评分这个细节能让界面观感上一个档次。5. 初始化数据、程序骨架与联调排错5.1 初始化脚本与一键重建数据db.py负责连接init_data.py负责让数据库有内容。我把初始化写成可重复执行的脚本每次重新跑一遍就能重置所有数据这对演示前清理现场数据特别有用。脚本入口函数是init_database(conn, resetFalse)reset为True时先DROP TABLE再重新建表相当于一键格式化。初始化脚本里生成模拟数据的方式我用的是Python内置的random模块造数据。逻辑分四步第一创建所有表第二插入6个用户第三插入80家餐厅第四给每个用户随机生成评分记录。注意造数据时要保持一致性——餐厅综合评分要跟用户评分大致对得上不能出现所有人给某餐厅打1分餐厅综合评分却是4.9这种低级矛盾。一个额外加分的动作在初始化脚本里生成一条推荐记录样例数据。比如给用户2生成一条历史推荐记录推荐列表是3个餐厅ID算法版本写v1时间戳写昨天。这样打开查看历史推荐页面时立刻有数据可展示不用等真人真正跑推荐。演示现场最怕冷场这招真的管用。5.2 运行效果与联调验证所有模块写完以后先别急着开界面按流程做一次无界面联调验证核心链路是否通畅。我自己常用的验证方式是写一个临时测试脚本直接用recommender.py的函数跑几个断言给用户1拉推荐要求返回5条给新用户6拉推荐要求返回5条兜底结果给一个已经评过分餐厅推荐要求结果里不出现该餐厅ID。这些断言全过核心逻辑就稳了。然后是界面联调。启动app.py之后按操作链走一遍选择用户1点击刷新看推荐列表点击第一条推荐看右侧详情提交一个评分再次刷新列表看推荐结果是否变化。这个流程如果没报错项目基本就交付了。我这里给一个启动入口示例# 首次运行或想重置数据时执行 python init_data.py --reset # 启动GUI python app.pyinit_data.py里用argparse接收--reset参数这样骨架就完整了。打包这块建议用PyInstaller但注意打包时SQLite的db文件路径要处理成相对路径否则双击exe时候会找不到数据库文件这是我踩过一次的坑后面排错表格里再细说。5.3 常见问题翻车现场这个项目虽然结构不复杂但实际跑起来新手至少会在下面几个位置翻车我把常见问题和排查方案整理成表。现象可能原因解决办法界面没显示中文窗口标题乱码Windows控制台代码页问题源码文件保存为UTF-8Python入口指定encoding或设置字体为Microsoft YaHei UI推荐结果总是空列表用户评分记录太少协同过滤没有共同评分项检查模拟数据每个用户至少要有8条以上评分落到熟悉的位置提交评分后列表不更新评分写入了数据库但界面没重新调用推荐函数在on_submit_rating末尾手动调用on_refresh别依赖事件自动触发SQLite报database is locked同时打开了多个数据库连接写操作相互阻塞全部模块共用一个连接或用connect后再with开事务快速提交打包exe后找不到restaurants.db运行时路径和当前工作目录不一致用os.path.abspath和相对路径拼接打包后用sys._MEIPASS处理资源路径定位中文乱码这个坑尤其要提因为很多人的Windows默认编码是GBKPython源码写的中文会直接变成乱码。我处理方案是源码文件开头声明encoding为UTF-8同时给tkinter控件统一设置一个字体配置用Microsoft YaHei UI替代默认字体基本解决问题。推荐结果为空的逻辑坑也很隐蔽。if len(ratings) 5这个阈值如果定成5而新用户可能有4条评分协同过滤已经能算了但被拦在门外直接走兜底又看不到个性化效果。我的做法是阈值设成5小于5走兜底大于等于5才进个性化。这样模拟数据保证每个正常用户有8条以上记录演示时结果绝对有差异。5.4 后续扩展思路项目做完之后如果还想往上加内容最值得扩展的地方有两个半。第一个是推荐结果的可解释性——界面里给每条推荐加一行推荐理由比如因为你喜欢川菜并且和你口味相似的用户也高分评价了这里这在答辩和项目展示时非常有说服力。第二个是换一套基于物品的协同过滤复用评分表数据对比两种算法的推荐差异项目复杂度一下就上去了。剩下半个方向是Web化把SQLite数据交给Flask暴露API算法模块复用GUI换成网页这套架构迁移过去成本很低。矩阵那块还有太多可以深挖但桌面版的核心骨架到这里已经完整了。代码量大概在600到800行之间数据结构清晰、算法有解释、界面可用做课程设计或毕业设计的核心交付绰绰有余。如果照着这篇的目录结构自己从零写一遍我对你的唯一建议是不要跳过init_data.py的数据自洽设计推荐效果好不好、答辩能不能讲清楚一半的胜负手就压在模拟数据是否合理上。最后再送你一个细节——演示前先跑一次reset把评分数据清干净让评委亲眼看着从无到有的推荐变化这个由用户操作产生的数据闭环比任何漂亮的静态截图都有说服力。

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

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

免费获取报价 →
↑