资讯动态

Django+ECharts全国地铁数据可视化与大模型查询系统

发布时间:2026/10/24 2:39:36 来源:尧图企业网站定制
毕业设计选题里“某某数据可视化系统”是最容易被低估的一类。乍一看它门槛不高爬点数据、画几张图、套一个 Django 后台好像答辩前一周拼一拼就能交。但真正做过的人都知道这类项目翻车的点非常集中——数据源断供、爬虫被封、图表数据量一大就卡死、答辩时老师问一句“你这个系统得出了什么结论”人就哑了。这篇文章想聊的不是“怎么套模板交作业”而是一个全国地铁数据可视化分析系统怎么才算做扎实从数据获取、清洗入库、指标体系设计、可视化交互到用 Django 把前后端串起来再到把大模型和 Agent 用在“自然语言查数据”这种真正能加分的场景上。我会把技术选型的理由、容易踩的坑、可复制的代码都写清楚。先给一个判断这个项目的技术难点从来不在“画图”本身。ECharts 上手半小时就能出图真正决定成绩高低的是三件事——数据管道的稳定性、分析维度的设计、以及你能不能讲清楚每一张图背后的业务结论。把这三件事做好项目就是良以上只堆图表不讲分析最多及格。下面按一个完整项目的推进顺序展开你可以对照着自己动手实现。1. 这个选题真正要解决的问题很多人对这个题目的第一反应是“数据可视化”于是把精力全砸在前端美化上结果做出来一个花哨但空洞的仪表盘。老师翻两页就会问你分析了什么你的数据从哪来这些指标说明了什么问题换个角度理解这个题目它其实是一个小型的数据工程闭环数据采集 → 清洗加工 → 结构化存储 → 多维聚合 → 可视化呈现 → 结论输出。可视化只是最后一环前面每一环都可能出问题而每一环出问题都会让最后一环变成空壳。具体来说一个合格的地铁数据分析系统至少要回答四类问题。第一类是规模类全国有多少城市开通了地铁总里程多少线路数量如何分布。第二类是结构类不同城市的线网密度、平均站间距、换乘站占比有什么差异。第三类是趋势类某城市近几年线路扩张速度、客流量的季节性波动。第四类是对比类一线城市和新一线城市在运营指标上的差距在哪里。这四类问题对应四种不同的数据聚合方式和四种图表类型也对应答辩时你要讲的四个结论。如果你的系统能把这四类问题都答清楚那它就不是“画图作业”而是一个有分析能力的系统。另外要提醒一点全国地铁的官方客流数据并不完全公开很多城市只公布月度或年度总量日粒度数据几乎拿不到。所以这个项目在数据层面必须做好两手准备——能用公开数据的地方用公开数据拿不到的地方用符合真实分布的模拟数据并且在论文和答辩中如实说明数据来源和模拟方法。这一点不丢人反而是严谨的体现。老师最反感的不是数据不完美而是你假装数据是真的。2. 核心技术概念与整体架构在动手之前先把技术栈的职责边界划清楚否则很容易写着写着把业务逻辑塞进模板里最后没法维护。2.1 Django 在系统里的角色Django 在这个项目里承担三件事数据模型定义、后台管理、以及给前端提供 JSON 接口。它的 ORM 特别适合做多维聚合查询比如“按城市分组统计线路数量和总里程”这种需求用annotate加Sum几行就写完了比手写 SQL 更安全也更易读。我推荐用 Django 而不是 Flask理由很实际毕设项目通常需要后台管理界面来维护数据Django Admin 开箱即用admin.py注册一下模型就有了增删改查页面省下来的时间可以投入到数据分析和前端上。Flask 更轻但后台、ORM、迁移工具都要自己搭对本项目的收益不大。需要强调的是Django 只做数据服务不要用它自带的模板引擎去渲染图表。图表渲染交给前端用 ECharts 完成后端只返回 JSON。这样做的好处是前后端解耦调试时你可以直接用浏览器或curl验证接口返回定位问题快得多。2.2 可视化链路拆解整条链路我建议这样拆数据源公开数据/模拟生成 ↓ pandas 清洗脚本去重、补全、类型转换 ↓ Django ORM 入库City / MetroLine / MetroStation / StationFlow ↓ 聚合接口按城市、按线路、按时间维度 ↓ ECharts 渲染地图、折线、柱状、散点、热力这条链路里最容易出问题的是“清洗脚本”和“聚合接口”两段。清洗不干净后面所有统计都是错的聚合接口设计得不好前端每画一张图就要发一次全量查询数据量一大就卡。2.3 大模型、多模态、Agent 的合理边界标题里带了“大模型、多模态、Agent”这是近几年毕设很喜欢加的词。但加词容易落地难。我的建议是只做一个真正跑得通的点不要样样都碰。比较靠谱的落地方式是“自然语言查询”。用户在输入框里问“北京和上海哪个城市地铁线路更多”系统把这句话交给大模型模型输出一个结构化的查询意图JSON后端根据这个意图调用对应的聚合接口最后把结果返回并生成一句话总结。这个方案的好处是模型不直接碰数据库安全性可控出错也容易兜底。Agent 可以理解为这个方案的进阶版让模型在多步之间做决策。比如用户问“找出近三年客流增长最快的三个城市”模型需要先调用“获取城市列表”再对每个城市调用“获取年度客流”最后排序。这就是典型的多步工具调用用函数调用Function Calling机制可以实现。多模态则要谨慎。比较现实的用法是让用户上传一张客流热力图截图模型描述图中趋势或者把生成的图表截图交给模型做文字解读。这个功能可以作为扩展点写进论文的“未来工作”不建议作为核心功能因为稳定性和成本都不好控制。一句话总结边界核心系统必须是确定性的大模型只做自然语言到结构化查询的翻译层。这样即使模型不可用系统依然能正常跑。3. 环境准备与项目初始化3.1 环境清单下面是我建议的环境配置具体版本请以你本机实际安装为准本文重点演示通用思路不锁定小版本号。组件建议版本说明Python3.10 及以上3.9 以下部分库兼容性差Django4.x LTS长期支持版文档全pandas2.x数据清洗主力requests2.x采集公开数据用mysqlclient 或 psycopg2与数据库匹配生产建议 MySQL 或 PostgreSQLECharts5.x前端通过 CDN 或本地引入数据库方面开发阶段用 SQLite 就够零配置起步快但如果你要处理几十万条客流记录建议换成 MySQL聚合查询性能差别明显。切换时注意把DATABASES配置改掉并重新执行迁移。3.2 项目骨架搭建先建虚拟环境这是好习惯避免污染系统 Pythonpython -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install django pandas requests mysqlclient然后创建项目和核心应用django-admin startproject metro_system cd metro_system python manage.py startapp analysis在metro_system/settings.py里注册应用并配置中文和时区# 文件路径metro_system/settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, analysis, ] LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ FalseUSE_TZ设为False是刻意的选择。数据分析和日期聚合场景下时区转换很容易让“某天的数据”落到前一天新手排查起来非常痛苦。如果项目不涉及跨时区业务直接关掉更省心。这一步做错的表现是明明导入了 1000 条记录按日期分组统计却少了一部分因为部分记录被归到了相邻日期。4. 数据模型设计与数据入库4.1 核心表结构模型设计决定了后面能做什么分析。我建议至少四张表城市表、线路表、站点表、客流表。下面给出可直接使用的模型定义。# 文件路径analysis/models.py from django.db import models class City(models.Model): name models.CharField(max_length64, uniqueTrue, verbose_name城市名称) province models.CharField(max_length64, blankTrue, verbose_name所属省份) open_year models.IntegerField(nullTrue, blankTrue, verbose_name首条线路开通年份) class Meta: db_table t_city verbose_name 城市 def __str__(self): return self.name class MetroLine(models.Model): city models.ForeignKey(City, on_deletemodels.CASCADE, related_namelines) line_name models.CharField(max_length64, verbose_name线路名称) line_color models.CharField(max_length16, blankTrue, verbose_name线路标识色) start_station models.CharField(max_length64, blankTrue, verbose_name起点站) end_station models.CharField(max_length64, blankTrue, verbose_name终点站) station_count models.IntegerField(default0, verbose_name站点数) length_km models.FloatField(default0.0, verbose_name运营里程(公里)) class Meta: db_table t_metro_line unique_together (city, line_name) verbose_name 地铁线路 class MetroStation(models.Model): line models.ForeignKey(MetroLine, on_deletemodels.CASCADE, related_namestations) station_name models.CharField(max_length64, verbose_name站点名称) is_transfer models.BooleanField(defaultFalse, verbose_name是否换乘站) longitude models.FloatField(nullTrue, blankTrue, verbose_name经度) latitude models.FloatField(nullTrue, blankTrue, verbose_name纬度) class Meta: db_table t_metro_station verbose_name 地铁站点 class StationFlow(models.Model): station models.ForeignKey(MetroStation, on_deletemodels.CASCADE, related_nameflows) stat_date models.DateField(verbose_name统计日期) in_flow models.IntegerField(default0, verbose_name进站客流) out_flow models.IntegerField(default0, verbose_name出站客流) class Meta: db_table t_station_flow indexes [models.Index(fields[stat_date])] unique_together (station, stat_date) verbose_name 站点客流这里有两个设计细节值得说明。第一unique_together是必须加的它能防止重复导入造成数据翻倍这是数据导入最常见的坑。第二客流表上加了stat_date索引因为绝大多数聚合查询都会按日期过滤没有索引时几十万条数据的分组统计会慢到无法接受。建完模型执行迁移python manage.py makemigrations python manage.py migrate4.2 数据来源与模拟数据生成真实数据可以来自各城市地铁官网公示、统计年鉴、开放数据平台。采集时注意遵守网站的使用条款控制请求频率加合理的User-Agent不要对目标站点造成压力。对于拿不到的日粒度客流用模拟数据是合理做法。关键在于模拟要符合真实分布特征工作日客流明显高于周末早晚高峰有双峰换乘站客流高于普通站节假日有波动。下面这段脚本可以生成一段符合这些特征的样本数据。# 文件路径scripts/gen_mock_flow.py import os import random from datetime import date, timedelta import django os.environ.setdefault(DJANGO_SETTINGS_MODULE, metro_system.settings) django.setup() from analysis.models import MetroStation, StationFlow # noqa: E402 def weekday_factor(d: date) - float: 工作日高峰系数周末客流下降。 return 1.0 if d.weekday() 5 else 0.72 def generate(station: MetroStation, base: int, days: int 90): today date.today() batch [] for i in range(days): d today - timedelta(daysi) factor weekday_factor(d) noise random.uniform(0.9, 1.1) # 换乘站客流显著高于普通站 transfer_boost 1.8 if station.is_transfer else 1.0 in_flow int(base * factor * noise * transfer_boost) out_flow int(in_flow * random.uniform(0.92, 1.08)) batch.append( StationFlow( stationstation, stat_dated, in_flowin_flow, out_flowout_flow, ) ) StationFlow.objects.bulk_create(batch, ignore_conflictsTrue) print(f{station.station_name} 生成 {len(batch)} 条客流记录) if __name__ __main__: for st in MetroStation.objects.select_related(line)[:50]: generate(st, baserandom.randint(8000, 30000))这段脚本的核心是weekday_factor和transfer_boost两个系数它们让模拟数据在统计上有意义。如果随便用random.randint生成产出的折线图会是一条毫无规律的噪音线答辩时一眼就被看穿。批量插入必须用bulk_create逐条save()在几千条以上会慢到让人怀疑人生。5. 数据清洗与指标计算入库之后不要急着画图先做一轮质量检查。我一般跑这三个统计总记录数、按城市的记录分布、以及空值和异常的占比。如果某城市记录数为零说明采集环节漏了如果某站点日客流出现负数或极端异常值说明原始数据有问题。清洗的关键动作有三个第一类型统一。里程字段经常混着“23.5公里”“23.5km”“23.5”三种写法入库前统一转成float。用 pandas 处理时先做字符串清洗再astype(float)转换失败的记录单独记录到日志文件不要静默丢弃。第二去重。同一城市同一线路可能被多个数据源重复采集以(city, line_name)为键去重。站点级别以(line, station_name)为键。第三派生指标计算。原始数据只能告诉你“有多少站、多少公里”真正有分析价值的是派生指标指标计算方法业务含义平均站间距线路里程 / (站点数 - 1)反映线路覆盖密度换乘站占比换乘站数 / 站点总数反映线网互联程度线网密度总里程 / 城市面积反映轨道交通发达程度客流强度日均客流 / 线路里程反映运营效率客流不均衡系数高峰日客流 / 平峰日客流反映客流波动特征这些指标才是论文里“分析”部分的内容来源。只写“北京有 27 条线”是描述写“北京线网密度是 XX 城市的 1.6 倍反映其轨道交通在同等面积下的覆盖优势”才是分析。6. 可视化接口与前端渲染6.1 后端聚合接口接口设计的原则是“前端要什么就返回什么聚合在后端做完”。下面这个视图返回全国各城市的线路数和总里程用于首页柱状图和地图。# 文件路径analysis/views.py from django.db.models import Count, Sum, Avg from django.http import JsonResponse from django.views.decorators.http import require_GET from .models import City, MetroLine require_GET def city_summary(request): 全国城市地铁规模概览。 rows ( MetroLine.objects .values(city__name, city__province) .annotate( line_countCount(id), total_kmSum(length_km), avg_stationAvg(station_count), ) .order_by(-total_km) ) data [ { city: r[city__name], province: r[city__province], lineCount: r[line_count], totalKm: round(r[total_km] or 0, 2), avgStation: round(r[avg_station] or 0, 1), } for r in rows ] return JsonResponse({code: 0, data: data}) require_GET def line_flow_trend(request): 某线路近 30 天客流趋势用于折线图。 line_id request.GET.get(line_id) if not line_id: return JsonResponse({code: 400, msg: line_id 不能为空}, status400) rows ( StationFlow.objects .filter(station__line_idline_id) .values(stat_date) .annotate(total_inSum(in_flow), total_outSum(out_flow)) .order_by(stat_date) ) data [ { date: r[stat_date].strftime(%Y-%m-%d), inFlow: r[total_in], outFlow: r[total_out], } for r in rows ] return JsonResponse({code: 0, data: data})注意line_flow_trend里对line_id做了非空校验并及时返回 400。这个细节看起来不起眼但前端传空值时如果不拦截filter(station__line_idNone)会静默返回空列表前端就显示一张空白图排查起来要多走好几个弯路。路由配置# 文件路径analysis/urls.py from django.urls import path from . import views urlpatterns [ path(api/city-summary/, views.city_summary), path(api/line-flow/, views.line_flow_trend), ]# 文件路径metro_system/urls.py from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(analysis.urls)), ]6.2 前端 ECharts 渲染前端建一个 HTML 页面引入 ECharts把接口数据喂给图表配置即可。!-- 文件路径analysis/templates/dashboard.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 title全国地铁数据可视化分析/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script style #bar { width: 100%; height: 480px; } /style /head body div idbar/div script const chart echarts.init(document.getElementById(bar)); fetch(/api/city-summary/) .then(res res.json()) .then(res { const data res.data || []; chart.setOption({ title: { text: 各城市地铁运营里程排行 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.map(d d.city) }, yAxis: { type: value, name: 里程(公里) }, series: [{ type: bar, data: data.map(d d.totalKm), itemStyle: { color: #3b82f6 } }] }); }) .catch(err console.error(加载失败, err)); window.addEventListener(resize, () chart.resize()); /script /body /html这里有个容易被忽略的点window.addEventListener(resize, ...)这一行必须有。ECharts 画布尺寸是初始化时确定的浏览器窗口变化时不监听resize图表不会自适应答辩演示时切换分辨率就会出现图表被裁切的情况。7. 大模型自然语言查询接入这是能给项目加分但也要谨慎处理的部分。核心原则前面说过不让模型直接执行 SQL 连数据库。我的做法是让模型输出一个受限的查询意图对象后端根据白名单映射到具体接口。先定义意图结构{ intent: compare_city, cities: [北京, 上海], metric: total_km, top_k: 2 }再写一个解析函数。下面用伪代码风格展示调用逻辑具体 SDK 请以你选用的模型服务文档为准不要照抄不存在的接口。# 文件路径analysis/nl_query.py import json from django.db.models import Sum from .models import MetroLine ALLOWED_INTENTS {city_rank, compare_city, line_count} ALLOWED_METRICS {total_km, line_count} def build_prompt(question: str) - str: return ( 你是地铁数据分析助手。根据用户问题输出 JSON字段包括 intent取值为 city_rank/compare_city/line_count、 metric取值为 total_km/line_count、cities城市名数组可为空。 只输出 JSON不要任何解释。用户问题 question ) def safe_resolve(intent_obj: dict): 把模型意图映射到受限查询杜绝任意 SQL。 intent intent_obj.get(intent) metric intent_obj.get(metric, total_km) cities intent_obj.get(cities) or [] if intent not in ALLOWED_INTENTS: return {code: 400, msg: 不支持的查询类型} if metric not in ALLOWED_METRICS: return {code: 400, msg: 不支持的指标} qs MetroLine.objects.values(city__name).annotate( total_kmSum(length_km) ) if intent compare_city and cities: qs qs.filter(city__name__incities) rows sorted(qs, keylambda r: r[total_km] or 0, reverseTrue) return { code: 0, data: [ {city: r[city__name], totalKm: round(r[total_km] or 0, 2)} for r in rows ], }这段代码有三个安全设计值得在论文里单独说明。其一ALLOWED_INTENTS和ALLOWED_METRICS是白名单模型输出任何不在名单内的值都会被拒绝。其二查询走 ORM 而不是拼接字符串天然免疫 SQL 注入。其三模型只负责“翻译”不负责“执行”即使模型输出格式错误最坏结果只是返回错误提示不会破坏数据。如果要做 Agent 版本把safe_resolve拆成几个独立工具函数查城市清单、查线路客流、城市对比交给模型的函数调用机制去决定调用顺序。这样多步查询也有了而且每一步都在你的控制范围内。至于多模态一个相对稳妥的扩展是把生成的图表用canvas导出为图片上传给支持视觉输入的模型让它生成一段文字解读附在图表下方。这个功能的定位是“辅助说明”不要把系统的核心结论依赖在它上面。8. 运行结果与效果验证启动服务python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000访问http://127.0.0.1:8000/admin/能看到四张表的管理页面说明模型和迁移都正常。访问http://127.0.0.1:8000/api/city-summary/如果返回{code: 0, data: [...]}并且data里有内容说明数据入库和聚合都通了。访问仪表盘页面能看到柱状图说明前后端联调成功。排查顺序建议固定成这个流程先查数据库有没有数据python manage.py shell里MetroLine.objects.count()再查接口返回浏览器直接打开 API 地址最后查前端控制台。三步能覆盖 90% 的问题。如果接口返回 500第一件事是看终端里的异常堆栈而不是改前端代码。Django 的报错信息通常很明确比如字段名写错、外键不存在、日期格式不匹配照着堆栈改就行。9. 常见问题与排查思路问题现象可能原因排查方式解决方案页面图表空白接口返回 404 或跨域浏览器 Network 面板看请求检查 URL 配置开发期用同源访问数据导入后翻倍重复执行导入脚本统计表记录数并与源文件比对加unique_together用ignore_conflictsTrue日期分组结果错位时区配置不一致检查USE_TZ和数据库时区统一USE_TZFalse或全程使用 UTC聚合统计很慢缺少日期索引查看查询执行计划给stat_date加db_index外键删除报错on_delete策略冲突看异常里的约束提示用CASCADE或先清理子表大模型返回非法 JSON提示词约束不足打印原始返回内容提示词加“只输出 JSON”解析加try/except兜底图表分辨率变化被裁切未监听 resize调整浏览器窗口观察绑定resize事件调用chart.resize()中文显示乱码数据库字符集不是 utf8mb4查表字符集配置建库时指定 utf8mb4 并重建表这里单独说一条不要在生产库上直接跑导入脚本。所有批量写入先在测试库验证确认记录数和预期一致再上线。如果脚本里包含delete()或update()全表操作执行前必须备份并且加--dry-run参数先看一眼影响范围。这类事故在毕设里很常见一条写错的清理语句就能把几天的数据全部清空。10. 最佳实践与答辩建议项目能跑通只是及格线能不能拿到高分取决于几个工程细节和表达方式。代码组织上把数据采集、清洗、分析、接口分成独立模块。scripts/放一次性脚本analysis/放模型和视图analysis/services/放聚合逻辑。视图函数只做参数校验和调用服务层不要把几十行聚合逻辑全塞进视图否则后期改一个指标要动十几个地方。配置管理上数据库密码、模型服务密钥这类信息不要写死在settings.py里然后连文件一起提交。用环境变量读取或者建一个local_settings.py加进.gitignore。答辩时如果老师问起安全性这一点能直接加分。日志与可观测性上给导入脚本和接口加上基本日志。至少记录三件事导入了多少条、耗时多少、失败了哪些。数据量一上来没有日志你根本不知道是 SQL 慢还是网络慢。答辩表达上我建议把讲解重心放在“分析结论”而不是“技术堆砌”。老师更想听的是你从数据里发现了什么规律北京和成都的线网结构差异说明了什么为什么换乘站占比高的城市客流强度更大能把这些讲清楚比罗列十个技术名词有用得多。最后补一个实用建议把系统做成可演示的状态。准备一份“一键启动”的说明数据库预先灌好数据图表打开就有内容。答辩现场最怕的不是功能少而是环境起不来——投影仪连着你的笔记本python manage.py runserver报个依赖缺失的错后面十分钟就在装包了。如果时间充裕可以再往下做两件事一是把分析结果导出成报告用pandas导出 Excel或者生成 PDF二是加一个简单的定时任务每天自动增量更新客流数据。这两点都属于“工程完整度”的体现成本不高但观感很好。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