☰
基于Python的智能旅游推荐系统,毕设实现与答辩攻略
2026/10/3 3:15:59 网站建设 项目流程

简介:这份Python实战项目资源以智能旅游推荐系统为核心,面向需要完成毕业设计、课程设计或期末项目的计算机相关专业学生,解决从需求分析、数据库设计到前后端联调、运行演示的完整流程。项目基于Python 3.7开发,采用HTML搭配Vue构建前端界面,MySQL存储业务数据,包含项目源码、数据库脚本及配套工具,功能完善、界面直观,适合中等基础学习者直接运行体验,也可作为二次开发的原型。资源包大小为20.19MB,共797个文件,以45个Python后端源码、53个Vue/HTML前端页面、53个CSS样式、53个JavaScript脚本为主体,并配SQL数据库文件、图标图片及说明文档。目录内附安装、运行等批处理脚本,便于快速搭建环境。目前已有132人学习下载,该案例覆盖典型管理系统常见模块,适合想完整走一遍项目开发流程、提升数据库操作与全栈整合能力的读者。

1. 智能旅游推荐系统这个毕设选题,为什么每年都有人做

拿到这套“Python毕业设计-基于python的智能旅游推荐系统”压缩包的人,多半是正在为选题发愁的学生。智能旅游推荐系统恰好卡在一个舒服的位置:它不像纯管理系统那样只有增删改查没亮点,也不像深度学习课题那样对数据和硬件要求高,算法上有一个协同过滤就够讲,工程上又有 Web、数据库、爬虫三块可展示,答辩老师能问的每一层都有东西可答。简单说,这个标题对应的是一款基于 Python Web 框架、内置推荐算法、带 MySQL 数据库的景点推荐应用,做完它能覆盖一个标准毕设的完整链路:需求分析、数据库设计、算法实现、系统测试。适合三类人:想省选题时间的、想低成本拿高分的、以及准备转行做数据或后端、需要一个完整项目来练手的新手。这套资源里打包的东西,基本也按这三块走:源码负责功能、数据库脚本负责数据地基、教程负责让你在答辩前能把它讲圆。

2. 先把系统拆开:功能边界、技术选型与最小项目骨架

2.1 功能边界:能参加答辩的旅游推荐系统最少要有哪几块

很多毕设翻车,不是因为算法太烂,而是功能边界没划清楚,做着做着就把系统做成“景点信息管理系统”——那跟旅游推荐已经没关系了。一个能站住脚的智能旅游推荐系统,至少要包含下面五块,按优先级排列:

模块核心功能是否必须答辩常见追问点
用户模块注册、登录、个人信息必须密码怎么存的,session 怎么管理
景点模块景点列表、详情、搜索、分类筛选必须分页怎么做,搜索用没用到索引
行为采集模块收藏、评分、浏览记录必须行为数据存哪张表,怎么去重
推荐模块个性化推荐、热门推荐必须算法原理、冷启动怎么处理
后台管理景点 CRUD、用户管理建议权限怎么控制,有没有做防注入

用户模块和景点模块是一切的底座,行为采集模块是推荐算法的数据来源,推荐模块才是你区别于“管理系统”的核心亮点。我见过不少学生把大量时间花在后台上,结果推荐模块只写了一个“按评分排序”——那是热门榜,不是推荐系统,评委一句话就能问穿。

2.2 技术选型:Flask 还是 Django,推荐算法用什么切入

这个标题限定死了 Python,剩下的选型只要围绕“与毕设匹配”来定,没必要追求生产级。我一般这样搭:

  • Web 框架用 Flask。它轻、路由直观、适合把推荐算法单独拆成模块来写,学生也容易在教程里讲清楚 Django 的 admin 后台虽然省事,但算法逻辑和框架耦合太深,答辩时容易被追问“ORM 底层怎么实现的”,Flask 配合原生 SQL 反而更好讲。
  • 数据库用 MySQL。它是最常见的毕设数据库,面试也认;千万别为了省事用 SQLite,答辩老师会直接问“为什么不用 MySQL,是不是不会装”。用 MySQL 还能顺带讲清楚连接池、字符集这些加分点。
  • 推荐算法用协同过滤(基于物品的 ItemCF)而不是深度学习。原因很实际:毕设的数据量撑不起深度学习,而 ItemCF 原理简单、可解释性强,你能在五分钟内把“因为你看过 XX,所以推荐 YY”讲明白。深度学习模型在黑匣子里,评委一问参数就卡壳。
  • 前端用服务端模板 + Bootstrap,不做前后端分离。少一套跨域和鉴权问题,把精力留给推荐算法。VSCode 里如果配了 Python 环境但跑不起来,多半是解释器选错了,这个放到第 5 章讲。

