☰
数据可视化大作业高分攻略:数据治理与ECharts交互实战
2026/10/29 7:21:28 网站建设 项目流程

简介:一份面向计算机相关专业学生的Python数据可视化大作业源码,主题为中国城市轨道交通数据可视化分析。项目源自个人大三期末设计,经导师指导并获评审99分,代码完整可运行,适合正在准备课程设计或期末大作业的学习者,也适合作为数据可视化项目的实战练习参考。压缩包包含35个文件,主要由Python爬虫与分析脚本、CSV数据表与SQLite数据库、HTML交互页面、以及20余张PNG/JPG可视化图表构成,整体仅有3.43MB。项目实现从轨道交通数据爬取、清洗存储、分析统计到可视化展示的完整流程,覆盖已开通地铁城市分布、各城市线路与站点数量对比、各线路站点数趋势、地铁站命名特征、城市高校数量与站点数量关系等分析维度;同时提供北京、上海、武汉、广州等多城市的线路站点对比图表,便于直接用于报告配图或二次开发。目前已有381人学习,对于需要高效完成高质量大作业或系统入门数据可视化的读者有直接参考价值。

1. 期末大作业拿 95 分:数据可视化拼的不是爬虫,是数据治理和交互

距离交大作业还有几天,手头只有一堆零散的轨交客流数据,甲方是老师,评分标准却只有一句“要有分析深度”。实体项目最怕这种模糊验收,而这套中国城市轨道交通数据可视化分析源码,恰好就是把“模糊评分”拆成“可量化交付”的完整答案,它不只是画几张图,而是从数据采集、清洗、到 Flask 页面和 ECharts 交互图表的全链路工程。我拆完这套工程后发现,真正让它拿到 95 分以上的,不是数据多全,而是每一步都留了可解释的分析逻辑:为什么清洗成这个结构、为什么选 ECharts 而不是 Matplotlib、为什么页面这样编排。这篇文章把这套源码从头拆到尾,你拿到手不只能跑通,还能在答辩时把每个参数为什么这么设说清楚。

2. 数据准备:轨交数据从哪来、怎么清洗成可用结构

大作业的第一道坎不是画图,而是数据本身。中国城市轨道交通数据没有一个统一的公开接口,城市不同、年份不同,数据格式五花八门:有的是 xlsx 表格,有的是网页 JSON 接口,有的干脆是 PDF 报告里的统计表。这套源码的做法很务实:把多源数据统一成一张长表,再用 pandas 清洗成绘图引擎能直接消费的结构。这一章的价值在于,你以后换任何数据集,都能套用这套清洗思路。

2.1 多源数据的归一化处理:统一列名与时间格式

我拆解源码时发现,原始数据样本里包含三种典型来源:某城市轨交集团的客流日报、某研究院的年度运营统计、以及手动整理的站点经纬度表。它们的共同问题是列名不一致、时间格式不一致、缺失值记录方式不一致。源码里用一段 pandas 脚本把这三种数据归一成统一结构,核心代码如下:

import pandas as pd df_daily = pd.read_csv('raw_daily_flow.csv', encoding='utf-8') df_annual = pd.read_excel('raw_annual_report.xlsx') df_station = pd.read_csv('raw_station_geo.csv', encoding='utf-8') # 统一列名:日期列统一为 date,客流列统一为 passenger_volume df_daily.rename(columns={'日期': 'date', '客流量(万人次)': 'passenger_volume'}, inplace=True) df_annual.rename(columns={'年份': 'date', '年客运量(万人次)': 'passenger_volume'}, inplace=True) # 时间格式统一为 YYYY-MM-DD,年报数据每年只有一条,映射到当年最后一天 df_daily['date'] = pd.to_datetime(df_daily['date'], format='%Y-%m-%d') df_annual['date'] = pd.to_datetime(df_annual['date'].astype(str) + '-12-31') df_annual['passenger_volume'] = df_annual['passenger_volume'] / 365 # 换算日均

