做旅游数据分析与推荐系统,是我今年接到的非常多的一类Python后端项目。整套系统的核心并不在登录注册,而在两件事:一是把分散的旅游数据整理成有价值的分析结论,二是根据用户浏览、收藏、评分行为,从几十万条能够跨越的景点记录里挑出最可能感兴趣的几条推荐给用户。技术栈看着复杂——标题里挂着Python+Django+SSM,实际跑起来就是Django负责接口和后台管理,Pandas做数据清洗和统计,协同过滤和内容召回做推荐,最后用ECharts画几张数据看板。无论你是毕设答辩还是想沉淀一套可复用的推荐系统代码,都可以参考这篇整理的内容。我习惯在项目开始前花10分钟把需求拆开。别急着写代码,旅游推荐系统最怕的就是“看起来什么都有,但推荐页面打开像随机摊大饼”。很多同学的第一版系统,登录、搜索、景点详情、排行榜全做了,偏偏推荐模块只是按评分倒序输出,答辩时一问推荐逻辑就卡壳。所以这篇文章会从需求拆解、数据准备、推荐算法工程化、可视化展示到常见坑位,完整讲一遍我是怎么把这类系统做成能交付的。1. 项目到底是做什么的:需求拆解与技术选型1.1 这类项目的核心模块并非只有推荐“旅游数据分析与推荐系统”听上去很聚焦,实际一拆就有六个模块。数据采集/读取:从公开数据集、爬虫或者手动录入获取景点、酒店、评论、用户行为数据。数据存储:MySQL作为主库,Redis放缓存和热数据。数据挖掘与分析:统计热门目的地、评分分布、用户画像、季节趋势。推荐引擎:计算相似度、预测评分、生成TopN推荐列表。可视化后台:展示统计图和推荐效果反馈。后端API:供前端网页或小程序调用。如果不先定义边界,开发到一半很容易崩溃。比如数据是从CSV来的,那Django的ORM表结构怎么设计?爬虫要不要单独写?推荐结果要不要落库存历史?这些问题都要在开工前拍板。如果再碰上“跨平台”“多端适配”这类需求,你还得提前想好API返回结构,否则前端小程序改一个字段,后端就要跟着调半天。我在实际交付中通常建议把范围锁定在“一个后台管理端 + 一个普通用户端”。管理端给运营人员看数据分析看板和推荐结果调试,用户端只保留“首页/发现页”和“我的推荐”两个页面。这样既覆盖了核心功能,又不至于把精力耗在不必要的复杂交互上。提示:如果只是毕业设计或者个人项目,建议把“数据采集”降级为使用公开数据集或预置CSV,把精力放在分析、推荐和展示上,不要沉迷爬虫。爬虫写起来很有意思,但对推荐系统的完成度没有直接帮助。1.2 为什么选择Python+Django,SSM又是怎么回事选型逻辑其实很朴素。Python生态做数据分析最快,pandas、numpy、scikit-learn都是现成的;Django自带ORM、Admin、认证系统,做一个带后台管理的Web应用效率极高。Django的Admin后台尤其适合这种“运营需要看数据”的场景,我只需要把模型注册进去,增删改查页面就全出来了,省掉大量手写表格的时间。至于推荐算法,Python表达矩阵运算最自然。你写一段协方差计算,用Java可能要十几行,用numpy一行就出结果。再加上后面要做分词、统计、算法对比,这个优势会越来越明显。SSM这个标签经常出现在我的任务描述里。第一次看到时我也愣了一下,Django是Python的,SSM是Java的Spring+SpringMVC+MyBatis,怎么可能整合到一起?后来翻了几份类似参考文档才明白:这类项目早期大多用Java技术栈演示,后来大家改用Python+Django,标题里的SSM更像是模板遗留的搜索关键词。真正动手时不需要被这个标签绑住。如果项目文档里一定要提,可以理解成项目管理方希望保留分层架构思想,把Controller、Service、Model拆清楚思路,但代码层面完全不用Java。1.3 推荐算法选型思路推荐算法很多,但选型不是越高级越好。旅游场景有两个特点:数据稀疏,用户没给多少景点打过分;时效性强,用户最近浏览了哪些地方很关键。我最终选了“ItemCF + UserCF + 内容标签匹配”的混合模式,原因很直白:数据集小时计算量可控,解释性强,答辩时能讲清楚每一步。热门推荐:作为兜底方案,保证任何用户进入推荐页都不为空。ItemCF(基于物品协同过滤):适合游客行为数据,景点相对固定,相似度可以离线算。UserCF(基于用户协同过滤):适合找相似人群,但冷启动用户不好办。内容推荐:按景点类型、城市、价格、评分做标签匹配,适合新用户。矩阵分解SVD等:效果好,但工程复杂,需要离线训练和定期更新。旅行场景中,用户更关心“这个景点和哪个景点相似”“我是不是能看到同类型但更值得去的地方”,ItemCF天然契合。热点事件类推荐反而相对少,因为门票消费是低频决策。后续我们会在第3节具体展开实现细节。2. 数据准备与挖掘:从原始数据到可用特征2.1 数据来源与采集策略个人开发建议优先用公开数据集,比如某些大学实验室开放的中文旅游评论数据,或者自己造几千条模拟数据。别一上来就爬某旅游平台,反爬、字体加密、封IP,随便一个都能折腾掉两周时间。如果实在想爬,也要控制频率,只取少量示例数据,尊重目标站点的robots规则和使用协议。我见过太多同学因为爬虫数据格式不统一,清洗起来比写推荐算法还痛苦。我们以“公开数据集 + 本地模拟数据”为例,通常会构造以下字段:数据表核心字段用户表用户ID、昵称、年龄、性别、职业、城市景点表景点ID、名称、城市、类别、等级、门票价格、简介评论表用户ID、景点ID、评分、评论内容、评论时间行为表用户ID、景点ID、浏览时长、点击时间、行为类型实际项目中这些字段可能更多,比如经纬度、图片数量、开放时间等。但别贪多,字段越多,清洗和存储的成本越高。找够支持推荐和可视化的字段就可以开工了。2.2 数据清洗与中文舆情处理拿到数据先动手清洗,三步必做。第一步去重,同一用户同一天对同一景点的重复评论要去掉,否则后面算评分矩阵时会出现同一个(user, item)多条记录。第二步处理缺失值,评分缺失就按该用户平均分或全局平均分填充;文本缺失直接丢弃,因为评论内容对情感分析有用。第三步中文分词,评论是中文,需要分词后才能做情感极性判断。下面是一段很实用的清洗代码:import pandas as pd df = pd.read_csv('travel_comments.csv') # 重复评论去重 df = df.drop_duplicates(subset=['user_id', 'spot_id', 'comment_time']) # 缺失评分按用户均分填充 df['score'] = df['score'].fillna(df.groupby('user_id')['score'].transform('mean')) # 评论缺失直接丢弃 df = df.dropna(subset=['comment']) # 简单情感词典打分 pos_words = ['好玩', '惊艳', '值得', '喜欢', '方便'] neg_words = ['失望', '后悔', '太差', '拥挤', '不值'] def sentiment_score(text): return sum(1 for w in pos_words if w in text) - sum(1 for w in neg_words if w in text) df['sentiment'] = df['comment'].apply(sentiment_score)这里情感词典只是演示,实际可以换成大词典或者调用现成的情感分析库。但演示项目里这套逻辑已经足够支撑“分析评论情感分布”的看板了。2.3 特征工程:构建用户画像与景点向量清洗完要能把数据喂给推荐模型。我一般构建三张特征表。第一张是用户特征表。年龄分桶,比如0-18、18-30、30-45、45+;性别按标签编码;偏好类型通过历史评论里出现频率最高的景点类型推断,比如用户经常订“亲子乐园”,那他的偏好标签就是“亲子”。第二张是景点特征表。包括类别(自然、人文、乐园、古镇、城市)、热度(近30天被浏览或收藏次数)、性价比(门票除以平均游玩时长)、好评率(好评数/总评论数)。这些特征既可以用在内容推荐里,也可以作为分析看板里的指标。第三张是用户-景点评分矩阵。这里要融合显式评分和隐式行为,不能只用用户给的分。融合公式可以这样写:综合评分 = 用户显式评分 × 0.7 + 评论情感得分 × 0.2 + 行为热度得分 × 0.1行为热度得分可以继续拆。浏览一次记0.3分,收藏记0.5分,购买或预约记1.0分,累加后归一化到1-5区间。这个公式不复杂,但能很好解决“用户没评分但浏览了很多次”的数据稀疏问题。3. 推荐算法的工程化实现3.1 基于物品的协同过滤:ItemCFItemCF的关键是计算景点之间的相似度。相似度可以用余弦相似度或皮尔逊相关系数,最常用的是余弦相似度。在已经构建用户-景点评分矩阵的前提下,相似度公式是:similarity(i, j) = sum(r_ui * r_uj) / (sqrt(sum(r_ui^2)) * sqrt(sum(r_uj^2)))直接用pandas和scikit-learn实现非常简单:from sklearn.metrics.pairwise import cosine_similarity import pandas as pd # rating_pivot: 行是用户,列是景点,值是综合评分 matrix = rating_pivot.to_numpy() sim_matrix = cosine_similarity(matrix.T) # 得到景点相似度矩阵 spot_sim = pd.DataFrame(sim_matrix, index=rating_pivot.columns, columns=rating_pivot.columns)有了景点相似度,对某个用户做推荐时,找出他评过分或浏览过的景点,取这些景点的TopK相似景点,加权求分,再把已经去过的景点排除掉。预测公式:pred(u, i) = sum(sim(i, j) * r_uj) / sum(|sim(i, j)|)分母一定要加,否则相似度越高的物品会把分数无限制叠加,推荐列表容易被极端值带偏。这个细节我在很多代码里看到漏掉,结果推荐分数直接溢出。3.2 基于用户的协同过滤:UserCFUserCF是找出与目标用户兴趣相似的其他用户,再用这些相似用户的评分来预测目标用户对某个景点的偏好。相似用户数量一般取10到20个。预测公式里建议每个用户都减去自己的平均评分,这样能避免打分习惯差异。比如有的用户习惯给4分以上,有的用户习惯给2分,如果不处理,高分用户会在推荐结果中霸榜。pred(u, i) = avg_u + sum(sim(u, v) * (r_vi - avg_v)) / sum(|sim(u, v)|)这里的avg_u和avg_v分别是目标用户和相似用户的历史平均评分。当数据量不大时,UserCF可以离线算好用户相似度矩阵,请求进来直接查表。但如果用户数量超过几万,矩阵会变得很重,要考虑用户维度降采样或者改成别的方法。在实际对比时我发现,旅游场景里UserCF的整体效果经常没有ItemCF稳定。原因也简单:游客的消费决策受假期、天气、活动影响很大,同一个人在不同时间可能去完全不同类型的目的地,“相似用户”这个概念的预测力有限。但UserCF对发现新奇景点有优势,所以我没有完全抛弃它,而是并入混合策略。3.3 混合加权与冷启动兜底两种协同过滤各有问题。ItemCF对新景点不友好,因为新景点没有评分,无法算相似度;UserCF对新用户不友好,新用户没有行为,找不到相似人群。所以我设计了混合推荐规则:若用户最近有评分或浏览行为,且行为记录数大于等于3:混合分数 = 0.4 × ItemCF评分 + 0.4 × UserCF评分 + 0.2 × 热度分。若用户行为记录数小于3:直接用内容推荐,根据用户注册时选择的兴趣标签加当前城市热门景点。若用户完全没有信息:返回全局热门TopN,并打上“大家都在看”的标签。关键点是混合前一定要做归一化。ItemCF和UserCF预测出的分数范围可能完全不同,一个在2到5分之间,另一个在0到1分之间,直接加权没有意义。我会用Min-Max缩放把每个来源的分数都压到0到1区间再融合。下面是一个伪代码结构:def hybrid_rec(user_id, top_n=10): u = get_user(user_id) if user_behavior_count(u) >= 3: item_scores = item_cf_predict(u) user_scores = user_cf_predict(u) hot_scores = hot_normalized_score(spot_ids) final_scores = 0.4 * item_scores + 0.4 * user_scores + 0.2 * hot_scores elif user_has_tag(u) > 0: final_scores = content_rec_by_tag(u) else: final_scores = hot_scores return rank_and_filter(u, final_scores)[:top_n]这个代码逻辑简单,但已经把冷启动、稀疏、推荐多样性全部考虑进去了。3.4 推荐结果的缓存与API下发推荐结果如果每次请求都实时算,几千用户可能还能忍,几万用户就会卡死。我的做法是安排一个定时任务,每天凌晨重算一次所有活跃用户的TopN列表,写入Redis。数据格式用JSON,Django视图层直接读,缓存未命中时再计算一次并回填。import json import redis redis_client = redis.Redis(host='localhost', port=6379, db=0) def get_recommendations(user_id): key = f'rec:{user_id}' cache_data = redis_client.get(key) if cache_data: return json.loads(cache_data) rec = compute_recommendation(user_id) redis_client.setex(key, 86400, json.dumps(rec)) return rec定时任务可以用APScheduler或者Django配置的Celery。毕业设计用不着太重,直接用Django Management Command写一个命令,配合系统crontab每天跑一次就够。另外,推荐结果尽量不要只返回景点ID,最好把景点名称、封面图、价格、评分一起序列化好,前端拿到就能渲染,减少一次联表查询。4. 数据分析可视化与前端展示4.1 看板设计:要展示哪些指标只做推荐没有“数据分析”味道,所以通常还需要一个可视化看板。我常用这些图表:热门景点TOP10柱状图;目的地城市热度地图;游客年龄与性别分布饼图;月度旅游趋势折线图;景点类别好评率散点图;推荐效果漏斗图:曝光、点击、收藏、购买转化。这些图不是为了好看,而是能回答实际问题。比如主力用户是18-30岁人群,那推荐策略就得更注重“网红打卡”和“高性价比”,如果看板显示大部分流量集中在一线城市,那热门推荐的城市权重就要适当提高。看板是推荐引擎的“眼睛”,没有数据反馈,算法调优就只能靠感觉。4.2 ECharts联动Django后端图表前端用ECharts比较省事。Django后端只需要提供JSON接口,例如 /api/analysis/hot_spots、/api/analysis/tourism_trends。视图层直接调pandas聚合,转成JSON返回。from django.http import JsonResponse import pandas as pd def tourism_trends(request): orders = load_order_data() df = pd.DataFrame(orders) df['month'] = df['order_time'].dt.to_period('M') grouped = df.groupby('month').size() data = { 'months': [str(x) for x in grouped.index], 'counts': grouped.values.tolist(), } return JsonResponse(data)这个接口非常简单,但要注意一个性能问题:不要把pandas聚合直接放在高并发请求线程里做,尤其是千万级数据。我的做法是把聚合结果定时写入Redis,接口只读缓存。前端ECharts拿到以后,通过setOption直接渲染。如果地图展示需要按城市聚合,建议后端预先把城市名和数值组成一个列表,前端再配置地图组件。经纬度字段要统一精度,否则地图上的点会飘。5. 常见问题排查与优化心得5.1 环境搭建踩坑:Python版本与依赖Django 5.x要求Python 3.10以上,但很多老教程还在写Python 2,环境配好之后项目跑不起来是常态。我强烈建议用虚拟环境:python -m venv venv source venv/bin/activate # Windows则使用 venv\Scripts\activate pip install -r requirements.txtrequirements.txt可以参考这样:Django>=4.2,<5.0 djangorestframework pandas scikit-learn jieba redis安装依赖时最容易出问题的是scikit-learn和pandas的版本冲突。用pip安装如果报错,可以尝试用conda建环境。另外,如果系统里装了新版Django但项目里有些老写法不兼容,建议锁定版本号,不要追求最新。5.2 协同过滤计算慢的处理刚开始数据小,直接算相似度没问题。但游客数据量上来后,内存和耗时都会成问题。先用稀疏矩阵压缩,再计算相似度:from scipy.sparse import csr_matrix sparse_matrix = csr_matrix(matrix.T) sim_matrix = cosine_similarity(sparse_matrix)另外,相似度矩阵可以只保留每个景点的TopK邻居。比如K取20,存成字典格式,推荐时只需要查20个相似景点,而不是遍历全量。这个优化能把单次推荐耗时从秒级压到毫秒级。5.3 评分矩阵稀疏怎么办旅游景点少则几百,多则几千,用户往往只对几个景点有行为,评分矩阵稀疏度会超过95%。缓解手段有这么几个:用隐式反馈填充评分,把浏览、收藏、停留时长换算成隐式分值,这个在第2节提到过。用热度兜底,保证推荐结果不为空。降低推荐列表中冷门景点的比例,但要保留一定新颖度。全部推热门会变成排行榜,全部推冷门又会让用户觉得莫名其妙。相似度计算用皮尔逊相关系数而不是余弦相似度,因为皮尔逊会自动去除每个用户的评分均值偏移,对稀疏评分更友好。5.4 推荐结果“看起来不靠谱”的调优方向如果推荐结果全是热门,或者跟用户刚看过的景点太相似,可以按顺序调几个参数。第一步调相似度阈值,只保留相似度大于0.3的景点;第二步调TopK,相似景点从10个改成20个,增加多样性;第三步加入惩罚项,用户已经去过的城市里同类景点降权;第四步做结果打散,每连续3个推荐中至少要有一个不同类型。如果推荐结果过于单一,打散规则是最快见效的手段,但不是靠随机打散,而是按类别轮换,比如自然、人文、美食、乐园循环输出。6. 项目可以怎么扩展6.1 接入Redis做实时推荐前面提到用Redis做推荐结果缓存,再进一步可以让Redis参与实时行为收集。用户浏览、点击、收藏时立即写入Redis里的行为队列,再通过定时任务批写入MySQL。这样推荐的时效性可以提升好几个量级,用户白天浏览了某地,晚上再看推荐里就能出现相关景点。实现并不复杂,Django视图里增加行为上报接口,前端埋点把事件发过来。推荐引擎重算时从Redis取最近N天的行为数据作为输入。6.2 用Docker封装方便交付如果要把系统交给别人跑或者上服务器,Docker是最省心的方式。写一个Dockerfile跑Django,再加一个docker-compose.yml启动MySQL和Redis。services: web: build: . ports: - "8000:8000" depends_on: - mysql - redis mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: travel_rs redis: image: redis:7Docker部署能大幅度减少“在我电脑上跑得好好的”这类问题。第一次启动时把数据库迁移和初始化数据导入命令集成到entrypoint里,别人clone下来一键启动就能看到效果,体验差异非常明显。最后分享一点我自己的体会:旅游推荐系统这种项目,不要一开始就把推荐算法搞得很复杂。先把数据流和工程链路跑通,再花时间调协同过滤的参数,效果往往比直接上深度学习模型好得多。我在实际项目里踩过最深的坑就是数据稀疏情况下一顿SVD训练,结果准确率还没有热门推荐高。后来老老实实做特征融合和混合策略,线上效果才稳定。另外,项目展示时一定要准备两三个“为什么这样设计”的说明,比如为什么用ItemCF不用矩阵分解,因为评分矩阵稀疏且景点数量相对少。把工程取舍讲清楚,比堆一堆指标渲染效果更值钱。这套系统的代码结构不算复杂,但数据清洗、推荐算法、Dashboard三块都敲完之后,你基本就掌握了一个完整数据应用项目的标准姿势。
做旅游数据分析与推荐系统,是我今年接到的非常多的一类Python后端项目。整套系统的核心并不在登录注册,而在两件事:一是把分散的旅游数据整理成有价值的分析结论,二是根据用户浏览、收藏、评分行为,从几十万条能够跨越的景点记录里挑出最可能感兴趣的几条推荐给用户。技术栈看着复杂——标题里挂着Python+Django+SSM,实际跑起来就是Django负责接口和后台管理,Pandas做数据清洗和统计,协同过滤和内容召回做推荐,最后用ECharts画几张数据看板。无论你是毕设答辩还是想沉淀一套可复用的推荐系统代码,都可以参考这篇整理的内容。我习惯在项目开始前花10分钟把需求拆开。别急着写代码,旅游推荐系统最怕的就是“看起来什么都有,但推荐页面打开像随机摊大饼”。很多同学的第一版系统,登录、搜索、景点详情、排行榜全做了,偏偏推荐模块只是按评分倒序输出,答辩时一问推荐逻辑就卡壳。所以这篇文章会从需求拆解、数据准备、推荐算法工程化、可视化展示到常见坑位,完整讲一遍我是怎么把这类系统做成能交付的。1. 项目到底是做什么的:需求拆解与技术选型1.1 这类项目的核心模块并非只有推荐“旅游数据分析与推荐系统”听上去很聚焦,实际一拆就有六个模块。数据采集/读取:从公开数据集、爬虫或者手动录入获取景点、酒店、评论、用户行为数据。数据存储:MySQL作为主库,Redis放缓存和热数据。数据挖掘与分析:统计热门目的地、评分分布、用户画像、季节趋势。推荐引擎:计算相似度、预测评分、生成TopN推荐列表。可视化后台:展示统计图和推荐效果反馈。后端API:供前端网页或小程序调用。如果不先定义边界,开发到一半很容易崩溃。比如数据是从CSV来的,那Django的ORM表结构怎么设计?爬虫要不要单独写?推荐结果要不要落库存历史?这些问题都要在开工前拍板。如果再碰上“跨平台”“多端适配”这类需求,你还得提前想好API返回结构,否则前端小程序改一个字段,后端就要跟着调半天。我在实际交付中通常建议把范围锁定在“一个后台管理端 + 一个普通用户端”。管理端给运营人员看数据分析看板和推荐结果调试,用户端只保留“首页/发现页”和“我的推荐”两个页面。这样既覆盖了核心功能,又不至于把精力耗在不必要的复杂交互上。提示:如果只是毕业设计或者个人项目,建议把“数据采集”降级为使用公开数据集或预置CSV,把精力放在分析、推荐和展示上,不要沉迷爬虫。爬虫写起来很有意思,但对推荐系统的完成度没有直接帮助。1.2 为什么选择Python+Django,SSM又是怎么回事选型逻辑其实很朴素。Python生态做数据分析最快,pandas、numpy、scikit-learn都是现成的;Django自带ORM、Admin、认证系统,做一个带后台管理的Web应用效率极高。Django的Admin后台尤其适合这种“运营需要看数据”的场景,我只需要把模型注册进去,增删改查页面就全出来了,省掉大量手写表格的时间。至于推荐算法,Python表达矩阵运算最自然。你写一段协方差计算,用Java可能要十几行,用numpy一行就出结果。再加上后面要做分词、统计、算法对比,这个优势会越来越明显。SSM这个标签经常出现在我的任务描述里。第一次看到时我也愣了一下,Django是Python的,SSM是Java的Spring+SpringMVC+MyBatis,怎么可能整合到一起?后来翻了几份类似参考文档才明白:这类项目早期大多用Java技术栈演示,后来大家改用Python+Django,标题里的SSM更像是模板遗留的搜索关键词。真正动手时不需要被这个标签绑住。如果项目文档里一定要提,可以理解成项目管理方希望保留分层架构思想,把Controller、Service、Model拆清楚思路,但代码层面完全不用Java。1.3 推荐算法选型思路推荐算法很多,但选型不是越高级越好。旅游场景有两个特点:数据稀疏,用户没给多少景点打过分;时效性强,用户最近浏览了哪些地方很关键。我最终选了“ItemCF + UserCF + 内容标签匹配”的混合模式,原因很直白:数据集小时计算量可控,解释性强,答辩时能讲清楚每一步。热门推荐:作为兜底方案,保证任何用户进入推荐页都不为空。ItemCF(基于物品协同过滤):适合游客行为数据,景点相对固定,相似度可以离线算。UserCF(基于用户协同过滤):适合找相似人群,但冷启动用户不好办。内容推荐:按景点类型、城市、价格、评分做标签匹配,适合新用户。矩阵分解SVD等:效果好,但工程复杂,需要离线训练和定期更新。旅行场景中,用户更关心“这个景点和哪个景点相似”“我是不是能看到同类型但更值得去的地方”,ItemCF天然契合。热点事件类推荐反而相对少,因为门票消费是低频决策。后续我们会在第3节具体展开实现细节。2. 数据准备与挖掘:从原始数据到可用特征2.1 数据来源与采集策略个人开发建议优先用公开数据集,比如某些大学实验室开放的中文旅游评论数据,或者自己造几千条模拟数据。别一上来就爬某旅游平台,反爬、字体加密、封IP,随便一个都能折腾掉两周时间。如果实在想爬,也要控制频率,只取少量示例数据,尊重目标站点的robots规则和使用协议。我见过太多同学因为爬虫数据格式不统一,清洗起来比写推荐算法还痛苦。我们以“公开数据集 + 本地模拟数据”为例,通常会构造以下字段:数据表核心字段用户表用户ID、昵称、年龄、性别、职业、城市景点表景点ID、名称、城市、类别、等级、门票价格、简介评论表用户ID、景点ID、评分、评论内容、评论时间行为表用户ID、景点ID、浏览时长、点击时间、行为类型实际项目中这些字段可能更多,比如经纬度、图片数量、开放时间等。但别贪多,字段越多,清洗和存储的成本越高。找够支持推荐和可视化的字段就可以开工了。2.2 数据清洗与中文舆情处理拿到数据先动手清洗,三步必做。第一步去重,同一用户同一天对同一景点的重复评论要去掉,否则后面算评分矩阵时会出现同一个(user, item)多条记录。第二步处理缺失值,评分缺失就按该用户平均分或全局平均分填充;文本缺失直接丢弃,因为评论内容对情感分析有用。第三步中文分词,评论是中文,需要分词后才能做情感极性判断。下面是一段很实用的清洗代码:import pandas as pd df = pd.read_csv('travel_comments.csv') # 重复评论去重 df = df.drop_duplicates(subset=['user_id', 'spot_id', 'comment_time']) # 缺失评分按用户均分填充 df['score'] = df['score'].fillna(df.groupby('user_id')['score'].transform('mean')) # 评论缺失直接丢弃 df = df.dropna(subset=['comment']) # 简单情感词典打分 pos_words = ['好玩', '惊艳', '值得', '喜欢', '方便'] neg_words = ['失望', '后悔', '太差', '拥挤', '不值'] def sentiment_score(text): return sum(1 for w in pos_words if w in text) - sum(1 for w in neg_words if w in text) df['sentiment'] = df['comment'].apply(sentiment_score)这里情感词典只是演示,实际可以换成大词典或者调用现成的情感分析库。但演示项目里这套逻辑已经足够支撑“分析评论情感分布”的看板了。2.3 特征工程:构建用户画像与景点向量清洗完要能把数据喂给推荐模型。我一般构建三张特征表。第一张是用户特征表。年龄分桶,比如0-18、18-30、30-45、45+;性别按标签编码;偏好类型通过历史评论里出现频率最高的景点类型推断,比如用户经常订“亲子乐园”,那他的偏好标签就是“亲子”。第二张是景点特征表。包括类别(自然、人文、乐园、古镇、城市)、热度(近30天被浏览或收藏次数)、性价比(门票除以平均游玩时长)、好评率(好评数/总评论数)。这些特征既可以用在内容推荐里,也可以作为分析看板里的指标。第三张是用户-景点评分矩阵。这里要融合显式评分和隐式行为,不能只用用户给的分。融合公式可以这样写:综合评分 = 用户显式评分 × 0.7 + 评论情感得分 × 0.2 + 行为热度得分 × 0.1行为热度得分可以继续拆。浏览一次记0.3分,收藏记0.5分,购买或预约记1.0分,累加后归一化到1-5区间。这个公式不复杂,但能很好解决“用户没评分但浏览了很多次”的数据稀疏问题。3. 推荐算法的工程化实现3.1 基于物品的协同过滤:ItemCFItemCF的关键是计算景点之间的相似度。相似度可以用余弦相似度或皮尔逊相关系数,最常用的是余弦相似度。在已经构建用户-景点评分矩阵的前提下,相似度公式是:similarity(i, j) = sum(r_ui * r_uj) / (sqrt(sum(r_ui^2)) * sqrt(sum(r_uj^2)))直接用pandas和scikit-learn实现非常简单:from sklearn.metrics.pairwise import cosine_similarity import pandas as pd # rating_pivot: 行是用户,列是景点,值是综合评分 matrix = rating_pivot.to_numpy() sim_matrix = cosine_similarity(matrix.T) # 得到景点相似度矩阵 spot_sim = pd.DataFrame(sim_matrix, index=rating_pivot.columns, columns=rating_pivot.columns)有了景点相似度,对某个用户做推荐时,找出他评过分或浏览过的景点,取这些景点的TopK相似景点,加权求分,再把已经去过的景点排除掉。预测公式:pred(u, i) = sum(sim(i, j) * r_uj) / sum(|sim(i, j)|)分母一定要加,否则相似度越高的物品会把分数无限制叠加,推荐列表容易被极端值带偏。这个细节我在很多代码里看到漏掉,结果推荐分数直接溢出。3.2 基于用户的协同过滤:UserCFUserCF是找出与目标用户兴趣相似的其他用户,再用这些相似用户的评分来预测目标用户对某个景点的偏好。相似用户数量一般取10到20个。预测公式里建议每个用户都减去自己的平均评分,这样能避免打分习惯差异。比如有的用户习惯给4分以上,有的用户习惯给2分,如果不处理,高分用户会在推荐结果中霸榜。pred(u, i) = avg_u + sum(sim(u, v) * (r_vi - avg_v)) / sum(|sim(u, v)|)这里的avg_u和avg_v分别是目标用户和相似用户的历史平均评分。当数据量不大时,UserCF可以离线算好用户相似度矩阵,请求进来直接查表。但如果用户数量超过几万,矩阵会变得很重,要考虑用户维度降采样或者改成别的方法。在实际对比时我发现,旅游场景里UserCF的整体效果经常没有ItemCF稳定。原因也简单:游客的消费决策受假期、天气、活动影响很大,同一个人在不同时间可能去完全不同类型的目的地,“相似用户”这个概念的预测力有限。但UserCF对发现新奇景点有优势,所以我没有完全抛弃它,而是并入混合策略。3.3 混合加权与冷启动兜底两种协同过滤各有问题。ItemCF对新景点不友好,因为新景点没有评分,无法算相似度;UserCF对新用户不友好,新用户没有行为,找不到相似人群。所以我设计了混合推荐规则:若用户最近有评分或浏览行为,且行为记录数大于等于3:混合分数 = 0.4 × ItemCF评分 + 0.4 × UserCF评分 + 0.2 × 热度分。若用户行为记录数小于3:直接用内容推荐,根据用户注册时选择的兴趣标签加当前城市热门景点。若用户完全没有信息:返回全局热门TopN,并打上“大家都在看”的标签。关键点是混合前一定要做归一化。ItemCF和UserCF预测出的分数范围可能完全不同,一个在2到5分之间,另一个在0到1分之间,直接加权没有意义。我会用Min-Max缩放把每个来源的分数都压到0到1区间再融合。下面是一个伪代码结构:def hybrid_rec(user_id, top_n=10): u = get_user(user_id) if user_behavior_count(u) >= 3: item_scores = item_cf_predict(u) user_scores = user_cf_predict(u) hot_scores = hot_normalized_score(spot_ids) final_scores = 0.4 * item_scores + 0.4 * user_scores + 0.2 * hot_scores elif user_has_tag(u) > 0: final_scores = content_rec_by_tag(u) else: final_scores = hot_scores return rank_and_filter(u, final_scores)[:top_n]这个代码逻辑简单,但已经把冷启动、稀疏、推荐多样性全部考虑进去了。3.4 推荐结果的缓存与API下发推荐结果如果每次请求都实时算,几千用户可能还能忍,几万用户就会卡死。我的做法是安排一个定时任务,每天凌晨重算一次所有活跃用户的TopN列表,写入Redis。数据格式用JSON,Django视图层直接读,缓存未命中时再计算一次并回填。import json import redis redis_client = redis.Redis(host='localhost', port=6379, db=0) def get_recommendations(user_id): key = f'rec:{user_id}' cache_data = redis_client.get(key) if cache_data: return json.loads(cache_data) rec = compute_recommendation(user_id) redis_client.setex(key, 86400, json.dumps(rec)) return rec定时任务可以用APScheduler或者Django配置的Celery。毕业设计用不着太重,直接用Django Management Command写一个命令,配合系统crontab每天跑一次就够。另外,推荐结果尽量不要只返回景点ID,最好把景点名称、封面图、价格、评分一起序列化好,前端拿到就能渲染,减少一次联表查询。4. 数据分析可视化与前端展示4.1 看板设计:要展示哪些指标只做推荐没有“数据分析”味道,所以通常还需要一个可视化看板。我常用这些图表:热门景点TOP10柱状图;目的地城市热度地图;游客年龄与性别分布饼图;月度旅游趋势折线图;景点类别好评率散点图;推荐效果漏斗图:曝光、点击、收藏、购买转化。这些图不是为了好看,而是能回答实际问题。比如主力用户是18-30岁人群,那推荐策略就得更注重“网红打卡”和“高性价比”,如果看板显示大部分流量集中在一线城市,那热门推荐的城市权重就要适当提高。看板是推荐引擎的“眼睛”,没有数据反馈,算法调优就只能靠感觉。4.2 ECharts联动Django后端图表前端用ECharts比较省事。Django后端只需要提供JSON接口,例如 /api/analysis/hot_spots、/api/analysis/tourism_trends。视图层直接调pandas聚合,转成JSON返回。from django.http import JsonResponse import pandas as pd def tourism_trends(request): orders = load_order_data() df = pd.DataFrame(orders) df['month'] = df['order_time'].dt.to_period('M') grouped = df.groupby('month').size() data = { 'months': [str(x) for x in grouped.index], 'counts': grouped.values.tolist(), } return JsonResponse(data)这个接口非常简单,但要注意一个性能问题:不要把pandas聚合直接放在高并发请求线程里做,尤其是千万级数据。我的做法是把聚合结果定时写入Redis,接口只读缓存。前端ECharts拿到以后,通过setOption直接渲染。如果地图展示需要按城市聚合,建议后端预先把城市名和数值组成一个列表,前端再配置地图组件。经纬度字段要统一精度,否则地图上的点会飘。5. 常见问题排查与优化心得5.1 环境搭建踩坑:Python版本与依赖Django 5.x要求Python 3.10以上,但很多老教程还在写Python 2,环境配好之后项目跑不起来是常态。我强烈建议用虚拟环境:python -m venv venv source venv/bin/activate # Windows则使用 venv\Scripts\activate pip install -r requirements.txtrequirements.txt可以参考这样:Django>=4.2,<5.0 djangorestframework pandas scikit-learn jieba redis安装依赖时最容易出问题的是scikit-learn和pandas的版本冲突。用pip安装如果报错,可以尝试用conda建环境。另外,如果系统里装了新版Django但项目里有些老写法不兼容,建议锁定版本号,不要追求最新。5.2 协同过滤计算慢的处理刚开始数据小,直接算相似度没问题。但游客数据量上来后,内存和耗时都会成问题。先用稀疏矩阵压缩,再计算相似度:from scipy.sparse import csr_matrix sparse_matrix = csr_matrix(matrix.T) sim_matrix = cosine_similarity(sparse_matrix)另外,相似度矩阵可以只保留每个景点的TopK邻居。比如K取20,存成字典格式,推荐时只需要查20个相似景点,而不是遍历全量。这个优化能把单次推荐耗时从秒级压到毫秒级。5.3 评分矩阵稀疏怎么办旅游景点少则几百,多则几千,用户往往只对几个景点有行为,评分矩阵稀疏度会超过95%。缓解手段有这么几个:用隐式反馈填充评分,把浏览、收藏、停留时长换算成隐式分值,这个在第2节提到过。用热度兜底,保证推荐结果不为空。降低推荐列表中冷门景点的比例,但要保留一定新颖度。全部推热门会变成排行榜,全部推冷门又会让用户觉得莫名其妙。相似度计算用皮尔逊相关系数而不是余弦相似度,因为皮尔逊会自动去除每个用户的评分均值偏移,对稀疏评分更友好。5.4 推荐结果“看起来不靠谱”的调优方向如果推荐结果全是热门,或者跟用户刚看过的景点太相似,可以按顺序调几个参数。第一步调相似度阈值,只保留相似度大于0.3的景点;第二步调TopK,相似景点从10个改成20个,增加多样性;第三步加入惩罚项,用户已经去过的城市里同类景点降权;第四步做结果打散,每连续3个推荐中至少要有一个不同类型。如果推荐结果过于单一,打散规则是最快见效的手段,但不是靠随机打散,而是按类别轮换,比如自然、人文、美食、乐园循环输出。6. 项目可以怎么扩展6.1 接入Redis做实时推荐前面提到用Redis做推荐结果缓存,再进一步可以让Redis参与实时行为收集。用户浏览、点击、收藏时立即写入Redis里的行为队列,再通过定时任务批写入MySQL。这样推荐的时效性可以提升好几个量级,用户白天浏览了某地,晚上再看推荐里就能出现相关景点。实现并不复杂,Django视图里增加行为上报接口,前端埋点把事件发过来。推荐引擎重算时从Redis取最近N天的行为数据作为输入。6.2 用Docker封装方便交付如果要把系统交给别人跑或者上服务器,Docker是最省心的方式。写一个Dockerfile跑Django,再加一个docker-compose.yml启动MySQL和Redis。services: web: build: . ports: - "8000:8000" depends_on: - mysql - redis mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: travel_rs redis: image: redis:7Docker部署能大幅度减少“在我电脑上跑得好好的”这类问题。第一次启动时把数据库迁移和初始化数据导入命令集成到entrypoint里,别人clone下来一键启动就能看到效果,体验差异非常明显。最后分享一点我自己的体会:旅游推荐系统这种项目,不要一开始就把推荐算法搞得很复杂。先把数据流和工程链路跑通,再花时间调协同过滤的参数,效果往往比直接上深度学习模型好得多。我在实际项目里踩过最深的坑就是数据稀疏情况下一顿SVD训练,结果准确率还没有热门推荐高。后来老老实实做特征融合和混合策略,线上效果才稳定。另外,项目展示时一定要准备两三个“为什么这样设计”的说明,比如为什么用ItemCF不用矩阵分解,因为评分矩阵稀疏且景点数量相对少。把工程取舍讲清楚,比堆一堆指标渲染效果更值钱。这套系统的代码结构不算复杂,但数据清洗、推荐算法、Dashboard三块都敲完之后,你基本就掌握了一个完整数据应用项目的标准姿势。