每年毕业季总有人来问我同一个问题Django做旅游景点数据分析的毕设到底该怎么写。这个问题我在带学生的过程中被问了几十次大多数人的需求其实高度一致——要一个能跑起来的前后端系统有数据、有图表、有分析逻辑还能过答辩。今天我把这个项目从头到尾拆一遍从选题逻辑、技术选型、数据表设计到分析指标、前端图表、部署展示以及学生最容易踩的坑全部按实操顺序写清楚。这篇东西适合三类人看正在做相关毕设的学生、想快速上手Django加数据清洗的转行者、以及打算把类似题目改造成自己项目的开发者。全文以源码编写者的视角来展开你可以直接把它当成一份项目复盘来读。1. 项目整体设计与思路拆解1.1 这个毕设题目为什么值得做旅游景点数据分析这个题目能成为毕业设计的热门选择背后有很现实的原因。它的业务场景足够直观景点数据可以通过公开渠道或现成数据集拿到不需要像金融、医疗那样考虑很多隐私和合规问题。更重要的是这个题目天然覆盖了计算机专业毕业设计考核的几个核心方向Web开发能力、数据库设计能力、数据处理能力和可视化表达能力。一个项目中同时出现CRUD操作、数据清洗、聚合统计和图表渲染答辩老师想提问都找不到死角。从技术栈来看Django对于这类管理后台加数据展示的系统几乎是量身定做的。它自带Admin后台不用额外开发管理界面就能完成景点信息维护自带ORM不需要手写SQL就能完成多表关联查询自带模板引擎和URL路由让页面跳转和数据传递变得非常顺滑。如果你用的是Python 3.10以上版本配Django 4.x开发效率会更高。很多学生纠结要不要用前后端分离我的建议是毕业设计不用硬上Vue加Django Rest Framework用Django模板加少量Ajax和JavaScript图表库就够了这样工作量小答辩也好讲清楚。1.2 功能模块拆分和页面流转这个项目的核心功能其实可以拆成两大块数据管理和数据分析展示。数据管理这块是给管理员用的负责景点信息、评论信息、用户信息的增删改查数据分析展示这块是给普通访客或运营人员看的负责展示景点热度排行、评分分布、价格区间、评论情感倾向等统计结果。页面流程大致是这样用户访问首页看到系统大盘数据比如景区总数、评论总数、平均评分、热门目的地Top5点击景点列表可以查看每个景点的详细信息包括评分、开放时间、门票价格、游客评价进入数据分析页面通过ECharts图表查看不同维度的统计结果。管理员的入口放在后台通过登录跳转到Django Admin所有数据模型都会在Admin后台注册方便维护。在设计这套结构时有一个容易被忽略的点数据分析的页面不要只展示最终成品图一定要保留原始数据表格作为对照。也就是说图表旁边附一张分页的明细列表既可以证明数据是真实的也方便答辩时回答“你这个统计结果怎么来的”这类问题。1.3 数据流转链路的设计整个系统处理的数据链路可以分为四层数据获取层负责把景点、评论、用户行为等原始数据采集进来统一整理为CSV文件或直接入库数据清洗层负责去重、缺失值处理、异常值修正、文本字段裁剪数据存储层用Django模型映射到MySQL或SQLite数据表数据分析层用Pandas进行分组聚合计算再把结果通过JSON接口传递给前端ECharts渲染。这样做的好处是逻辑清晰、便于排查问题。每一层都有独立的代码文件比如数据清洗的脚本单独放在项目的tools目录下不跟Web请求逻辑混在一起分析计算单独封装成服务模块views.py里只负责调用。最直观的优势是如果某个图表的数字不对你只需要检查计算函数和原始数据不需要在视图层和模板层来回找原因。关于“源码”的完整性这个项目的核心代码就体现在整个数据链路上尤其是数据清洗和分析计算这两个模块它们决定了图表能不能真实反映业务问题。2. 核心细节解析与实操要点2.1 数据模型设计与表结构分析Django的ORM对毕设来说足够强大但设计表结构时还是要认真想清楚字段关系。以景点数据模型为例通常会拆成这几张表景点表ScenicSpot、评论表Comment、用户表UserProfile、订单表Order或者收藏表Favorite。景点表里必须有景点名称、所在城市、门票价格、评分、开放时间、图片链接、简介等字段评论表里必须有评论用户、评分、评论内容、评论时间、所属景点收藏表和订单表则用于体现用户的互动行为。下面是景点表的字段设计参考class ScenicSpot(models.Model): name models.CharField(max_length100, verbose_name景点名称) city models.CharField(max_length50, verbose_name所在城市) price models.DecimalField(max_digits8, decimal_places2, verbose_name门票价格) rating models.FloatField(default0.0, verbose_name综合评分) open_time models.CharField(max_length50, verbose_name开放时间) address models.CharField(max_length200, verbose_name详细地址) desc models.TextField(verbose_name景点简介) image models.URLField(blankTrue, verbose_name图片链接) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间)这里有几个实操细节要提醒。第一门票价格用DecimalField而不用FloatField因为价格涉及展示和统计分析浮点数会产生精度问题第二评分不直接用单一字段而是预留一个计算逻辑评分可能是从评论表聚合来的这样改起来灵活第三每个字段都要加verbose_name写完模型后直接生成Admin后台界面会非常友好答辩演示时加分。设计完模型后在settings.py里配置数据库。毕设建议直接用MySQL如果本机没装MySQL也可以用SQLite顶一下。我给学生的标准配置是这样DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: travel_db, USER: root, PASSWORD: 你的密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }2.2 数据清洗与评分计算的必备流程数据分析类项目的核心工作量其实不在Web端而在数据清洗和分析计算上。如果你的数据是从网上下载的公开数据集或者用脚本抓取的景点评价信息里面一定有大量脏数据。常见的问题包括景点名称重复、城市字段为空、评分超出0到10的范围、价格里面有“免费”或“待定”这种非数字文本、评论内容里带表情符号和超链接。这些不处理干净后面统计出来的数字就是错的。数据清洗的脚本建议这样写先读取原始CSV然后逐步执行去重、缺失值填充、类型转换三个步骤。去重时注意不只按名称去重要按“名称城市”联合去重因为不同城市可能会有同名景区。缺失值方面城市缺失可以用地址字段解析评分缺失可以用该景点的评论平均分填充确实没有的就删除该条记录。价格字段要专门处理把“免费”变成0“待定”先视为缺失值再决定是否删除。清洗完成后把数据导入Django数据库。写一个独立脚本在项目根目录下执行python manage.py shell tools/import_data.py导入脚本里用ORM的update_or_create方法可以根据名称和城市判断记录是否存在存在就更新不存在就新建。这个方法在处理重复导入时非常省心比先查询再创建要少写很多代码。评分计算是另一个重点环节。一个景点可能有几千条评论每条评论的评分维度不同有的是打综合分有的是分“景色”“服务”“交通”几个子项打分。简化处理时可以直接计算平均值但为了提高分析深度可以引入加权评分def calc_weighted_score(ratings_dict): weights { scenery: 0.5, service: 0.3, traffic: 0.2, } score 0.0 for key, weight in weights.items(): score ratings_dict.get(key, 0) * weight return round(score, 2)加权评分的口径在答辩时记得讲清楚说明为什么景色权重最高、交通权重最低。因为有这个逻辑评审老师就觉得你不是在简单套公式而是在认真做数据分析。2.3 可视化图表选型与适配技巧可视化方案在毕设项目里往往是决定演示效果的关键因素。我的建议是常规静态表格用Bootstrap样式统计图表统一用ECharts因为它中文文档好、图表类型丰富、对Django开发者友好。不需要额外引入大数据框架也不建议用matplotlib生成图片再塞进页面那样跟系统整体不搭交互性也差。图表的适配要注意一个细节Django视图返回JSON给模板JavaScript拿到的数据结构要提前约定好。我的习惯是让后端返回固定的格式{ code: 0, message: success, data: { categories: [北京, 上海, 广州, 深圳], values: [120, 85, 78, 66] } }前端拿到data里的categories和values后直接赋值给ECharts的xAxis和series。这样一来后端逻辑和前端展示分离维护成本很低。如果在答辩时被问到图表数据来源问题你只需要说“数据是后端Pandas统计后通过JSON接口返回的”一句话就能解释清楚。图表颜色和标题也值得花点时间调。ECharts默认配色在演示现场可能偏淡建议使用深色背景配合明亮色系或者保持白色背景使用主色调统一为蓝绿系。另外图表尽量加tooltip和dataZoom组件方便演示时放大查看细节也体现开发完整性。3. 实操过程与核心环节实现3.1 环境搭建与项目初始化实际动手时环境问题最容易卡住第一次做Django的人。这里给出一套经过验证的流程。首先确认Python版本建议用3.10或3.11版本太低可能装不上新版Django太高了有些库还没适配。创建虚拟环境python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate然后安装依赖。核心依赖就这几个django、pandas、mysqlclient、requests。执行pip install django pandas mysqlclient requests如果mysqlclient编译报错Windows上可以直接下载对应的whl文件安装也可以用pymysql并在项目__init__.py里做兼容处理import pymysql pymysql.install_as_MySQLdb()环境确认没问题后创建Django项目和appdjango-admin startproject travel_analysis cd travel_analysis python manage.py startapp scenic这里项目名叫travel_analysisapp名叫scenic。创建完app后一定要去settings.py里把scenic注册到INSTALLED_APPS中很多人做完项目启动起来才发现新功能不生效往往就是忘了这一步。接下来跑迁移命令python manage.py makemigrations python manage.py migrate迁移成功后创建管理员账号并注册模型到Admin后台python manage.py createsuperuser在admin.py里把模型注册上去from django.contrib import admin from scenic.models import ScenicSpot, Comment admin.site.register(ScenicSpot) admin.site.register(Comment)到这里基础框架已经搭好后面就是往里填业务逻辑。3.2 从数据准备到分析输出的完整链路数据准备这一步是整套系统的地基。拿到原始数据后先把它整理成统一字段的DataFrame。举例来说数据中包含景点名称、城市、门票价格、评分、评论数量等信息可以用Pandas直接读取import pandas as pd df pd.read_csv(data/travel_raw.csv, encodingutf-8-sig) # 统一列名 df.columns [name, city, price, rating, comment_count, desc]接下来做清洗和转换。价格列把“免费”替换成0其他非数字内容先转成pd.NA再dropna评分列限定在0到5的区间超出区间的记录删除或修正名称列和城市列做strip处理去掉前后空格。清洗完成后用to_sql方法直接写入MySQL或者转换成ORM的bulk_create批量插入。批量插入时要注意一次性插入几千条没有问题但数据量大时建议分批否则可能超时。数据导入完成后分析计算模块就可以工作了。在scenic目录下新建analysis.py文件专门放统计逻辑。热度排行功能可以这样做from django.db.models import Sum, Avg, Count from scenic.models import ScenicSpot, Comment def hot_scenic_top(cityNone, top5): qs ScenicSpot.objects.all() if city: qs qs.filter(citycity) result ( qs.annotate(avg_ratingAvg(comment__rating)) .annotate(comment_countCount(comment)) .order_by(-comment_count, -avg_rating)[:top] ) data [ { name: item.name, comment_count: item.comment_count, avg_rating: round(item.avg_rating or 0, 2), } for item in result ] return data这个查询只用了一次ORM通过comment的关联聚合完成排序比先从数据库全部查出来再在Python里排序要高效得多。分析结果显示在页面上的时候一定要记得对平均分做round处理否则接口返回的一大串小数会让页面很难看。3.3 图表接口与页面渲染实现视图层要分两类写一类是普通页面渲染用Django模板直接返回HTML另一类是数据接口返回JSON给前端ajax读取。数据分析页面的接口可以做成分离式的比如/analysis/hotspot/接口专门返回热门景点统计结果/analysis/price-distribution/接口返回价格区间分布/analysis/sentiment/返回评论情感分布。以价格区间分布为例后端逻辑是这样from django.http import JsonResponse import json from scenic.models import ScenicSpot def price_distribution_data(request): all_prices list(ScenicSpot.objects.all().values_list(price, flatTrue)) bins [0, 50, 100, 200, 500] labels [0-50, 50-100, 100-200, 200-500, 500以上] counts {label: 0 for label in labels} for price in all_prices: if price 50: counts[0-50] 1 elif price 100: counts[50-100] 1 elif price 200: counts[100-200] 1 elif price 500: counts[200-500] 1 else: counts[500以上] 1 return JsonResponse({code: 0, data: {labels: labels, values: list(counts.values())}})理论上这块用Pandas的cut函数会更简洁但直接从ORM拿数据循环也能完成。两种方案在答辩时都说得通用cut函数时可以展示数据分析能力更强纯循环则更容易解释逻辑。我建议代码里两种都准备主流程用Pandas说明文档里附上纯Python版本。前端模板里通过fetch调用接口并渲染图表这部分就是标准的ECharts用法。在static目录下引入echarts.min.jsHTML页面中定义容器div然后在script标签里写加载数据的逻辑fetch(/analysis/price-distribution-data/) .then(response response.json()) .then(data { if (data.code 0) { const chart echarts.init(document.getElementById(priceChart)); chart.setOption({ title: { text: 景点门票价格分布 }, tooltip: {}, xAxis: { data: data.data.labels }, yAxis: {}, series: [{ type: bar, data: data.data.values }] }); } });有一点要特别提醒ECharts在页面初始化时如果容器被隐藏或宽度为0图表会渲染不出来。如果在Tab切换或者折叠面板里放图表切换后再调一次chart.resize()。3.4 管理后台与权限控制的实现细节Django Admin是毕设展示中的一个亮点很多学生不知道它到底能有多省事。只要做好模型注册和字段配置后台直接就有搜索、排序、分页、过滤功能。为了让演示效果更好建议在admin.py里做一些定制class ScenicSpotAdmin(admin.ModelAdmin): list_display [name, city, price, rating] search_fields [name, city] list_filter [city] ordering [-rating]这样配置后后台页面的所有列都可以点击排序搜索框可以根据名称或城市查找景点筛选条可以按城市快速过滤。这个功能在答辩现场非常能说明“系统设计完整”几乎不需要额外工作量。权限控制方面Django自带的认证系统已经覆盖了登录、权限分组、session管理。如果要区分普通用户和管理员可以给UserProfile加一个role字段并在视图函数中用装饰器判断角色。比如数据分析页面只允许管理员访问普通用户则跳转到首页。from django.contrib.auth.decorators import login_required from django.http import HttpResponseForbidden login_required def analysis_page(request): if request.user.profile.role ! admin: return HttpResponseForbidden(仅管理员可访问) return render(request, scenic/analysis.html)当然了毕设阶段的权限验证做到这个程度已经足够完全不用去实现复杂的安全体系。4. 常见问题与排查技巧实录4.1 环境配置和数据库连接的经典报错做Django毕设的学生遇到最多的报错就集中在环境配置环节。这里把高频问题列成一个速查表大家在排错的时候直接对照报错信息出现原因解决方式ModuleNotFoundError: No module named mysqlclient没安装MySQL驱动或本机编译环境缺失安装mysqlclient或改用pymysql兼容方案django.core.exceptions.ImproperlyConfiguredsettings里数据库配置有误通常是HOST或PORT不对检查MySQL服务是否启动账号密码是否正确SystemCheckError: (...) must return a HttpResponse视图函数没返回响应对象检查函数末尾是否缺少return render或JsonResponseRuntimeError: Model class ... doesnt declare an explicit app_label模型导入路径错误或迁移冲突检查model所在的app是否在INSTALLED_APPS里注册pymysql.err.OperationalError: (2003, ...)MySQL端口不是3306或本机只开了IPv6使用127.0.0.1连接本机确保监听端口匹配这些坑几乎每个项目都会遇到一次记录下来以后遇到相同问题可以直接修复不用再去搜索引擎翻半天。数据处理阶段最常见的坑是中文乱码。CSV文件用Excel打开然后另存时很容易把文件变成gbk编码。处理方式是读取时统一指定编码反复试编码太耗时# 先试 utf-8-sig再试 gbk try: df pd.read_csv(path, encodingutf-8-sig) except UnicodeDecodeError: df pd.read_csv(path, encodinggbk)数据库连接串里面也记得加上charsetutf8mb4否则插入中文时可能出现无法识别字符的问题。凡是后台出现中文变成问号的情况第一件事就是检查数据库字符集而不是检查代码。4.2 图表数据为空的排查思路图表显示为空是最让人头疼的问题明明接口有数据页面就是空白。需要按顺序排查四个点第一容器div有没有写错idECharts初始化的div必须存在并且有高度如果div高度为0图表会被隐掉要做容器判空第二检查ECharts的script引入路径加载顺序是jQuery先于ECharts再于自定义脚本第三确认接口返回的JSON字段名和前端代码里取的字段名一致一个字母对不上就显示不出来第四网页控制台是否有报告Failed to load resource有的话就是URL路径错误。我用过一个能有效加快排查速度的办法在接口返回前先把结果打印到终端上确认后端没问题后再改前端。Django的视图里可以直接print开发服务器会打印出结果非常方便。前端则多用console.log(data)把接口拿到的东西先看一眼再决定怎么渲染。有了这两手输出图表为空的问题不出十分钟就能定位。4.3 答辩必问的问题和答题思路答辩环节是毕业设计最后一道坎很多代码写得好的同学在问答环节反而吃亏。根据我带学生的经验以下几个问题被问到的频率极高你的数据从哪里来答数据主要来自公开的数据集或平台上清洗后的数据并在项目中完成了去重、缺失值填充等预处理确保数据质量满足分析需求。为什么用Django不用SpringBoot答项目需要在短时间内完成一套完整的数据分析Web系统Django自带ORM、Admin和模板渲染开发效率高同时Python生态对数据分析更友好做景点数据统计不需要额外换语言。这个系统的可扩展性体现在哪里答模型设计考虑了景点、评论、用户、订单等多实体关系后续可以增加推荐算法、用户行为分析等功能模块不需要改动现有表结构。平均评分和热度排名是怎么算的答热度排序按评论数量降序评论数相同按平均评分降序评分计算采用了加权平均规则景色权重50%、服务30%、交通20%。网站上数据量大会不会卡答目前数据规模在万级以下不需要特殊优化后续可以使用Redis做缓存、使用索引和分页查询来提升性能这个回答体现了你对扩容路径有思考。在回答技术选型问题时不要踩低别的技术最重要的是讲清自己当时考虑了什么约束条件。回答“用什么方案”比“为什么不用另一个方案”更稳妥因为技术选型没有绝对的优劣只有是否适合项目规模和时间要求。4.4 线上部署细节和源码整理建议如果毕设需要现场演示而不是在自己的电脑上跑提前部署在云服务器上是明智的做法。部署时使用Nginx加uWSGI或Gunicorn这是Django项目最成熟的方案。把数据库迁移到服务器上的MySQL然后执行collectstatic收集静态文件再配置Nginx把静态文件请求交给别名目录处理。pip install gunicorn gunicorn travel_analysis.wsgi:application --bind 0.0.0.0:8000开发环境跑在8000端口生产环境建议换一个高位端口同时在settings.py中关闭DEBUG。关闭DEBUG后数据库错误不会抛出敏感信息页面显示更接近线上状态。部署过程中最容易被遗忘的是ALLOWED_HOSTS如果你忘记设置服务器IP到ALLOWED_HOSTS里访问时会报DisallowedHost错误启动后立即确认一下这个配置ALLOWED_HOSTS [你的服务器IP, localhost, 127.0.0.1]源码的整理同样是拿到高分的关键因素。把数据清洗脚本、导入脚本、分析模块、视图逻辑分别放进不同目录README里面写清楚项目简介、环境依赖、运行步骤、管理员账号说明。整个项目结构看起来应该是这样travel_analysis/ ├── manage.py ├── travel_analysis/ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── scenic/ │ ├── views.py │ ├── models.py │ ├── analysis.py │ ├── admin.py │ └── migrations/ ├── tools/ │ ├── clean_data.py │ └── import_data.py ├── data/ │ └── travel_raw.csv └── static/ ├── echarts.min.js └── css/这种清单式的结构即使答辩老师不细看代码一眼扫过去也能看出你的工程规范意识。千万别把所有代码都堆在views.py里一是维护起来非常痛苦二是答辩时被问“这个模块为什么放在这里”也很尴尬。最后再分享一个我一直在用的习惯把分析指标的定义和数据口径单独写成一个templates/documented_data.md文件。比如“热度按评论数量排序”这样的指标定义在做项目报告和答辩PPT时直接就能复用。这个文件平时看起来不起眼但等到写指导记录或者系统说明书的时候能省不少时间去回忆当时的设计想法。说到底用Django做旅游景点数据分析这个项目技术难点不在某一个环节本身而在把Web框架、数据分析、可视化串成一条完整的链路。只要把数据清洗、ORM聚合、JSON接口、ECharts渲染这四个点逐个打通整套系统就是一套很完整的作品既能体现开发能力又能体现数据分析素养。我个人在这个问题上踩过最多的坑是一开始急着写代码忽略了数据质量结果后续所有图表都建立在错误的数据上返工成本极高。如果你正卡在这个项目上先把数据梳理清楚再动手做功能你会发现后面每一步都顺很多。