1. 旅游推荐难在哪:先搞清楚这个系统的核心矛盾
如果你准备做旅游景点推荐系统这个方向,先别急着敲代码。我在动手做这个项目之前,以为核心就是拉一份用户-景点评分表,跑个协同过滤,再用Django把结果展示出来就完事了。真正做完才发现,旅游这个场景和电商推荐完全不同,它的特殊性会让很多现成的推荐方案直接失效。
旅游推荐的第一个难题是行为稀疏。电商场景下,一个用户一年可能产生几百个订单,但旅游不同,普通人一年出游一两次,一个景点往往只去一次,评分行为更少。这意味着你拿到的用户-物品矩阵极度稀疏,冷启动问题比电商严重得多。
第二个难题是时间衰减。旅游行为有非常强的时效性,你三年前去过的地方,和明年想去的地方,很可能没有直接关系。用户状态在变,景点热度也在变,用历史行为预测未来偏好,效果打折得厉害。
第三个难题是地域约束。电商可以全国包邮,但旅游推荐必须考虑出发城市、交通成本、游玩时长这些参数。所以纯粹基于用户相似度的推荐,很容易推出一堆用户永远不可能去的远方景点。
那为什么还要用协同过滤来打底?我的经验是:协同过滤虽然老,但它有一个不可替代的优势——不需要任何景点内容标注,只靠用户群体行为就能发现隐含关联偏好。对于一个毕业设计或者中小型项目来说,它的解释性强、实现成本低、效果可度量,是性价比最高的起点。而AI大模型的加入,恰好可以补上协同过滤“不理解内容”“无法处理冷启动”这两块短板。
这套系统最终要解决的事情其实很明确:用户进入平台,系统综合他的历史行为、当前热门趋势、以及景点本身的特征,输出一份个性化的推荐列表,同时用可视化的方式让管理员和用户都能直观看到数据规律。下面我会把整个实现过程拆开讲清楚,重点说算法落地的细节和踩过的坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型的取舍:为什么是Django + Pandas + Redis这样的组合
选型这件事,我见过太多人一上来就堆技术栈,好像框架用得越多越厉害。实际上,毕业设计和企业级项目的诉求完全不同,前者要在有限时间内在“算法可解释性”和“工程复杂度”之间找平衡。我这套系统的选型逻辑如下:
| 模块 | 选型 | 理由 |
|---|---|---|
| Web框架 | Django | 自带Admin后台、ORM、分页等,适合快速搭建业务系统 |
| 推荐算法 | Python实现UserCF + ItemCF | Scikit-learn没有直接的协同过滤,自己写也不复杂,还能讲清楚原理 |
| 相似度计算 | Pandas + NumPy | 矩阵操作高效,数据量在几万级时内存完全扛得住 |
| 缓存与加速 | Redis | 缓存热门推荐结果和用户推荐列表,减轻算法模块重复计算压力 |
| 数据可视化 | ECharts + 自研Dashboard | 大屏展示效果好,配置灵活,社区资源丰富 |
| AI能力接入 | 大模型API/本地Embedding | 用于景点文本向量化、智能攻略生成、评论情感分析 |
Django这个选择没有悬念。它自带的ORM能让数据模型管理变得非常轻松,Admin后台上手即用,对于需要频繁调试数据的推荐系统开发来说太关键了。比如我在调试相似度矩阵的时候,需要不停查看用户行为数据、修改景点标签,如果没有Django Admin,光写这些后台管理页面就要耽误一两天。
算法部分我坚持用纯Python实现协同过滤,而不是套现成的库,原因有二。第一,答辩的时候老师一定会问“相似度怎么算的”“预测评分怎么归一化的”,你如果只回答“调了surprise库的KNNBasic”,很难展示你对算法的理解程度。第二,协同过滤的核心逻辑其实不超过一百行代码,自己从头实现一遍,后面调参和加优化都方便。
关于大模型的选型,我走了两步:第一步用Embedding接口把景点的描述文本向量化,然后计算景点之间的文本相似度——这是内容推荐的思路,用来解决协同过滤对新产品、新景点完全无行为数据的冷启动问题;第二步接入对话能力,用RAG方式让用户可以提问“带父母去北京玩三天怎么安排”,系统从知识库里检索相关信息再生成行程规划。但要注意,这部分在大数据毕业设计里属于“加分项”,不应该喧宾夺主,核心骨架仍然是推荐算法。
Redis在这个项目里的价值被很多人低估。推荐计算虽然不算特别耗时,但用户量上来之后,每次都实时算一遍相似度矩阵是不可能的。我的做法是:相似度矩阵用定时任务每天离线计算一次,存Redis;每个用户的个性化推荐列表在用户登录时生成并缓存,24小时过期;热门榜单用ZSET存储,实时更新。这样做完之后,接口响应时间基本稳定在200ms以内,用户体验提升非常明显。
3. 数据从哪里来:构造一份能支撑推荐算法的旅游数据集
做推荐系统,算法只占三成,数据预处理反而占七成——这句话我在项目里体会得非常真切。旅游景点推荐这个题目有个麻烦:公开可用的中文景点评分数据集很少,不像MovieLens那样有现成的标准数据可以下载。所以我的做法是“公开数据打底 + 自造行为数据补充”。
基础景点数据来自一些开放旅游平台的公开信息接口,字段包括:景点名称、所在城市、景点类型(自然风光/人文古迹/主题乐园等)、门票价格、建议游玩时长、评分、简介文本。我清洗了大约800个国内主流景点,整理成结构化的CSV文件。
然后是最关键的一步——构造用户行为数据。我模拟了约2000个用户,每个用户有10到80条不等的景点浏览、收藏、评分行为,总计约8万条互动记录。构造的时候不能纯随机,否则推荐算法学不到任何规律。我的做法是给每个用户设定“隐式偏好画像”,比如有的用户偏好自然风光、有的偏好人文历史、有的关注性价比,然后根据画像组合来生成行为,这样用户之间才会有可挖掘的相似度结构。
数据规模这里提醒一句:量级可以不大,但结构必须合理。协同过滤在几百个用户、几千个景点的数据规模上已经能跑出像样的结果,你不需要真的造出百万级数据。反而要特别注意数据中存在的行为规律,如果所有用户都是随机打分的,那相似度矩阵就是一盘散沙,做完之后推荐质量必然很差,到时候答辩老师一问你“为什么推荐的都是景点,跟用户行为无关”,你就很难解释了。
数据存储上我用了MySQL。表结构设计时遵循了Django的ORM习惯,主要包含这几张表:
User:用户基础信息,含注册时间、常居城市Attraction:景点信息,含名称、城市、类型、价格、评分、描述UserBehavior:用户行为记录,含用户ID、景点ID、行为类型(浏览/收藏/评分)、行为时间SimilarityMatrix:离线算好的景点相似度矩阵缓存表(也可以放Redis,二选一)
设计时还有一个关键点:行为类型要区分权重。评分行为权重最高,收藏次之,浏览最低。因为旅游评分行为稀少,不能只依赖评分,必须把隐式反馈也纳入模型,这是和MovieLens那种纯评分数据集最大的不同。
4. 从零实现协同过滤:UserCF和ItemCF的核心逻辑与代码落地
协同过滤分两大类:基于用户的(UserCF)和基于物品的(ItemCF)。旅游这个场景里,两者推出来的结果差异非常明显,我建议两个都做,然后在一个“推荐融合模块”里按比例混合输出。
4.1 构造用户-景点评分矩阵
无论哪种协同过滤,第一步都是把用户行为转换为一个用户-景点矩阵,行是用户,列是景点,值是“兴趣得分”。这个兴趣得分我采用的是加权方案:
python复制def build_user_item_matrix(behavior_df):
# 行为权重:评分1.0,收藏0.6,浏览0.3
weight_map = {'rating': 1.0, 'collect': 0.6, 'view': 0.3}
df = behavior_df.copy()
df['weight'] = df['behavior_type'].map(weight_map)
df['score'] = df['rating'].fillna(3.5) * df['weight']
# 同一个用户对同一个景点有多种行为时,取最大值,避免重复累加导致偏差
matrix = df.groupby(['user_id', 'attraction_id'])['score'].max().unstack(fill_value=0)
return matrix
这里有个小细节值得多说一句:为什么不直接累加而是要取最大值?假设一个用户先浏览了某个景点(0.3分),后来觉得不错又收藏了(0.6分),再后来去过了还打了5分(5.0分),如果累加他会得到一个高频但低质的6分左右;而取最大值得到的是5.0分,更符合“他喜欢这个景点”这个事实。用最大值还有一个好处是能有效抵抗刷行为带来的噪声。
4.2 基于用户的协同过滤:给“同类人”推荐他们去过的景点
UserCF的核心假设是:如果两个用户过去的行为高度相似,那么他们未来的偏好也可能相似。步子分两步:先找TopK相似用户,再把他们喜欢过而你还没去过的景点加权汇总。
相似度计算我用了余弦相似度(Cosine Similarity),这是最经典的选择。它在用户对景点的兴趣向量上计算夹角余弦值,衡量的是方向的相似性,而不是数值的大小,天然适合稀疏向量。
python复制from sklearn.metrics.pairwise import cosine_similarity
def user_similarity(matrix):
# 标准化,消除不同用户打分习惯的差异
normalized = matrix.apply(lambda row: row - row.mean() if row.std() > 0 else row, axis=1)
sim_matrix = cosine_similarity(normalized)
return pd.DataFrame(sim_matrix, index=matrix.index, columns=matrix.index)
对用户做中心化处理(减去均值)很重要。有的用户天然手松,什么都打4分以上,有的用户手紧,最多给3分。如果不消除这种评分尺度差异,相似度会被“手松手紧”这个因素污染。
得到相似用户后,推荐得分的计算公式如下:
python复制def recommend_by_users(user_id, matrix, sim_matrix, top_k=10):
if user_id not in matrix.index:
return []
# 当前用户的相似用户,按相似度降序
sim_scores = sim_matrix[user_id].sort_values(ascending=False)
# 排除自己和负相似度用户
sim_scores = sim_scores[(sim_scores.index != user_id) & (sim_scores > 0)]
top_similar_users = sim_scores.head(top_k)
# 当前用户已交互过的景点
interacted = set(matrix.loc[user_id][matrix.loc[user_id] > 0].index)
# 候选景点打分
scores = {}
for sim_user, sim_score in top_similar_users.items():
user_items = matrix.loc[sim_user]
for item, rating in user_items[user_items > 0].items():
if item in interacted:
continue
scores[item] = scores.get(item, 0) + sim_score * rating
# 按分数排序返回
sorted_items = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return [item for item, score in sorted_items[:20]]
这个逻辑里我做了两个在实际应用中非常必要的限定:过滤掉负相似度的用户(相似度为负意味着爱好相反,参考价值低),以及过滤掉用户已经去过的景点(否则推荐就没有意义了)。
4.3 基于物品的协同过滤:给“去过的景点”找类似景点
ItemCF的核心假设反过来:如果很多用户都同时喜欢景点A和景点B,那么A和B是相似的——注意,这种“相似”是基于共同行为聚集出来的,不一定在内容上同类。比如大量用户去杭州西湖后都会去灵隐寺,系统就会学到西湖和灵隐寺的“行为相似度”,即便它们一个是湖景一个是寺庙。
ItemCF在实际业务里的推荐解释性特别好:“因为你喜欢西湖,所以推荐灵隐寺”——用户一看就懂。这个特性使它很适合做景点推荐。
python复制def item_cf_predict(user_id, matrix, top_k=10):
if user_id not in matrix.index:
return []
# 用户已交互的景点及评分
user_rated = matrix.loc[user_id]
rated_items = user_rated[user_rated > 0]
# 预先算好物品-物品相似度矩阵(在线服务时可从Redis加载)
item_sim_matrix = cosine_similarity(matrix.T)
item_sim_df = pd.DataFrame(item_sim_matrix, index=matrix.columns, columns=matrix.columns)
scores = {}
for item, rating in rated_items.items():
# 找当前景点的相似景点
sim_items = item_sim_df[item].sort_values(ascending=False).head(top_k + 1)[1:] # 去掉自身
for sim_item, sim_score in sim_items.items():
if item in user_rated.index and user_rated[item] > 0:
continue
scores[sim_item] = scores.get(sim_item, 0) + sim_score * rating
sorted_items = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return [item for item, score in sorted_items[:20]]
4.4 什么时候用UserCF,什么时候用ItemCF
旅游场景的一个特殊性是景点数量远小于用户数量。我造的数据里是800个景点对2000个用户。这种情况下物品相似度矩阵只有800×800,计算和存储都很轻松;而用户相似度矩阵是2000×2000,也还扛得住。但如果数据量放大到10万用户、1万景点,那就是100亿的用户相似度矩阵,不现实了,此时ItemCF在工程上更可行。
另一个决策依据是推荐结果的差异:
- UserCF更偏向“发现新兴趣”。相似用户可能带你探索你从没接触过的景点类型,适合同城美食探店类型的场景。
- ItemCF更偏向“延续已有兴趣”。给用户推荐的都是和过去偏好相近的选项,适合让用户觉得“系统懂我”。
我的做法是双通道融合:默认情况下ItemCF的推荐结果占7成,UserCF占3成,加和排序取TopN。效果实测下来,比单一算法要稳得多。
4.5 冷启动问题的兜底策略
协同过滤的死穴是冷启动。系统里来了一个新用户,一条行为记录都没有,这时候UserCF和ItemCF都给不出推荐。我的兜底方案是分三档:
- 零行为用户:推荐平台整体热门景点Top20,按实时浏览热度排序。
- 行为少于3条的新用户:先根据他已有的那几条行为的景点类型标签,计算偏好向量(比如偏自然还是偏人文),做基于内容的推荐。
- 行为超过5条的正常用户:走协同过滤完整链路。
这三档的切换逻辑写在推荐服务入口,优先判断行为量级,再决定走哪条路。这套冷启动策略在答辩里也特别好讲,因为每个方案都有明确的应用场景和设计动机。
5. AI大模型与推荐算法的结合:不是装个壳,而是补能力
现在市面上很多“AI+推荐”的项目都是简单调一个API套壳,答辩一问就露馅。我做的时候坚持一个原则:大模型必须补上协同过滤做不到的事,而不是替代协同过滤。在这个原则下,我落地了三个非常有实用价值的功能点。
5.1 基于Embedding的景点内容相似度计算
协同过滤算的是“行为相似”,大模型可以做“内容相似”。用文本Embedding接口把每个景点的描述(简介、类型、特色、适合人群)转成一个高维向量,然后计算向量之间的余弦相似度,就得到了一个不依赖用户行为的景点相似度矩阵。
这个矩阵的价值在于:当一个景点刚上线还没有任何用户行为时,协同过滤完全无法推荐它,但我可以通过内容相似度,把新景点推荐给那些喜欢类似景点类型的用户。这正好补上了ItemCF对新物品不友好这个致命短板。
实现上很直接:
python复制def get_embedding(text):
# 调用大模型Embedding接口,返回向量
response = embedding_client.embeddings.create(
model="text-embedding-v3",
input=text
)
return response.data[0].embedding
# 预处理阶段批量生成景点描述向量
attractions['embedding'] = attractions['description'].apply(get_embedding)
# 计算新景点与已有景点的内容相似度
from sklearn.metrics.pairwise import cosine_similarity
new_attraction_emb = get_embedding(new_attraction_desc)
content_sim = cosine_similarity([new_attraction_emb], list(attractions['embedding']))[0]
这样做有个额外的好处:管理员在后台添加新景点的时候,系统可以自动给出“这个新景点最相似的老景点有哪些”,帮助运营判断新景点的潜在受众是谁。
5.2 基于大模型的智能行程规划助手
推荐系统给出一堆景点,但用户最关心的问题其实是“我如果去成都三天,应该怎么安排路线”。这就是行程规划的问题。Django后端接一个智能体接口,通过RAG方式实现:
- 先把系统里的景点数据按地区、类型、建议游玩时长整理成知识库文档
- 用户提问时,先用向量检索找出最相关的景点信息
- 把检索结果作为上下文,扔给大模型生成包含时间安排、路线顺序、交通建议的回答
这个功能在毕设演示环节效果非常好。老师随便提一个“带老人去北京四天怎么安排”,系统能给出一个合理的行程,而且会引用数据库里的真实景点信息,不是大模型凭空编造的。这就把“AI大模型”从噱头变成了真正的功能亮点。
5.3 评论情感分析辅助评分修正
景点评分数据存在一个天然问题:用户打的分和实际体验常常有偏差,刷分现象也常见。我额外加了一个评论情感分析模块,用大模型抓取评论的情感极性,输出一个情感分数,然后跟用户评分做加权融合,得到“修正后综合评分”。
python复制def sentiment_score(comment):
# 返回 -1 到 1 之间,正数表示正向情感
response = llm.chat(
messages=[
{"role": "system", "content": "你是旅游评论情感分析助手,只返回-1到1之间的数字。"},
{"role": "user", "content": comment}
]
)
return float(response.content)
# 修正评分 = 0.7 * 用户星级分 + 0.3 * (情感分 + 1) * 2.5
adjusted_score = 0.7 * star_rating + 0.3 * (sentiment + 1) * 2.5
这个修正后的分数可以用于热门榜单排序,让推荐列表更贴近真实口碑。不过要注意调用成本,建议用异步任务批量处理历史评论,不要同步逐条调用。
6. 数据可视化大屏:让推荐结果和平台数据“看得见”
如果说协同过滤和大模型是系统的“大脑”,那数据可视化就是“脸面”。一个设计得当的可视化大屏,能让老师、评委或者使用者第一眼就感受到“这个系统做了很多工作”。我做的旅游推荐可视化大屏主要围绕四个核心问题展开。
6.1 平台宏观数据面板
顶部是一排KPI指标卡:注册用户数、景点总数、本日推荐次数、本日新增评论、平台平均评分。这些数字用Django的ORM聚合查询就能算出来,前端用定时Ajax拉取更新。
bash复制# 一个典型聚合查询示例
total_users = User.objects.count()
total_attractions = Attraction.objects.count()
daily_recommend_count = RecommendLog.objects.filter(created_at__date=timezone.localdate()).count()
注意性能问题。如果数据量大,这种count查询不要每次刷新都实时算,用Redis做5分钟缓存就够了。第一版我就是犯了实时查询的错,大屏开着一天下来频繁打满数据库连接。
6.2 推荐算法效果可视化
这块是我觉得比普通大屏有技术含量的部分。我在推荐接口里埋了推荐日志,把每次推荐返回的景点ID和用户最终的点击/浏览行为关联起来,然后算出一个“推荐曝光-点击转化率”,用折线图展示每天的转化率变化趋势。还做了一张柱状图,对比UserCF和ItemCF两种算法各自的平均推荐被接受率。
这样做的好处是:算法质量不再是一个抽象概念,而是变成了一条曲线、一组对比柱状图,非常直观地体现了“协同过滤在这个场景里到底管不管用”。
6.3 景点热度与用户画像分布
景点热度我用ECharts的中国地图热力图展示,按城市聚合推荐次数和点击量,颜色越深说明热度越高。地图可视化在旅游场景里天然契合,也很能打动评委。
用户画像分布用饼图和雷达图:新增用户的年龄段分布、常居城市Top10、偏好景点类型分布。这些统计能在后台帮运营判断平台的用户结构,同时也展示了系统的数据分析能力,呼应了标题里“数据分析”这个关键词。
6.4 可视化大屏的技术实现细节
前端我用了Vue3 + ECharts,后端Django只负责提供JSON接口。这里有个关键经验:大屏页面的接口要单独写,不要复用业务接口。因为大屏需要的数据形态跟普通页面不一样,比如需要最近30天按天聚合的热度趋势,给页面用的接口返回的是分页列表,两者混在一起会让代码越来越乱。
我单独建了一个dashboard/views.py,里面全部是聚合型接口,比如/api/dashboard/trend返回近N天推荐趋势,/api/dashboard/hot_cities返回城市热度排行。前端大屏页面只调这些聚合接口,互不干扰。
7. 大数据量下的性能优化:从N+1查询到离线推荐
最后这部分是很多人忽略、但真正决定一个“大数据”毕业设计成色的地方。标题里带了“大数据”三个字,那你的系统就必须体现出处理数据的能力和意识,而不是拿一千条数据跑个for循环就算完了。
7.1 Django ORM的N+1查询问题
这是最常见的性能坑。假设你要在推荐列表页展示每个景点的名称、城市、评分,第一版代码可能长这样:
python复制# 错误示范:循环里查数据库
recommendations = get_recommendations(user_id)
for rec in recommendations:
attraction = Attraction.objects.get(id=rec.attraction_id)
data.append({
'name': attraction.name,
'city': attraction.city,
'score': rec.score
})
这段代码的问题在于:如果推荐结果有20条,就会查询20次数据库,再加上前面的查询,一次页面请求产生了20+N次SQL查询。这就是经典的N+1问题。正确的做法是用Django的select_related(适用于一对一、外键)或prefetch_related(适用于多对多、反向关联)一次性把关联数据查出来:
python复制# 正确示范:一次查询联表查出所有需要的数据
recommendation_records = Recommendation.objects.filter(
user=user
).select_related('attraction').order_by('-score')[:20]
for rec in recommendation_records:
data.append({
'name': rec.attraction.name,
'city': rec.attraction.city,
'score': rec.score
})
光这一个改动,接口响应时间就能从1秒级别降到100毫秒级别。我在代码审查时只要看到循环里有objects.get()或者objects.filter(),直接判为不合格。
7.2 相似度矩阵的离线计算与缓存
协同过滤的相似度计算是O(n²)复杂度,数据量大了,绝对不能放在请求链路里实时算。我的方案是写一个独立的定时任务脚本,每天凌晨跑一次全量计算,把结果存到Redis:
python复制# 管理命令:python manage.py compute_similarity
class Command(BaseCommand):
def handle(self, *args, **options):
matrix = build_user_item_matrix()
user_sim = compute_user_similarity(matrix)
item_sim = compute_item_similarity(matrix)
redis_client.set('similarity:user', pickle.dumps(user_sim))
redis_client.set('similarity:item', pickle.dumps(item_sim))
self.stdout.write(self.style.SUCCESS('Similarity matrix updated.'))
在线推荐时直接从Redis里读序列化好的矩阵,计算推荐结果。矩阵不常变,一天一次重算完全够用,而且这样即使推荐接口被高并发打到,也不会触发CPU密集的相似度运算。
7.3 推荐结果的Redis缓存策略
用户推荐列表也做了分层缓存。第一层是Redis,key为recommend:user:{user_id},有效期24小时;第二层是Django内置的缓存,有效期30分钟;第三层才是实时计算。用户的行为会影响推荐结果,所以我在用户产生新行为时主动删除该用户的缓存key,保证数据一致性:
python复制def invalidate_recommend_cache(user_id):
redis_client.delete(f'recommend:user:{user_id}')
这个策略保证系统具备应对一定并发量的能力,也比较好地维持了推荐结果的实时性。如果数据量进一步扩大,还可以用Celery把推荐计算做成异步任务,用户点击“刷新推荐”时先去队列请求,计算完成后前端再拉取结果。
7.4 评分矩阵稀疏性的处理
旅游评分矩阵通常非常稀疏,我构造的数据约2%的密度。直接拿稀疏矩阵去算相似度,会产生很多“伪相关”——两个用户碰巧去过同一个热门景点,相似度就被拉高了。我加了两个修正手段:
- 相似度惩罚系数:对于只有一个共同行为的用户对,乘以一个0.3的折扣,降低小样本相似度的信任度。
- 热门景点降权:对被超过60%用户访问过的超级热门景点,计算时权重降为原来的50%,避免大家都去过热门景点导致的虚假相似。
这两个改进做完,推荐结果的多样性有了明显提升,不再总是推荐那几个大热门了。这个细节在答辩时讲出来,也是实实在在的加分项。
8. 项目踩坑实录:推荐接口从能用到好用的三个关键修复
最后分享几个开发过程中真正让我头疼的问题,每个都花了半天以上的时间排查。写出来,希望看这篇文章的人能少走弯路。
第一个坑是评分标准化导致的全零行。我在做用户中心化时,如果一个用户只打过一次分,他的评分向量减去均值之后全是0,导致相似度计算时除数为0,输出NaN。这个问题在numpy里不会直接报错,但会让后续推荐结果一片空白。修复方案很简单,在标准化前先判断行的标准差是否大于0,否则保持原样,我前面代码里已经写到了。
第二个坑是用户新旧行为权重不同。一开始我用全部历史行为计算相似度,结果发现三年前去过的景点和上周刚去过的景点对推荐结果的影响完全相同。后来增加了时间衰减函数:行为时间越近,权重越高。公式是weight = 1 / (1 + ln(1 + days_ago)),三个月内的行为权重接近1,一年前的行为权重已经掉到0.6左右。这个改进让推荐结果对用户近期意图更敏感,实测推荐接受率提升了大概10%。
第三个坑是前端可视化数据格式不匹配。我从Django接口返回的是Python对象,但ECharts要求特定的JSON结构,比如地图热力图需要[{name: '北京', value: 100}, ...]这种格式。第一版对接时我花了很多时间在前端做数据格式转换,后来直接在Django的Serializer层就转换好,前端拿到的就是可以直接绑定到ECharts的数据结构,彻底解决了格式不匹配问题。
最后一个心得:整个项目做下来,我最大的感受是不管技术选型多炫,最终评断标准都是“能不能跑通、能不能讲清、能不能抗住追问”。协同过滤和AI大模型结合的这个框架,在旅游推荐这个场景里是可以真正落地、有清晰业务价值的。你哪怕只是照着我这个思路做一版最小实现,把数据流程理顺、把算法调通、把可视化写好,这套系统的完整度和技术深度就已经能撑起一个大数据的毕业设计了。