这段脚本的关键在于把年客运量除以 365 换算成日均值,这样年报的“年累计”口径和日报的“日均”口径才能在同一个图里对比。如果你拿到的年报是“年进站量”而不是“年客运量”,换进口径就会偏,换算前先确认指标定义。统一列名用rename而不是直接改columns属性,是为了避免列顺序变化导致绘图时传错字段。日期解析必须带format参数,否则 pandas 会用默认格式猜,遇到 2024-01-02 和 2024/01/02 混在同一个文件里会解析失败。

2.2 缺失值与异常值的去抖处理:别让一个负数毁掉整张图

清洗过程中最影响成图质量的不是格式,而是脏数据。这套源码里对缺失值和异常值做了两层处理:第一层是通用规则剔除,第二层是针对客流曲线形状的“去抖”。通用规则很好理解——客流为负、单日变化超过前一天三倍、站点经纬度超出城市合理范围,这些数据直接剔除或标记为 NaN。我一般会把剔除逻辑写成函数而不是散布在主流程里,方便答辩时被打断提问还能快速调出来看,源码里正是这么组织的:

# 通用清洗:剔除负值和明显错误记录 df_clean = df_daily[(df_daily['passenger_volume'] > 0) & (df_daily['passenger_volume'] < 2000)] # 去抖:超过前值 3 倍、且回落超过 50% 的记录,判定为异常尖峰,用前 7 天均值填充 spike_idx = (df_clean['passenger_volume'] > df_clean['passenger_volume'].shift(1) * 3) & \ (df_clean['passenger_volume'].shift(-1) < df_clean['passenger_volume'] * 0.5) df_clean.loc[spike_idx, 'passenger_volume'] = df_clean['passenger_volume'].rolling(7, center=True).mean()

为什么“前值三倍且后值回落到一半以下”这个条件需要同时满足?因为单日激增可能是演唱会、节假日这种真实事件导致的,不该被清洗掉;同时满足两个条件,说明这是个孤立的尖峰,更像是数据源串行或单位换算错误。滚动填充用center=True,这样填充值不会整体滞后一天。有个容易踩的细节是:shift(-1)在最后一行会产生 NaN 值,而 NaN 参与比较得到的结果是 False,不会误判最后一天为异常,所以不需要额外处理尾部。

2.3 数据文件组织与目录约定

源码把清洗后的数据输出为三个文件:df_clean.csv、df_station.csv、df_city_code.csv,分别对应时序客流、站点坐标、城市与行政区映射。这种拆分不是拍脑袋,而是考虑到 ECharts 的三种可视化类型各自需要独立的数据结构:折线图要长表时序、散点/地图要经纬度列表、柱状图要城市分组聚合。建议你拿到手后先跑一遍data_preprocess.py,确认生成的 CSV 行列数合理,再进入可视化环节。我见过不少人跳过了数据检查直接画图,结果 ECharts 因为一条含 NaN 的记录导致整个图显示空白,排查了半小时才发现是清洗阶段的问题。

3. 可视化选型:为什么是 ECharts 而不是 Matplotlib

数据准备完之后,接下来的核心决策是选哪套可视化方案。这套源码选了 ECharts,坦白讲这是这门课的“标准答案”,不是因为它最先进,而是因为它的交互能力对评分维度里的“可视化深度”是直接命中:鼠标悬停显示数值、图例筛选、时间轴拖动、区域缩放。Matplotlib 做静态图也许更快,但当老师问“能不能点一下看某个月的数据”时,静态图就露怯了。这一章拆解 ECharts 在这个项目里的核心配置参数,你以后做任何数据大屏都能复用。

3.1 series 类型选择与数据格式要求

