ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

交通事故数据可视化系统:Pandas清洗+Flask接口+ECharts实战

交通事故数据可视化系统:Pandas清洗+Flask接口+ECharts实战 简介数据分析是当今各行业决策的重要支撑数据可视化则将复杂统计结果转化为直观图表。Pandas作为Python最核心的数据处理库擅长清洗非结构化数据Flask提供轻量级Web接口ECharts实现高交互性前端渲染。三者结合可构建从数据清洗到可视化展示的完整链路。以交通事故数据为例讲解如何利用Pandas完成字段归一化、时间解析等预处理通过Flask设计聚合接口最终用ECharts呈现热力图、趋势图和原因分布图并解决常见编码、性能问题。这套实践适用于毕业设计、商业报表及数据分析项目助力快速搭建可用的可视化系统。1. 把 Python 毕设做成能答辩的可视化系统从数据处理到上线展示每年毕业季都能看到一批 Python 数据分析与可视化方向的题目大多撞车到某领域数据可视化系统设计与实现上。中国交通事故数据这个题听起来普通但真做起来坑不少数据字段乱、时间格式不统一、省市名称有别名、死亡人数分布极度偏斜处理不好图表就是一片糊。这套高分毕业设计资料我拆过一遍核心是三条线Pandas 清洗 Flask 后端接口 ECharts 前端渲染覆盖了从原始 CSV 到可交互大屏的完整链路还附了详细文档、开题报告、答辩 PPT 和源码头文件。适合正在做 Python 毕设、需要一套能跑通且能讲清楚的数据可视化项目的人也适合想快速上手数据分析 Web 展示这套组合的从业者。2. 数据清洗与预处理交通事故数据从 CSV 到 DataFrame 的完整流程2.1 原始数据长什么样先弄清楚字段再说分析交通事故数据最麻烦的不是分析而是源头数据不规范。这套资料里给的示例数据是按国内交管部门公开事故台账的常见格式整理的字段大概包括事故时间、事故地点、死亡人数、受伤人数、天气、道路类型、事故原因、责任方、车辆类型、经纬度等十几列。拿到手先别急着画图第一步是逐列看类型和缺失情况。我一般会先跑一个最基础的数据探查脚本确认数据规模和字段类型再决定清洗策略。这个环节决定了后面所有可视化的数据质量省不掉。import pandas as pd df pd.read_csv(accident_data.csv, encodingutf-8) print(原始数据形状:, df.shape) print(字段列表:, df.columns.tolist()) print(每列非空统计:) print(df.notnull().sum()) print(每列数据类型:) print(df.dtypes)这里有几个细节值得注意。encodingutf-8不是所有环境都能直接跑通如果报解码错误换成gbk或gb2312再试这是国内 CSV 最常见的坑。df.dtypes打印出来之后重点看时间列是不是object类型、死亡人数是不是被读成了浮点数——这两类问题在事故数据里出现频率极高。数据形状能告诉你有没有明显异常的大块缺失比如某列非空数量直接少了三分之一那就要考虑是记录缺失还是数据源导出问题。2.2 时间字段解析与省市字段拆分两个最常翻车的清洗点时间字段是第一个重灾区。原始数据里可能混着2023-05-12 14:30、2023/5/12、20230512好几种写法Pandas 的to_datetime能处理一部分但遇到纯数字格式或者带中文的日期就得提前做归一化。省市字段也不能直接用。原始数据经常是湖南省长沙市岳麓区这种全称但可视化时按省聚合和按市聚合是两个颗粒度拆开之前做不了地图热力。# 时间字段归一化 df[事故时间] pd.to_datetime(df[事故时间], format%Y-%m-%d %H:%M:%S, errorscoerce) # 拆分省、市字段 df[省] df[事故地点].str.extract(r(.?(?:省|自治区|市))) df[市] df[事故地点].str.extract(r(?:省|自治区)?(.?市)) # 提取小时和月份 df[小时] df[事故时间].dt.hour df[月份] df[事故时间].dt.month print(df[[事故时间, 省, 市, 小时, 月份]].head(10))参数说明errorscoerce是关键它会把解析失败的非法时间转成NaT而不是直接让程序崩掉。后续分析时对缺失时间做丢弃或者单独统计。str.extract用正则拆省和市(.?(?:省|自治区|市))这一段在匹配广西壮族自治区这类长名称时比单纯按字符切割要稳。dt.hour和dt.month是 Pandas 的 datetime 访问器提取之后可以用于后续按小时分布、按月份趋势的分析。这个环节做完数据基本就能用了。务工批次处理时还有一个补充字段的常见做法把事故严重程度按死亡人数分档比如 0 死亡为一般事故、1-2 人死亡为较大事故、3 人以上为重大事故这样后面出堆叠图或者饼图时维度更有意义。def severity_label(row): if row[死亡人数] 0: return 一般事故 elif row[死亡人数] 2: return 较大事故 else: return 重大事故 df[事故等级] df.apply(severity_label, axis1) print(df[事故等级].value_counts())df.apply按行遍历生成新列效率上在几万行数据内没问题几十万行以上建议用np.select或者向量化条件赋值否则等待时间会让人怀疑程序卡死了。3. 可视化架构设计Flask ECharts 的分层实现逻辑3.1 为什么选 Flask 而不是 Pyecharts 或 Django很多毕设选型时纠结可视化层用什么。纯 Pyecharts 做出来的图表是静态 HTML嵌进页面可以但参数联动、数据筛选交互做起来别扭。Django 给一个数据展示型项目带上 admin 后台、模型层和 ORM属于杀鸡用牛刀而且答辩时被问到 ORM 配置和中间件逻辑容易暴露功课不足。这套资料用的是 Flask ECharts 的组合。Flask 只承担数据接口的角色ECharts 在浏览器端负责渲染和交互两者职责清晰。核心逻辑是这样的Flask 读取清洗后的数据按前端请求的参数做聚合返回 JSON前端拿到数据后塞进 ECharts 的option配置项。这样前后端通过接口交互每一层都能单独验证答辩演示时也方便分段讲。3.2 后端接口设计与 JSON 返回格式约定后端接口设计上资料里把接口拆成了几个独立路由按年趋势、按月份分布、按事故等级堆叠、按省市统计、按事故原因分类。每个路由接受一个或多个参数时间段、省市筛选返回统一格式的 JSON。from flask import Flask, jsonify, request import pandas as pd app Flask(__name__) # 全局加载清洗后的数据 df pd.read_csv(accident_cleaned.csv, encodingutf-8) df[事故时间] pd.to_datetime(df[事故时间]) app.route(/api/trend/year, methods[GET]) def trend_by_year(): start_year request.args.get(start_year, default2018, typeint) end_year request.args.get(end_year, default2023, typeint) mask (df[事故时间].dt.year start_year) (df[事故时间].dt.year end_year) filtered df.loc[mask] result filtered.groupby(filtered[事故时间].dt.year).agg( 事故起数(事故编号, count), 死亡人数(死亡人数, sum), 受伤人数(受伤人数, sum) ).reset_index() return jsonify({ code: 200, data: { years: result[事故时间].astype(str).tolist(), accident_counts: result[事故起数].tolist(), death_counts: result[死亡人数].tolist(), injury_counts: result[受伤人数].tolist() } }) if __name__ __main__: app.run(debugFalse, host127.0.0.1, port5000)这个接口做的事情很直接从请求参数里拿年份范围过滤数据按年分组聚合再把结果转成jsonify可序列化的结构返回前端。request.args.get的default参数和typeint组合是后端接口健壮性的基础前端没传参时不会直接 500 报错。聚合时agg里同时算了事故起数和死亡、受伤人数两个关键指标前端可以一个请求出三条曲线。3.3 前端页面结构与 ECharts 数据装配方式前端是一个单页面应用HTML 文件里通过fetch或者axios发起请求拿到 JSON 后填入 ECharts。这套资料里前端分了几个div容器每个对应一张图表。async function loadYearTrend() { const res await fetch(/api/trend/year?start_year2018end_year2023); const result await res.json(); if (result.code ! 200) return; const data result.data; const chart echarts.init(document.getElementById(yearTrendChart)); const option { tooltip: { trigger: axis }, legend: { data: [事故起数, 死亡人数, 受伤人数] }, xAxis: { type: category, data: data.years }, yAxis: [{ type: value, name: 事故起数 }, { type: value, name: 人数, splitLine: { show: false } }], series: [ { name: 事故起数, type: bar, data: data.accident_counts }, { name: 死亡人数, type: line, yAxisIndex: 1, data: data.death_counts }, { name: 受伤人数, type: line, yAxisIndex: 1, data: data.injury_counts } ] }; chart.setOption(option); } loadYearTrend();注意这里双 Y 轴的设计事故起数和人数量级可能差很多倍混在一个坐标轴里起数少的年份在柱状图里会直接压成一条线。yAxis数组里放两个轴series里用yAxisIndex指定柱子走左轴、折线走右轴。如果数据量级差距更大比如死亡人数全年总和还不如某些地区单月事故起数零头可以把柱子改成对数轴或者在数据预处理时就先做归一化。前端图表自适应是另一个好习惯。window.onresize事件里调用chart.resize()避免浏览器窗口缩放后图表变形。资料里写了一个公共函数批量注册 resize 监听器改动不大但答辩演示时拖窗口这个操作会显得很专业。4. 核心可视化模块落地热力图、趋势图和原因分布图4.1 全国事故热力地图数据聚合与可视化映射热力图是这套毕设的视觉核心。用 ECharts 的地图组件把清洗后的省、市字段按行政区域聚合映射到地图色块上。但 ECharts 的地图 JSON 依赖 GeoJSON 数据国内省市边界数据需要单独加载。app.route(/api/map/province, methods[GET]) def map_province(): result df.groupby(省).agg( 事故起数(事故编号, count), 死亡人数(死亡人数, sum) ).reset_index() return jsonify({ code: 200, data: result.to_dict(orientrecords) })后端按省聚合后返回记录列表前端把这个数组直接传进 ECharts 的map系列。async function loadProvinceMap() { const res await fetch(/api/map/province); const result await res.json(); if (result.code ! 200) return; const chart echarts.init(document.getElementById(provinceMap)); fetch(/geo/china.json) .then(res res.json()) .then(geoJson { echarts.registerMap(china, geoJson); const option { tooltip: { trigger: item, formatter: function(params) { return params.name br/事故起数: params.value?.[0] br/死亡人数: params.value?.[1]; } }, visualMap: { min: 0, max: 500, left: left, top: bottom, text: [高, 低], inRange: { color: [#e0f3f8, #abd9e9, #74add1, #4575b4] } }, series: [{ type: map, map: china, roam: false, data: result.data.map(item ({ name: item[省], value: [item[事故起数], item[死亡人数]] })) }] }; chart.setOption(option); }); } loadProvinceMap();这段代码里有个取舍visualMap的max设置成 500意味着超过 500 起事故的省份颜色全部饱和。示例数据如果不调整几个事故高发大省会把全国地图整体压成深色看不出梯度。我一般建议先看一眼数据的describe()统计把max设在 80% 分位数左右让颜色分布拉开层次。4.2 按小时分布和事故原因图表堆叠图与饼图的组合事故按小时分布是一个很有展示价值的维度。交管部门的统计通常呈现早晚高峰两个峰值但不同省份因为产业结构不同分布形态差异很大。这里我用堆叠柱状图把不同事故等级叠在一起每个小时一根柱子能同时看到总趋势和等级结构。app.route(/api/hour, methods[GET]) def hour_distribution(): result df.groupby([小时, 事故等级]).size().reset_index(name数量) return jsonify({ code: 200, data: result.to_dict(orientrecords) })前端处理这种多维度数据时需要一个透视表结构。ECharts 的堆叠图用stack字段控制同一类柱子叠放。async function loadHourChart() { const res await fetch(/api/hour); const result await res.json(); if (result.code ! 200) return; const hours Array.from({length: 24}, (_, i) i); const levels [一般事故, 较大事故, 重大事故]; const seriesData levels.map(level ({ name: level, type: bar, stack: total, data: hours.map(h { const item result.data.find(d d[小时] h d[事故等级] level); return item ? item[数量] : 0; }) })); const chart echarts.init(document.getElementById(hourChart)); chart.setOption({ tooltip: { trigger: axis }, legend: { data: levels }, xAxis: { type: category, data: hours, name: 小时 }, yAxis: { type: value, name: 事故起数 }, series: seriesData }); } loadHourChart();这个find在 24×3 个循环里嵌套查找数据量小无所谓但如果前端要做城市级别的同日对比建议后端直接返回透视好的嵌套结构避免前端做了太多数据装配工作。我一般会在 Flask 里用pivot_table先生成宽表再转 JSON前端拿到什么就用什么。事故原因分布图就简单了饼图展示各原因占比。但有一个细节事故原因的原始字段经常有近义词比如超速行驶和超速驾驶疲劳驾驶和疲劳驾车。这类字段如果不做归一化饼图会出现两个几乎一样的扇区答辩时老师一眼就能看出数据没洗干净。reason_map { 超速行驶: 超速, 超速驾驶: 超速, 疲劳驾驶: 疲劳, 疲劳驾车: 疲劳 } df[事故原因_归一] df[事故原因].replace(reason_map)用replace一次映射到位比str.contains逐行判断要快得多。4.3 省市联动下钻从全国到城市的交互筛选省级热力图只能证明数据基础没问题真正让答辩加分的是下钻交互。用户点击某个省下方图表联动切换到该省各市的分布。实现上不必真的加载多级 GeoJSON更实用的做法是点击省名后后端按省过滤返回市内各区的数据前端用柱状图或列表展示。const provinceMapChart echarts.init(document.getElementById(provinceMap)); provinceMapChart.on(click, async function(params) { const provinceName params.name; const res await fetch(/api/city?province${encodeURIComponent(provinceName)}); const result await res.json(); if (result.code ! 200) return; // 渲染城市级柱状图 const cityChart echarts.init(document.getElementById(cityBar)); cityChart.setOption({ xAxis: { type: category, data: result.data.map(d d[市]) }, yAxis: { type: value }, series: [{ type: bar, data: result.data.map(d d[事故起数]) }] }); });encodeURIComponent用于处理省市中文名直接拼 URL 时空格和特殊符号会出问题。这里还隐含一个设计原则下钻不是刷新整个页面而是通过事件触发局部图表更新交互响应快页面不闪烁这在演示时比整页跳转体感好很多。5. 常见问题排查Python 环境、ECharts 加载和数据错乱的典型案例5.1 图表加载不出来页面白屏或一直转圈现象启动 Flask 后访问页面HTML 能打开但图表区域空白浏览器控制台报echarts is not defined或者Failed to fetch。原因两种常见情况。一是echarts.min.js是本地文件但路径写错浏览器加载脚本 404二是 Flask 的静态目录配置不对前端通过/static/echarts.min.js访问时被拦下来了。还有一种隐蔽情况浏览器直接双击打开 HTML 文件file://协议而数据接口跑在localhost:5000跨域请求被浏览器拦截。解决前端脚本引用改成相对路径../static/echarts.min.js或者直接走 CDN。但答辩现场的机器可能没外网所以我建议把 JS 文件下载到本地。Flask 应用默认会服务static目录下的文件只要 HTML 里引用路径对得上就不会有这个问题。开发调试时如果用的fetch请求报跨域在 Flask 里加一个after_request钩子给响应头加上Access-Control-Allow-Origin: *。5.2 Pandas 的 to_datetime 解析后全列变成 NaT时间全丢了现象数据读取正常但pd.to_datetime转换后整列变成了NaT后续按年份聚合结果全空。原因原始时间字段里混入了非标准格式比如2023年5月或者2023.5.12而format参数按严格模式匹配时直接把整列标记为解析失败——errorscoerce是把单个非法值变成NaT但如果格式完全不对整列全部NaT的翻车现场就会发生。解决先跑一次df[事故时间].head(50).astype(str)看看实际长什么样。常见做法是去掉format参数让 Pandas 自动推断格式。自动推断速度慢一点但兼容性最好。如果数据里混合多种格式还可以分段处理先处理标准格式剩余非空值再单独用apply逐个推断。5.3 ECharts 地图显示空白只有坐标轴没有色块现象折线图、柱状图都能正常显示唯独地图系列的区域全是空白也没有报错。原因地图组件依赖 GeoJSON 的注册。echarts.registerMap必须在setOption之前执行。如果异步加载 GeoJSON 文件fetch还没返回就执行了chart.setOption此时map: china引用的地图不存在图表自然画不出来。解决确保注册和渲染是串行的。把setOption放在fetch().then()回调里。另一个坑是 GeoJSON 文件本身有问题——下载的地图数据里的name属性和后端返回的省名字不一致比如 GeoJSON 里写湖南数据里写湖南省匹配不上就显示空白。保险做法是在前端做一层映射或者后端输出前统一去掉省字。5.4 数据量一大图表交互卡顿明显现象数据总量在十万行以上时前端每次筛选请求要等 2-3 秒图表切换有肉眼可见的延迟。原因后端每次请求都在做全表groupby聚合且前端把原始明细数据全部塞进了 ECharts。十万条以上的数据点绘制折线图浏览器渲染压力很大。解决我一般习惯在后端预计算。把常用维度的聚合结果提前算好存成缓存Flask 里简单加一个lru_cache装饰器或者启动时将聚合结果加载到内存。前端只请求聚合后的数据不在浏览器端做大数据量处理。明细数据展示部分用分页接口每次只取当前页面需要的记录。这是毕设答辩时讲性能优化最实在的一个点。5.5 汉字编码问题控制台输出乱码写入文件报错现象Windows 环境下启动 Flask控制台打印的中文全是乱码to_csv导出时直接抛UnicodeEncodeError。原因Windows 控制台默认用 GBK 编码读取输出Python 默认字符串是 UTF-8两者不一致。写 CSV 时如果不指定编码Pandas 用系统默认编码同样会撞上编码墙。解决控制台乱码在 Flask 的app.run前加sys.stdout.reconfigure(encodingutf-8)Python 3.7 支持。写文件一律显式指定编码参数df.to_csv(output.csv, indexFalse, encodingutf-8-sig)。utf-8-sig会在文件头加 BOM这个 BOM 能让 Excel 正确识别 UTF-8 编码用 WPS 打开也不会乱码。这是国内做数据导出时最实用的一个参数没有之一。6. 进阶自动化周报生成与可视化系统扩展的两个实用方向系统跑通图表展示后还能往两个方向做深都是答辩加分和简历可写的点。第一个方向是自动生成 PDF 或 Word 周报。用 Python 的reportlab或python-docx把图表以图片形式嵌入文档配合关键结论文字每周自动产出一份分析报告。实现上不复杂Flask 加一个/api/report/generate接口内部调用pyecharts或 Matplotlib 渲染图表到临时目录再插入 Word 文档。这里有个技巧Matplotlib 的中文显示需要font.sans-serif配置否则出图全是方框网上搜一下中文乱码解决方案就能搞定。第二个方向是把可视化系统对接数据库而不是每次启动都重新读 CSV。毕设如果写到这一层数据层就从文件导入升级成了数据仓库。常见做法是启动时把清洗后的数据写进 SQLite 或 MySQLFlask 查询时走 SQL 而不是 Pandas 全量读取。import sqlite3 import pandas as pd # 数据导入 SQLite conn sqlite3.connect(accident.db) df.to_sql(accident, conn, if_existsreplace, indexFalse) # 查询时段可用 SQL 聚合 query SELECT strftime(%Y, 事故时间) AS year, COUNT(*) AS accident_count, SUM(死亡人数) AS death_total FROM accident WHERE 事故时间 BETWEEN 2018-01-01 AND 2023-12-31 GROUP BY year result pd.read_sql_query(query, conn)to_sql是 Pandas 直接建表导入strftime在 SQLite 里做时间格式化。比起每次请求都在 Python 里加载整个 DataFrame 做分组这套方案在数据量涨到几百万行时优势明显。而且面试聊到架构演进从 CSV 到 SQLite 是完整的技术升级路径有故事可讲。验证可视化系统是否健壮我习惯用一个穷举参数的办法把接口所有参数组合写进一个测试脚本循环请求并断言返回code200和关键字段非空。几十组参数跑下来基本能把接口的边界情况覆盖掉。答辩前做一轮这种冒烟测试比现场被老师问到某个日期范围报错再手忙脚乱强得多。这套毕设资料真正值钱的地方不在代码本身而在数据清洗 → 接口设计 → 可视化装配这条链路是完整打通的。从第一次读入 CSV 到最终图表呈现每一步都有迹可循。从那以后我再做类似的数据分析项目都强制自己先把字段探查和清洗脚本写成可复用模块而不是每次临时写一段。磨刀不误砍柴工数据不干净后面所有可视化都是空中楼阁希望帮到你。本文还有配套的精品资源点击获取
返回列表