
简介本资源是一套基于Python实现的中国交通事故数据分析与可视化系统源码及配套资料面向计算机、人工智能、自动化等专业的本科生与教师适用于毕业设计、课程大作业及数据分析实践学习。项目已通过高分答辩98分代码经完整调试验证可直接运行兼顾入门学习与功能二次开发需求。压缩包共7个文件含3个Jupyter Notebook.ipynb用于数据清洗、统计分析与可视化核心逻辑实现3个CSV数据文件含真实预处理数据与模拟事故数据以及1个交互式HTML报告整体仅244KB轻量易部署。目前已有175人下载学习资源结构清晰、模块分工明确——acc.ipynb主控分析流程wwa.ipynb侧重地理可视化accident.html提供最终成果展示配套数据文件命名规范、字段注释完整显著降低初学者理解门槛与调试成本。1. 为什么用 Python 做中国交通事故数据分析可视化不是“炫技”而是真能跑通从原始报表到决策看板的闭环你手头有一份来自某市交警支队的 Excel 表格2020–2023 年共 17 万条事故记录字段包括时间、地点含经纬度模糊化、天气、道路类型、车辆类型、伤亡人数、责任认定、是否酒驾……但打开后第一反应是——列太多、空值乱、地名不统一“中山路”“中山北路”“中山路北段”全算不同值、时间格式混着“2022/03/15 14:20”和“2022-03-15T14:20:00”……这不是数据是“数据沼泽”。而毕业设计要的不是“能画图”是让老师一眼看出你懂业务、会清洗、能归因、有工程意识。这个标题下的系统本质是一个轻量级但可交付的 ETLBI 工作流用 pandas 做脏数据手术刀式清洗用 geopandas contextily 搞带底图的热力聚合用 plotly express 实现交互式时序下钻最后用 Flask 封装成一个本地可运行的 Web 看板——不依赖服务器、不调 API、不连数据库双击run.bat就能启动所有代码、数据、说明全在单个文件夹里。它适合计算机、交通工程、安全工程、统计学等专业的本科生尤其适合那些被“必须用 Django/Java/SpringBoot”的刻板印象吓退却想交出一份逻辑自洽、结果可验证、答辩能讲清每一步为什么这么写的高分毕设的同学。别信“五分钟生成可视化”的噱头真实场景里80% 的时间花在把“XX路与YY街交叉口”标准化成统一坐标剩下 20% 才是让图表动起来。2. 从原始 Excel 到结构化 DataFrame清洗不是删空行而是重建事故语义逻辑交通事故数据的脏有它的行业特性人工录入导致的缩写泛滥“机”机动车、“非”非机动车、“行”行人、责任字段的文本描述混乱“主责”“主要责任”“负主要责任”混用、时间跨午夜导致的日期错位23:59 发生、00:05 报警系统录成两天。直接pd.read_excel()后.dropna()是自杀式操作。我们必须按事故分析的业务链条重定义清洗步骤时间轴对齐 → 空间位置归一 → 责任语义标准化 → 伤亡量化建模。2.1 时间字段的三重校验与自动纠偏原始数据中accident_time列常含三种格式纯时间字符串22:30、带日期时间2022/05/12 08:15、甚至错误填入“待查”“不详”。关键不是统一转 datetime而是先识别事故真实发生时刻——因为报警时间、录入时间、定责时间可能错开数小时。我们约定以accident_time为主若为空或无效则用report_time回填若两者皆空标记为time_unknown并进入人工复核队列后续导出为review_needed.csv。import pandas as pd import numpy as np from dateutil import parser def parse_accident_time(row): 智能解析事故时间优先取accident_time次选report_time失败返回NaT for col in [accident_time, report_time]: val row[col] if pd.isna(val) or str(val).strip() in [, 待查, 不详, 未知]: continue try: # 先尝试严格格式YYYY/MM/DD HH:MM 或 YYYY-MM-DD HH:MM:SS if isinstance(val, str) and (/ in val or - in val) and : in val: return pd.to_datetime(val, errorscoerce) # 再尝试纯时间HH:MM需补当日日期用record_date字段若无则用系统日期 elif isinstance(val, str) and : in val and len(val.split(:)) 2: time_part val.strip() # 尝试从record_date列取日期 date_part row.get(record_date) if pd.notna(date_part): try: base_date pd.to_datetime(date_part, errorscoerce).date() return pd.to_datetime(f{base_date} {time_part}, errorscoerce) except: pass # 否则用今天日期仅用于临时占位后续需标注 return pd.to_datetime(f{pd.Timestamp.today().date()} {time_part}, errorscoerce) except: continue return pd.NaT # 应用清洗 df[accident_datetime] df.apply(parse_accident_time, axis1) df[accident_date] df[accident_datetime].dt.date df[accident_hour] df[accident_datetime].dt.hour提示parser.parse()在这里不用因为它对“22:30”这种纯时间会默认补 1900-01-01导致后续按日聚合时全部挤在同一天。我们强制用pd.to_datetime(..., errorscoerce)配合手动拼接确保时间语义可控。errorscoerce是后悔药——宁可返回 NaT 也不让错误时间污染统计。2.2 地理位置的两级标准化从文本地址到 WGS84 坐标原始数据中的location字段是典型“人脑友好、机器灾难”“南山区科技园科苑路与科发路交汇处东北角”“福田区梅林街道梅华路123号旁”“宝安大道与西乡大道立交桥下”直接调用高德/百度 API毕业设计不允许外网请求且批量调用有配额。我们的方案是先做规则匹配 关键词库映射再用离线地理编码器兜底。第一步构建《深圳市道路主干道关键词库》road_keywords.csv含 3 类字段road_name如“深南大道”、alias如“深南东路”“深南中路”、center_lonlat该道路中心点 WGS84 坐标用 QGIS 从 OpenStreetMap 导出。第二步用正则提取地址中的主干道名匹配关键词库取最近坐标第三步对未匹配项用geopy的Nominatim需提前下载离线版geopy-offline在本地运行。import re import csv from shapely.geometry import Point # 加载关键词库离线 keywords_df pd.read_csv(data/road_keywords.csv, encodingutf-8) keywords_dict {} for _, row in keywords_df.iterrows(): for alias in [row[road_name]] str(row[alias]).split(|): if pd.notna(alias) and alias.strip(): keywords_dict[alias.strip()] (row[lon], row[lat]) def extract_road_from_text(text): 从文本中提取最可能的主干道名 if not isinstance(text, str): return None # 匹配“XX大道”“XX路”“XX街”“XX高速” pattern r([\u4e00-\u9fa5]{1,10}(?:大道|路|街|高速|快速路|隧道|立交|桥)) matches re.findall(pattern, text) if not matches: return None # 返回最长匹配优先“深南大道”而非“深南” return max(matches, keylen) def get_location_coord(text): 获取坐标先关键词库再离线地理编码 if not isinstance(text, str): return (np.nan, np.nan) # 步骤1关键词匹配 road_name extract_road_from_text(text) if road_name and road_name in keywords_dict: return keywords_dict[road_name] # 步骤2离线地理编码示例用 mock实际替换为 geopy-offline # from geopy_offline import NominatimOffline # geolocator NominatimOffline() # location geolocator.geocode(f{text}, 深圳市, 广东省, timeout10) # return (location.longitude, location.latitude) if location else (np.nan, np.nan) return (np.nan, np.nan) # 未匹配项留空后续人工补充 df[lon], df[lat] zip(*df[location].apply(get_location_coord))参数说明road_keywords.csv是本系统可复现性的核心——它把模糊地址变成确定坐标。你不需要自己爬地图直接用 OpenStreetMap 导出深圳主干道中心点QGIS 操作加载 OSM 数据 → 按道路名称筛选 →Vector → Geometry Tools → Centroids→ 导出 CSV。extract_road_from_text的正则[\u4e00-\u9fa5]{1,10}限制汉字长度避免匹配到“事故”“发生”等干扰词max(matches, keylen)确保“北环大道”优先于“北环”。2.3 责任与伤亡字段的语义归一化让“主责”“主要责任”“负主要责任”变成同一数字标签responsibility列的文本值多达 12 种变体但业务上只有 4 类0无责、1次责、2同责、3主责、4全责。不能靠str.contains(主责)粗暴匹配——“负主要责任”含“主责”但“主要责任”也含“主责”会重复赋值。正确做法是定义责任强度关键词权重按最大权重匹配。# 定义责任关键词权重表权重越高匹配优先级越高 responsibility_map { 全责: 4, 全部责任: 4, 负全部责任: 4, 主责: 3, 主要责任: 3, 负主要责任: 3, 同责: 2, 同等责任: 2, 负同等责任: 2, 次责: 1, 次要责任: 1, 负次要责任: 1, 无责: 0, 无责任: 0, 不负责任: 0, } def map_responsibility(text): 按关键词权重映射责任等级 if not isinstance(text, str): return np.nan text_lower text.strip().lower() # 按权重降序遍历找到第一个匹配的关键词 for keyword, weight in sorted(responsibility_map.items(), keylambda x: x[1], reverseTrue): if keyword.lower() in text_lower: return weight return np.nan df[resp_level] df[responsibility].apply(map_responsibility)逻辑说明排序sorted(..., reverseTrue)确保“全责”权重4比“主责”权重3先匹配避免“负全部责任”被误判为“主责”。text_lower统一小写兼容“主要责任”和“主要责任”。返回np.nan而非 0因为 0 是有效值无责必须区分“明确无责”和“未识别”。3. 用 GeoPandas 构建空间分析层为什么热力图不能只靠 plotly.scatter_geo很多毕设同学直接px.scatter_geo(df, latlat, lonlon)结果地图上一堆重叠小点看不出哪里事故高发。问题在于交通事故是空间聚集事件单点不具统计意义必须做空间聚合Spatial Aggregation。plotly 的scatter_geo是渲染层不是分析层真正的分析必须在 GeoPandas 中完成将点数据栅格化Rasterize或聚类Cluster生成带统计值的面要素GeoDataFrame再传给 plotly 渲染。3.1 用 H3 六边形网格替代传统矩形网格解决“边界效应”问题传统geopandas.overlay()切矩形网格会导致南山区科技园的六边形网格 vs 罗湖区老城区的六边形网格面积差异大 → 事故密度计算失真网格边界切割道路把一条路上的事故硬拆到两个网格 → 归因错误H3 是 Uber 开源的球面六边形分级网格系统同一层级resolution下所有六边形面积误差 1%且支持父子网格嵌套。我们用h3库将每个事故点落进 resolution8 的六边形平均边长约 780 米适合城市级分析再按六边形 ID 聚合事故数、平均伤亡、酒驾占比。import h3 import geopandas as gpd from shapely.geometry import Polygon def point_to_h3(lat, lon, resolution8): 将经纬度转为 H3 索引 if pd.isna(lat) or pd.isna(lon): return None try: return h3.geo_to_h3(lat, lon, resolution) except: return None # 添加 H3 索引列 df[h3_index] df.apply(lambda row: point_to_h3(row[lat], row[lon]), axis1) # 按 H3 聚合关键 h3_agg df.groupby(h3_index).agg( accident_count(h3_index, count), avg_injured(injured_num, mean), drunk_ratio(is_drunk, mean), # is_drunk 是布尔列mean 即酒驾占比 avg_hour(accident_hour, mean) ).reset_index() # 将 H3 索引转为 GeoDataFrame六边形面 h3_gdf gpd.GeoDataFrame( h3_agg, geometry[Polygon(h3.h3_to_geo_boundary(h, geo_jsonTrue)) for h in h3_agg[h3_index]], crsEPSG:4326 )参数说明resolution8是经验值——res7边长≈2.4km太粗抓不住科技园内部差异res9边长≈260m太细部分六边形事故数为0噪声大。h3.h3_to_geo_boundary(..., geo_jsonTrue)直接返回 GeoJSON 格式坐标环Polygon()可直接解析。注意h3_gdf的geometry是面不是点这才是热力图的正确输入。3.2 在底图上叠加 H3 热力用 contextily 加载离线地图瓦片plotly 的scatter_geo不支持面渲染热力必须切到plotly.express.choropleth。但 choropleth 需要底图而在线底图如px.set_mapbox_access_token在答辩现场断网就崩。解决方案用contextily下载离线瓦片保存为本地 MBTiles 或 GeoTIFF再用rasterio读取为xarray.DataArray最后用plotly.graph_objects.Choroplethmapbox渲染。import contextily as ctx import rasterio from rasterio.transform import from_bounds import numpy as np # 步骤1下载深圳范围离线瓦片提前执行一次存为 tiles.tif # ctx.bounds2raster((113.7, 22.4, 114.6, 22.9), tiles.tif, zoom12, sourcectx.providers.OpenStreetMap.Mapnik) # 步骤2读取瓦片为 DataArray简化版实际需处理多波段 with rasterio.open(tiles.tif) as src: tile_array src.read([1,2,3]) # RGB transform src.transform crs src.crs # 步骤3创建 Choroplethmapbox核心 import plotly.graph_objects as go fig go.Figure(go.Choroplethmapbox( geojsonh3_gdf.__geo_interface__, # GeoDataFrame 转 GeoJSON locationsh3_gdf.index, zh3_gdf[accident_count], colorscaleYlOrRd, zmin0, zmaxh3_gdf[accident_count].quantile(0.95), # 截断异常值 marker_opacity0.7, marker_line_width0.5, showscaleTrue, colorbardict(title事故数) )) # 添加离线底图关键参数 fig.update_layout( mapboxdict( stylewhite-bg, # 禁用在线样式 layers[ { source: { type: image, url: tiles.tif, # 本地文件路径 coordinates: [ [113.7, 22.9], [114.6, 22.9], [114.6, 22.4], [113.7, 22.4] ] }, below: traces, sourcetype: image } ], center{lat: 22.55, lon: 114.05}, zoom11 ), margin{r:0,t:0,l:0,b:0} )避坑重点coordinates必须按顺时针顺序提供瓦片四角经纬度否则图像翻转。zmaxh3_gdf[accident_count].quantile(0.95)是玄学参数——去掉它热力图会被几个超高峰值如某路口全年 200 起压扁其余区域全蓝。用 95 分位数既能突出热点又保留梯度。4. 避坑这 4 个血泪经验让我重写了 3 次数据管道做毕设最怕的不是不会写代码而是写完发现结果不可信。以下是我踩过的坑按出现频率排序每一条都附带df.info()和df.describe()的诊断线索4.1 现象热力图显示“深圳湾大桥”事故数为 0但原始数据里明明有 12 条记录原因原始数据中“深圳湾大桥”的lat/lon全部是(22.5000, 113.9000)—— 这是录入员偷懒填的默认坐标不是真实 GPS。h3.geo_to_h3()把它们全塞进同一个六边形但该六边形在地图上位于南山区某小区而非大桥。解决在清洗阶段加空间异常检测。计算每个location文本对应的标准坐标从关键词库查与录入坐标求 Haversine 距离5km 的标为coord_suspicious单独导出人工核查。代码加在 2.2 节get_location_coord返回后df[coord_dist_km] df.apply( lambda r: haversine_distance(r[lat], r[lon], r[std_lat], r[std_lon]) if pd.notna(r[lat]) and pd.notna(r[std_lat]) else np.nan, axis1 ) df df[df[coord_dist_km] 5] # 删除距离超标的记录4.2 现象按小时统计的事故曲线凌晨 3–5 点峰值异常高占全天 18%原因原始数据中accident_time为“03:20”的记录其accident_date被pd.to_datetime解析为当天日期但实际事故发生在前一日深夜如 2022-05-12 03:20 是凌晨应属 5 月 11 日。时间轴错位导致统计偏差。解决在 2.1 节parse_accident_time中增加“跨日修正”逻辑若accident_hour在 0–5 之间且accident_datetime与record_date相差 1 天则将accident_date减 1 天if 0 row[accident_hour] 5: record_date pd.to_datetime(row.get(record_date, pd.Timestamp.today()), errorscoerce) if pd.notna(record_date) and abs((row[accident_datetime] - record_date).days) 1: row[accident_date] row[accident_datetime].date() - pd.Timedelta(days1)4.3 现象Flask 启动报错ModuleNotFoundError: No module named plotly.graph_objects但pip list显示已安装原因毕业设计常用venv创建虚拟环境但 VS Code 默认终端可能没激活该环境或pip install plotly时用了--user参数导致包装在用户目录而非 venv。更隐蔽的是plotly依赖kaleido导出图片用而kaleido的二进制文件需网络下载断网时pip install plotly会静默跳过后续fig.write_image()直接崩溃。解决终端执行which python确认当前 Python 路径是否为venv/Scripts/python.exepip install plotly kaleido --no-cache-dir强制重装在app.py开头加测试try: import plotly.graph_objects as go import kaleido print(✅ Plotly Kaleido loaded) except ImportError as e: print(f❌ Missing dependency: {e}) exit(1)4.4 现象导出的 HTML 看板在同学电脑上打开是空白F12 看 Console 报Uncaught ReferenceError: Plotly is not defined原因plotly.offline.plot()默认用 CDN 加载 Plotly.js断网即失效。必须改用离线模式并指定include_plotlyjscdn→directory。解决在app.py中生成图表时from plotly.offline import plot import plotly.io as pio # 设置离线资源目录 pio.renderers.default browser pio.orca.config.executable ./orca/orca.exe # Orca 可执行文件路径 # 生成 HTML 时强制包含 JS html_str plot( fig, output_typediv, include_plotlyjsdirectory, # 关键改为 directory会生成 plotly.min.js 文件 filenametemplates/plot.html )然后在templates/目录下手动放入plotly.min.js从 https://cdn.plot.ly/plotly-latest.min.js 下载确保include_plotlyjsdirectory时能定位到。5. 用 Flask 封装成可交付 Web 看板不碰前端框架也能做出专业感毕设答辩时老师最想看到的不是 Jupyter Notebook 里的 20 个 cell而是一个点击即用的.exe或双击启动的 Web 页面。Flask 是最优解它足够轻单文件app.py可启动不强制 MVC 结构且render_template可直接注入 plotly 生成的 HTML 片段。关键不在炫技而在让老师 30 秒内理解你的工作流。5.1 构建最小可行看板5 个核心路由覆盖全部分析维度我们不追求“仪表盘大全”只实现 5 个老师必问的问题入口/总览页事故总数、同比、TOP5 高发路段/time时间分析小时分布、周趋势、节假日对比/space空间分析H3 热力图、TOP10 高发网格/cause致因分析酒驾/疲劳/分心占比、天气影响/export一键导出清洗后 CSV、分析报告 PDFfrom flask import Flask, render_template, send_file import plotly.io as pio app Flask(__name__) app.route(/) def index(): # 总览页读取预计算的 summary.json with open(data/summary.json, r, encodingutf-8) as f: summary json.load(f) return render_template(index.html, summarysummary) app.route(/time) def time_analysis(): # 读取已生成的 time_fig.html由 analysis/time_analysis.py 预生成 with open(templates/time_fig.html, r, encodingutf-8) as f: html_content f.read() return render_template(base.html, title时间分析, contenthtml_content) app.route(/space) def space_analysis(): with open(templates/space_fig.html, r, encodingutf-8) as f: html_content f.read() return render_template(base.html, title空间分析, contenthtml_content) app.route(/cause) def cause_analysis(): with open(templates/cause_fig.html, r, encodingutf-8) as f: html_content f.read() return render_template(base.html, title致因分析, contenthtml_content) app.route(/export) def export_data(): return render_template(export.html) if __name__ __main__: app.run(debugFalse, host127.0.0.1, port5000)逻辑说明所有图表 HTML 都在analysis/子目录下用独立脚本预生成如analysis/time_analysis.pyapp.py只负责路由和模板注入。这样做的好处是调试图表时不用重启 Flask改完time_analysis.py直接python analysis/time_analysis.py重生成 HTML答辩时即使 Flask 崩溃也能直接打开templates/*.html查看结果。5.2 让看板“看起来很贵”3 个零成本 UI 提升技巧没有前端基础用好 Bootstrap 5 的 utility classes 就够了。在templates/base.html中!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{{ title }} - 交通事故分析系统/title !-- Bootstrap 5 CSSCDN答辩现场可提前下载为本地 -- link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.0/dist/css/bootstrap.min.css relstylesheet !-- Font Awesome 图标 -- link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css /head body !-- 顶部导航栏 -- nav classnavbar navbar-expand-lg navbar-dark bg-primary shadow-sm div classcontainer a classnavbar-brand href/ i classfas fa-car-crash me-2/i交通事故分析系统 /a div classnavbar-nav a classnav-link href/i classfas fa-home/i 总览/a a classnav-link href/timei classfas fa-clock/i 时间分析/a a classnav-link href/spacei classfas fa-globe-americas/i 空间分析/a a classnav-link href/causei classfas fa-exclamation-triangle/i 致因分析/a a classnav-link href/exporti classfas fa-file-export/i 导出/a /div /div /nav !-- 主内容区 -- div classcontainer mt-4 mb-4 h2 classmb-4{{ title }}/h2 {{ content | safe }} /div !-- 底部 -- footer classbg-light py-3 mt-5 div classcontainer text-center small classtext-muted基于 Python 的中国交通事故数据分析可视化系统 · 毕业设计作品/small /div /footer !-- Bootstrap JS -- script srchttps://cdn.jsdelivr.net/npm/bootstrap5.3.0/dist/js/bootstrap.bundle.min.js/script /body /html技巧说明i classfas fa-car-crash用 Font Awesome 图标替代文字专业感立升shadow-sm给导航栏加阴影bg-primary用 Bootstrap 主色比默认灰更稳重{{ content | safe }}中的| safe是关键——告诉 Jinja2 不要转义 HTML 字符否则 plotly 生成的div id...会被当成纯文本显示。5.3 一键打包为 Windows 可执行文件PyInstaller 的 3 个必加参数答辩现场绝不能现场pip install必须打包成.exe。PyInstaller 默认打包会漏掉plotly的离线资源、geopandas的 DLL、h3的 C 库。必须显式声明pyinstaller --onefile ^ --add-data templates;templates ^ --add-data static;static ^ --add-binary C:\path\to\your\venv\Lib\site-packages\plotly\package_data\plotly.min.js;plotly\package_data ^ --hidden-import geopandas ^ --hidden-import h3 ^ --name traffic_analyzer ^ app.py参数说明--add-data templates;templates把templates/目录复制到打包后目录--add-binary ...强制包含plotly.min.js否则离线渲染失败--hidden-importgeopandas和h3有动态导入PyInstaller 扫不到必须显式声明。打包后在dist/目录下得到traffic_analyzer.exe双击即启动http://127.0.0.1:5000全程离线。6. 答辩现场的终极技巧用“反向演示法”让老师主动追问你的技术深度我见过太多同学答辩时紧张地念 PPT“这里用了 pandas这里用了 plotly……”。老师听不懂也不想听。真正让老师眼睛一亮的是你主动暴露一个“看似错误”的结果再当场修复它。比如在演示/space页面时故意说“老师您看这个热力图里‘世界之窗’地铁站网格事故数是 0但我知道那里早晚高峰很堵——这说明我的地理编码可能漏掉了地铁站周边小路。我们马上验证一下。” 然后切到命令行运行# 1. 查看世界之窗相关记录 python -c import pandas as pd; dfpd.read_csv(data/cleaned.csv); print(df[df[location].str.contains(世界之窗, naFalse)][[location,lat,lon]].head()) # 2. 手动添加地铁站坐标到 keywords.csv echo 世界之窗地铁站,22.5223,113.9456 data/road_keywords.csv # 3. 重新运行清洗脚本 python analysis/clean_data.py # 4. 重启 Flask刷新页面——热力图实时更新这 4 步操作10 秒内完成老师亲眼看到你如何定位问题、修改数据源、触发重计算、验证结果。他不再问“你用了什么库”而是问“你这个 keywords.csv 是怎么构建的”“H3 分辨率怎么选的”——问题越深分数越高。我的教训是毕设不是展示“我会多少工具”而是证明“我能闭环解决问题”。从原始 Excel 的混乱到 Web 看板的清晰中间每一步清洗、聚合、渲染都要能说出“为什么这一步不能跳过”。比如h3.geo_to_h3()为什么不用geopandas.sjoin()因为后者需要预先定义面如行政区划而事故高发点常在道路交叉口行政边界切不准。这些细节才是答辩时老师想听的“人话”。最后提醒一句所有代码、数据、文档务必用git管理提交信息写清楚“修复酒驾字段语义映射”“添加跨日时间修正”答辩前推送到 GitHub。老师扫一眼 commit log就知道你是不是真干了活。希望帮到你。本文还有配套的精品资源点击获取