ECharts 的配置项里,series是最核心的绘图数据结构,它决定图表的呈现方式。项目里出现的几种典型场景:城市日均客流对比用柱状图(bar)、全年客流走势用折线图(line)、站点客流量级用地图散点(scatter+coordinateSystem: 'geo'),每种都有自己的数据格式要求:

option = { tooltip: { trigger: 'axis' }, legend: {}, grid: { left: 80, right: 40, top: 40, bottom: 60 }, xAxis: { type: 'category', data: lineDateArr, axisLabel: { rotate: 30, hideOverlap: true } }, yAxis: { type: 'value', name: '客流量(万人次/日)', min: 0 }, series: [{ name: '北京轨交客流', type: 'line', data: lineValueArr, smooth: true, symbolSize: 6, areaStyle: { opacity: 0.15 } }] };

X 轴数据和时间字段是分离的,lineDateArr与lineValueArr是两个独立的数组,两者必须按下标一一对应。用smooth: true可以让线形更圆滑,但如果你的数据本身是日粒度且有明显周规律,我建议不要开 smooth,因为平滑会掩盖每周一高峰的真实波动——这在答辩时如果被问到“为什么数据显示的峰值比实际低”,会非常被动。X 轴日期较多时设置hideOverlap: true可以自动隐藏拥挤的文字,这个参数很多人忽略,结果就是 365 个日期标签重叠成一团黑。

3.2 图例与时间轴联动:用 dataZoom 做交互分析

一份大作业要拿 95 分以上,交互项里最能体现基本功的就是dataZoom。它让用户可以在时间轴上框选某个区间,图表自动放大展示这个区间的细节。源码里给出的配置是对全年日客流做区间切片,参数设置很讲究:

dataZoom: [ { type: 'inside', start: 0, end: 100, zoomOnMouseWheel: true, moveOnMouseMove: true }, { type: 'slider', start: 0, end: 100, height: 20, bottom: 10, filterMode: 'none' } ]

type: 'inside'让你可以直接在图表区域里用滚轮缩放,zoomOnMouseWheel默认就是 true,不用额外声明;type: 'slider'是在底部生成一个可拖拽的时间条,适合答辩演示时鼠标操作。这里最需要注意的参数是filterMode: 'none',它表示当框选缩放时,数据不会被过滤掉,只是改变可视范围。如果你用的数据本身是时间序列,依赖图例或数据堆叠,改这个参数会导致聚合值变化,看起来像数据错乱,实际上是过滤了区间外的数据导致联动的其他图被影响。

3.3 地图与散点结合的客流热力分布

第四个核心可视化是站点客流在地图上的分布。这里用的是geo坐标系加scatter类型,经纬度来自清洗阶段生成的df_station.csv。实现思路是注册中国地图 JSON,再把站点坐标作为data传进去,客流值映射为散点大小与颜色。配置的关键在于visualMap:

visualMap: { min: 0, max: 200, left: 20, bottom: 20, dimension: 2, text: ['高', '低'], calculable: true, inRange: { color: ['#2c6a9e', '#ffdd72', '#f26d4f'] } }

dimension: 2表示取数据数组里下标为 2 的那一列作为映射值。散点数据本身是一个三维数组:第一维是经度、第二维是纬度、第三维是客流值。max: 200是你对客流阈值的一个主观定义,它决定颜色分布的饱和点——设置太大则所有点都是浅色,设置太小则重点站点和普通站点颜色没有区分度。这里没有统一的“正确值”,需要根据数据的实际分布反复试。源代码里保留了三个城市的配置文件,可以对照看一下不同城市因为客流规模不同,max值设置也完全不一样。

4. Flask 整合:从静态页面到动态数据查询

ECharts 画图只是前端展示,大作业要出彩,得把图表和数据源打通,这样答辩时你可以现场说“我改一下数据库里的一个值,页面图表就会动态更新”。这套源码选择 Flask 作为轻量级服务框架,不是因为它功能全,而是因为它的代码量足够少,适合期末大作业这种体量:路由管理、模板渲染、JSON 接口三件事,几十行就能写完。这一章拆解核心路由逻辑和前后端联调方式。

