ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

城市轨道交通数据可视化:Python大作业从数据到页面全攻略

城市轨道交通数据可视化:Python大作业从数据到页面全攻略

简介:这是一份以中国城市轨道交通数据为主题的Python数据可视化大作业源码,适合计算机相关专业学生用作期末大作业、课程设计或数据可视化实战练习。项目为大三学生高分设计,经导师指导认可,评审分达99分,代码完整可运行,零基础也能快速上手。压缩包共35个文件、约3.43MB,其中包含3个Python脚本(爬虫、数据分析、检索模块),subway.csv与city_line.db等原始数据文件,20张PNG图表和4个HTML交互页面,覆盖各城市线路站点数量、换乘站分布、地铁站命名风格、大学数量与站点关系等多个维度,并配有README说明文档帮助理解项目结构与运行流程。目前已有381人学习下载。通过该项目可掌握从数据爬取、清洗、统计到图表呈现的完整数据可视化流程,学习饼图、散点图、趋势图、双变量图等图形的绘制方法,并可直接在此基础上扩展新城市数据或调整分析维度,用于完善自身课程作业。

1. 为什么中国城市轨道交通数据可视化是性价比最高的 Python 大作业选题

如果期末只剩两周,又想拿 95 分以上,选题比写代码重要得多。中国城市轨道交通数据可视化分析这个方向,数据好找、维度丰富、图表类型能撑起一个完整的 Web 页面,而且主题自带“城市发展”的现实意义,答辩时天然有的聊。相比电商爬虫、疫情数据这类被写烂的选题,轨道交通数据既不会太冷门到没数据,也不会太热门到跟同学撞车。

这个选题真正考察的不是画图,而是三层能力:数据清洗干不干净、图表选得合不合理、页面有没有形成一个完整的分析闭环。大作业拿高分的本质,是让老师一眼看出你做了“数据分析”而不是“画了几张图”。下面从数据、工程、图表、避坑四条线展开,全程给可复现的步骤。

2. 数据是可视化的地基:中国城市轨道交通数据集从哪找、怎么清洗

2.1 数据来源和字段设计:先想清楚要回答什么问题

动手爬数据之前,先列清楚要回答的问题。常见做法是围绕四个维度建数据表:城市线路统计、站点分布、客流强度、开通时间线。这四个维度能支撑起“中国城市轨道交通发展现状”这个核心命题,也恰好对应答辩时老师最爱问的“你的分析维度是什么”。

数据的获取渠道,常见的有三个方向。第一是各城市地铁官网或轨道交通集团年报,能拿到线路里程、站点数量、年客运量这类权威数据,缺点是格式不统一,PDF 和网页混杂。第二是百科类网站的城市轨道交通词条,有整理好的线路列表和开通年份,适合批量处理。第三是地图服务的 POI 查询,能拿到站点经纬度坐标,但需要处理配额和返回结果的分页问题。我的建议是:不要只依赖一个渠道,用年报数据做底数,用百科数据补全线路开通年份,用 POI 数据补坐标,三份数据交叉验证。

字段设计上,先定义四张表,字段清晰后面所有图表都不用返工。线路表放城市、线路名称、开通年份、线路长度、站点数量、运营状态;站点表放站点名称、所属线路、城市、经纬度、换乘线路数;客流表放城市、年份、年客运量、日均客运量、客流强度;城市表放城市名称、省份、城市等级、GDP(可选)。这样分表的直接好处是,画地图用城市表,画趋势用客流表,画排名用线路表,职责单一,后期加图表不用改原有逻辑。

爬取过程我一般会做一层缓存,把每次请求结果存成 JSON 文件,一方面避免重复请求被限流,另一方面大作业需要提交源码和数据文件,本地留一份完整数据是加分项。常见做法是加一个带重试的函数,超时重试三次,每次间隔两秒,实测对大多数公开数据源都够用。

2.2 数据清洗的四个标准动作:类型、缺失、重复、口径

数据拿到手后,最花时间的不是写代码,而是清洗。轨道交通数据里典型的脏数据有三类:里程单位不统一(有的写“公里”有的写“km”),开通年份格式混乱(“2010年12月”和“2010/12”并存),还有换乘站被不同线路重复计数。这些问题在清洗阶段不解决,后面做图表时全部会翻车。

以下是清洗的核心片段,pandas 直接处理:

import pandas as pd # 读取原始线路数据 df = pd.read_csv("data/raw_lines.csv") # 1. 统一里程单位:把 'km'、'公里'、'千米' 统一转为数值型 def parse_length(x): if isinstance(x, str): num = ''.join(ch for ch in x if ch.isdigit() or ch == '.') return float(num) if num else None return x df["length_clean"] = df["length"].apply(parse_length) # 2. 解析开通年份:从 '2010年12月28日' 这类文本里提取年份 df["year"] = df["opening_date"].str.extract(r"(20\d{2})").astype("Int64") # 3. 工程口径:只保留运营中的线路,过滤在建和规划线路 df = df[df["status"] == "运营中"] # 4. 去重:同一线路名在不同数据源可能重复出现 df = df.drop_duplicates(subset=["city", "line_name"], keep="first")

逻辑说明:第一步把字符串型的里程统一成浮点数,Python 的isdigit()对中文数字和字母不生效,所以先过滤非数字字符;第二步用正则抽取四位年份,.astype("Int64")是 pandas 的可空整型,能保留缺失值而不是转成浮点;第三步工程口径必须做,年报数据里常混入“在建”“规划”状态的线路,不筛掉会把总里程数算错;第四步去重要指定subset,因为不同城市可能有一条同名线路(比如“1号线”在十几个城市都有),只按线路名去重会误删。

参数说明:keep="first"表示保留第一条记录,如果两个数据源有冲突,建议先在前面把权威数据源排在前面再执行去重。extract(r"(20\d{2})")这个正则只匹配 20 开头的四位年份,目前所有开通的地铁线路都满足,但如果未来有 19xx 年的老线路,需要把正则在 2 后面加一个(?:0|1|9)的兼容。过滤运营状态那一步,建议先输出一步 value_counts 看看状态字段到底有几种取值,不同数据源可能叫“已运营”“运营中”“Open”,要先归一化再过滤。

站点坐标的补全通常单独处理,因为 POI 数据拿过来是 JSON 嵌套结构。核心操作是用 pandas 的json_normalize展开,然后按站点名称去重,再跟线路表做关联。站点表和线路表是一对多的关系,一个换乘站出现在多条线路里,这是正常现象,但画散点图时要先去重再画,否则同一个站点坐标会被画好几次,导致图上有重叠点。

3. 技术栈选型与最小工程:pyecharts + Flask 的组合为什么最稳

3.1 选型理由:ECharts 图表生态和 Python 工程能力的平衡点

可视化大作业的技术栈,常见选择有三条路:纯 matplotlib 静态图、pyecharts 生成 HTML、前后端分离的 Web 工程。纯 matplotlib 的硬伤是默认样式太“科研风”,跟“数据可视化分析”的名字不匹配,而且交互能力几乎为零,做不出下钻和联动。前后端分离对两周工期来说太重,Vue 或 React 的学习成本会吃掉写分析的时间。

pyecharts 加 Flask 是这条路线上最稳的组合。pyecharts 底层是百度的 ECharts,图表类型覆盖地图、散点、漏斗、桑基图,而且代码是链式调用,跟写 Python 的习惯一致,不需要碰 JavaScript 一行。Flask 只承担两个职责:把 pyecharts 生成的 HTML 拼装进模板,以及启动一个本地服务方便答辩演示。整个工程只有 Python 一种语言,老师检查源码时不用面对一堆 node_modules。

版本上有一点要提醒:pyecharts 1.x 和 0.5.x 的 API 差异很大,网上很多旧教程用的是 0.5 的add()写法,新版本已经改成add_coord这种新接口。装的时候直接pip install pyecharts会拿到 1.x 以上版本,代码写法以官方文档为准,别照抄老博客。地图数据包要单独装,用pip install echarts-countries-pypkg这类 pypkg 包补充中国地图和城市地图,不装的话地图组件只会渲染出空白网格。

3.2 跑通最小工程:从数据文件到网页展示的一条龙脚本

直接给一个能跑的最小工程骨架。项目目录结构按“数据、图表、服务”三层切分,data 放清洗后的 CSV,charts 放生成图表的脚本,app.py 是 Flask 入口。这个结构本身就是大作业的好看门面,老师打开目录就明白作者的工程意识。

先写一个生成图表的模块,把地图和趋势图都封装成函数:

# charts/china_map.py from pyecharts import options as opts from pyecharts.charts import Map def china_total_map(df_city): """绘制中国城市地铁里程分布图""" data = [(row["city"], row["total_length"]) for _, row in df_city.iterrows()] chart = ( Map(init_opts=opts.InitOpts(width="100%", height="600px")) .add( series_name="运营里程(km)", data_pair=data, maptype="china", is_roam=True, label_opts=opts.LabelOpts(is_show=False), ) .set_global_opts( title_opts=opts.TitleOpts(title="中国城市轨道交通运营里程分布"), visualmap_opts=opts.VisualMapOpts(min_=0, max_=800, is_piecewise=False), ) ) return chart def save_chart(chart, filename): """渲染为 HTML 文件""" chart.render(f"templates/{filename}")

逻辑说明:data_pair接收的是(城市名, 值)的元组列表,城市名必须跟地图组件内置的省份名一致才能对上地理坐标,这是地图组件最常踩的坑之一。is_roam=True开启缩放和平移,答辩演示时能放大地图看细节,这个小交互是加分项。visualmap_opts的max_参数决定了颜色映射上限,建议先查看数据的最大值,如果最大值是 800 但max_设成 1000,整个地图的颜色都会偏浅,视觉上区分度很差。

Flask 入口的写法:

# app.py from flask import Flask, render_template import pandas as pd from charts.china_map import china_total_map, save_chart app = Flask(__name__) @app.route("/") def index(): # 读取清洗后的数据 df_city = pd.read_csv("data/city_lines_summary.csv") # 生成图表 HTML(首次访问时生成一次,后续直接用缓存文件) save_chart(china_total_map(df_city), "china_map.html") return render_template("index.html") if __name__ == "__main__": app.run(debug=False, port=5000)

逻辑说明:这里把图表生成放在请求处理函数里,是为了保证每次启动服务都是最新数据。debug=False是答辩演示时的稳妥设置,debug=True虽然改了代码能自动重载,但一旦代码里有异常,页面会直接抛出一个带调用栈的报错页面,演示现场很尴尬。port=5000是 Flask 默认端口,如果本地被占用,改成 5001 或 8080 都行。

模板页面把图表和文字结论拼在一起。用一个最简单的 iframe 嵌图表文件,再配一段分析结论,形成“图 + 文”的完整分析块,避免整页全是图没有观点。实际运行时,建议启动后先自己完整访问一遍四个页面,确认没报错再保存截图,答辩 PPT 的演示环节直接切浏览器,不要用录屏。

4. 可视化落地:中国城市轨道交通分析页面的图表组合与分析深度

4.1 图表选型逻辑:每种图都对应一个具体问题,不搞装饰图

很多大作业翻车在“堆图表”——一个页面放了十几个图,每个图都在展示不同信息,但串不成一条分析线。高分页面的标准是:每个图回答一个问题,所有问题连起来支撑一个核心结论。轨道交通数据可视化适合的核心结论可以定为“中国城市轨道交通发展的空间不均衡性与时间阶段性”,围绕这个结论,至少需要四类图来支撑。

地图类图表回答“空间上哪里强哪里弱”。用中国地图展示各城市运营里程和站点数量,颜色深浅一眼看出东部沿海和成渝地区的集中度。pyecharts 的Map组件对数值型数据有视觉映射,不需要额外处理就能看到梯度。排名类图表回答“谁在前谁在后”。横向条形图展示 TOP10 城市线路里程,用Bar组件的reversal_axis()方法把坐标轴反转,让数据从高到低纵向排列,更符合阅读习惯。时间类图表回答“发展节奏怎么变化”。折线图展示全国每年新增线路里程,可以看到几个明显的跃升节点,这条曲线是整个项目里最有“分析感”的图。比例类图表回答“线网结构怎么样”。饼图或环形图展示已开通线路的制式分布(地铁、轻轨、有轨电车、市域快线),或者换乘站占比,这类数据很容易从线路表里聚合出来。

选图的核心不是“好看”,而是“这个信息用什么编码方式最直观”。城市名和里程的对应关系用地图的位置编码比用表格强得多;排名关系用条形图的长度编码比饼图强得多,因为人眼对长度比对角度敏感。答辩时如果老师问“为什么这里用条形图不用饼图”,能答出这条就说明真的理解了可视化原理。

4.2 组合联动:让图表之间形成分析闭环,而不是孤立拼盘

图表之间不能是孤岛。两个维度让页面“活”起来:一个是筛选联动,一个是叙事顺序。筛选联动用 Flask 的查询参数就能实现,不需要上 JavaScript 框架。点击城市跳转到该城市的详情页,详情页展示该城市各条线路的长度对比和开通时间轴,这是最直观的下钻。

