资讯动态

Python构建酒庄推荐系统:数据库设计与协同过滤实战

发布时间:2026/9/19 1:45:21 来源:尧图企业网站定制
简介这份基于Python的酒庄数据分析推荐系统项目实例面向具备Python编程基础、熟悉数据分析与Web开发的研发人员、数据科学家或软件工程师解决酒庄商城及门店的个性化推荐与精细化运营问题。系统采用协同过滤与内容过滤结合的混合推荐策略覆盖数据清洗、用户-酒款交互矩阵构建、SVD矩阵分解、余弦相似度计算、Flask接口封装及Tkinter GUI设计并配套MySQL数据库设计与完整代码详解可帮助读者打通从数据准备到推荐服务上线的全链路。资源为单个docx文档约128KB目录按模块组织从数据加载与清洗、交互矩阵构建、SVD协同过滤到内容过滤和冷启动策略再配合模型评估与可视化示例便于按图索骥逐模块实践。已有65人学习浏览内容结构清晰既适合毕业设计参考也利于工程落地时二次开发。1. 用 Python 把酒庄销售的“经验”变成可运行的推荐系统有经验的侍酒师能记住老熟客的口味谁偏爱黑皮诺、谁只喝旧世界、谁能接受的价位大概在什么区间。可当酒庄的 SKU 超过一百、会员超过三百人时光靠脑子和通讯录做个性化推荐漏掉的机会远多于抓住的机会。基于 Python 的推荐系统把这套经验转成可运行的程序先按产区、品种、年份、价格和评分把酒款整理进数据库再用 pandas 做统计分析找出被低估的酒最后用推荐算法为每位会员生成一份专属酒单并把这些能力包进图形界面点几下鼠标就能完成一次推荐。这篇内容适合正在做课程设计、想把自己的 Python 案例做厚的人也适合酒类零售相关从业者了解推荐系统的落地路径。2. 数据库设计先行葡萄酒、会员和评分的三种关系2.1 为什么朴素的三张表比一张大宽表更适合推荐系统做推荐系统前先把数据建模做对。网上搜酒庄数据分析项目很多人直接把所有字段塞进一张表每行是一条评分记录顺便带上酒的名字、产地、价格、会员的姓名和偏好。这种“小宽表”写 SQL 很方便但坏处很明显。同一酒款的产区和价格在每一行重复出现改一处要同步所有记录没有外键约束酒款 ID 随便塞一个推荐结果就会串味做数据分析时还得先 GROUP BY 去重白白增加计算量。常见的做法是按“实体”拆成三个表葡萄酒表、会员表、评分表。评分表只存 user_id、wine_id 和分数用外键关联两张主表。推荐系统要计算相似用户或相似酒款时直接查评分表就够要拿到酒款属性拼推荐理由时再 JOIN 葡萄酒表。表数量不多SQL 复杂度可控推荐逻辑的数据读取也更快。表名主键外键关键字段winewine_id无variety, region, price, rating_avgmemberuser_id无prefer_variety, prefer_region, budget_maxratingrating_iduser_id → member, wine_id → winescore, rating_time具体建表语句用 SQLite 写逻辑清晰且能直接跑。如果课程设计指定 MySQL把 AUTOINCREMENT 换成 AUTO_INCREMENT、TEXT 换 VARCHAR 即可。CREATE TABLE wine ( wine_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, category TEXT, -- 红/白/起泡/甜酒 variety TEXT, -- 葡萄品种 region TEXT, -- 产区 vintage_year INTEGER, -- 年份 alcohol REAL, -- 酒精度单位 % price REAL, -- 零售价单位元 rating_avg REAL DEFAULT 0.0 -- 全站平均分用于冷启动排序 ); CREATE TABLE member ( user_id INTEGER PRIMARY KEY AUTOINCREMENT, nickname TEXT NOT NULL, join_date TEXT, prefer_variety TEXT, -- 偏好品种例如 赤霞珠 prefer_region TEXT, -- 偏好产区 budget_max REAL -- 可接受的最高价位 ); CREATE TABLE rating ( rating_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, wine_id INTEGER NOT NULL, score INTEGER NOT NULL CHECK(score BETWEEN 1 AND 5), rating_time TEXT DEFAULT (datetime(now, localtime)), FOREIGN KEY (user_id) REFERENCES member(user_id), FOREIGN KEY (wine_id) REFERENCES wine(wine_id) ); CREATE INDEX idx_rating_user ON rating(user_id); CREATE INDEX idx_rating_wine ON rating(wine_id);几个字段细节值得展开。价格用 REAL 而不是 TEXT否则后面排序和价格区间筛选会按字符串字典序排出现99 1000这类低级错误。rating 表的 score 加 CHECK 约束防止程序 bug 写入 0 分或 8 分的脏数据。索引建在 user_id 和 wine_id 上因为推荐系统的所有关联查询几乎都从这两个字段入手建索引是成本最低的优化手段。2.2 用 Python 建库并灌入首批数据建表语句写好后用 Python 连接数据库并执行。为了项目能直接拷走运行建议优先用 SQLite无服务端、单文件携带、环境装了 Python 就能跑。如果课程设计要求展示 MySQL 的图形化操作连接方式换掉即可推荐算法层完全不用动。import sqlite3 DB_PATH winery.db def init_db(): ddl open(schema.sql, encodingutf-8).read() conn sqlite3.connect(DB_PATH) conn.executescript(ddl) conn.commit() conn.close() if __name__ __main__: init_db()executescript 能一次执行多条建表语句这样把 SQL 留在独立文件里程序升级时不需要改 Python 代码。sqlite3.connect 用的是进程当前工作目录要保证脚本所在目录可写。如果后续想迁移到 MySQL只需要把连接方式换成 pymysql.connect(host、user、password、database)再把建表语句中的 AUTOINCREMENT 改成 AUTO_INCREMENT、TEXT 改成 VARCHAR。有一个 SQL 差异评审老师最喜欢问SQLite 的DEFAULT (datetime(now, localtime))在 MySQL 里要写成DEFAULT CURRENT_TIMESTAMP。初次调试时没有真实销售数据常见做法是写一个造数脚本模拟 120 款酒、200 个会员、每人 15 条评分。这个量级足以看出推荐算法之间的差异又不至于让相似度矩阵算得太慢。import random import sqlite3 VARIEITES [赤霞珠, 黑皮诺, 霞多丽, 雷司令, 西拉, 马尔贝克] REGIONS [波尔多, 勃艮第, 纳帕谷, 门多萨, 麦克拉伦谷] PRICE_BANDS [(100, 300), (300, 600), (600, 1500)] def populate(): conn sqlite3.connect(DB_PATH) cur conn.cursor() for i in range(120): variety random.choice(VARIEITES) region random.choice(REGIONS) low, high random.choice(PRICE_BANDS) cur.execute( INSERT INTO wine(name, category, variety, region, vintage_year, alcohol, price) VALUES(?, ?, ?, ?, ?, ?, ?), (f{region}-{variety}-{i:03d}, random.choice([红, 白, 起泡]), variety, region, random.randint(2012, 2024), round(random.uniform(11.5, 15.0), 1), round(random.uniform(low, high), 2))) for uid in range(1, 201): cur.execute( INSERT INTO member(nickname, prefer_variety, prefer_region, budget_max) VALUES(?, ?, ?, ?), (f会员{uid:03d}, random.choice(VARIEITES), random.choice(REGIONS), random.choice([500, 800, 1500]))) users list(range(1, 201)) for _ in range(15 * 200): uid random.choice(users) wid random.randint(1, 120) if cur.execute(SELECT 1 FROM rating WHERE user_id? AND wine_id?, (uid, wid)).fetchone(): continue cur.execute(INSERT INTO rating(user_id, wine_id, score) VALUES(?, ?, ?), (uid, wid, random.randint(2, 5))) conn.commit() conn.close()这个脚本刻意模仿真实酒庄数据的“长尾现象”少数酒款被大量人评多数酒款只有零星几票。最后的评分循环里加了先查再插的去重判断防止同一用户评同一款酒两次。SQLite 里也可以直接建联合唯一索引 (user_id, wine_id) 来兜底但对造数脚本而言先查后插已经够用。还有一个数据口径要三处统一评分表用 5 分制推荐代码里就别写 10 分制的统计变量GUI 里的评分显示也要跟着走否则后面做均值中心化时会差一倍。3. 数据分析从 SQL 里拿数据用 pandas 和 matplotlib 看清酒款分布3.1 先把评分表和葡萄酒表 JOIN 成一张分析宽表推荐系统做得好不好先看数据基础。第 2 章的数据库是标准的三范式结构但做分析时通常直接 JOIN 生成一张分析宽表每个分析脚本不用重复写关联条件。常见做法是在 analysis.py 里统一封装一个 load_data 函数。import pandas as pd import sqlite3 def load_analysis_table(db_pathwinery.db): conn sqlite3.connect(db_path) df pd.read_sql_query( SELECT r.user_id, r.score, r.rating_time, w.wine_id, w.name, w.category, w.variety, w.region, w.vintage_year, w.price FROM rating r JOIN wine w ON r.wine_id w.wine_id , conn ) conn.close() return dfread_sql_query 直接返回 DataFrame后续的透视、分组都在这份表上做。注意这里不 JOIN 会员表因为偏好字段是冗余画像大多数统计用不上把字段控制在必要范围可以减少内存占用。用这张宽表可以回答酒庄老板最关心的几个问题哪个产区的酒被评最多、平均分最高哪个价位区间最受欢迎不同葡萄品种的评分差距有多大。df load_analysis_table() region_stats df.groupby(region).agg( avg_score(score, mean), rating_cnt(score, count), avg_price(price, mean) ).sort_values(rating_cnt, ascendingFalse) print(region_stats)agg 用元组(列名, 统计函数)的方式同时计算均值、条数和价格均值一次 DataFrame 扫描就完成。rating_cnt 列非常关键平均值容易被少数极端样本带偏必须结合条数一起看。条数少、均分高的产区往往是被小圈子追捧出来的不适合直接作为推荐优先项。跑完品种维度的 groupby 后输出大致长这样品种被评次数平均分平均价格赤霞珠2863.82532黑皮诺2413.71688霞多丽2073.65401这组数字是用随机数据跑出来的示例不同机器每次生成会不一样。读表的重点是看 rating_cnt 和 avg_score 两列的组合而不是某个数字本身。3.2 可视化时先解决中文字体问题再看图matplotlib 默认字体不包含中文字形直接在界面里画“波尔多”三个字会显示成方块。这是每个酒庄数据分析项目遇到的第一个可视化问题解决办法是在绘图前声明中文字体。import matplotlib import matplotlib.pyplot as plt matplotlib.rcParams[font.sans-serif] [Microsoft YaHei, SimHei, Arial Unicode MS] matplotlib.rcParams[axes.unicode_minus] False第一条指定字体列表matplotlib 会按顺序选一个可用的。Windows 上 Microsoft YaHei 一般自带macOS 用 Arial Unicode MSLinux 如果都没装要安装 fonts-noto-cjk 并把 Noto Sans CJK SC 加进列表。第二条修复坐标轴负号显示成方块的问题价格和评分虽然很少出现负数但留这一行能防止某些图表报错。价格分布和品种均分是两幅最常用的图fig, (ax1, ax2) plt.subplots(1, 2, figsize(12, 5)) ax1.hist(df[price], bins20, edgecolorblack) ax1.set_title(酒款价格分布) ax1.set_xlabel(价格元) ax1.set_ylabel(酒款数) variety_score df.groupby(variety)[score].mean().sort_values() ax2.barh(variety_score.index, variety_score.values) ax2.set_title(各品种平均评分) fig.tight_layout() plt.show()价格直方图适合看整体形态如果明显右偏说明低价酒多、高溢价款少推荐系统做价格区间匹配时要用分位数而不是固定阈值。品种平均分的横向条形图则直接反映偏好差异但和产区统计一样必须先看被评次数避免一两条高分拉高整个品种的均值。3.3 把分析结果反向喂给推荐系统数据分析不只是画图给答辩看还要把结论抽象成特征导回数据库。一个常见思路是把各酒款的平均分回填到 wine.rating_avg 字段推荐系统冷启动时按这个值兜底排序再统计每个会员的历史评分均值协同过滤算相似度时用这个均值做中心化抵消“有的人打分普遍偏高、有的人普遍偏低”的差异。avg_scores df.groupby(wine_id)[score].mean().round(2) conn sqlite3.connect(DB_PATH) for wid, avg in avg_scores.items(): conn.execute(UPDATE wine SET rating_avg ? WHERE wine_id ?, (avg, wid)) conn.commit() conn.close()这个 UPDATE 在项目初始化脚本里执行一次即可不要在每次推荐请求时重算。把结果写回数据库而不是推荐时临时算是为了让 GUI 查询更快也把“数据分析”和“在线推荐”两层逻辑隔开。在线推荐进程只读不写分析进程负责定期刷新统计这是大多数课程设计能做到的清晰分层。4. 推荐系统设计内容召回 协同过滤排序的混合方案4.1 为什么单用协同过滤在酒庄场景会翻车现在进入标题里最核心的“推荐系统设计”。网上的扫盲文章一搜一堆讲协同过滤的都拿电影评分公开数据集举例但酒庄数据有一个本质差异用户行为非常稀疏而且新会员没有评分记录。假设库里有 600 个会员其中很多人只评过十几款酒user-based 协同过滤的相似用户基本算不出来item-based 也面临同样问题大部分酒款只有十几个评分算出来的协方差没有参考价值。如果照搬大流量电商的纯协同过滤方案冷启动阶段能推的内容几乎是空的。所以常见做法是先做基于内容的召回再用协同过滤做排序。具体拆成三步先用会员资料里的偏好品种、预算区间、产区画像从酒款表里过滤出候选集再对候选集按内容相似度打分筛出候选然后看该会员的历史评分条数是否超过阈值。超过就叠加协同过滤分数按加权融合后的最终分排序没超过就直接用内容推荐的排序结果。这种“召回 排序”的结构把推荐问题拆成两步既照顾了冷启动会员也让老会员的个性化需求有协同过滤兜底。和电影推荐系统 scala 实现那种大数据形态不同酒庄项目只用 pandas 和 NumPy 就能完成全部计算不需要分布式框架。4.2 基于会员画像的内容召回实现先写内容召回。核心逻辑是读 member 表里的 prefer_variety 和 budget_max匹配 wine 表里品种和价格符合的酒款再按平均分排序。def recall_by_profile(user_id, top_n50): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row member conn.execute( SELECT * FROM member WHERE user_id ?, (user_id,) ).fetchone() sql SELECT * FROM wine WHERE ($variety IS NULL OR variety $variety) AND ($region IS NULL OR region $region) AND price $budget ORDER BY rating_avg DESC LIMIT $top_n params {variety: member[prefer_variety], region: member[prefer_region], budget: member[budget_max] or 3000, top_n: top_n} rows conn.execute(sql, params).fetchall() conn.close() return [dict(r) for r in rows]这段 SQL 用了带$前缀的命名参数SQLite 和 sqlite3 都支持。虽然比?占位符繁琐但参数多时可读性更好。$variety IS NULL OR variety $variety这个写法很实用调用方传入 NULL 时跳过该条件避免在 Python 里拼 SQL 字符串造成注入风险。price $budget 做价格上限过滤会员没填预算时默认给 3000 元保证候选集不为空。但画像匹配出来的都是“这个偏好下大家都在买的酒”还不够个性化。给它加一个内容相似度打分比较候选酒与会员偏好画像的品种、产区、价位段匹配程度。def content_score(wine_row, member_row): score 0.0 if wine_row[variety] member_row[prefer_variety]: score 0.5 if wine_row[region] member_row[prefer_region]: score 0.3 if member_row[budget_max]: gap abs(wine_row[price] - member_row[budget_max] * 0.6) score max(0.0, 0.2 - gap / 5000.0) return scorecontent_score 的权重设计是品种 0.5、产区 0.3、价格 0.2总和正好 1.0。价格项用预算的 60%作为参考中心是因为多数消费者买酒不会顶格花钱。如果酒庄主更看重产区把 region 的权重调到 0.5、品种降到 0.3 就行权重分只要总和保持 1.0排序结果就是可比的。4.3 item-based 协同过滤从历史评分找相似酒款内容召回解决“会员喜欢什么类型的酒”协同过滤解决“这个会员喝过的酒还有哪些类似款别人也在喝”。场景是这样的会员 A 给一款波尔多赤霞珠打 5 分给另一款纳帕谷赤霞珠打 2 分这种情况只看品种完全解释不了需要大量用户行为来判断哪些酒同时被喜欢、哪些同时被讨厌。item-based 协同过滤的思路就是拿全体会员的评分构建“酒款 × 酒款”相似度矩阵。from sklearn.metrics.pairwise import cosine_similarity import pandas as pd def build_item_similarity(ratings_df): # 用户 × 酒款评分矩阵行为用户列为酒款 pivot ratings_df.pivot_table(indexuser_id, columnswine_id, valuesscore).fillna(0) # 按行做均值中心化减弱打分宽严尺度差异 centered pivot.sub(pivot.mean(axis1), axis0) sim cosine_similarity(centered.T) # 转置后逐对酒款计算 sim_df pd.DataFrame(sim, indexpivot.columns, columnspivot.columns) return sim_df关键一步是pivot.sub(pivot.mean(axis1), axis0)先把每个用户的评分减掉自己的平均分再算余弦相似度。如果不做这一步心软的用户统一打 5 分、心硬的用户统一打 2 分两者的余弦相似度会虚高推荐结果偏向“评价手松的用户喜欢的酒”。中心化之后相似度更接近“口味相似”而不是“打分习惯相似”这等价于皮尔逊相关系数在稀疏矩阵上的一种近似。算完矩阵后给目标用户推荐def recommend_by_collaborative(user_id, ratings_df, sim_df, top_n10): user_ratings ratings_df[ratings_df.user_id user_id] scores {} for _, row in user_ratings.iterrows(): wid row[wine_id] score row[score] if wid not in sim_df.index: continue for cand in sim_df.columns: if cand wid: continue sim_val sim_df.loc[wid, cand] if sim_val 0: continue scores[cand] scores.get(cand, 0.0) score * sim_val already_rated set(user_ratings.wine_id.tolist()) ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) ranked [(wid, val) for wid, val in ranked if wid not in already_rated] return ranked[:top_n]这段代码把目标用户评过的每一款酒作为“种子”把相似款酒按“评分 × 相似度”加权累加scores 是未归一化的候选得分字典最后按降序取 top N。过滤已评酒款要放在排序之后这样即使种子款相似度极高也不会推荐回用户已经喝过的酒。用第 2 章生成的 200 用户 × 15 条评分做测试算出的相似度矩阵是 120×120整个计算不到 0.1 秒。如果数据量涨到几万会员、几千酒款就需要用 NumPy 向量化替代两层 Python 循环课程设计阶段不用考虑这个优化。4.4 混合推荐把内容分和协同分融合成最终排序列内容召回和协同过滤单独用都有局限前者不知道同产区的酒里哪款口感更接近后者冷启动时直接没有候选。成熟方案是做一个混合推荐函数根据用户行为条数决定走哪条路。同时给它加一个 train_df 参数方便第 6 章的留一法验证把训练数据单独传入。def hybrid_recommend(user_id, top_n5, min_cf_ratings10, train_dfNone): if train_df is None: ratings_df load_analysis_table() else: ratings_df train_df user_cnt (ratings_df.user_id user_id).sum() content_hits recall_by_profile(user_id, top_ntop_n * 4) if user_cnt min_cf_ratings: # 冷启动直接按内容分排序返回 content_hits.sort(keylambda w: w[rating_avg], reverseTrue) return content_hits[:top_n] sim_df build_item_similarity(ratings_df) cf_hits recommend_by_collaborative( user_id, ratings_df, sim_df, top_ntop_n * 4) merged {} for w in content_hits: merged[w[wine_id]] 0.4 * (w[rating_avg] / 5.0) for wid, cf_score in cf_hits: merged[wid] merged.get(wid, 0.0) 0.6 * min(cf_score / 10.0, 1.0) ranked sorted(merged.items(), keylambda x: x[1], reverseTrue) return [wid for wid, _ in ranked[:top_n]]混合步骤很清晰先按画像召回再判断评分条数是否达到协同过滤门槛。min_cf_ratings 设为 10 是经验值评分太少时协同过滤算出的相似度噪声太大。两条来源的候选各取 4 倍于 top_n是为了避免某一方的候选集被截断后直接空缺。加权公式的 0.4/0.6 是最常见默认比例实际项目里可以对召回覆盖率做一次小规模搜索选离线指标最优的那组。注意冷启动分支返回的是 dict 列表协同过滤返回的是 wine_id 列表两种返回形态不同GUI 层要分别处理。想让接口统一可以把冷启动分支也转成 id 列表再返回。5. GUI 设计用 tkinter 把数据库、分析和推荐整合成交付界面5.1 三层设计界面只做展示不让业务逻辑陷进按钮回调课程设计要展示 GUI 时最常见的反模式是把 SQL 查询、pandas 统计、推荐函数全部写进一个按钮的 click 回调里代码动辄几百行排查一个字段报错要找半天。我的做法是提前拆成三个文件界面层、推荐层、数据层严格分开。winery_project/ ├── schema.sql # 数据库建表脚本 ├── db_helper.py # 连接、初始化、公共查询 ├── analysis.py # 数据加载与统计 ├── recommender.py # 内容召回 协同过滤 混合 ├── app.py # tkinter 界面入口 └── winery.db # SQLite 数据文件运行时生成前面写的 recall_by_profile、build_item_similarity、hybrid_recommend 都放在 recommender.py 里界面层只负责 import 和调用。界面布局调整不会影响推荐结果推荐逻辑修改也不会让 tkinter 控件报错。开发环境建议用 VSCode 加 Python 插件打开项目根目录后选 .venv 里的解释器调试 tkinter 窗口会顺很多。5.2 界面布局左侧表单、右侧表格、下方图表tkinter 内置的 ttk 子模块提供了比原生控件更好看的组件其中 ttk.Treeview 非常适合做酒款列表展示。主窗口分成左右两列左侧放会员选择、推荐数量、触发按钮右侧上方用 Treeview 展示推荐结果下方放图表区域。import tkinter as tk from tkinter import ttk from recommender import hybrid_recommend from db_helper import get_member_list, get_wine_by_ids class WineryApp: def __init__(self, root): root.title(酒庄数据分析与推荐系统) root.geometry(1100x700) left ttk.Frame(root, padding10) left.pack(sideleft, filly) right ttk.Frame(root, padding10) right.pack(sideright, fillboth, expandTrue) ttk.Label(left, text会员).pack(anchorw) self.member_var tk.StringVar() self.member_cb ttk.Combobox(left, textvariableself.member_var, statereadonly, width20) self.member_cb.pack(pady4) ttk.Label(left, text推荐数量).pack(anchorw) self.top_n_var tk.IntVar(value5) ttk.Spinbox(left, from_1, to20, textvariableself.top_n_var).pack(pady4) ttk.Button(left, text生成推荐, commandself.on_recommend).pack(pady10) cols (酒款名, 产区, 品种, 价格, 均分) self.tree ttk.Treeview(right, columnscols, showheadings, height18) for c in cols: self.tree.heading(c, textc) self.tree.column(c, width120, anchorcenter) self.tree.pack(fillboth, expandTrue) self.chart_area ttk.Frame(right) self.chart_area.pack(fillx) self.load_members()combobox 的 state 设为 readonly防止用户输入不存在的会员编号。Spinbox 限制推荐数量在 1 到 20 之间配合 intvar 绑定用户调整后推荐函数立即读到最新值。Treeview 的 columns、heading、column 三处都要写全缺一个就只显示空白列。get_member_list 从 member 表读出 user_id 和 nickname拼成“1-会员001”这种格式填进下拉框。5.3 把推荐结果和图表动态刷新到界面上推荐按钮的回调函数只做三件事拿到选中的会员、调用 hybrid_recommend、把结果填入表格并刷新图表。业务流程全部封装在函数里界面层保留清晰的调用关系。def on_recommend(self): uid self.member_var.get() if not uid: return uid int(uid.split(-)[0]) # 下拉框显示 1-会员001 top_n self.top_n_var.get() result_ids hybrid_recommend(uid, top_ntop_n) wines get_wine_by_ids(result_ids) for row in self.tree.get_children(): self.tree.delete(row) for w in wines: self.tree.insert(, end, values(w[name], w[region], w[variety], w[price], w[rating_avg])) self.refresh_chart(uid)每次刷新前先清空 Treeview 的所有行否则多次点击会不断追加数据。图表刷新更要注意画布的释放from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure def refresh_chart(self, uid): wines get_wine_by_ids(hybrid_recommend(uid, top_n5)) fig Figure(figsize(8, 3), dpi100) ax1 fig.add_subplot(121) ax1.bar([w[name] for w in wines], [w[price] for w in wines]) ax1.set_title(推荐酒款价格) ax1.tick_params(axisx, rotation30) ax2 fig.add_subplot(122) ax2.bar([w[name] for w in wines], [w[rating_avg] for w in wines]) ax2.set_title(推荐酒款均分) if hasattr(self, canvas): self.canvas.get_tk_widget().destroy() self.canvas FigureCanvasTkAgg(fig, masterself.chart_area) self.canvas.draw() self.canvas.get_tk_widget().pack(fillboth, expandTrue)refresh_chart 里保存了 self.canvas第二次刷新时先销毁旧画布再创建新的否则多个 matplotlib 画布叠加在同一个 Frame 上界面会越点越卡甚至出现黑块。窗口初始化时调用一次 refresh_chart让界面打开就有数据可看。如果课程设计要更现代的外观可以换 PyQt5但推荐算法层不需要任何改动这正体现了前面分层的价值。6. 推荐系统的留一法验证与两个必调参数6.1 在不动界面代码的情况下验证推荐质量推荐系统不能只看“能出结果”要有一套可量化的验证方法。小数据场景下留一法最直观把某一条评分从数据里挖掉当测试集用剩下的数据训练推荐看这条被挖掉的酒款在不在推荐列表里。上一章的 hybrid_recommend 已经预留了 train_df 参数正好派上用场。def leave_one_out(user_id, ratings_df, top_n10): user_rows ratings_df[ratings_df.user_id user_id] hits 0 total 0 for _, row in user_rows.iterrows(): test_wine row[wine_id] train_df ratings_df[ ~((ratings_df.user_id user_id) (ratings_df.wine_id test_wine)) ] rec_ids hybrid_recommend(user_id, top_ntop_n, train_dftrain_df) if test_wine in rec_ids: hits 1 total 1 return hits / total验证思路是把“是否覆盖到用户真实喜欢的酒”作为指标。循环里每次挖掉一条测试记录把剩余数据传给推荐函数避免模型偷看答案。实际跑的时候要选 20 到 30 个用户求平均命中率再对比内容推荐、协同过滤、混合推荐三者的差异。正式项目中应该把相似度矩阵缓存起来只计算一次而不是在每次留一法循环里重建。6.2 两个一定要记住的参数第一个是相似度过滤阈值。协同过滤算出的相似度矩阵里大多数酒对之间的余弦值都很低不过滤的话低相似度酒的累加噪声会把排序搞乱。代码里用if sim_val 0: continue做的软过滤是最低要求实际项目中再加一个 sim_threshold只保留相似度大于 0.25 的酒对继续参与排序。第二个是冷启动阈值 min_cf_ratings。不要拍脑袋定成 10先统计一下会员评分条数的分布。如果多数会员评分都少于 5 条阈值就下调到 5让更多用户走进协同过滤分支如果普遍评了 20 条以上上调到 15 也不迟。这个参数直接决定有多少人走内容推荐、多少人走协同过滤是混合推荐里最值得花时间调的指标。所以与其继续堆算法不如先把这条最小闭环跑通。一次推荐点击背后是数据库查询、内容召回、协同过滤、混合排序、GUI 刷新五个环节的协同。调参顺序按照 min_cf_ratings 到 sim_threshold、再到 0.4/0.6 融合权重的顺序来每个参数只影响从“召回”到“排序”的一个环节推荐结果不对时翻到对应代码就能定位问题所在。本文还有配套的精品资源点击获取

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

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

免费获取报价