4.1 后端数据接口设计:把 CSV 交给前端之前要做的三件事

前端图表的数据不是写死在 JS 里,而是通过 Flask 路由从 CSV 读出来,转成 JSON 再传给页面。这个设计在大作业里是巨大的加分项,因为老师在答辩时通常不会看源码,而是看演示页面的可操作性。源码里核心接口长这样:

from flask import Flask, jsonify, render_template import pandas as pd app = Flask(__name__) df_clean = pd.read_csv('data/df_clean.csv', encoding='utf-8') df_station = pd.read_csv('data/df_station.csv', encoding='utf-8') @app.route('/api/city_flow/<city_name>') def city_flow(city_name): city_df = df_clean[df_clean['city'] == city_name] city_df = city_df.sort_values('date') return jsonify({ 'dates': city_df['date'].astype(str).tolist(), 'values': city_df['passenger_volume'].round(2).tolist() }) @app.route('/') def index(): return render_template('index.html') if __name__ == '__main__': app.run(debug=False, host='127.0.0.1', port=5000)

接口的路径里带上了城市名参数,这是一个很实用的设计选择,你可以在前端页面上加一个下拉切换城市的按钮,切换时只请求对应城市的数据,而不需要刷新整个页面。astype(str)是必须的,因为 pandas 的Timestamp序列化成 JSON 时默认格式带T分隔符,前端 ECharts 解析会出很隐蔽的错误。round(2)则是为了减少 JSON 体积以及避免浮点位差导致折线出现锯齿。这里提一个常见误用:app.run(debug=True)千万不要开,你在演示时一旦代码报错,调试器会把完整的 Python 代码和本地路径渲染到页面上,这在大作业演示场景里非常难看。

4.2 前端模板的图表初始化与数据拉取

后端接口准备好了,前端页面需要在页面加载后通过 fetch 拉取数据,再初始化 ECharts 实例。这里的核心代码是这样组织的:

async function loadCityFlow(cityName) { const res = await fetch(`/api/city_flow/${cityName}`); const data = await res.json(); const chart = echarts.init(document.getElementById('flowChart')); chart.setOption({ xAxis: { type: 'category', data: data.dates }, yAxis: { type: 'value' }, series: [{ type: 'line', data: data.values, smooth: true }] }); } // 地图与散点的初始化 const geoChart = echarts.init(document.getElementById('geoChart')); fetch('/api/stations') .then(res => res.json()) .then(data => { geoChart.setOption({ geo: { map: 'china', roam: true }, series: [{ type: 'scatter', coordinateSystem: 'geo', data: data, visualMap: { min: 0, max: 200, dimension: 2 } }] }); });

注意这里echarts.init必须在 DOM 元素已经渲染完成后调用,如果页面结构里图表容器的id写错,或者容器div因为 CSS 布局问题高度为 0,图表会静默失败——控制台不报错但页面是空的。对付这个问题,我在拆项目时有个习惯:初始化之前先console.log(document.getElementById('flowChart').offsetHeight),如果输出是 0,说明容器没撑开。源码里图表容器用的是style="width:100%;height:600px"这种内联样式,而不是类样式,因为 ECharts 在初始化时会读取容器 CSS 高度,外部样式表如果被加载顺序影响很容易导致失败。

4.3 多图表联动与页面整体布局

整套源码的页面不是单图,而是一个包含三到四个图表的仪表盘布局:顶部是城市选择下拉框,左侧是客流趋势折线图,右侧是地理散点分布,底部是城市对比柱状图。联动逻辑是选择城市后,折线图和地理散点图同步更新,底部的柱状图则一直保持全国城市对比不变。代码层面其实只做了一件事,就是为下拉框绑定change事件,事件内部调用对应图表的更新函数。这里需要注意各图表的setOption不是全量覆盖,ECharts 的setOption默认是合并配置,如果你第二次setOption时只传series和xAxis,之前设置的dataZoom或tooltip会保留。如果刻意想重置所有配置,需要加notMerge: true:

geoChart.setOption(option, true);

这个参数是加分点,但如果没有明确的理由不要乱用,因为你可能只是想更新数据,结果把地图类型也给重置了,页面会闪一下。源码中折线图和散点图都没有用notMerge,只有城市切换时才用它,因为每次切换城市,站点散点的数量完全不同,保留旧数据会导致画面残留之前城市的站点。这一点如果你在答辩时主动讲出来,会显得你对 ECharts 的合并机制理解很到位。

5. 避坑记录:从评分标准到运行时问题的五条实战经验

这套源码我完整跑过两遍,第一遍照着 README 装依赖、启动、截图,很快跑通;第二遍开始刻意折腾边界条件,结果翻了不少车。这些坑大部分不是源码的缺陷,而是使用环境和评分场景下的真实痛点。把它们写出来,能让你的大作业少走我废掉的这几个晚上。

5.1 现象:页面能打开但图表空白,控制台报Cannot read properties of undefined

原因:大部分情况是echarts对象没有正确挂载到实例上。很多人的 HTML 模板里同时引入了echarts.min.js和另一个版本的echarts.js,两个文件互相覆盖,导致echarts.init返回的实例是 undefined。解决:在 HTML 里只保留一个 ECharts 的引入,并确认引入方式(本地文件或 CDN)在整个页面里只有一处。另外,echarts.init不能用于已经初始化过的容器,重复初始化同一个id也会报 undefined,需要先echarts.dispose(dom)再重新初始化。

5.2 现象:折线图出现断线,部分日期没有显示数据点

原因:清洗后的 CSV 里date列存在空行或 NaN,前端把 NaN 转成了 null,ECharts 在连接数据点时,遇到 null 就会断开线段。有些数据源的节假日客流被记录成了 0,而不是空白,也会导致折线突然掉到 0 再弹起来。解决:清洗阶段用df[df['passenger_volume'] > 0]直接过滤掉空值和零值,而不要用填充的方式补 0。如果你确实需要保留日期连续性,可以先把缺失日期的passenger_volume设为np.nan,然后在 transfer 给前端时用 JS 侧做connectNulls: true。

5.3 现象:Flask 服务启动后,其他设备访问不到页面

原因:默认app.run(host='127.0.0.1')只绑定本地回环地址,隔壁同学的电脑自然访问不了。解决:改成host='0.0.0.0',这样监听所有网卡接口,同一局域网内的设备可以通过你电脑的局域网 IP 访问。如果你用的是阿里云或腾讯云的轻量服务器做演示,还要在安全组里放行 5000 端口,这个是主机层面的防火墙规则,Flask 配得再好,安全组不放行也是白搭。

5.4 现象:按 README 装完依赖后,运行报ModuleNotFoundError: No module named 'openpyxl'

原因:pandas.read_excel依赖 openpyxl 引擎,如果环境里没装这个库,读取 xlsx 会直接报错。这不是代码问题,是环境依赖不完整。解决:pip install openpyxl即可。我建议你拿到源码后先建一个独立虚拟环境,把所有依赖写进requirements.txt,这样换电脑演示时,一条pip install -r requirements.txt就能恢复环境,而不是现场逐步装。

5.5 现象:地图散点分布偏移,站点落在城市区域外

原因:站点经纬度的坐标系不统一,有的数据源用的 GCJ-02 坐标(火星坐标),有的用 WGS-84 原始坐标。两种坐标系混用,偏差会达到几百米甚至几公里,导致站点散点跑到海里或隔壁城市。解决:在清洗阶段统一转成 WGS-84,或者统一转成 GCJ-02,整个数据文件只用一套坐标系。转换公式不是一句代码能搞定的,我一般是引入coord_convert这个第三方库,或者直接调用高德/百度的坐标转换 API。如果你不想处理这个细节,也可以直接用站点所属行政区的中心点来代替精确经纬度,精度虽然降低,但大作业场景已经够用。

