news 2026/9/18 14:22:59

Python旅游推荐系统实战:从ItemCF协同过滤到Flask部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python旅游推荐系统实战:从ItemCF协同过滤到Flask部署

简介:基于Python的旅游推荐系统毕业设计论文文档(docx格式),面向计算机相关专业学生、毕业设计作者以及需要构建旅游推荐系统的小型项目开发者。文档完整覆盖论文规范章节,从研究背景与现状、开发技术选型(Django、MySQL)、系统架构与E-R数据库设计,到用户管理、信息展示、推荐算法、反馈模块及系统测试均有阐述,能够帮助读者快速把握旅游推荐系统的整体设计流程与实现要点。包内仅含1个docx文件,压缩包约1.6MB,内容紧凑便于阅读与二次修改。目前已有332人在线学习,适合作为毕业论文写作参考、开题报告辅助资料或系统开发前的方案依据,可为同类课题节省从零梳理框架的时间。

1. 一个论文标题背后的完整工程:Python 旅游推荐系统不只是写个推荐算法

「基于python的旅游推荐系统的设计与实现」这种文件名在毕业设计存档里很常见,但它对应的工程通常不是「装个 Flask、跑个页面」那么简单。旅游推荐与电商推荐的差异在于:出行决策低频,用户一年撑死积累几十次收藏或下单行为;一个景点同时受城市、季节、预算、假期约束,几乎不存在「买了又买」的强信号。所以这套系统的难点不在算法本身,而在数据表怎么设计、相似度矩阵如何解释、冷启动拿什么兜底、推荐结果用什么离线指标证明有效。下面按我实际做这类系统的路径来讲:从数据建模、协同过滤选型,到 Flask 接口和评测脚本,全部是可复现的 Python 方案。

2. 旅游推荐系统的设计落点:数据模型、画像字段与召回通道

写「设计与实现」类文档,最忌讳一上来就写user_cf(user_id, spot_id)函数。评审或答辩时被问得最多的三个问题:数据从哪来、字段为什么这么建、推荐分数由几部分组成。先把这三件事定死,后面代码就是填空。

2.1 数据来源:爬虫、开源数据集还是手工构造最小可用集

常见做法是爬虫抓取公开旅游平台的景点信息和用户点评,配合开源数据集做结构参照。这里有两个现实约束:一是爬虫要控制频率、遵守目标站点的 robots 协议,测试阶段别对线上接口做高并发请求;二是公开数据集多为电影或商品评分,缺少城市、季节、门票这类旅游专属字段,直接套用会让后续规则权重没有数据支撑。

我一般会先手工构造一个「最小可用集」:30 个城市、100 个景点、800 条行为记录。这个数据量用 SQLite 完全跑得动,足以验证协同过滤、热度召回、预算筛选三条链路是通的。论文里写「数据规模」时也说得清:小规模数据用来说明方案可行性,真实场景下数据接入方式不变,只是存储换 MySQL、行为表按天分区。

关键点是行为表必须设behavior_type字段,不要只留评分。旅游场景里评分极其稀疏,大量有效信号藏在浏览、收藏、下单这三类隐式反馈里,它们的权重完全不同。

2.2 三张核心表的字段设计与建表语句

系统里最稳定的部分是表结构,它在论文里对应「数据库设计」章节。三张表如下:

表名核心字段说明
useruser_id, age, city, budget, travel_styletravel_style 存「亲子/休闲/穷游」这类画像标签
spotspot_id, name, category, city, ticket, season, labelslabels 存「海滨」「古镇」「雪山」等逗号分隔标签
behaviorbehavior_id, user_id, spot_id, behavior_type, create_timebehavior_type 枚举 0/1/2,对应浏览/收藏/下单

建表时就把索引建好,避免后续推荐查询全表扫描:

CREATE TABLE spot ( spot_id INTEGER PRIMARY KEY, name TEXT NOT NULL, category TEXT, city TEXT, ticket REAL DEFAULT 0, season TEXT, labels TEXT ); CREATE TABLE behavior ( behavior_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, spot_id INTEGER NOT NULL, behavior_type INTEGER DEFAULT 0, create_time TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE INDEX idx_behavior_user ON behavior(user_id); CREATE INDEX idx_behavior_spot ON behavior(spot_id); CREATE INDEX idx_behavior_time ON behavior(create_time);

create_time用 SQLite 的本地时间作为默认值,插入时不关心时间赋值;召回查询要按时间窗口过滤,所以create_time必须建索引。tags字段故意用逗号分隔文本而不是单独建关联表,是因为旅游景区标签量级小,关联表收益不高,查询时用LIKE足够,还能简化论文里的 ER 图。

2.3 推荐流程分层:召回、排序、解释

工程上不做一个「大而全」的算法算总分,而是拆成三段流水线:

  1. 召回层:从地域热度、类别热度、协同过滤三条通道各取 TopN 候选,合并去重。
  2. 排序层:对候选做加权融合,加入预算、季节、城市距离约束,需要时用 0-1 背包把候选压进用户预算。
  3. 解释层:给每条推荐附加「命中标签」,前端直接展示「因为你在杭州,推荐附近古镇」,这是论文里最容易出彩的功能点。

这个分层模型同时满足两个诉求:算法部分方便做 A/B 对照实验;论文架构图里每个模块都有明确输入输出。下面先用 SQL 把最简单的热度召回通道落地。

2.4 用 SQL 直接实现地域热度召回通道

热度召回是协同过滤之外的兜底通道,也天然是冷启动用户的初始推荐。按「近 30 天行为数」排序,取目标城市前 20 个景点:

SELECT s.spot_id, s.name, s.city, s.category, COUNT(b.behavior_id) AS hot_score FROM spot s LEFT JOIN behavior b ON b.spot_id = s.spot_id AND b.create_time >= datetime('now', '-30 days') WHERE s.city = ? GROUP BY s.spot_id, s.name, s.city, s.category ORDER BY hot_score DESC LIMIT 20;

LEFT JOIN是关键细节:没有产生任何行为的景点会以hot_score = 0出现在结果尾部,这比INNER JOIN直接丢弃新景点更安全。时间窗口 30 天是推荐系统里常用的经验值,景区数据日更,窗口太长热门景点会统治结果,太短则噪声大。条件里的参数用?占位符,避免字符串拼接注入问题,SQLite 和 MySQL 都支持这种写法。

召回解决的是「候选集从哪来」,下一步要解决「候选之间如何排序」。

3. 基于用户行为的协同过滤实现:ItemCF 的相似度矩阵与行程规划

旅游推荐系统论文里,算法章节的核心通常是协同过滤。选型要先讲清楚理由:旅游景点数量远小于用户数量,物品更新频率低,所以静态度量「景点与景点的相似度」比「用户与用户的相似度」更稳定,也更好解释。

3.1 为什么主推 ItemCF 而不是 UserCF

UserCF 给用户推荐「和他行为相似的人喜欢的东西」,在新闻、短视频这类内容快速流转的场景有效。旅游景点一年到头变化不大,且一个城市的景点数量有限,UserCF 的用户相似度矩阵会随着新行为反复抖动。反过来,ItemCF 的相似度矩阵可以离线算好、增量更新,线上只做查表和聚合,响应速度快得多。

论文实验里可以把两个算法都实现,最后只报 ItemCF 的结果,理由就写:旅游决策重在「相似物品扩展」,UserCF 在同一城市内行为稀疏时精度下降明显。这一句话既显专业,又避开 UserCF 的冷启动硬伤。

3.2 用 Python 构建 ItemCF 相似度矩阵

基于行为表构造「用户-景点」倒排表,再计算景点间 cosine 相似度:

from collections import defaultdict import math def build_item_sim(behavior): """ behavior: list of (user_id, spot_id, behavior_type) """ # 1. 用户到景点的倒排表 user_items = defaultdict(set) for user_id, spot_id, _ in behavior: user_items[user_id].add(spot_id) # 2. 景点热度与共现矩阵 item_pop = defaultdict(int) co_count = defaultdict(int) for user_id, items in user_items.items(): for spot_a in items: item_pop[spot_a] += 1 for spot_b in items: if spot_a == spot_b: continue co_count[(spot_a, spot_b)] += 1 # 3. cosine 相似度 + 热门惩罚 sim = {} for (spot_a, spot_b), co in co_count.items(): denominator = math.sqrt(item_pop[spot_a] * item_pop[spot_b]) if denominator == 0: continue score = co / denominator # 对热门景点做衰减,log 平滑避免除零 score *= 1.0 / math.log(1 + item_pop[spot_b]) sim[(spot_a, spot_b)] = score return sim

相似度计算里最容易被忽略的是热门惩罚项。只看 cosine,故宫这种热门景点会和几乎所有景点产生高共现,1.0 / math.log(1 + item_pop[spot_b])会把被比较侧的高热度分数拉低。behavior列表只取前两个字段,是因为浏览、收藏、下单行为已经在构造阶段统一映射为「一次交互」,权重差异放到排序阶段处理。

3.3 召回排序:行为权重加权与 TopK 候选生成

拿到相似度矩阵后,为用户生成推荐候选:

def recommend_for_user(user_id, user_behavior, sim, top_k=20): interacted = {spot_id for uid, spot_id, _ in user_behavior if uid == user_id} # 行为权重:浏览0.2,收藏0.5,下单1.0 weight_map = {0: 0.2, 1: 0.5, 2: 1.0} user_action_weight = defaultdict(float) for uid, spot_id, btype in user_behavior: if uid == user_id: user_action_weight[spot_id] += weight_map.get(btype, 0.2) score = defaultdict(float) for spot_a, base_weight in user_action_weight.items(): for (x, y), sim_val in sim.items(): if x == spot_a: target = y elif y == spot_a: target = x else: continue if target in interacted: continue score[target] += base_weight * sim_val # 按分数排序,取 zui 高的 top_k return sorted(score.items(), key=lambda kv: kv[1], reverse=True)[:top_k]

interacted集合用于排除用户已经去过的景点,这是旅游推荐与电商推荐在业务规则上的差异:电商允许重复购买,景点不会重复消费同一地点。行为权重直接把收藏的下单倾向映射成数值,三个值之间的倍数关系不需要精确,论文里可以给出一组对比实验说明权重对指标的影响。

3.4 把 TopN 候选压进预算:0-1 背包做行程规划

排序分数只解决「哪些景点值得去」,还要解决「用户预算内选哪几个」。这是个标准 0-1 背包问题:每个景点有门票成本ticket和推荐价值score,在总预算内取价值最高的组合。

def knapsack_select(spots, budget): n = len(spots) # dp 记录每个预算下的最大价值 dp = [0] * (budget + 1) # choice 记录当前预算下是否选了第 i 个景点 choice = [[False] * (budget + 1) for _ in range(n + 1)] for i in range(1, n + 1): cost = int(spots[i - 1]['ticket']) value = spots[i - 1]['score'] for b in range(budget, cost - 1, -1): if dp[b - cost] + value > dp[b]: dp[b] = dp[b - cost] + value choice[i][b] = True # 回溯,找出被选中的景点 selected = [] b = budget for i in range(n, 0, -1): if choice[i][b]: selected.append(spots[i - 1]) b -= int(spots[i - 1]['ticket']) return selected

背包容量budget单位是「元」,遍历时用range(budget, cost - 1, -1)逆序更新,保证每个景点最多被选一次;二维choice表记录决策路径,用于最后的回溯。这套代码在论文里对应「行程规划模块」,比单纯展示 TopN 列表更接近真实需求。数据量不大时不必优化空间复杂度,可读性优先。

4. 基于 Python 的工程实现:Flask 接口、冷启动策略与跨浏览器展示

算法链路跑通后要做成可访问的系统。工程环境建议 Python 3.9 以上版本,用 pycharm 或 vscode 配置虚拟环境后安装flaskrequests两个第三方库即可;数据库继续用 SQLite 文件,避免论文答辩现场还要启动 MySQL 服务。

4.1 用 Flask 封装推荐接口

接口设计遵循「一个用户一个请求拿全量结果」的原则,把召回、排序、背包三个步骤串成一条服务端链路:

from flask import Flask, request, jsonify import sqlite3 app = Flask(__name__) def query_db(sql, args=()): conn = sqlite3.connect('travel.db') conn.row_factory = sqlite3.Row cur = conn.cursor() cur.execute(sql, args) rows = [dict(row) for row in cur.fetchall()] conn.close() return rows @app.route('/api/recommend', methods=['GET']) def recommend(): user_id = request.args.get('user_id', type=int) city = request.args.get('city', default='') if not user_id: return jsonify({'code': 400, 'msg': 'user_id is required'}), 400 # 1. 取用户行为 rows = query_db( 'SELECT user_id, spot_id, behavior_type FROM behavior WHERE user_id=?', (user_id,) ) # 2. 无行为:走城市热度冷启动 if not rows: spots = query_db( 'SELECT spot_id, name, city, ticket FROM spot WHERE city LIKE ? ORDER BY hot_score DESC LIMIT 20', (f'%{city}%',) ) return jsonify({'code': 0, 'data': spots}) # 3. 有行为:协同过滤 + 背包筛选 spots = recommend_for_user(user_id, rows, sim, top_k=20) spots_with_info = [fill_spot_info(sid, score) for sid, score in spots] selected = knapsack_select(spots_with_info, budget=800) return jsonify({ 'code': 0, 'data': selected, 'strategy': 'itemcf' if rows else 'hot_rank' }) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)

fill_spot_info负责把spot_id补全成景点名、城市、门票等信息,可以从spot表逐条查询,也可以一次性WHERE spot_id IN (...)批量取回。接口里budget=800是写死的默认预算,真实场景应作为参数传入,论文里可以描述成「默认按人均 800 元一日游标准规划」。debug=True只在开发期打开,答辩演示时也建议开着,方便当场看报错。

4.2 冷启动的三个兜底策略

新用户没有任何行为记录,协同过滤直接失效。按三个层次兜底:

  1. 地域热度:用户在前端选了城市,就返回该城市hot_score最高的 20 个景点。
  2. 类别偏好:前端可让用户选择「亲子/古镇/登山」等兴趣标签,对应spot.labels LIKE '%古镇%'这类查询。
  3. 兜底列表:没有城市和标签,返回全局热门 Top20。

这三个策略的优先级要写清楚:用户主动选择的标签 > 城市维度热度 > 全局热度。工程实现就是几段if ... elif,但论文里值得用一小节单独描述每个策略的数据来源和适用条件。

4.3 推荐结果展示:跨浏览器兼容的前端页面

推荐系统论文的「系统实现」章节通常含界面截图,前端页面不必复杂,但要求评审机器上的浏览器能正常渲染。用原生 HTML +fetch最稳妥,避免引入 Node 构建链:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>旅游推荐系统</title> </head> <body> <div id="rec-list"></div> <script> const userId = 1; const city = '杭州'; // 老版本浏览器降级:XMLHttpRequest 兜底 fetch const request = window.fetch ? fetch(`/api/recommend?user_id=${userId}&city=${city}`).then(r => r.json()) : new Promise((resolve) => { const xhr = new XMLHttpRequest(); xhr.open('GET', `/api/recommend?user_id=${userId}&city=${city}`); xhr.onload = () => resolve(JSON.parse(xhr.responseText)); xhr.send(); }); request.then(json => { const html = json.data.map(item => `<div>${item.name} - ${item.city} - 门票¥${item.ticket}</div>` ).join(''); document.getElementById('rec-list').innerHTML = html; }); </script> </body> </html>

兼容性处理的重点是window.fetch判断,老内核浏览器没有fetch时自动切到XMLHttpRequest。前端不引入框架,不依赖构建工具,评审现场即使网络不通,页面也能靠本地 Flask 服务跑起来。展示层面只输出推荐结果就不会出错,后续要加用户反馈按钮,只需在map里追加一个收藏按钮并绑定事件。

4.4 把论文「系统测试」变成可执行的接口用例

论文必备「系统测试」一章,与其截图凑数,不如写一段requests冒烟测试脚本,输出结果粘贴到论文里更可信:

import requests cases = [ ('无用户ID', {'city': '杭州'}, 400), ('冷启动用户', {'user_id': 999, 'city': '杭州'}, 200), ('正常推荐', {'user_id': 1, 'city': '杭州'}, 200), ('不存在的景点', {'user_id': 1, 'city': '不存在城市'}, 200), ] for name, params, expected in cases: resp = requests.get('http://127.0.0.1:5000/api/recommend', params=params) assert resp.status_code == expected, f'{name} 状态码异常' if expected == 200: data = resp.json() assert 'data' in data, f'{name} 缺少data字段' print(f'{name}: PASS (status={resp.status_code})')

测试用例表可以直接搬进论文,格式为「用例名称 — 输入参数 — 预期结果 — 实际结果」。这里特别加了一条「不存在的城市」用例,预期返回空列表而不是 500,因为LIKE '%不存在城市%'匹配不到任何行,返回自然的空数组。

5. 离线评测:用 P@K 与 NDCG 验证推荐效果,而不是靠感觉调参

推荐系统写没写对,不能用「看起来挺准」评价。论文实验章节需要一组离线指标。最后一节给出一个完整的评测流程,顺便讲清楚三个必调的坑。

5.1 按时间切分训练集与测试集

行为数据按create_time排序,前 80% 做训练,后 20% 做测试。不能用随机切分:随机切分等于用未来行为预测过去,评测结果虚高。

import sqlite3 conn = sqlite3.connect('travel.db') cur = conn.cursor() cur.execute('SELECT COUNT(*) FROM behavior') total = cur.fetchone()[0] cur.execute('SELECT * FROM behavior ORDER BY create_time LIMIT ?', (int(total * 0.8),)) train = cur.fetchall() cur.execute('SELECT * FROM behavior ORDER BY create_time LIMIT -1 OFFSET ?', (int(total * 0.8),)) test = cur.fetchall()

LIMIT -1 OFFSET n是 SQLite 取「从第 n 行到末尾」的写法。训练集喂给协同过滤构建相似度矩阵,测试集里每个用户已交互的景点作为标准答案,系统推荐结果需要命中它们。

5.2 三个核心评估指标与实现

def precision_at_k(rec_list, ground_truth, k=10): rec_hit = len(set(rec_list[:k]) & set(ground_truth)) return rec_hit / k def recall_at_k(rec_list, ground_truth, k=10): rec_hit = len(set(rec_list[:k]) & set(ground_truth)) return rec_hit / len(ground_truth) if ground_truth else 0 def ndcg_at_k(rec_list, ground_truth, k=10): dcg = 0 idcg = 0 for i, spot_id in enumerate(rec_list[:k]): if spot_id in ground_truth: dcg += 1.0 / (i + 1) # 注意是 i+1 不是 i+2,保持第一名为满分位置 for i in range(min(k, len(ground_truth))): idcg += 1.0 / (i + 1) return dcg / idcg if idcg > 0 else 0

P@K 衡量推荐列表前 K 个里有多少命中,分数偏低正常,因为用户去过的景点本身有限;NDCG 额外惩罚排得靠后的命中项,推荐列表第 10 位命中只贡献1/10的增益。aggregate 时取所有测试用户的平均值,并记录用户数。

5.3 调参方向与实验记录表

参数取值范围影响趋势建议值
TopK 召回数10 / 20 / 50K 越大,Recall 越高,P@K 先升后降20
热门惩罚 λ0 / 1 / 2惩罚过强,长尾曝光多但精度下降1
行为权重 order0.5 / 1.0 / 2.0下单行为权重越大,结果越偏向热门1.0

网格搜索的方式是:固定其他参数,遍历top_k = [10, 20, 50],打印每组 P@10、Recall@10、NDCG@10,选 NDCG 最高的一组。这里有个常见误用:precision_at_k的 K 和top_k召回数不是一个东西,召回数决定候选集合大小,评测 K 决定截断位置,论文里要写清楚。

最后一个技巧:把评测代码和指标输出写成一个evaluate.py模块,每次调参后直接跑一遍,输出 CSV 或表格,实验记录自动沉淀,论文里的「不同参数下的指标对比表」就是从这里来的。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 14:21:45

单片机毕业设计-基于 STM32 的物联网养殖环境监测与远程控制系统开发 基于 STM32 单片机的智能鱼池自动投喂增氧系统设计(012308)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/18 14:21:26

限流与免费额度,public-apis 调研 Agent 用 TaoToken 管模型 Key

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 14:16:25

计算机单片机毕设实战-基于 STM32 的环境参数采集与 Android APP 远程监控系统 基于 STM32 的多阈值智能养殖设备控制系统设计(012308)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/18 14:16:16

VimWiki折叠/大纲功能指南:快速浏览长Wiki页面的键盘技巧

VimWiki折叠/大纲功能指南&#xff1a;快速浏览长Wiki页面的键盘技巧 【免费下载链接】vimwiki Personal Wiki for Vim 项目地址: https://gitcode.com/GitHub_Trending/vi/vimwiki VimWiki 是一款运行在 Vim 里的个人 Wiki 工具&#xff0c;让你用纯文本管理笔记、任务…

作者头像 李华