叙事顺序按“总分总”的结构组织页面。首页放全国总览:地图加全国累计里程数、城市数量等几个关键指标卡片,让老师一眼看到结论。中间页放分维度分析:TOP 城市排名、年度新增里程趋势、线网制式结构。末页放城市详情:选一个代表性城市(比如成都或武汉,近十年地铁发展最快的城市)做案例拆解。这样页面结构本身就是一份分析报告,老师往下滚动时思路是连贯的,不会被东一张西一张的图表打乱。

以下是条形图和折线图的生成代码,注意看两个图的参数差异:

# charts/trend_charts.py from pyecharts import options as opts from pyecharts.charts import Bar, Line def top10_bar(df_top10): """TOP10 城市运营里程横向条形图""" cities = df_top10["city"].tolist() lengths = df_top10["total_length"].tolist() chart = ( Bar(init_opts=opts.InitOpts(width="100%", height="500px")) .add_xaxis(cities) .add_yaxis( series_name="运营里程(km)", y_axis=lengths, label_opts=opts.LabelOpts(position="right"), itemstyle_opts=opts.ItemStyleOpts(color="#337ecc"), ) .reversal_axis() .set_global_opts( title_opts=opts.TitleOpts(title="中国城市轨道交通运营里程 TOP10"), yaxis_opts=opts.AxisOpts(name="城市"), xaxis_opts=opts.AxisOpts(name="里程(km)"), ) ) return chart def yearly_trend_line(df_yearly): """全国年度新增里程折线图""" years = df_yearly["year"].astype(str).tolist() added = df_yearly["added_length"].tolist() chart = ( Line(init_opts=opts.InitOpts(width="100%", height="450px")) .add_xaxis(years) .add_yaxis( series_name="新增里程(km)", y_axis=added, is_smooth=True, label_opts=opts.LabelOpts(is_show=True), markpoint_opts=opts.MarkPointOpts( data=[opts.MarkPointItem(type_="max", name="峰值")] ), ) .set_global_opts( title_opts=opts.TitleOpts(title="全国城市轨道交通年度新增里程趋势"), yaxis_opts=opts.AxisOpts(name="新增里程(km)"), ) ) return chart

逻辑说明:条形图里reversal_axis()必须放在add_yaxis之后调用,它交换的是 X 和 Y 轴的角色,数据本身不用动。label_opts=opts.LabelOpts(position="right")在反转后才会显示在条形右侧,如果忘记设置,默认标签显示在条形内部,数字大时会被遮住。折线图的markpoint_opts是最直观的“分析痕迹”——自动标记出新增里程最高的年份,这个细节老师在答辩时常会指着问,回答“自动找最大值并标注”就能体现对组件的熟练度。is_smooth=True把折线变成平滑曲线,数据本身没有精度损失,只是视觉更连贯。

细节上,图表的标题不要只写“折线图”三个字,要写成“全国城市轨道交通年度新增里程趋势”,这个标题本身就是分析结论的浓缩。X 轴的年份建议转成字符串再传,如果传整数而年份跨度太大,ECharts 默认按连续刻度显示,可能出现没有数据的空档年份。所有图表的width设置成百分比而不是像素,这样浏览器缩放时图表自适应,投影仪上演示也不会变形。

5. 大作业避坑:城市轨道交通数据可视化最常见的 5 个翻车点

5.1 地图组件白屏或城市名对不上

现象:地图渲染出来是完整中国轮廓,但所有省份和城市都是灰色,或者某些城市的数据没有显示。原因:两类情况居多,一是 pyecharts 的地图数据包没装完整,只装了核心包没装中国地图包;二是 CSV 里的城市名和地图组件内置的名称不一致,比如数据里写“成都”但地图包收录的是“成都市”,或者写“台北”而地图组件里是“台湾”。解决:先确认地图包安装完整,再执行一步名称映射,常见做法是在预处理时给数据加一列“地图名”,用字典手动对应。检查城市名最快的方法是先打印地图包支持的区域名列表,跟数据里的城市名单做一次集合运算,差异一眼就看出来。

5.2 CSV 里的数字被 pandas 读成字符串

现象:图表画出来,数值轴上一片空白,或者柱子的高度都一样。原因:从网页或 PDF 复制数据时混入了全角字符、空格、单位,pandas 读取时识别成字符串。这个问题在里程字段上尤其常见,因为源数据写着“28.5公里”而不是“28.5”。解决:在清洗阶段统一做类型转换,pd.to_numeric(series, errors="coerce")可以把非法字符串转成 NaN,再做一次dropna()。这里注意errors="coerce"会把“28.5公里”转成 NaN 而不是 28.5,所以要先做正则提取数字再做类型转换,顺序不能反。

