Python旅游景点信息可视化系统,理解起来并不复杂:用 Django 做 Web 框架,把景点基础信息、游客量、评分、评论等数据管理起来,再通过 pandas 做数据分析,最后用 ECharts 把结果展示在页面上。如果还想让使用者用自然语言直接问数据,可以在这个基础上接一个大模型 Agent,例如 DeepSeek,把“哪个城市的综合评分最高”这类问题转成可执行的查询和解释。
这个方向比较适合正在学 Django 和数据分析的人,也适合做课程设计、毕设,或者是景区、旅游平台的内部看板。相比那种只做一个“静态大屏”的演示项目,这套系统的核心价值在于:数据从哪里来、怎么清洗、如何统计、如何呈现、如何被检索追问,是一条完整的链路。大模型只是其中一层“问答入口”,不是必需品。
下面按我实际开发时会走的顺序拆开讲。先定边界,再搭骨架,然后处理数据,最后做可视化和大模型问答。
1. 先定边界:数据链路、展示对象和大模型 Agent 的定位
1.1 系统不是“爬虫加页面”,重点在数据能不能流转起来
只看“旅游景点信息可视化”这几个字,很容易把重点放在页面效果上。实际开发时,数据链路才是最需要先想清楚的部分。
数据从哪里来,是整个系统的起点。常见的来源包括:景区公开统计数据接口、旅游平台的开放数据、地方文旅部门发布的数据文件,以及企业内部上报的 Excel、CSV。如果是自己抓取网页数据,要提前确认数据来源是否允许采集,只取公开且合规的数据,不要触碰用户隐私和平台限制。这个边界一开始就要写进项目说明里,避免后面做偏。
数据拿到之后,要经过这么几步才能到页面:
- 数据采集:脚本或管理命令定时拉取、导入。
- 数据清洗:空值、重复值、格式统一、异常数值处理。
- 数据建表:按业务拆分成基础信息表、统计表、评分评论表。
- 数据分析:聚合、趋势、对比、分类。
- 数据可视化:接口返回 JSON,前端图库渲染。
- 数据问答:大模型 Agent 基于结构化结果回答自然语言问题。
这六步里,前三步最琐碎,也最容易被忽略。很多人一上来就写页面,结果图表数据是写死的,导致系统换个数据源就崩。这是这类项目最常见的失败原因。
1.2 先做一个最小可用闭环,再谈大模型
我建议第一个版本不要做大而全,先做“单数据源、单页面、单图表”的闭环。比如:导入一份包含景点、省份、评分、游客量的 CSV,在页面展示一张省份统计柱状图,再做一个筛选下拉框。这个闭环跑通后,再逐步加地图、趋势图、导出功能和评论分析。
这样做的好处是:你可以在每个环节验证结果。CSV 导入了多少条记录?统计结果和 Excel 透视表是否一致?接口返回的 JSON 是否完整?如果一上来就同时做十个图表和 Agent,出了问题很难定位是数据问题、统计问题还是渲染问题。
大模型 Agent 在这个系统里的定位应该是“查询助手”,不是“数据唯一出口”。用户可以直接看图表,也可以像聊天一样问“上季度游客量下降最多的省份是哪个”。Agent 需要先把问题翻译成数据查询条件,或者先从已统计好的结果里检索,再由模型组织自然语言答案。它不要自己去编数字,也不要绕过数据管道直接回答。这个定位决定了后续的接口设计和提示词写法。
2. 环境准备和项目骨架:先把能跑起来的环境搭好
2.1 Python 版本、虚拟环境和依赖清单
开发这套系统,不需要很高的机器配置。普通办公电脑,8G 内存,CPU 即可。大模型部分走 API 调用,不需要本地 GPU。
Python 版本建议 3.9 或更高,安装时勾选“Add Python to PATH”。Django 版本以你实际安装的稳定版为准,下面的命令在 4.x 下都能用。
创建虚拟环境,避免多个项目的依赖互相污染:
python -m venv venv # Windows venv\Scripts\activate # Linux / macOS source venv/bin/activate安装基础依赖:
pip install django pandas numpy requests openpyxlpandas 用来做数据清洗和聚合,numpy 在很多统计计算里会用到,requests 用来请求外部接口和大模型 API,openpyxl 用来读取 Excel 文件。如果后面要做聚类分析,再加 scikit-learn。如果要做 Redis 缓存,再加 redis。
这套依赖足够完成主体开发。不要一上来就安装 Spark、Hadoop 之类的大数据组件,旅游景点项目在绝大多数情况下用 pandas 就能处理。
2.2 创建 Django 项目和 App,目录按职责划分
在虚拟环境激活后,执行:
django-admin startproject tourism_visual cd tourism_visual python manage.py startapp stats python manage.py startapp agent_chat我习惯把目录拆成两个 App:stats 负责景点数据模型、分析服务和可视化接口;agent_chat 负责大模型问答逻辑。如果评论功能很重,也可以单独拆一个 reviews App。职责划分清楚之后,后面排查问题会省很多时间。
创建完项目后,先修改 settings.py:
- 把
stats、agent_chat加入INSTALLED_APPS。 - 设置
LANGUAGE_CODE = 'zh-hans',TIME_ZONE = 'Asia/Shanghai'。 - 在项目根目录建一个
static目录,并配置STATICFILES_DIRS,用来放本地化的 ECharts 文件。 - 如果要用环境变量管理 API Key,可以安装
python-dotenv或直接读取环境变量。
然后执行:
python manage.py migrate python manage.py createsuperuser数据库默认用 SQLite,开发阶段足够了。等数据量变大再换 MySQL 或 PostgreSQL,迁移成本主要是在连接配置和字符集上,模型代码可以保持不变。
2.3 数据库选型:先用 SQLite 起步,遇到并发再切换
很多人会纠结数据库选型。我的建议很明确:开发阶段直接 SQLite,零配置、文件方式备份方便。如果多人同时写、数据量大到 SQLite 明显卡顿,再迁移到 PostgreSQL。
切换数据库时注意两件事:一是字符集,MySQL 要确保使用 utf8mb4,否则中文或 emoji 评论会乱码;二是settings.py的数据库连接改为环境变量,不要把密码写死在代码里。Django 的 ORM 在切换数据库之后,大部分查询代码不需要改,但涉及日期函数和聚合函数的 SQL 方言可能会有差异,需要回归测试一遍。
3. 数据建模和数据导入:表结构决定分析上限
3.1 核心表和字段设计
旅游景点信息不能只建一张表。建议至少拆成三张:
ScenicSpot景点基础信息表:景点名称、省份、城市、等级、经纬度、类型、创建时间。SpotDailyStat每日游客统计表:景点、日期、游客量、门票价格、收入。SpotReview评分评论表:景点、用户标识、评分、评论内容、评论日期。
基础信息表和数据统计表分开,核心原因是同一个景点会有多天的游客量,如果塞在同一张表里,会导致字段大量重复,统计接口写起来也痛苦。
下面是一个简化的模型示例:
from django.db import models class ScenicSpot(models.Model): name = models.CharField(max_length=128, verbose_name="景点名称") province = models.CharField(max_length=64, verbose_name="省份") city = models.CharField(max_length=64, verbose_name="城市", blank=True) level = models.CharField(max_length=32, verbose_name="等级", blank=True) category = models.CharField(max_length=64, verbose_name="景点类型", blank=True) longitude = models.DecimalField(max_digits=10, decimal_places=6, null=True, blank=True) latitude = models.DecimalField(max_digits=10, decimal_places=6, null=True, blank=True) class Meta: db_table = "scenic_spot" class SpotDailyStat(models.Model): spot = models.ForeignKey(ScenicSpot, on_delete=models.CASCADE, related_name="daily_stats") stat_date = models.DateField(verbose_name="统计日期") visitor_count = models.IntegerField(default=0, verbose_name="游客量") ticket_price = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name="门票价格") revenue = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name="收入") class Meta: db_table = "spot_daily_stat" unique_together = ("spot", "stat_date") class SpotReview(models.Model): spot = models.ForeignKey(ScenicSpot, on_delete=models.CASCADE, related_name="reviews") rating = models.IntegerField(verbose_name="评分") # 假设1-5分 comment = models.TextField(blank=True, verbose_name="评论内容") review_date = models.DateField(verbose_name="评论日期") class Meta: db_table = "spot_review"字段类型里,金额和经纬度用DecimalField而不是FloatField,避免浮点误差。统计表的unique_together约束,可以防止同一天重复导入同一景点的数据。
3.2 批量导入数据:用 Django management command
数据导入不要写在 views.py 里,也不要放在每次请求的视图函数里。正确做法是写成 Django management command,这样既能手动执行,也能用定时任务跑。
以一个 CSV 导入命令为例:
# stats/management/commands/import_spot_data.py import csv from django.core.management.base import BaseCommand from stats.models import ScenicSpot class Command(BaseCommand): help = "导入景点基础信息CSV" def add_arguments(self, parser): parser.add_argument("csv_path", type=str) def handle(self, *args, **options): path = options["csv_path"] with open(path, encoding="utf-8-sig") as f: reader = csv.DictReader(f) ok = 0 for row in reader: _, created = ScenicSpot.objects.update_or_create( name=row["name"], defaults={ "province": row.get("province", ""), "city": row.get("city", ""), "level": row.get("level", ""), "category": row.get("category", ""), }, ) ok += 1 self.stdout.write(self.style.SUCCESS(f"导入完成:{ok} 条"))使用update_or_create而不是get_or_create的好处是:同一景点再次导入时,会更新已有记录,不会产生重复数据。
执行方式:
python manage.py import_spot_data data/spot.csv数据文件建议统一放在data/目录,并按照导入日期命名,例如spot_20250101.csv,方便追溯。
3.3 数据清洗:一定不要跳过这一步
导入数据时,最容易出现的几类问题:
- CSV 文件用 Excel 编辑后带有 UTF-8 BOM,读取时第一列字段名带
\ufeff。解决办法是读取文件时使用utf-8-sig。 - 省份和城市名称不统一,比如“广西壮族自治区”和“广西”混用。
- 游客量字段里出现
--、逗号、空格,导致int()失败。 - 评分字段缺失,统计平均值时需要用
dropna()或填充默认值。
清洗逻辑可以抽成一个函数,放在stats/services.py里。导入命令调用它,后面如果是接口采集,同样调用它。这样能保证不同来源的数据进入数据库前,口径一致。
4. 数据分析层:不要把统计逻辑堆在视图函数里
4.1 用 service 层隔离分析逻辑
Django 的 views.py 适合做 HTTP 请求处理,不适合写复杂统计。我会在stats/services.py里集中写数据查询和聚合逻辑,views 只负责调用 service 并返回 JSON。
这样做有几个直接好处:
- 同一个统计结果可以被页面、API、Agent 三种方式复用。
- 单独跑脚本测试统计逻辑时,不需要起 HTTP 服务。
- 后续加缓存时,只需要在 service 外部包一层。
一个统计接口的典型流向:
- 页面