2.3 最小可运行的项目结构与启动顺序

这类毕设包解压后,代码目录我习惯组织成下面的样子,也建议你一旦自己动手就按这个结构建:

travel_recommend/ ├── app.py # Flask 应用入口,路由全部在这 ├── config.py # 配置:数据库地址、密钥、推荐参数 ├── models.py # 数据访问层:连接池 + 增删改查 ├── recommend.py # 推荐算法:ItemCF + 热度兜底 ├── requirements.txt # 依赖清单 ├── sql/ │ └── init.sql # 建库建表脚本,含种子数据 ├── data/ │ └── scenic.csv # 景点数据源(爬虫或手工整理) ├── templates/ │ ├── base.html │ ├── index.html # 首页 + 推荐结果页 │ └── scenic_detail.html └── static/ └── css/ js/ # Bootstrap 和前端资源

先装环境,再建库,最后启动,顺序别反:

cd travel_recommend python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt mysql -uroot -p < sql/init.sql python app.py

两条踩坑提示:第一,pip install装的是哪个 Python 的包,取决于你激活的是哪个虚拟环境,VSCode 右下角解释器选错会出现“明明装了却 import 不到”的玄学问题;第二,mysql -uroot -p < sql/init.sql这行命令在 Windows 的 CMD 里如果报mysql 不是内部或外部命令,说明 MySQL 的 bin 目录没加进系统 PATH,用全路径执行,或者直接在 Navicat 里导入脚本。等看到Running on http://127.0.0.1:5000,项目就算跑通了。先跑通再改代码,这是所有项目的第一条纪律。

3. 数据库设计与数据准备:推荐系统真正的地基

3.1 三张核心表:用户表、景点表、行为表的设计要点

推荐系统的数据库设计和普通管理系统有个关键差异:一定要有一张独立的“行为表”,记录用户和景点之间的交互。只把收藏操作挂在用户表下面,推荐算法取数会非常痛苦。核心三张表这样建:

CREATE DATABASE IF NOT EXISTS travel_recommend DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE travel_recommend; CREATE TABLE user ( user_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE scenic ( scenic_id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, city VARCHAR(50) NOT NULL DEFAULT '未知', category VARCHAR(50) DEFAULT '自然风光', price DECIMAL(10,2) DEFAULT 0, rating FLOAT DEFAULT 0, description TEXT, image_url VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_city (city), KEY idx_category (category) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE user_behavior ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, scenic_id INT NOT NULL, behavior_type ENUM('view','collect','score') NOT NULL, score TINYINT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id), KEY idx_scenic (scenic_id), KEY idx_user_scenic (user_id, scenic_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表时有三个点值得多写两行说明。一是字符集必须用 utf8mb4,只写 utf8 的话遇到生僻地名或特殊符号会直接报错;二是user_behavior表尽量加一个(user_id, scenic_id)联合索引,推荐算法要反复按用户取历史行为,这个索引能省一半时间;三是我没有建物理外键,只保留逻辑关联。毕设阶段物理外键会拖慢批量写入,而且删除景点时容易因为外键约束报错,逻辑外键在代码里控制就够了,这个取舍答辩时可以主动讲给评委听。

ENUM类型建议保留,它把行为类型限定死在 view(浏览)、collect(收藏)、score(评分)三种,代码里不用写一堆 if 判断来兜非法值。score字段只在 behavior_type 是 score 时才有意义,浏览和收藏行为它是 0,这样设计是为了后面推荐算法取数时统一口径。

3.2 连接池与增删改查:PyMySQL 连接 MySQL 的标准姿势

毕设里最常见的低分写法是每次请求都pymysql.connect()一次,用完再 close。这在小流量下能跑,但被问到“数据库连接池怎么优化”就哑火了。用 DBUtils 的 PooledDB 把连接缓存起来,代码量只多三行,面试能多聊五分钟:

# models.py import pymysql from dbutils.pooled_db import PooledDB pool = PooledDB( creator=pymysql, maxconnections=10, mincached=2, maxcached=5, blocking=True, host='127.0.0.1', port=3306, user='root', password='你的密码', database='travel_recommend', charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor ) def get_scenic_by_id(scenic_id): conn = pool.connection() try: with conn.cursor() as cursor: cursor.execute("SELECT * FROM scenic WHERE scenic_id=%s", (scenic_id,)) return cursor.fetchone() finally: conn.close() # 注意:这里不是真断开,是还给连接池

参数说明:maxconnections是连接池能维护的最大连接数,Flask 开发模式下开 10 足够;mincached是启动时就预热的空闲连接数,设 2 避免第一个请求进来时现场建连;blocking=True表示连接被借完时请求排队等待而不是直接报错。SQL 里的参数一律用%s占位符传值,绝对不要拼字符串,这是防 SQL 注入的基本功。

增删改查的另外三个操作同理,只是把 SQL 换成INSERT、UPDATE、DELETE。推荐算法取数时最常用的一条 SQL 是“按用户取行为”,这里有个细节:同一用户可能对同一景点既收藏又评分,推荐算法取数要用GROUP BY user_id, scenic_id配合MAX(score)取一条,不然后面算相似度矩阵时会踩“重复索引”的坑。

3.3 数据从哪来:爬虫采集与 CSV 导入的取舍

景点数据是推荐系统的原料,没有真实景点数据,整个系统就是个空壳。常见做法是爬马蜂窝或携程的城市景点页,但反爬强度这几年一直在涨。我给出的建议是分两步走:先用小规模爬虫验证流程,再用手工整理 CSV 补充。爬虫骨架长这样:

# spider.py(示意,选择器按实际页面结构调整) import requests import time import pandas as pd from bs4 import BeautifulSoup HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' 'AppleWebKit/537.36 Chrome/120.0 Safari/537.36', 'Accept-Language': 'zh-CN,zh;q=0.9', } def fetch_city_spots(city_code, pages=3): rows = [] for page in range(1, pages + 1): url = f'https://example.com/poi/{city_code}?page={page}' resp = requests.get(url, headers=HEADERS, timeout=10) if resp.status_code != 200: continue soup = BeautifulSoup(resp.text, 'html.parser') for item in soup.select('.poi-item'): # 选择器要按实际页面改 name = item.select_one('.name').get_text(strip=True) rows.append({'name': name, 'city': city_code}) time.sleep(1.5) # 频率控制,别用 0.01s 这种自杀间隔 return pd.DataFrame(rows)

爬虫里最容易被忽略的是频率控制。目标站点封你通常不是因为你用了 Python,而是因为每秒十几次请求太像扫描器。time.sleep(1.5)是底线,追求稳妥用 2~3 秒,爬个三五百条景点数据也就多等几分钟。BeautifulSoup 的选择器没有统一的写法,F12看真实页面结构再改.poi-item、.name这种类名就是全部工作量。

如果爬虫中途被验证码拦了,别硬刚反爬,直接换手工整理:把景点的 name、city、category、price 填进 CSV,用 pandas 清洗后导入:

import pandas as pd df = pd.read_csv('data/scenic.csv') df = df.drop_duplicates(subset=['name']) df['city'] = df['city'].fillna('未知') df['price'] = pd.to_numeric(df['price'], errors='coerce').fillna(0) df = df[df['price'] >= 0] # index=False:别把行号写进库里;utf-8-sig:给 Excel 打开不乱码 df.to_csv('data/scenic_clean.csv', index=False, encoding='utf-8-sig')

清洗这一步是给你兜底的后悔药:跑推荐算法前数据必须干净,drop_duplicates(subset=['name'])去掉同名景点,to_numeric(...errors='coerce')把“免费”“待定”这种非数值价格变成 NaN 再填 0。别小看这几行,很多人的推荐结果里出现“价格 0 的 5A 级景区排名第一”,问题不是算法,是数据里混了一堆脏值。

4. 推荐算法落地:从协同过滤公式到可调用的接口

4.1 基于物品的协同过滤(ItemCF)原理与最小实现

基于物品的协同过滤是毕设里性价比最高的算法,核心思想就一句话:如果用户喜欢 A 景点,而 B 和 A 被同一批用户喜欢过,那就把 B 也推荐给他。注意这里的“同一批用户”,不是地域或分类相同,而是历史行为模式相似——这是协同过滤和纯分类推荐最本质的区别。

实现分三步:构造用户-景点行为矩阵、计算景点间相似度、按用户历史行为加权生成推荐列表。上代码:

# recommend.py import numpy as np import pandas as pd from sklearn.metrics.pairwise import cosine_similarity def build_similarity_matrix(behavior_df): """behavior_df: 必须含 user_id, scenic_id, score 三列""" pivot = behavior_df.pivot_table( index='user_id', columns='scenic_id', values='score', aggfunc='max' ).fillna(0) # 关键一步:按用户均值中心化,消除“有人全打5分,有人全打3分”的尺度差 centered = pivot.sub(pivot.mean(axis=1), axis=0) # 转置后算景点与景点的余弦相似度 sim = cosine_similarity(centered.T) sim_df = pd.DataFrame(sim, index=pivot.columns, columns=pivot.columns) # 热门惩罚:行为数越多的景点,相似度权重压得越低,避免推荐结果被头部景点霸占 popularity = pivot.astype(bool).sum(axis=0) penalty = pd.Series(1.0 / np.log1p(popularity), index=pivot.columns) sim_df = sim_df.multiply(penalty, axis=0).multiply(penalty, axis=1) return np.clip(sim_df.to_numpy(), 0, 1), list(pivot.columns)

三个参数和运算细节要弄清楚。aggfunc='max'是对同一用户同一景点的多条行为取最大评分,避免收藏和评分两条记录导致透视表报重复索引错误;sub(pivot.mean(axis=1), axis=0)是按行减均值,把用户的评分习惯归一,否则一个爱打低分的用户会拉低所有他喜欢景点的相似度;最后的np.clip(..., 0, 1)把负相似度截断为 0,负相关在推荐里没有意义,只会干扰排序。

有了相似度矩阵,推荐生成就快了:

def item_based_recommend(user_id, behavior_df, sim_matrix, scenic_ids, top_n=10): history = behavior_df[behavior_df['user_id'] == user_id] scores = {} for _, row in history.iterrows(): sid = row['scenic_id'] if sid not in scenic_ids: continue idx = scenic_ids.index(sid) # 当前景点和所有候选景点的相似度权重 × 用户对它的评分 for cand_idx, sim_val in enumerate(sim_matrix[idx]): cand_id = scenic_ids[cand_idx] if cand_id == sid or sim_val <= 0: continue scores[cand_id] = scores.get(cand_id, 0.0) + sim_val * row['score'] # 过滤掉看过的,按累积分排序取前 N seen = set(history['scenic_id']) ranked = sorted( ((sid, s) for sid, s in scores.items() if sid not in seen), key=lambda x: x[1], reverse=True ) return [sid for sid, _ in ranked[:top_n]]

打分逻辑是标准 ItemCF:对用户历史里的每个景点,把“它和目标候选景点的相似度”乘以“用户对它的评分”,再累加。用户给过 5 分的景点,它的相似景点会得到更大权重,这符合直觉。过滤掉seen里的景点是必须的,否则用户会反复收到自己已经收藏过的东西,演示时极其尴尬。

4.2 冷启动兜底:热度推荐与规则候选集的配合

协同过滤的致命弱点是冷启动——新用户一条行为都没有,history为空,scores为空,返回空列表。这不是 bug,是算法边界,所以接口层必须做兜底。我一般用热度推荐来扛冷启动,规则很简单:按“有多少个不同用户收藏或评分过”排序,而不是按平均评分排序。

def hot_recommend(cursor, top_n=10): sql = """ SELECT scenic_id, COUNT(DISTINCT user_id) AS user_cnt FROM user_behavior WHERE behavior_type IN ('collect', 'score') GROUP BY scenic_id ORDER BY user_cnt DESC, AVG(score) DESC LIMIT %s """ cursor.execute(sql, (top_n,)) return [row['scenic_id'] for row in cursor.fetchall()]

为什么用COUNT(DISTINCT user_id)而不是AVG(score)?因为评分均值会被极少数高分刷上去,一个只有两条 5 分的新景点会排在有一千条 4.8 分老景点前面,这是推荐系统最常见的翻车现场之一。用“人数排序 + 均值做第二排序键”能同时保证覆盖面和口碑。推荐接口里把两条路合起来:

def recommend_for_user(user_id, behavior_df): if behavior_df[behavior_df['user_id'] == user_id].empty: return hot_recommend(), 'hot' recs = item_based_recommend(user_id, behavior_df, sim_matrix, scenic_ids) if not recs: return hot_recommend(), 'hot' return recs, 'itemcf'

返回的第二个标识位是给前端和答辩演示用的,告诉页面这次推荐是“热门推荐”还是“猜你喜欢”,这也让评委一眼看到冷启动处理逻辑。注意一个边界:老用户产生了新兴趣,ItemCF 结果可能很短甚至为空,这时也要回落热度推荐,不能硬展现一个空列表。

4.3 把推荐结果接到 Web 接口与页面展示

算法算出来的是一串scenic_id,要变成用户能看的页面,还需要回查景点信息并渲染。Flask 接口这样接:

# app.py from flask import Flask, session, jsonify, render_template from models import pool, get_scenic_by_id from recommend import recommend_for_user, build_similarity_matrix, load_behavior app = Flask(__name__) app.secret_key = 'set-a-random-key-here' app.config['JSON_AS_ASCII'] = False # 否则接口返回中文变 \uXXXX @app.route('/api/recommend') def api_recommend(): user_id = session.get('user_id') if not user_id: return jsonify({'code': 401, 'msg': '请先登录'}) behavior_df = load_behavior() rec_ids, source = recommend_for_user(user_id, behavior_df) items = [get_scenic_by_id(sid) for sid in rec_ids] return jsonify({'code': 200, 'items': items, 'source': source})

app.config['JSON_AS_ASCII'] = False不加的话,接口返回的景点名全是\uXXXX编码,前端能用但调试时肉眼看不了,属于开发期恶心你、答辩时丢分的小坑。load_behavior()把行为表全量读成 DataFrame,在数据量小于几万行时完全够用,不用搞实时流式计算,那个是面试官才追问的话题。

前端展示用一个简单的 Jinja2 模板,把推荐理由直接写在卡片上:

<!-- templates/index.html 片段 --> <div class="row"> {% for item in items %} <div class="col-md-4 card" onclick="location.href='/scenic/{{ item.scenic_id }}'"> <h4>{{ item.name }}</h4> <span class="badge">{{ item.city }}</span> <span class="badge">{{ item.category }}</span> <p>评分 {{ item.rating }} · 门票 ¥{{ item.price }}</p> <p class="text-muted">{{ item.description[:50] }}...</p> </div> {% endfor %} </div>

不要试图在推荐卡片里塞太多字段,用户扫一眼能认出“城市、分类、评分”就够了。真正的加分项是加一行“推荐理由”,比如“因为你收藏了西湖,所以推荐西溪湿地”——这个在答辩时比任何公式都有说服力,第 6 章会讲具体做法。

5. 常见问题排查与避坑记录

5.1 环境与运行时的四个高频坑

现象一:pip install -r requirements.txt之后,python app.py报ImportError: cannot import name 'soft_unicode' from 'markupsafe'。原因:老版本 Werkzeug 依赖的 MarkupSafe 接口在新版被移除,常见于 Python 3.9+ 配了最新版依赖。解决:固定住兼容组合pip install werkzeug==2.3.8 markupsafe==2.0.1,然后重启。这类坑属于“能跑就行,别追新”,毕设项目锁死依赖版本是最稳的。

现象二:代码里import MySQLdb报ModuleNotFoundError。原因:MySQLdb 是 Python 2 时代的库,Python 3 下没有官方包。解决:统一用 PyMySQL,并在app.py或models.py开头加一行pymysql.install_as_MySQLdb(),这样老代码里所有MySQLdb的引用都能跑通,不用逐行改。

现象三:VSCode 里运行项目,明明pip list里有 Flask,运行时却报ModuleNotFoundError: No module named 'flask'。原因:左下角选中的 Python 解释器不是虚拟环境里的那个,装包的 Python 和跑代码的 Python 根本不是同一个。解决:打开命令面板(Ctrl+Shift+P),输入Python: Select Interpreter,选venv目录下的解释器;命令行一律用python -m pip install而不是裸pip install。这条能救回一半以上的“环境玄学”问题。

现象四:mysql -uroot -p < sql/init.sql在 Windows 下报mysql 不是内部或外部命令。原因:MySQL 的 bin 目录没进系统 PATH。解决:用全路径执行,比如"C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe" -uroot -p < sql/init.sql,或者打开 Navicat 手动执行这条 SQL 脚本。另外注意:SQL 文件路径里如果含中文,CMD 可能识别不了,把项目路径改成纯英文最省事。

5.2 推荐效果与数据质量的三个坑

现象五:导入数据库后,页面上景点名全是问号。原因:导入 SQL 脚本时客户端连接没指定字符集,数据被按 latin1 存了。解决:命令行导入时加--default-character-set=utf8mb4,连接串里也手动指定charset='utf8mb4'——只靠建表语句写 utf8mb4 不够,连接层也要对齐。已经导进脏数据的,DROP DATABASE重建库重新导入,别想着ALTER TABLE转换,血的教训不值得重演。

现象六:跑build_similarity_matrix时报ValueError: cannot reindex on duplicate labels。原因:user_behavior 表里存在同一个人对同一景点的多条记录,pivot_table 不知道取哪条。解决:读取时先behavior_df = behavior_df.groupby(['user_id','scenic_id'], as_index=False)['score'].max(),保证每个用户-景点对只有一行,再去 pivot。这句去重逻辑建议直接写进load_behavior(),一劳永逸。

现象七:推荐结果全是热门景点,十个里有八个是同一个城市的 5A 级。原因:没做热门惩罚和评分中心化,热门景点和所有景点都相似,打分时永远排在前面。解决:回看第 4.1 节的中心化和log1p惩罚,两项缺一不可。只加惩罚不加中心化,少数高分用户的偏好会被放大;只做中心化不加惩罚,头部景点照样通吃。

5.3 排查工具与调试习惯

遇到推荐算法结果不对时,先别改参数,先看数据链路。我习惯按三层查:第一层查行为表里的原始记录,用SELECT user_id, COUNT(*) FROM user_behavior GROUP BY user_id LIMIT 10确认有用户真有行为数据;第二层查相似度矩阵,直接打印sim_df.loc['西湖'].sort_values(ascending=False).head(10),看看西湖的相似景点里有没有明显不相关的;第三层查推荐打分,抽一个用户手动算一条,比对着代码查是哪里多乘了或少加了权重。

把这三条 SQL 和调试打印固化成一个小脚本debug_recommend.py,每次改完算法跑一遍,比每次都用浏览器点半天快得多。推荐算法是个黑匣子不假,但你可以主动把黑匣子的中间输出拿出来看,不要靠肉眼猜结果。最后能稳定复现的坑,永远是“数据问题优先于算法问题” —— 先确认行为数据干净,再动算法参数。

6. 答辩前把系统从“能跑”提到“能讲”

6.1 演示前先算两个指标:覆盖率与多样性

很多人的毕设演示只展示“推荐列表挺像样”,但评委一句“你这个推荐效果到底怎么量化”就卡住了。论文里没必要上离线 AUC——你的数据量根本没建模测试集——但有两个指标在答辩现场算得出来又有说服力:覆盖率(推荐结果能覆盖多少不同景点)和类别多样性(推荐结果在多少个分类间分布)。十行代码能出结果:

def coverage_and_diversity(rec_ids): total_scenic = scenic_count() # 景点总数 rec_scenic = len(set(rec_ids)) # 推荐结果里不同景点数 coverage = rec_scenic / total_scenic # 多样性:类别数量 / 推荐总数,按 category 字段去重 distinct_cats = distinct_category_count(rec_ids) diversity = distinct_cats / max(len(rec_ids), 1) return round(coverage, 3), round(diversity, 3)

这两个数不追求高,追求的是“有数可讲”。覆盖率低说明推荐集中,你可以用热门惩罚解释;多样性低说明用户当前兴趣聚焦,你可以用协同过滤本身的特性解释。手里有指标,任何追问都能接住。

6.2 一个让评委点头的“推荐理由”组件

在推荐卡片上补一行字,权重比翻十倍:“因为你去过/收藏了 A,所以把相似的 B 推荐给你”。实现方式很简单,在item_based_recommend里记录每个候选景点的最大相似来源,回传前端展示。这一行字直接命中评委最想听的“可解释性”——它证明你不是拿了个黑匣子糊弄,而是真的理解 ItemCF 的推荐链路。我带去答辩的几个项目,评委每次都在这个组件上多问了两分钟,然后转向下一项——这已经是毕设答辩里很好的结果了。

最后说一个习惯:拿到这类毕设包,我从来不会解压完就冲动改代码,而是先按第 2 章的启动顺序跑通一遍,再按第 3、4 章的人肉跑一遍数据链路,最后才动手加自己的功能。代码能跑不代表你懂它,答辩翻车的从来不是不会写的人,而是没想清楚就上台的人。希望你把这个项目当成一块跳板,跑通之后自己加一个“相似景点地图”或“行程天数筛选”的小功能,把它变成你自己的作品。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询