5.3 图表渲染后中文全部变成方块

现象:浏览器打开 HTML,所有标题和图例的中文显示为豆腐块。原因:绝大多数情况是 HTML 文件缺charset声明,或者 pyecharts 生成模板的编码跟浏览器默认编码不一致。解决:在 HTML 的 head 里手动加<meta charset="utf-8">,如果用的是 Flask 的render_template,确保模板文件本身以 UTF-8 保存。这个问题在 Windows 上格外高发,因为本地文件可能被编辑器默认存成了 GBK,用 VS Code 右下角把编码切换成 UTF-8 重新保存一次就行。

5.4 数据口径不统一导致结论自相矛盾

现象:首页写“全国累计运营里程 1 万公里”,趋势图里加总却是 9000 公里,答辩时被老师当场发现。原因:不同数据源对“运营里程”的定义不同,有的算地铁制式,有的包含有轨电车和市域快线;有的统计到去年年底,有的统计到今年年中。解决:在数据文件旁边放一个 README,写明每条数据的统计时点和口径说明,这里不需要写多复杂,两句话讲清楚即可。图表上也要做标注,比如“数据截至 2023 年 12 月”,这个细节能把严谨性拉高一个档次。数据的一致性检查要写进清洗流程里,做完每张表都sum加总核对一遍。

5.5 大作业只交一个 HTML 文件,没有任何工程结构

现象:源码目录打开只有index.html和一个data.csv,图表全部是静态文件。原因:图省事,直接用 pyecharts 生成 HTML 后就没再管工程结构。这类作业即便图表再漂亮,也大概率被归到“低分档”,因为大作业考察的是“用 Python 做数据分析处理的能力”,而不是“调出一个图表的能力”。解决:按上一章的目录结构组织工程——data/放原始数据和清洗后数据,scripts/放清洗脚本,charts/放图表生成代码,app.py放服务入口,templates/放页面模板。哪怕整个工程一共只有五个.py文件,这个结构也会让老师觉得“这是一个完整的分析项目”。

6. 从 85 到 95:答辩口径、叙事串联与最后的验证方法

高分和低分的分界线,往往不在图表数量,在“分析有没有闭环”。最后一步做两件事:一是把页面叙事按“数据—图表—结论”三层串讲一遍,二是把隐藏的得分点挖出来展示给老师看,不要等着老师自己发现。

具体做一个“关键指标卡片”区块,放在地图上方:全国开通城市数量、累计运营里程、年度最高新增量、最大换乘站深度。这四个数字从四张数据表里聚合而来,每个卡片配一句结论式描述,比如“截至 2023 年底,全国共有 xx 个城市开通轨道交通”。这一屏不用任何图表,但它是整个页面最像“分析报告”的部分,答辩时先讲这四个数字,再进入图表逐个展开,节奏会非常清晰。

二级页面加一个“城市详情对比”功能,选两个城市做并排对比——比如北京和成都,一个老牌、一个新兴,对比线路总长、站点密度、年均新增里程三个维度。两个城市的并排对比比单看排名更有说服力,因为展示了“这个数据跟什么比才有意义”。答辩时主动讲这条,很容易把话题引向城市发展差异这个方向,老师觉得你是在做数据分析,不是在交作业。

最后验证环节,我自己吃过一次亏:所有图表在 Chrome 上正常,答辩现场的投影仪分辨率是 1024 x 768,页面直接横向溢出,右侧图表看不全。后来在代码里统一加了响应式配置——width="100%"加上 Flask 模板里的 viewport 设置。交作业前养成一个习惯:用浏览器开发者工具模拟 1280 和 1024 两种分辨率各截图一次,确认没有布局错乱,再交源码包。这一步的成本只有五分钟,却是最值得花的五分钟。

整个项目做完,你手里会留下一套可复用的分析工程:一份清洗干净的数据表、一张覆盖地图/条形图/折线图/饼图的图表库、一个 Flask 服务骨架。这套东西以后做其他选题(餐饮数据、房价数据、空气质量数据)都能直接换数据复用,不是一次性的“作业代码”。我当年做完这个项目后,把图表库的函数改改就接入了另一个课程设计,省了至少三个晚上的工作量。数据可视化大作业真正的收获不是说会调用 pyecharts,而是建立了“先想结论、再选图表、最后编码”的分析习惯——这套思路在任何数据岗面试的案例分析题里,都是实打实的加分项。希望这个方案能帮到你,少走一点我走过的弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进