1. 这个项目到底是什么
先别被“毕设源码分享”这几个字劝退。哪怕你不是应届生,哪怕你只是对“推荐系统”或者“白酒数据”有点兴趣,这个项目也可以当做一个完整的数据分析+算法落地的练手样本来看。它的核心是:把国内白酒相关数据(品牌、价格、产地、评分、口感描述、热度指标等)采集整理之后,构建一个基于 Python 的个性化推荐系统,最终以 Web 界面的形式让用户看到“推荐了哪些酒、为什么推荐这些酒”。
对于正在做毕业设计的人来说,这个题目的定位很精准:数据部分有真实的业务含义,算法部分能体现“协同过滤 + 文本相似度 + 热度加权”的组合拳,功能上又能兼顾后台管理和前台交互。换句话说,它既有理论深度,又有工程落地,不是那种一眼假的“管理系统增删改查”。哪怕你自己从头写一遍,把每一行代码吃透,它也能成为你在求职面试中能讲清楚、扛得住追问的实战项目。
这里我先把项目的整体轮廓画出来,后续会按模块拆解,并且给出具体的实现思路和我在类似项目里踩过的坑。
2. 整体设计与技术选型思路
2.1 为什么用 Python 做推荐系统
选 Python 做这类系统,说实话是“当下最优解”,而不是唯一解。推荐系统的核心工作分三块:数据处理、算法模型、后端接口。Python 在这三块都有非常成熟的生态:Pandas 处理表格数据是公认的顺手,Scikit-learn 直接提供了余弦相似度、KNN、矩阵分解这些基础组件,Flask 或 FastAPI 写起接口来又快又轻。
换别的语言当然也能做,比如 Java 写推荐算法会牵扯到更重的工程框架,Node.js 则没有同等成熟的数据处理生态。对大多数毕设或者个人项目来说,Python 能把“从数据到接口”的链路压缩到最短,调试效率也高。我见过不少人纠结“用不用 Hadoop、Spark”,对这个量级的数据来说纯属自我加戏——一台笔记本完全足够,别把简单问题复杂化。
2.2 数据层的设计:白酒数据从哪来、存哪里
这个项目的根基是数据。关于数据来源,正规做法是爬取公开渠道的白酒信息,包括品牌官网、电商平台、垂直社区等,字段至少覆盖:酒名、品牌、香型(酱香、浓香、清香等)、度数、价格、产地、年份、评分、评论摘要。如果不想爬,也可以用构建好的模拟数据或者公开数据集来填充,但那样项目的说服力会打折扣。
存储方面我建议分两层:原始数据用 CSV 做备份,正式运行用 SQLite 或者 MySQL。CSV 的好处是数据交换方便,适合你反复清洗调试;数据库的好处是支持结构化查询和事务。对这个项目而言,SQLite 是最省事的选择,单文件、免安装、Python 内置操作支持。
这里有一个特别重要的设计决策:数据清洗必须单独封装成一个模块,不要和推荐算法混在一起。为什么?因为白酒价格字段可能带“元”“起”这类噪音字符,香型和度数存在同一款酒多种规格的情况,产地可能缺失。如果你不抽出一个独立的清洗函数去处理,后面每一次调用推荐算法时就都要面对这些脏数据,调试起来非常痛苦。
2.3 推荐策略的取舍:混合推荐才是正解
很多初学者一开始只想做“协同过滤”,看完《推荐系统实践》就兴冲冲写一个 UserCF 或 ItemCF。但实际跑起来你会发现,白酒这个品类和电影、图书有本质差异:用户的浏览和购买记录非常稀疏,大部分人只看过几个品牌的酒,冷启动问题特别严重。
所以这个项目的正确打开方式是混合推荐,至少包含三个分支:
- 基于物品的协同过滤:如果用户喜欢“A 品牌酱香型白酒”,就把“其他用户也喜欢的酱香型白酒”推荐过去。
- 基于内容的推荐:计算酒品之间在香型、度数、价格区间、产地、口感标签这些属性上的相似度,找出“和你收藏风格相近”的未看过酒。
- 热度加权:新用户没有任何行为数据时,按综合热度(销量、评分、口碑)排序推荐。
为什么必须三分支并行?因为单一算法都有致命短板:协同过滤在数据稀疏时推荐质量急剧下降;内容过滤容易造成“信息茧房”,用户永远只看到一种风格;纯热度推荐没有个性化可言。把它们加权融合之后,结果才比较像一个“懂酒的朋友”的推荐。
2.4 技术框架清单
| 层级 | 技术选型 | 说明 |
|---|---|---|
| 开发语言 | Python 3.8+ | 生态成熟稳定 |
| 数据处理 | Pandas + NumPy | 表格处理与向量运算 |
| 相似度计算 | Scikit-learn 或原生实现 | 余弦相似度为主 |
| 后端框架 | Flask | 轻量、上手快、文档多 |
| 前端页面 | 原生 HTML/CSS/JS 或 Vue 简化版 | 重点是展示推荐结果与打分反馈 |
| 数据库 | SQLite / MySQL | SQLite 适合演示,MySQL 加分 |
| 可视化 | ECharts | 展示数据统计报表 |
这套组合不是随便堆出来的,每选一样都有理由:Flask 比 Django 更轻,适合单页应用加若干接口;ECharts 生成的图表非常美观,能有效提升系统的“成品感”;SQLite 省去了安装数据库服务器的麻烦,下载代码就能直接跑。以后想扩展,也可以无缝切到 MySQL,改动量集中在数据库连接层。
3. 核心功能模块与实现拆解
3.1 数据采集模块
这个模块的任务是拿到“原始数据”。我从实践角度讲三种方式:
- 爬虫采集:对公开网页做请求解析。比较好爬的是静态渲染页面,直接请求 HTML 再解析;如果页面是动态加载,就需要用 Selenium 模拟浏览器操作。建议基于第三方库 Requests + BeautifulSoup 起步,不要一上来上 Scrapy。Scrapy 的异步引擎和中间件概念对新手负担偏重。
- 开放接口:有些平台直接提供 JSON 数据接口,返回的数据比 HTML 更容易处理。用浏览器开发者工具找到 XHR 请求,分析接口参数即可。
- 人工整理:如果只做原型,可以手动整理 100 条左右真实数据。给足字段覆盖度,样例如下:
| 酒名 | 品牌 | 香型 | 酒精度 | 价格(元) | 产地 | 评分 | 评论关键词 |
|---|---|---|---|---|---|---|---|
| 示例酱酒 A | 某品牌 | 酱香型 | 53 | 399 | 贵州 | 8.6 | 酱香突出, 回味悠长 |
| 示例浓香 B | 某品牌 | 浓香型 | 42 | 128 | 四川 | 8.2 | 窖香浓郁, 入口绵甜 |
不管你用哪种方式,数据落地之后要留个心眼:白酒数据的“规格”很碎,同一个品牌可能有礼盒装、单瓶装、整箱装,价格差别很大。我的建议是统一存储“单瓶最常被购买的规格”,否则推荐模型会把同一款酒当成多个物品,相似度计算就会被带偏。
3.2 数据清洗与特征工程
数据清洗是这整套系统里最琐碎但最关键的一环。你需要做这么几件事:
- 去除重复记录:同一种酒在不同源可能重复出现,以酒名+度数+规格作为联合唯一键去重。
- 类型统一:价格字段统一为浮点数,去掉“¥”“元”等前缀;度数统一为纯数字;评分统一到 0-10 分制。
- 缺失值处理:产地、口感描述缺失时,要么用众数填充,要么直接丢弃记录。我个人倾向于丢弃那些关键字段缺失超过一半的样本,因为它们对推荐结果贡献很小。
- 文本清洗:评论摘要和口感描述会有各种噪音词,比如“酒不错”这种废话,保留反而干扰相似度。我建议提取“酱香”“窖香”“绵柔”“醇厚”“清爽”这类酒类专业形容词或名词,降低无关词的权重。
特征工程方面,要把半结构化的文本转成模型能用的数值。做法可以分两条线:连续特征(价格、度数、评分)直接归一化到 0-1 区间;离散特征(香型、产地、品牌)用 One-Hot 编码。口感描述这类自由文本,用 TF-IDF 向量化之后,和前面的结构化特征拼接成一个总特征向量。
这里我给一个关键提示:特征拼接时,各特征的权重不是相等的。比如两个人都在意“香型”,价格差异反而没那么重要。所以你在做相似度计算之前,最好对每个特征列乘以一个权重系数。这个系数可以用人工经验定,比如香型权重 0.3、价格权重 0.2、口感文本权重 0.3、产地和品牌权重各 0.1。权重不同,最终的推荐结果差异会非常大。这部分我后面会给出一个具体的加权相似度计算示例。
3.3 推荐算法核心实现
我直接给你一个可运行的骨架代码,它实现了“基于物品的协同过滤 + 内容相似度 + 热度加权”的混合推荐。这里的相似度采用余弦相似度,加权方式用的是最常用的线性加权。
import numpy as np import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity def compute_similarity(df): # 结构化特征:香型、度数、价格、产地做数值化 df["feature_vector"] = df.apply( lambda row: [ row["price"] / 1000.0, row["degree"] / 60.0, 1.0 if row["flavor"] == "酱香型" else 0.0, 1.0 if row["flavor"] == "浓香型" else 0.0, 1.0 if row["flavor"] == "清香型" else 0.0, ], axis=1 ) # 文本特征:评论关键词 tfidf = TfidfVectorizer(token_pattern=r"\w+") text_vec = tfidf.fit_transform(df["comment_keywords"].fillna("")) # 结构化特征转矩阵 struct_vec = np.array(df["feature_vector"].tolist()) # 加权拼接 combined = np.hstack([struct_vec * 0.5, text_vec.toarray() * 0.5]) sim_matrix = cosine_similarity(combined) return sim_matrix def hybrid_recommend(df, target_id, top_k=5, user_click_history=None): sim_matrix = compute_similarity(df) target_index = df.index[df["id"] == target_id][0] # 内容相似度排序 sim_scores = list(enumerate(sim_matrix[target_index])) content_based = sorted(sim_scores, key=lambda x: x[1], reverse=True)[1:top_k+1] # 热度加权:评分和价格的综合热度 df["heat_score"] = df["rating"] * 0.7 + np.log1p(df["price"]) * 0.3 heat_based = df.sort_values("heat_score", ascending=False).head(top_k) # 混合:取并集再按加权分排序 result = {} for idx, score in content_based: result[df.iloc[idx]["id"]] = score * 0.6 for idx in heat_based.index: if df.loc[idx, "id"] not in result: result[df.loc[idx, "id"]] = 0.4 else: result[df.loc[idx, "id"]] += 0.4 sorted_items = sorted(result.items(), key=lambda x: x[1], reverse=True) return [item_id for item_id, _ in sorted_items[:top_k]]这段代码的逻辑是这样的:先为每个酒品构建一个“结构化特征 + 文本特征”的混合向量,用余弦相似度算出酒和酒之间的相似度;然后对目标酒选出最像它的几款;同时把评分高、价格适中的热门酒也拉进来;最后按 0.6 和 0.4 的权重融合两路结果。这套结构和很多生产级推荐系统是一致的,只不过真实场景中的权重往往是离线训练或者线上调参得到的。
关于用户行为数据,如果系统里有用户点击记录,就用“与你浏览过的酒最相似”的逻辑来做,把浏览历史的平均特征向量作为 query,再与全量酒的向量算相似度。如果用户是首次访问,没有历史行为,那直接走热度分支推荐。
3.4 后端 Flask 接口设计
整个 Web 系统我建议拆成三个接口,结构非常清晰:
| 接口 | 请求方式 | 功能 |
|---|---|---|
| /api/recommend | POST | 传入用户ID或酒品ID,返回推荐列表 |
| /api/wines | GET | 获取酒品列表、支持筛选 |
| /api/feedback | POST | 用户对推荐结果点赞/收藏,用于后续优化 |
推荐接口的入参建议长这样:
{ "user_id": 1001, "target_wine_id": 5, "top_k": 6 }后端拿到参数后,从数据库读取数据,调用推荐函数,然后把结果组织成 JSON 返回。一个很关键的工程细节是:在推荐函数外面套一层缓存。如果两次请求的入参完全相同,就不要再重新计算相似度矩阵了。相似度矩阵在这个数据规模下计算一次就够,把它保存在内存或者磁盘里,响应速度会从秒级降到毫秒级。毕设答辩时,这个细节绝对能让你在“系统性能如何优化”的问题上多一个答法。
3.5 前端展示与交互设计
前端的核心不是炫技,而是让推荐的逻辑“看得见”。我建议首页放三块内容:顶部是一个搜索框,中间是“为你推荐”的结果列表,每张卡片展示酒名、香型、度数、价格、相似度百分比,右侧是推荐理由(例如“因为你收藏了 XXX”)。底部放一个数据统计面板,用 ECharts 画柱状图或饼图,展示推荐系统里热门品牌分布、香型占比等。
这个设计的意图很明确:推荐系统最大的痛点就是“黑盒感”。如果用户只看到结果不知道为什么,信任感会大打折扣。你加了“相似度”“推荐理由”这些外显信息,系统就从一个测试玩具变成了一个有说服力的产品。
交互上还有两个加分项:第一,用户可以对推荐结果点“喜欢/不喜欢”,这些反馈写入数据库,为后续“根据反馈修正推荐”预留空间;第二,把筛选功能做出来,用户可以按香型、价格区间、度数筛选,这在答辩时也很容易被问到“你系统做了哪些功能拓展”。
4. 实操过程与关键节点记录
4.1 我跑通整个项目的过程
我先说整体节奏,免得你走弯路。拿到这个项目后,我建议按下面的顺序推进:
- 先整理 100 条数据,字段齐全,编码为 UTF-8。这一步花半天就够。
- 把清洗模块写好,输出一份“干净”的 CSV。同时写几个单元测试验证清洗逻辑没跑偏。
- 实现相似度计算和推荐算法,先在命令行跑通,打印推荐结果。
- 搭建 Flask 应用,把推荐函数包成接口,再用 Postman 测试接口返回。
- 写前端页面,用模拟数据先调通展示,再对接真实接口。
- 最后做可视化报表,打磨交互细节。
我实际跑下来,最容易出问题的节点是第 3 步到第 4 步的转换。因为算法函数里直接操作的是 DataFrame,而接口层拿到的可能是 JSON 或数据库查询结果,两者之间存在字段类型不一致的问题。比如数据库里价格是字符串,Pandas 读进来之后没转 float,相似度计算直接报错;或者中文编码没有统一,推荐结果里出现乱码。提前做好数据类型的强制转换,能防掉一半的 bug。
4.2 关键参数与计算细节
关于权重系数的选择,我再展开讲一下。假设现在有一款目标酒,价格 300 元,酱香型,评分 8.5。另一款酒价格 350 元,酱香型,评分 8.8。在结构化特征里,它们的价格(归一化后)和香型几乎一致,评分差 0.3,所以余弦相似度可能高达 0.95 以上,这属于“同类替代品”推荐。但如果是一款 300 元的酱香型对比一款 800 元的酱香型,虽然香型相同,但价格差异过大,余弦相似度按照我的权重设计会降到 0.7 左右。
这就是为什么要加权而不是直接算原始向量的余弦相似度。直接算原始向量,价格 300 和价格 800 之间的欧式距离会被价格特征主导,香型和口感文本被稀释掉。权重系数的作用相当于你在手动强调“哪些维度对用户的决策更重要”。真实商业系统里,这个系数靠用户行为数据回归拟合,但在这个项目规模下,凭经验设定完全够用,答辩时你能讲出这个设计意图就非常加分。
我建议你跑几组不同的权重组合,把结果记录下来对比,比如:
| 权重方案 | 香型权重 | 价格权重 | 口感文本权重 | 推荐结果特征 |
|---|---|---|---|---|
| 方案A | 0.3 | 0.2 | 0.3 | 香型高度集中,价格跨度大 |
| 方案B | 0.2 | 0.4 | 0.2 | 价格接近,香型覆盖更广 |
| 方案C | 0.3 | 0.1 | 0.4 | 口感相关性强,价格敏感性弱 |
方案 A 适合推荐“替代品”,方案 B 适合推荐“花钱差不多但换换口味”,方案 C 适合强调“风格偏好一致”。把这个对比表放进论文分析章节,效果会很好。
4.3 遇到的典型问题及排查思路
先说一个我在实际调试中反复踩的坑:中文乱码。数据采集时不少页面用的是 GBK 编码,你用 UTF-8 打开就是一堆“锟斤拷”。解决方法是爬虫里先判断页面编码,再做解码转换。在 Python 里可以用 Requests 的response.apparent_encoding拿到猜测编码,然后手动指定response.encoding = 'gbk'或者'utf-8'。
第二个坑是相似度矩阵的稀疏问题。当白酒数量只有几十条时,文本特征向量维度很高,但大部分文档只出现过极少数的词,余弦相似度容易出现“完全没有相似项”的情况。解决思路是提高文本特征的最小词频下限,过滤掉只出现一次的词语,或者干脆扩大数据集到 200 条以上。
第三个坑是接口返回的 JSON 格式不对。Pandas 的to_dict()默认会返回嵌套结构,前端解析不友好。解决办法是在返回前统一转换:
def format_result(records): return [{"id": r["id"], "name": r["wine_name"], "score": round(float(r["score"]), 2)} for r in records]手动控制字段名和字段顺序,前端稳定性的提升立竿见影。
4.4 可复现的环境配置清单
为了让项目在你机器上也能直接跑起来,我把环境配置列出来。推荐使用 Python 3.8 以上版本,创建虚拟环境后安装以下核心依赖:
| 依赖包 | 版本建议 | 用途 |
|---|---|---|
| Flask | 2.x | 后端框架 |
| pandas | 1.5.x | 数据处理 |
| numpy | 1.24.x | 数值计算 |
| scikit-learn | 1.2.x | 相似度计算与向量化 |
| requests | 2.28.x | 数据采集 |
| beautifulsoup4 | 4.11.x | HTML 解析 |
| pyecharts | 2.x | 可视化(可选) |
安装命令统一为:
pip install flask pandas numpy scikit-learn requests beautifulsoup4 pyecharts数据库方面直接用 SQLite 内置模块,不需要额外安装。启动项目只需要运行python app.py,浏览器访问http://127.0.0.1:5000即可。
5. 几条独家避坑心得
5.1 数据来源一定不要“拿来主义”
网上确实有人直接分享白酒数据集,但直接下载来做毕设会有一个很尴尬的问题:答辩时导师如果问“这批数据怎么来的,字段含义是什么”,你大概率支支吾吾。我的建议是:公开数据集可以做算法验证,但最终项目里一定要展示一部分你自己的数据采集逻辑,哪怕只采集了 100 条,也能体现你完整走通了采集-清洗-建模-推荐链路。数据量大小不是重点,流程完整才是重点。
5.2 推荐评估不能只看“看起来合理”
很多做推荐系统的人自己看一眼推荐结果,觉得“挺合理的”,就认为系统完成了。这在毕设或者面试里都是不够的。你可以做一个简单的离线评估:把数据按 8:2 划分训练集和测试集,用训练集训练物品相似度,测试集里隐藏用户对某些酒的行为,看系统能否把这部分隐藏数据排在推荐列表前面。排序质量可以用 Top-N 命中率或者平均倒数排名来衡量。
对我上面给的混合推荐模型,一个最简单的评估指标是“命中率”:
hit_count = 0 for user in test_users: rec_list = recommend_for_user(user, top_k=10) if any(wine_id in rec_list for wine_id in test_set[user]): hit_count += 1 hit_rate = hit_count / len(test_users)这段代码把“系统推荐了 10 个结果,测试集里用户真实喜欢的酒出现在结果中的概率”算了出来。哪怕命中率只有 10%-20%,你也能解释这个数字在稀疏数据场景下已经比随机推荐好得多。这个评估结果放进论文里,比你口说“效果不错”有力得多。
5.3 代码讲解能力比代码本身更重要
标题里写了“一条龙定制”,这意味着项目提供方最后通常需要给你录制或者现场讲解代码。我见过不少同学拿到代码能跑通,但被问到“ItemCF 的原理是什么”“为什么用余弦相似度”就卡壳了。其实这正好是项目价值的体现。你要做的不是背代码,而是把三个点讲清楚:
- 协同过滤的核心假设:相似的用户喜欢相似物品,或者相似的物品会被同一用户喜欢。
- 相似度的本质:两个向量在空间中的夹角余弦值趋近 1 说明方向一致。
- 混合推荐的动机:单一算法有冷启动和稀疏问题,多路召回加权融合能取长补短。
这三句话能顺着讲出来,整个项目的可信度立刻就不一样了。
6. 项目如何扩展成一等一的亮点
如果时间充裕,我强烈建议你做两个方向的扩展,成本不高但收益明显。
第一个方向是“基于用户行为的实时反馈”。现在的混合推荐只用了静态数据和热度信息,如果你能引入一个简单的用户点击序列——比如用户点击了某款酒、收藏了某款酒——并把最近一次行为作为推荐依据,系统就从“离线推荐”升级成了“在线个性化推荐”。实现起来也不复杂,只要给每个用户维护一个最近行为的特征向量,实时加权即可。
第二个方向是引入“知识图谱”的概念。白酒的产地、香型、品牌之间存在天然的关系网络:茅台镇偏好酱香,四川多浓香,山西多清香。你可以用一两个表格存储这些实体关系,做一个简单的图查询,比如“你喜欢的品牌还产过哪些其他香型的酒”。虽然它不能做到大规模图计算,但展示出的架构视野比单纯算法更开阔,答辩的时候导师一般都会眼前一亮。
第三个更实用的扩展是导出推荐报告。系统生成一个 PDF 或 HTML 推荐清单,包含酒名、价格区间、口感特征和推荐理由,这个结果可以直接拿去做“个性化酒单”产品原型。很多实际场景里,用户并不想在线反复点来点去,而是需要一份可以直接照着买的清单。这个角度贴近真实商业需求,加分效果很明显。
我个人在实际操作中的体会是:做推荐系统,算法模型只占四成功夫,剩下的六成都花在数据质量和系统细节上。数据不干净,再花哨的模型也是空转;系统交互不友好,再准确的推荐也没有人用。把这两条基本盘抓牢,哪怕算法用的是最基础的余弦相似度,整个项目的完成度也能超过大多数“看起来高级”但支离破碎的方案。如果你现在正在准备毕设或者想用这个方向做简历项目,我建议你直接按上面这套路径复现一遍,多跑几遍权重组合,记录每一组的结果差异,你会对这个系统有真正的掌控感。