Python+Django照片分享旅游景点推荐系统:协同过滤与数据分析实战

发布时间:2026/10/12 13:36:32
Python+Django照片分享旅游景点推荐系统:协同过滤与数据分析实战 简介这是一套基于Python开发的照片分享与旅游景点推荐系统完整项目资源面向计算机相关专业的毕业设计、课程设计及项目开发学习者尤其适合需要融合数据分析与推荐算法实践的中等技能层级开发者。资源包共238个文件约170.04MB以64个py源码文件为核心辅以40个csv数据集、14个html页面、6个css与4个js前端资源另含pyc编译文件、xml配置、docx与pptx项目文档及md说明覆盖从数据到界面的完整链路。目前已有158人学习下载可作为可靠参考。项目源码经过严格测试读者可获得可运行的推荐系统实现、照片分享模块、数据分析脚本与配套文档便于理解推荐逻辑、数据流转与前后端协作方式并在此基础上进行功能扩展或二次开发降低从零搭建的门槛。1. 从一张旅游照片到一条推荐路线这套 Python 系统到底在做什么你在朋友圈发了一张青海湖的照片底下有人问“这是哪”“怎么去”“附近还有什么值得玩”。如果只靠人工回复十条问询就能把人累垮。这套基于 Python 开发的照片分享旅游景点推荐系统解决的就是这件事用户上传照片系统从照片里提取拍摄地点或视觉特征结合景点热度、评分、季节、距离等维度算出一批候选景点再按个性化偏好排序推给用户。它同时把用户行为日志沉淀下来做数据分析输出热门景点排行、用户活跃时段、景点共现关系等报表。适合谁正在做毕业设计、课程设计或想练手一个完整 Python Web 项目的同学也适合想理解推荐系统从数据到接口全链路的开发者。整套东西不依赖高精尖硬件一台普通笔记本就能跑通。2. 技术选型与数据模型为什么用 Django 协同过滤而不是硬上深度学习2.1 框架与算法选型的三条现实理由做毕业设计或课程设计最怕的是“技术栈堆得太高跑不起来”。我见过太多人一上来就写 Transformer 做景点推荐结果数据量只有几百条训练 loss 都不带动的。这套系统我一般会选 Django 作为 Web 框架原因很直接自带 ORM、Admin 后台、用户认证省掉大量重复代码Python 生态里 pandas、numpy、scikit-learn 直接可用数据分析模块不用另起炉灶。推荐算法层面用户-景点交互矩阵通常很稀疏深度学习没有足够数据支撑。常见做法是协同过滤UserCF / ItemCF打底再用基于内容的推荐做冷启动补充。ItemCF 在这个场景下更稳因为景点数量远小于用户数量物品相似度矩阵计算量可控。如果标题里强调“数据分析”那还得加一层用 pandas 做离线统计把结果写回数据库或缓存前端直接读。提示不要为了“看起来高级”强行上深度学习。数据量低于一万条交互记录时协同过滤的召回效果往往不输神经网络的粗排。2.2 数据表设计与核心字段系统至少需要五张核心表用户表、景点表、照片表、行为日志表、推荐结果表。下面用 Django ORM 风格给出关键字段定义你可以直接照着建。# models.py 核心模型定义 from django.db import models from django.contrib.auth.models import User class Spot(models.Model): name models.CharField(max_length100, verbose_name景点名称) city models.CharField(max_length50, verbose_name所在城市) latitude models.FloatField(verbose_name纬度) longitude models.FloatField(verbose_name经度) description models.TextField(blankTrue, verbose_name景点描述) avg_rating models.FloatField(default0.0, verbose_name平均评分) visit_count models.IntegerField(default0, verbose_name访问次数) tags models.CharField(max_length200, blankTrue, verbose_name标签逗号分隔) class Meta: db_table spot indexes [models.Index(fields[city]), models.Index(fields[avg_rating])] class Photo(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) spot models.ForeignKey(Spot, on_deletemodels.SET_NULL, nullTrue, blankTrue) image models.ImageField(upload_tophotos/%Y/%m/) exif_lat models.FloatField(nullTrue, blankTrue, verbose_nameEXIF纬度) exif_lng models.FloatField(nullTrue, blankTrue, verbose_nameEXIF经度) upload_time models.DateTimeField(auto_now_addTrue) class UserBehavior(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) spot models.ForeignKey(Spot, on_deletemodels.CASCADE) action models.CharField(max_length20, choices[ (view, 浏览), (rate, 评分), (collect, 收藏), (share, 分享) ]) score models.FloatField(default1.0, verbose_name行为权重) created_at models.DateTimeField(auto_now_addTrue)Spot表里tags字段用逗号分隔存标签是为了后续做基于内容的相似度计算时不用额外建关联表查询时用tags__contains过滤即可。Photo表同时存用户手动关联的spot和从 EXIF 读出的经纬度两者互为补充EXIF 可能缺失或被抹掉手动关联则依赖用户操作。UserBehavior表的score字段是行为权重浏览记 1 分、评分记 3 分、收藏记 5 分、分享记 4 分这个权重直接影响协同过滤的评分矩阵。2.3 照片到景点的匹配逻辑用户上传照片后系统需要判断这张照片对应哪个景点。常见做法是三步走先读 EXIF 经纬度用 Haversine 公式找最近景点如果没有 EXIF就用图片的感知哈希或颜色直方图做粗匹配再不行就落到用户手动选择。下面是最短距离匹配的核心代码。import math def haversine(lat1, lng1, lat2, lng2): 计算两个经纬度点之间的球面距离单位公里 R 6371.0 dlat math.radians(lat2 - lat1) dlng math.radians(lng2 - lng1) a (math.sin(dlat / 2) ** 2 math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlng / 2) ** 2) return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) def match_spot_by_exif(lat, lng, spots, max_km5.0): 在 max_km 范围内找最近的景点找不到返回 None best, best_dist None, float(inf) for spot in spots: d haversine(lat, lng, spot.latitude, spot.longitude) if d best_dist: best, best_dist spot, d return best if best_dist max_km else Nonemax_km这个参数需要根据景点密度调整。城市内景点密集设 2 公里就够西部地广人稀的区域可能得放到 10 公里。我一般会把它做成配置项按城市维度覆盖默认值。haversine函数里用atan2而不是asin是为了数值稳定性在短距离计算时精度更好。3. 推荐引擎实现从评分矩阵到 Top-N 推荐列表3.1 构建用户-景点评分矩阵协同过滤的第一步是把行为日志转成评分矩阵。用 pandas 做透视表是最快的方式行是用户、列是景点、值是加权评分。import pandas as pd from django.db.models import Sum def build_rating_matrix(): 从 UserBehavior 表构建用户-景点评分矩阵 qs (UserBehavior.objects .values(user_id, spot_id) .annotate(total_scoreSum(score))) df pd.DataFrame(list(qs)) if df.empty: return pd.DataFrame() matrix df.pivot_table( indexuser_id, columnsspot_id, valuestotal_score, fill_value0 ) return matrixfill_value0表示用户没有交互过的景点评分为 0这在后续计算相似度时会被当作“未评分”处理。注意不要用均值填充那会引入虚假偏好。如果数据量大到内存放不下可以改用稀疏矩阵scipy.sparse但毕业设计的数据规模通常用稠密矩阵就够了。3.2 ItemCF 相似度计算与推荐生成ItemCF 的核心是算景点之间的相似度然后根据用户历史喜欢的景点推荐相似景点。相似度用余弦相似度计算前先把矩阵转置让行变成景点。import numpy as np from sklearn.metrics.pairwise import cosine_similarity def item_similarity(rating_matrix): 计算景点-景点余弦相似度矩阵 item_matrix rating_matrix.T.values # 行景点列用户 sim cosine_similarity(item_matrix) np.fill_diagonal(sim, 0) # 自身相似度置零避免推荐自己 return sim def recommend_for_user(user_id, rating_matrix, sim_matrix, top_n10): 给指定用户生成 Top-N 景点推荐 if user_id not in rating_matrix.index: return [] # 冷启动走基于内容的兜底 user_idx list(rating_matrix.index).index(user_id) user_ratings rating_matrix.iloc[user_idx].values # 只取用户有过行为的景点 interacted np.where(user_ratings 0)[0] if len(interacted) 0: return [] # 加权求和用户评分 × 景点相似度 scores sim_matrix[interacted].T.dot(user_ratings[interacted]) # 屏蔽已交互景点 scores[interacted] -1 top_indices np.argsort(scores)[::-1][:top_n] spot_ids [rating_matrix.columns[i] for i in top_indices if scores[i] 0] return spot_idssim_matrix[interacted].T.dot(user_ratings[interacted])这行是 ItemCF 的核心公式对用户交互过的每个景点取其与其他景点的相似度乘以用户对该景点的评分累加得到候选景点的推荐分。scores[interacted] -1把已看过的景点排到最后避免重复推荐。top_n默认 10实际接口里可以做成参数前端下拉框选 5/10/20。3.3 冷启动兜底基于内容与热度的混合策略新用户没有行为数据协同过滤直接失效。这时候走兜底逻辑按景点标签匹配用户注册时选的偏好标签再叠加城市热度和平均评分排序。def cold_start_recommend(user_profile_tags, cityNone, top_n10): 冷启动推荐标签匹配 热度 评分加权 qs Spot.objects.all() if city: qs qs.filter(citycity) spots list(qs) scored [] for s in spots: tag_match 0 if user_profile_tags and s.tags: spot_tags set(s.tags.split(,)) tag_match len(spot_tags set(user_profile_tags)) / max(len(user_profile_tags), 1) # 热度归一化用对数平滑避免头部景点霸榜 heat np.log1p(s.visit_count) score 0.5 * tag_match 0.3 * heat 0.2 * s.avg_rating scored.append((s.id, score)) scored.sort(keylambda x: x[1], reverseTrue) return [sid for sid, _ in scored[:top_n]]三个权重 0.5 / 0.3 / 0.2 是我在几个类似项目里试出来的经验值。标签匹配权重最高因为用户主动选的偏好最能反映意图热度用log1p平滑防止某个爆款景点永远排第一评分权重最低因为新景点评分样本少波动大。4. 数据分析模块用 pandas 把行为日志变成可视化报表4.1 热门景点排行与趋势分析数据分析模块的价值在于让系统“有东西可展示”。毕业设计答辩时几张清晰的图表比一堆代码更有说服力。核心分析包括景点热度 Top-N、用户活跃时段分布、景点共现关系哪些景点经常被同一用户一起收藏。def spot_hot_ranking(days30, top_n20): 近 N 天景点热度排行 since timezone.now() - timedelta(daysdays) qs (UserBehavior.objects .filter(created_at__gtesince) .values(spot__name, spot__city) .annotate(totalSum(score), cntmodels.Count(id)) .order_by(-total)[:top_n]) return pd.DataFrame(list(qs)) def user_active_hours(): 用户活跃时段分布按小时聚合 qs UserBehavior.objects.extra(select{hour: strftime(%%H, created_at)}) df pd.DataFrame(list(qs.values(hour))) if df.empty: return df return df.groupby(hour).size().reset_index(namecount)spot_hot_ranking里同时算了total加权总分和cnt行为次数两个指标排序结果可能不同加权总分高的可能是被少数人深度互动的景点行为次数多的可能是被大量用户浏览过的。答辩时把两个榜单都展示出来能体现你对指标差异的理解。user_active_hours用了 SQLite 的strftime如果换成 MySQL 要改成DATE_FORMAT。4.2 用 matplotlib 生成可视化图表分析结果最终要落到图上。matplotlib 出图后存成 PNG前端用img标签加载比前端 JS 图表库省事得多。import matplotlib matplotlib.use(Agg) # 无 GUI 环境必须加这行 import matplotlib.pyplot as plt def plot_hot_ranking(df, save_pathstatic/charts/hot.png): 绘制景点热度横向柱状图 plt.rcParams[font.sans-serif] [SimHei] # 中文字体 plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(10, 6)) ax.barh(df[spot__name], df[total], color#4C72B0) ax.set_xlabel(热度总分) ax.set_title(近30天景点热度排行) plt.tight_layout() fig.savefig(save_path, dpi120) plt.close(fig)matplotlib.use(Agg)这行是血泪经验在服务器上没有图形界面不加这行会直接报错。中文字体设置也是必踩的坑不设SimHei或Microsoft YaHei图上的中文全是方块。dpi120是清晰度和文件大小的折中再高文件就太大了。4.3 景点共现矩阵与关联规则共现分析能回答“去了 A 景点的人还喜欢去哪些景点”这对推荐系统是很好的补充。用 pandas 的crosstab或矩阵乘法都能算。def spot_cooccurrence(min_support3): 计算景点共现矩阵过滤低频组合 qs UserBehavior.objects.filter(action__in[collect, rate]) df pd.DataFrame(list(qs.values(user_id, spot_id))) if df.empty: return pd.DataFrame() # 用户-景点 0/1 矩阵 binary pd.crosstab(df[user_id], df[spot_id]).clip(upper1) cooc binary.T.dot(binary) # 景点共现次数 np.fill_diagonal(cooc.values, 0) # 过滤低频 cooc cooc.where(cooc min_support, 0) return coocclip(upper1)把多次行为压成 0/1避免高频用户主导共现结果。min_support3表示至少被 3 个用户共同收藏过的景点对才保留这个阈值根据数据量调数据少就降到 2。5. 避坑与排查部署和调试中最容易翻车的五个点5.1 照片 EXIF 经纬度读出来是空的现象用户上传的照片明明在手机相册里能看到位置信息但系统读出来exif_lat和exif_lng都是 None。原因微信、部分社交软件在传输图片时会自动抹掉 EXIF 信息这是平台行为不是代码问题。另外 iPhone 的 HEIC 格式用 PIL 默认读不出 EXIF需要额外装pillow-heif。解决上传时同时提供手动选择景点的入口EXIF 只作为自动匹配的加速手段。读取 EXIF 前先判断格式HEIC 走pillow_heif.register_heif_opener()。代码里对None做兜底不要让异常冒到接口层。5.2 协同过滤推荐结果全是同一个景点现象不同用户拿到的推荐列表几乎一样Top-1 永远是那个最热门的景点。原因评分矩阵太稀疏大部分用户只交互过一两个景点ItemCF 算出来的相似度矩阵区分度不够热门景点因为被交互次数多相似度累加值天然偏高。解决在推荐分数上加热度惩罚项比如除以log(1 visit_count)或者对评分矩阵做归一化把每个用户的评分减去其均值消除用户评分尺度差异。另一个办法是限制每个景点在推荐列表里的出现次数做打散。5.3 matplotlib 在 Django 里报 “main thread is not in main loop”现象本地跑得好好的图表生成代码部署到服务器后报线程相关错误。原因matplotlib 默认使用交互式后端在非主线程或无 GUI 环境下会出问题。Django 的请求处理可能不在主线程。解决在settings.py或图表模块最顶部加matplotlib.use(Agg)强制使用非交互后端。这行必须在import matplotlib.pyplot之前执行顺序反了不生效。5.4 数据库查询在循环里逐条执行页面加载超过 10 秒现象推荐列表页打开要等很久日志里看到几百条相似的 SQL。原因在 for 循环里访问外键对象Django 默认懒加载每次photo.spot.name都触发一次查询典型的 N1 问题。解决查询时用select_related(spot)或prefetch_related预加载关联对象。推荐结果批量查景点时用Spot.objects.filter(id__inspot_ids)一次拿回来再在内存里按推荐顺序排。5.5 中文标签匹配时大小写和空格导致漏匹配现象用户偏好标签选了“自然风光”景点标签存的是“自然风光, 摄影”匹配结果为空。原因标签字符串拼接时带了空格或者中英文混用时大小写不一致。tags__contains是精确子串匹配多一个空格就匹配不上。解决入库前统一做strip()和lower()处理标签分隔符统一用英文逗号。匹配时先把两边都规范化再比较。更稳妥的做法是单独建标签表做多对多关联但毕业设计阶段用字符串处理也够用关键是规范化逻辑要统一。6. 让推荐结果可验证离线评估与 A/B 对比的轻量做法系统做完怎么证明推荐有效答辩时老师一定会问这个问题。不需要复杂的在线实验平台用离线评估就能给出有说服力的数字。核心思路是留出法把用户行为日志按时间切分前 80% 做训练后 20% 做测试看推荐列表命中了多少测试集里的景点。def evaluate_recommend(test_ratio0.2, top_n10): 离线评估命中率 HitRate 和 召回率 Recall qs UserBehavior.objects.filter(action__in[collect, rate]).order_by(created_at) df pd.DataFrame(list(qs.values(user_id, spot_id, created_at))) if df.empty: return {} # 按时间切分 split_idx int(len(df) * (1 - test_ratio)) train_df df.iloc[:split_idx] test_df df.iloc[split_idx:] # 用训练集构建矩阵和相似度 train_matrix train_df.pivot_table( indexuser_id, columnsspot_id, valuesspot_id, aggfunccount, fill_value0 ) sim item_similarity(train_matrix) hits, total 0, 0 for uid, group in test_df.groupby(user_id): if uid not in train_matrix.index: continue actual set(group[spot_id].tolist()) preds set(recommend_for_user(uid, train_matrix, sim, top_n)) hits len(actual preds) total len(actual) return {hit_rate: hits / max(total, 1), total_hits: hits, total_actual: total}hit_rate算的是推荐列表覆盖了多少用户实际交互过的景点。这个数字在稀疏数据上通常不高0.1 到 0.3 都算正常关键是和基线对比把 ItemCF 的结果和“直接推最热门 10 个景点”的基线比只要高出一截就能说明算法有效。我一般会在项目文档里放一张对比表列出 ItemCF、热度基线、随机推荐三组的 HitRate差距一目了然。另一个实用技巧是做参数敏感性分析把top_n从 5 调到 50看 HitRate 怎么变。通常 HitRate 随top_n增大而上升但边际收益递减找到那个拐点就能确定线上接口的默认值。这个分析用 matplotlib 画一条折线图答辩时非常加分。最后说个习惯每次改完推荐逻辑先跑一遍离线评估脚本把结果记在项目文档的表格里。我吃过亏改了一版相似度计算方式感觉“应该更好”结果 HitRate 掉了 5 个百分点没有评估脚本根本发现不了。数据不会骗人感觉会。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询