ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python实现旅游推荐系统:协同过滤与混合推荐算法详解

Python实现旅游推荐系统:协同过滤与混合推荐算法详解 简介智能旅游推荐系统毕业设计项目包面向计算机相关专业学生或正在准备毕设、课程设计的开发者。项目基于Python技术与MYSQL数据库完整实现了旅游资讯、景点信息、酒店信息、行程分享、交流论坛等核心模块可作为旅游类管理系统的开发范本。包含799个文件压缩包约25.71MB主要涉及Python源码py/pyc、Vue前端页面、JavaScript样式脚本、SQL数据库脚本及docx说明文档并附带安装.bat、运行.bat等快速启动脚本便于理解前后端交互与部署流程。包内还有对应的毕业论文文档覆盖需求分析、数据库设计、详细设计等论述能帮助读者梳理项目文档写法。目前已有229人学习下载适合需要快速入手整套毕业设计源码、参考课程设计实现方式的学习者。1. 智能旅游推荐系统Python源码和说明文档的zip包里装着什么用户在旅游App里搜成都有人看到宽窄巷子、有人看到青城山差别不在信息在系统对用户偏好的判断。智能旅游推荐系统负责三件事把行为日志整理成偏好信号给候选POI打分排序再按季节热度输出Top-K列表。落到Python技术栈就是pandas清洗数据、scikit-learn算相似度、Flask暴露接口源码加说明文档打包成zip就是标题里的交付物。这种方案依赖少、数据可自造适合做毕设或简历项目数据建模、算法选型、离线评估这些核心环节一个不落。zip里的说明文档决定别人能不能把项目跑起来Python版本、依赖安装、数据格式、接口调用参数少写一项接手的人就会卡住。源码部分最关键的则是推荐算法选型和相似度矩阵的构建方式这两块决定了系统推荐出来的景点是像那么回事还是随机播放。下面按数据建模、算法实现、工程化、评估调优四步展开代码均为本地可复现的最小实现读完能跑通推荐demo也清楚近邻数、Top-K这些参数改哪里、效果差在哪里。2. 数据建模旅游推荐系统的行为日志与POI特征怎么整理2.1 显式评分与隐式行为用户偏好怎么量化成可计算的矩阵推荐系统最常见的数据形态是用户-物品评分矩阵行是用户、列是POI、单元格是偏好强度。但旅游场景里主动打分的用户极少App里更常见的是点击、收藏、搜索、停留时长这类行为日志。前者叫显式反馈后者叫隐式反馈建模方式完全不同混在一起处理会把信号弄脏。反馈类型数据来源常见形态稀疏程度处理方式显式反馈打分、评论星级1-5分极稀疏直接作为标签使用隐式反馈点击、收藏、搜索、停留计数、时长相对稠密按行为类型加权映射我的习惯是把不同行为映射成不同权重的伪评分一次点击算1分一次收藏算3分一次搜索算0.5分。这样既保留了行为之间的强度差异又不会让高频低价值行为淹没真实偏好。停留时长做置信度更稳——同一个POI停留两分钟和停留半小时前者大概率是误触可以直接过滤掉。2.2 pandas清洗行为日志从原始CSV到用户-POI稀疏矩阵原始behavior_log.csv通常只有user_id、poi_id、action、timestamp四列但直接拿去算相似度会踩三个坑重复行为导致计数膨胀、无意义行为污染信号、缺失值让pivot报错。下面这段清洗代码是这类推荐系统标准开头py文件里通常叫preprocess.py。import pandas as pd logs pd.read_csv(data/behavior_log.csv) print(logs.shape, logs[action].value_counts()) # 1. 过滤低置信行为只保留点击和收藏 logs logs[logs[action].isin([click, fav])] # 2. 同一用户对同一POI多次行为保留时间戳最新一条 logs logs.sort_values(timestamp).drop_duplicates( subset[user_id, poi_id], keeplast) # 3. 行为映射成隐式评分点击1分收藏3分 logs[rating] logs[action].map({click: 1, fav: 3}) # 4. 透视成 user-item 稀疏矩阵缺失补0 matrix logs.pivot_table(indexuser_id, columnspoi_id, valuesrating, fill_value0) print(f矩阵形状: {matrix.shape}, 稀疏度: {(matrix0).mean().mean():.2%})逻辑说明第2步的drop_duplicates用subset指定去重键keeplast保证保留最近一次行为这在时间序列数据里是关键——用户第一次点可能是误触最后一次通常代表真实意图。第4步的pivot_table把长表转成宽表fill_value0把没交互过的格子补零后续cosine_similarity才能直接吃这个矩阵。稀疏度一般会高到95%以上这是正常现象不要强行稠密化稀疏矩阵才是推荐系统的常态。2.3 POI特征工程类别、价格带与季节标签的编码方式协同过滤只看行为但推荐结果需要可解释冷门新景点也没有行为数据这两件事都要靠POI属性特征解决。真实项目里特征列很多旅游推荐最常用的就五个类别、城市、价格带、最佳季节、标签文本。特征字段示例值编码方式说明category自然风光/人文古迹/主题乐园one-hot内容相似度的主因子city北京/成都/杭州one-hot跨城推荐无意义强约束price_levelfree/low/mid/high序数编码保持价格高低次序best_season春/夏/秋/冬one-hot季节错配直接拉低体验tags亲子/摄影/徒步多热编码自由标签补充语义poi pd.read_csv(data/poi_features.csv) # 类别、城市、季节做 one-hotprefix 控制列名前缀避免冲突 onehot pd.get_dummies( poi[[category, city, best_season]], prefix[cat, city, season]) # 价格带保序映射顺序信息不丢失 price_map {free: 0, low: 1, mid: 2, high: 3} onehot[price_level] poi[price_level].map(price_map) features pd.concat([poi[[poi_id]], onehot], axis1) print(features.shape)get_dummies的prefix参数很必要三个字段都可能出现成都这种值没有前缀列名会直接冲突。price_level用整数映射而不是one-hot因为贵和便宜之间存在天然次序one-hot会把次序信息丢掉。城市先别急着编码进向量——推荐阶段直接用城市字段过滤候选集比把城市放进相似度计算更有效这也是旅游推荐和电商推荐的一个明显差异。3. 推荐算法实现协同过滤、内容推荐与混合策略的Python代码3.1 基于用户的协同过滤Python里找口味相近的人UserCF思路直白A和B的历史行为高度重合那B喜欢的、A没去过的POI大概率A也会喜欢。第一步用余弦相似度算用户两两之间的相似度第二步取Top-N个邻居第三步按相似度加权聚合邻居的正反馈POI。余弦相似度吃的是矩阵的行向量用户向量越接近、夹角越小相似度越接近1。from sklearn.metrics.pairwise import cosine_similarity user_sim cosine_similarity(matrix) # matrix 是 user-item 矩阵 user_sim_df pd.DataFrame(user_sim, indexmatrix.index, columnsmatrix.index) def user_cf_recommend(user_id, top_k10, neighbor_n20): 基于用户的协同过滤找最像的20个邻居聚合他们喜欢的POI if user_id not in user_sim_df.index: return [] # 无历史用户交给热门兜底 sims user_sim_df[user_id].drop(user_id).sort_values(ascendingFalse) neighbors sims.head(neighbor_n) scores {} for neighbor, sim in neighbors.items(): for poi, rating in matrix.loc[neighbor].items(): if rating 0 and matrix.loc[user_id, poi] 0: scores[poi] scores.get(poi, 0) sim * rating ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return [poi for poi, _ in ranked[:top_k]]参数说明neighbor_n控制近邻规模取值太小推荐结果窄、太大噪声多旅游场景我先从20起调sim*rating是加权聚合的核心邻居相似度越高、评分越高对最终分数的贡献越大。drop(user_id)必须做否则用户自己和自己的相似度1.0会统治整个结果推荐列表全是邻居里恰好重合的那几个热门景点。3.2 基于物品的协同过滤旅游场景为什么比UserCF更稳做旅游推荐我一般默认用ItemCF而不是UserCF三个理由。第一POI数量远少于用户数量物品相似度矩阵规模小可以离线算好存成pkl线上只查表第二用户偏好变得快但故宫和天坛类似这种景点关系几年不变矩阵可以低频率更新第三推荐结果天然可解释——因为你收藏了故宫推荐天坛用户看得懂。代码和UserCF对称只是把矩阵转置了一次。item_matrix matrix.T # item-user 矩阵 item_sim cosine_similarity(item_matrix) item_sim_df pd.DataFrame(item_sim, indexitem_matrix.index, columnsitem_matrix.index) def item_cf_recommend(user_id, top_k10, sim_top_n20): 基于物品的协同过滤从用户去过的POI出发扩展相似POI user_row matrix.loc[user_id] seen set(user_row[user_row 0].index) if not seen: return [] scores {} for poi in seen: sim_pois (item_sim_df[poi].drop(poi) .sort_values(ascendingFalse).head(sim_top_n)) for cand, sim in sim_pois.items(): if cand in seen: continue scores[cand] scores.get(cand, 0) sim ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return [poi for poi, _ in ranked[:top_k]]两段代码的差异很直观UserCF每次请求要遍历邻居的评分行ItemCF只需要找用户历史POI的相似列后者在模型预计算后响应更快。sim_top_n的意思是每个种子POI最多取20个相似项防止热门POI的高相似度把整个推荐列表刷屏。顺带一提这两个函数的返回值都是POI ID列表评估脚本和接口层可以共用不用为每个场景单独写排序逻辑。3.3 内容推荐兜底TF-IDF向量化与余弦相似度计算ItemCF的一个盲区是冷门新POI——没有任何交互记录物物相似度矩阵里它的行和列全是零。内容推荐不看行为直接比较POI的属性文本把类别、城市、标签、介绍拼成一段描述TF-IDF向量化后算用户画像和候选POI的余弦相似度。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity poi[doc] (poi[category] poi[city] poi[tags] poi[description]) vec TfidfVectorizer(max_features3000, stop_wordsenglish) tfidf vec.fit_transform(poi[doc]) # 用户画像历史正反馈POI向量的均值 seen_mask poi[poi_id].isin(seen) user_profile tfidf[seen_mask].mean(axis0) # 每个POI与画像的余弦相似度 content_scores cosine_similarity(user_profile, tfidf).flatten()这里有个容易踩的坑中文描述直接进TfidfVectorizer会被按单字切分效果很差。常见做法是装jieba把description分词后用空格连接再喂给向量器。max_features3000限制词典大小防止长尾词造成维度爆炸mean(axis0)是mean pooling用历史POI向量的平均作为用户画像简单有效作为基线足够用。3.4 混合推荐与热门兜底权重怎么设才不打架冷启动永远存在新用户没有任何行为UserCF和ItemCF都返回空列表。生产系统的常规做法是线性加权混合把ItemCF分数、内容分数、热门度各自归一化到0-1后按权重相加。权重分配要看业务目标旅游场景我用过比较稳的一组策略适用对象权重作用物品协同过滤行为≥5条的用户0.5主排序精度担当内容推荐新POI、冷门景点0.3提升覆盖和解释性热门榜新用户、无行为0.2兜底保证有输出def hybrid_recommend(user_id, top_k10): itemcf_n normalize(itemcf_scores(user_id)) content_n normalize(content_scores_for(user_id)) pop_n normalize(popularity_scores()) final {} all_pois set(itemcf_n) | set(content_n) | set(pop_n) for poi in all_pois: final[poi] (0.5 * itemcf_n.get(poi, 0) 0.3 * content_n.get(poi, 0) 0.2 * pop_n.get(poi, 0)) ranked sorted(final.items(), keylambda x: x[1], reverseTrue) return [poi for poi, _ in ranked[:top_k]]normalize这一步不能省。三个分数量纲完全不一样ItemCF是相似度累加可能到几十TF-IDF余弦在0-1之间热门度是原始点击数。不归一化直接加权等于是让ItemCF单方面决定排序。权重的调试顺序我一般固定先不动ItemCF的0.5只调内容分和热门分观察新POI的曝光量变化再回头微调主权重。4. Python源码工程化目录结构、接口设计与说明文档的zip标准4.1 项目目录怎么拆训练、预测、接口四层分离zip解压后别人第一眼看的就是目录。推荐系统项目最忌讳把所有逻辑塞进一个main.py训练要跑半小时接口每个请求都要算一遍相似度数据路径写死在代码里换台机器就崩。我常用的目录结构是四层分离每层职责单一换数据、换模型、换接口都不互相牵连。travel-recsys/ ├── data/ # 原始数据与样例输出 │ ├── behavior_log.csv │ ├── poi_features.csv │ └── sample_output.csv ├── src/ # 核心源码 │ ├── preprocess.py # 数据清洗与特征构建 │ ├── train.py # 训练物品相似度产出模型文件 │ ├── recommend.py # 推荐逻辑供接口调用 │ ├── evaluate.py # 离线评估脚本 │ └── api.py # Flask接口 ├── models/ # 训练产物 │ └── item_sim.pkl ├── docs/ # 说明文档 │ └── README.md └── requirements.txt # 依赖清单锁定版本训练和推理分开是这套结构的核心逻辑train.py离线把物品相似度矩阵算好存成pklapi.py启动时一次性加载到内存之后每个请求只做查表聚合几百毫秒内返回。如果每次请求都现场调cosine_similarity用户量稍微上来接口就超时这是最常见的性能败笔。4.2 训练脚本与Flask推荐接口的代码组织train.py要能以命令行方式独立运行产出可复用的模型文件api.py要足够薄只做参数解析、调用推荐函数、序列化返回。下面两段源码对应zip里最常出现的两个入口# src/train.py —— 训练入口 import pandas as pd from sklearn.metrics.pairwise import cosine_similarity def build_item_sim(behavior_path, model_path): logs pd.read_csv(behavior_path) logs[rating] logs[action].map({click: 1, fav: 3}) matrix logs.pivot_table(indexuser_id, columnspoi_id, valuesrating, fill_value0) item_sim pd.DataFrame(cosine_similarity(matrix.T), indexmatrix.columns, columnsmatrix.columns) item_sim.to_pickle(model_path) print(f模型已保存: {model_path}, POI数: {item_sim.shape[0]}) if __name__ __main__: build_item_sim(data/behavior_log.csv, models/item_sim.pkl)# src/api.py —— 推理入口 from flask import Flask, request, jsonify import pandas as pd app Flask(__name__) item_sim pd.read_pickle(models/item_sim.pkl) # 启动时加载一次 app.route(/recommend, methods[GET]) def recommend(): user_id request.args.get(user_id) top_k int(request.args.get(top_k, 10)) result item_cf_recommend(user_id, top_k, item_sim) return jsonify({user_id: user_id, pois: result}) if __name__ __main__: app.run(host0.0.0.0, port8000)逻辑说明train.py把数据路径和模型路径作为参数暴露而不是写死在函数体里后续在命令行或定时任务里换参数重跑都不需要改代码。api.py在模块导入时就把item_sim加载好避免每个请求重复读pkltop_k从query参数读取方便前端按需调整推荐列表长度。item_cf_recommend把相似度矩阵作为第三个参数传进去推荐函数不依赖全局变量单元测试好写也方便在测试里注入不同的矩阵。4.3 说明文档README和requirements.txtzip包能不能跑起来看这里标题里的说明文档四个字实际交付时就是README.md加一份requirements.txt。很多zip包源码没问题接手人跑不起来问题基本都出在文档缺信息。requirements.txt必须锁定版本不锁版本的下场是半年后pandas升级改了接口训练脚本原地报错排查半天发现是依赖问题。flask3.0.0 pandas2.1.4 scikit-learn1.3.2 numpy1.26.2 jieba0.42.1说明文档最少覆盖六块内容每块都有对应的常见坑README章节必须写清的内容常见的坑环境要求Python版本、操作系统只写Python33.8和3.12的依赖不兼容安装步骤python安装后建虚拟环境、pip install -r requirements.txt漏虚拟环境依赖装进全局环境数据说明每个CSV的列名、类型、样例缺列说明preprocess直接KeyError训练命令python src/train.py 的运行方式只写运行main.py目录都对不上接口调用curl示例、请求参数、返回JSON结构只写参数不写response前端没法解析算法说明用了什么算法、哪些参数可调缺算法说明读者不知道权重改哪里README里放一段可复制的curl命令比任何文字描述都管用curl http://127.0.0.1:8000/recommend?user_idu001top_k5然后把返回的真实JSON贴一段样例。接手人复制粘贴能立刻确认接口工作比读十行接口描述效率高得多。5. 评估与调优旅游推荐系统上线前的离线验证技巧5.1 时间切分的离线评估PrecisionK怎么算旅游推荐不能随机切分数据——用户夏天的行为和冬天完全不同随机切会让训练集混进未来数据评估结果虚高。用时间戳按8:2切前80%训练、后20%预测才是贴近线上冷启动场景的做法。评估指标不用追求全套PrecisionK加一个覆盖率就够感知效果了。threshold logs[timestamp].quantile(0.8) train logs[logs[timestamp] threshold] test logs[logs[timestamp] threshold] def precision_at_k(recommended, test_user_pois, k10): 推荐列表前k个与真实行为集合的交集比例 rec set(recommended[:k]) real set(test_user_pois) return len(rec real) / k if rec else 0注意PrecisionK的分母始终是k不是交集大小。推荐10个中了3个就是0.3哪怕用户实际只去了2个地方也这么算。分子上的真实集合要去掉用户已经去过的POI否则评估结果会被历史行为重复计算撑高。5.2 Top-K与近邻数的网格搜索neighbor_n和sim_top_n这两个参数直接决定推荐效果逐个手调效率低。用两重循环做网格搜索每组参数在测试集上取所有用户的平均PrecisionK取最高的一组即可代码量很小。best_params (0, 0) best_p 0 for neighbor_n in [10, 20, 30, 50]: for sim_top_n in [10, 20, 30]: p evaluate_on_test(neighbor_nneighbor_n, sim_top_nsim_top_n) if p best_p: best_p, best_params p, (neighbor_n, sim_top_n) print(fbest: {best_params}, precision{10}: {best_p:.3f})调参时同时打印训练集和测试集两边的指标差值过大就是过拟合两个参数要往小调。旅游数据的季节性会让不同月份的最优参数漂移建议每月用增量日志重跑一次网格而不是一套参数用到底。5.3 冷启动验证新用户和新POI各写一组测试用例最后检查三件事行为为空的新用户调接口推荐列表不能返回空数组至少要回落到热门榜刚入库的新POI内容推荐权重要保证它能进入某些候选集连续两次调用同一用户的接口推荐列表不能出现重复热门项刷屏。手工构造三个新用户ID和一个无行为POI分别调一遍接口看返回是否满足预期。提示把新用户、新POI、重复推荐三个场景各写一条断言测试回归时跑一遍比看任何离线指标都直观。验证通过后把这三条规则写进README的FAQ接手的人调试时能少踩一半坑也顺便让那份说明文档从能跑起来升级成能改起来。本文还有配套的精品资源点击获取
返回列表