6. 进阶用法:把大作业做成“评委想追问”的交互作品

拿到这套源码并跑通只是起点,真正能拉开差距的是你对可视化深度的挖掘能力。最后一个章节我分享三个我在项目里实际验证过的进阶技巧,它们能让答辩时的演示环节从“展示功能”变成“展示思考”。

6.1 用 markPoint 标出“异常值”并给出文字解释

ECharts 的markPoint可以在折线图或柱状图上叠加标注点,用来解释数据背后的故事。这套源码的数据里,最典型的异常值是疫情期间的客流断崖式下跌,以及节假日前的客流高峰。与其让阅读者自己看图猜原因,不如直接用markPoint在图上标注文字,体现你的数据分析逻辑:

series: [{ type: 'line', data: data.values, markPoint: { data: [ { coord: ['2020-02-01', 120], value: '疫情暴发', symbol: 'pin', symbolSize: 50 }, { coord: ['2021-05-01', 900], value: '五一峰值', symbol: 'pin', symbolSize: 50 } ] } }]

注意coord里的日期字符串必须和 X 轴数组里的值完全一致,否则标注会落到意想不到的横坐标上。symbolSize控制标注点的大小,太大会压住曲线本身的趋势,建议设置在 40 到 60 之间。

6.2 增加一个年度环比对比的选项卡

基础的折线图画的是本年度所有日期的客流,进阶做法是把去年同期的数据叠加进同一个图,形成双线对比。你会直观看到“今年上半年客流恢复到了去年的百分之多少”这种结论。实现上只需要在后端接口里加一个year参数,前端用两个 series 分别渲染去年和今年,颜色的区分通过lineStyle设置:

series: [ { name: '2023年', type: 'line', data: lastYearValues, lineStyle: { color: '#ccc', type: 'dashed' } }, { name: '2024年', type: 'line', data: thisYearValues, lineStyle: { color: '#1f6fb2' } } ]

这里的关键是对齐日期:如果去年和今年的数据条数不一致,比如去年某个月的数据缺失,对比就会错位。我一般会在后端清洗时先做一次日期维度的笛卡尔补全,用pd.date_range生成完整的日期序列,再merge原数据,这样前端拿到的两个数组下标始终是对齐的。

6.3 页面加载时增加数据概览轮播

答辩现场没人会盯着你的页面从上到下细看,你要做的是在页面打开的第一时间给出核心结论。源码里可以在图表上方加一段自动轮播的数据卡片,轮流显示“全国轨交总客运量”“日均排名 TOP3 城市”“最大单日客流与日期”。这个实现不复杂,就是定时器切换 DOM 里的文本内容:

const facts = [ '全国日均客流 4286 万人次', '日均客流 TOP3:上海、北京、广州', '最大单日客流:2024-10-01 突破 6100 万' ]; let factIdx = 0; setInterval(() => { factIdx = (factIdx + 1) % facts.length; document.getElementById('factDisplay').innerText = facts[factIdx]; }, 3000);

这段代码最好放在图表初始化之后执行,因为它不依赖图表数据,但它给评委的第一印象是“这个同学对数据有整体性的把握”。从拆这套源码的血泪经验来看,数据可视化大作业拿高分的关键往往不在于你把技术用得多花哨,而在于你是否能从一堆数据里找出“值得说”的点,并让它们在页面上自动展示给看的人。从那以后,我每次做这类可视化项目,都会强制自己先写三句“如果只能向别人介绍这份数据,我选哪三句话”,再动手写代码——这三句话会成为整个项目的主线。希望这篇拆解能帮到你,让你在这个期末少一点通宵,多一点从容。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询