简介:基于Python的中国交通事故数据分析可视化系统毕业设计项目,面向计算机相关专业正在完成毕业设计、课程设计或期末大作业的学生,也适合需要Python数据分析实战练习的学习者,解决从交通事故数据清洗、预处理、特征分析到可视化呈现的完整实现问题,可作为同类选题的直接参考范本。压缩包共7个文件,整体大小约245KB,包含3个Jupyter Notebook(承载完整数据分析代码)、3个CSV数据文件(原始事故数据与预处理后的分析数据)和1个HTML可视化页面(用于展示最终图表与结论)。项目经导师指导并认可,评审分为99分,代码完整确保可运行,目前已有98人学习下载。读者可从中掌握pandas数据处理、事故特征探索与可视化图表设计的整体思路,并可复用其中的数据预处理脚本与Notebook代码,快速迁移到自己的项目中。
1. 先拆个彻底:这个 Python 交通事故项目到底值不值得跑
做数据分析项目的都知道,真正拉开差距的不是画图炫不炫,而是你拿到一份原始 CSV 之后,敢不敢说自己能把空值、时间格式、重复记录一次性清干净。这份基于 Python 的中国交通事故数据分析可视化系统,我拆完之后最大感受是:它不是那种网上只放几个图表截图、代码跑起来全是报错的半成品,而是把「模拟原始数据 → 清洗预处理 → 探索性分析 → 可视化出图 → 导出 HTML 报告」这条完整链路都给你塞进了一个压缩包里,特别适合做 Python 毕业设计、数据分析课程大作业,或者想拿真实业务场景练手的人。它在省事的同时也留了几个坑,比如假数据口径、中文字体、时间排序,这些我后面会一条条讲到。
2. 数据底子与选型:先弄清楚三个 CSV 和两组 Notebook 的分工
2.1 文件清单里每一份是干什么的
解压之后你会看到七样东西,别急着打开 notebook 开跑,先花两分钟搞清楚它们之间的关系。我按自己的习惯把它们分成三层:原始层是fake_accident_data.csv,它是模拟出来的事故原始记录,字段里带省份、时间、天气、事故类型、伤亡人数这些信息,用来模拟一张未经处理的事故登记表;中间层是preprocessed_accident_data.csv和preprocessed_data.csv,这是清洗之后的结果,你会发现记录数变了、时间字段变成了标准格式、空值被处理掉了;展示层是accident.ipynb、wwa.ipynb和accident.html,前两个是完整分析过程,最后一个是导出好的可视化报告,浏览器打开就能看。
主目录里的main是你跑通整个流程图之后生成的中间文件,我建议第一次跑先别删,后面调试要对照它确认清洗前后发生了哪些变化。
2.2 为什么选 Pandas + Jupyter + Matplotlib 这套组合
有人会问:都做可视化了,为什么不用 Tableau 或者 ECharts 写个大屏?这里有个非常实际的选型理由。毕设答辩时,老师关心的不是你的大屏转场动画好不好看,而是你处理数据的逻辑是否清晰、每一步是不是可解释。Jupyter Notebook 天然适合这种场景,因为它把「思考过程」和「结果图表」放在同一个文档里,考官翻到哪一格都能看到你的代码和输出。Pandas 负责清洗和聚合,这是整个项目最花时间的地方;Matplotlib 和 Seaborn 负责出图,虽然默认样式朴素,但胜在可控性强,每个坐标轴、每个标签都能手动调。可视化大屏那套炫酷效果,等你答辩完想丰富作品集时再补也不迟。
先读一遍数据,确认这个数据底子长什么样:
import pandas as pd acc = pd.read_csv('fake_accident_data.csv', encoding='utf-8-sig') print('原始形状:', acc.shape) print(acc.dtypes) print('前3行预览:') print(acc.head(3))这份代码的核心是解决「未知数据的探索」问题。encoding='utf-8-sig'是我推荐的首选参数,因为带sig会比不带更稳,能兼容 Excel 导出的 CSV 在首行添加 BOM 头的情况;shape帮你拿到行数和列数,dtypes看出每个字段的类型——这一步一定要做,不然你根本不知道日期到底是不是字符串、死亡人数是不是被读成了浮点。后面所有清洗动作都建立在这三行输出的基础上。
2.3 字段设计对分析维度的影响
观察完数据类型之后,值得把字段挨个在心里过一遍,因为这决定了你能做哪些维度的分析。我整理了一份字段用途对照表,建议你对照自己的数据处理逻辑确认一遍:
| 字段名(常见命名) | 类型 | 分析用途 |
|---|---|---|
| datetime | object → datetime | 提取月份、小时,做时间趋势分析 |
| province | object | 省份维度分布,横向条形图 |
| weather | object | 天气与事故类型交叉分析 |
| accident_type | object | 事故类别计数与占比 |
| death_count / injury_count | int | 严重程度对比,数值校验 |
| road_type | object | 道路条件维度 |
字段命名在不同的版本里可能略有差异,但核心思路一致:时间字段必须转类型,省份、天气这类文本字段适合分组计数,伤亡数字必须有下限校验。理清这层,你就知道acc.ipynb里为什么一会儿 groupby,一会儿画热力图了。
3. 数据清洗与预处理:把假数据变成能说清故事的干净表
3.1 清洗前的状态检查:先记录脏数据证据
很多初学者一上来就dropna(),这是最容易翻车的动作。因为你根本不记得原始数据里有多少空值、多少重复行,到后期写论文时,老师问一句「清洗前后数据量变化是多少、为什么变少两行」,你答不上来。我一般会先做一次状态快照:
import pandas as pd acc = pd.read_csv('fake_accident_data.csv', encoding='utf-8-sig') # 状态快照:用于论文中说明清洗前的情况 snapshot = { '原始行数': len(acc), '重复行数': acc.duplicated().sum(), '空值数量': acc.isnull().sum().sum(), '各列空值': acc.isnull().sum().to_dict(), } print(snapshot)这段代码实际上是在做数据审计。duplicated().sum()统计的是重复行的总数,isnull().sum().sum()是所有列空值和,to_dict()把每列空值数转换成一个可读的字典。建议把输出的数字抄到你的实验记录里,后面写论文「数据来源与预处理」那一章时能直接用,这也是答辩老师最容易追问的地方。
3.2 时间字段转换:字符串日期引发的连锁 bug
事故数据里最典型的坑是时间列。原始 CSV 里的datetime字段往往长这样:2023-05-12 08:30:00,看起来没什么问题,但它是字符串,不是时间类型。字符串状态下你没法dt.hour,没法按月份排序,画出来的折线图横坐标全是乱的。正确的转换姿势:
acc['datetime'] = pd.to_datetime(acc['datetime'], errors='coerce') acc['month'] = acc['datetime'].dt.month acc['hour'] = acc['datetime'].dt.hour # 空值检查:转换失败的行会被置为 NaT invalid_time = acc['datetime'].isna().sum() acc = acc.dropna(subset=['datetime']) print(f'时间解析失败:{invalid_time} 行,已删除')errors='coerce'是这里的核心参数,它的含义是:凡是解析不了的时间值,一律转成NaT,而不是抛异常中断程序。这样你能拿到一个失败数量invalid_time,再做定向删除。dt.month和dt.hour是两个最常用的时间提取操作,分别拿到月份和小时,下一步做月度趋势、24 小时分布都要靠它们。特别提醒:dropna时一定要加subset参数,只针对时间列处理,别把天气列或省份列的空值一起删了——这两种空的处理逻辑完全不同。
3.3 数值字段的边界校验:负的死亡人数必须处理
模拟数据虽然整体规律,但难免会有异常值,比如死亡人数为负数、受伤人数远大于总记录数之类的情况。这类字段如果不做边界校验,画图时会出现一个离谱的负柱,答辩时解释不清。常见做法是先描述再处理:
# 边界描述 print('死亡人数统计:') print(acc['death_count'].describe()) # 规则清洗:死亡人数和受伤人数不能为负数 acc = acc[acc['death_count'] >= 0] acc = acc[acc['injury_count'] >= 0] acc = acc.reset_index(drop=True) print('清洗后行数:', len(acc))describe()会输出最小值、最大值、均值等统计量,建议最先看min这一行,一旦是负数就说明数据源有问题。>= 0的布尔过滤是整段代码的安全底线,reset_index(drop=True)是清理索引的常规操作,因为在多次过滤之后索引全乱了,不重置会导致后面groupby和画图出现「跳行」的偶然问题。
3.4 导出预处理结果:给后续可视化提供稳定输入
清洗最终的产出不是一堆打印语句,而是一份干净的 CSV,方便你在acc.ipynb、wwa.ipynb两个 Notebook 之间反复读取,不用每次重新执行清洗逻辑:
acc.to_csv('preprocessed_accident_data.csv', index=False, encoding='utf-8-sig') print('已输出预处理数据,行数:', len(acc))index=False防止 Pandas 把行号写进 CSV 的第一列,encoding='utf-8-sig'保证 Excel 打开时中文不乱码。从这里开始,你的分析工作流就变成了「读取预处理数据 → 聚合 → 可视化」,这一行的稳定输出是你所有图表的唯一数据来源。如果后面发现某张图的数据不对,先回到这一步重新比对,别直接在绘图代码里打补丁。
4. 探索性分析与可视化:从聚合结果到四张核心图表
4.1 省份维度:横向条形图展示事故数量分布
拿到预处理数据后,第一个值得分析的维度是省份。为什么用横向条形图而不是纵向柱状图?因为省份名称是中文,短的能用 2 个字,长的能用 5 个字,纵向柱状图的 x 轴标签一密集,遮挡问题就会让人头疼。横向条形图能很好地解决这个问题,省份名放在 y 轴,读起来也符合视觉习惯:
import pandas as pd import matplotlib.pyplot as plt acc = pd.read_csv('preprocessed_accident_data.csv', encoding='utf-8-sig') province_cnt = acc.groupby('province').size().reset_index(name='counts') province_cnt = province_cnt.sort_values('counts', ascending=True) fig, ax = plt.subplots(figsize=(10, 8)) ax.barh(province_cnt['province'].tail(10), province_cnt['counts'].tail(10)) ax.set_title('事故数量 Top10 省份') ax.set_xlabel('事故数量') ax.set_ylabel('省份') plt.tight_layout() plt.savefig('province_top10.png', dpi=200, bbox_inches='tight') plt.show()groupby('province').size().reset_index(name='counts')是统计最喜欢用的语法,size()表示每个省份的记录数,reset_index把分组后的 Series 还原成普通 DataFrame。排序时我特意用ascending=True、画图时取tail(10),这样排名第一的省份出现在条形图最上方,读图最自然。figsize=(10, 8)适合 10 条数据的横向条形图,太窄会显得标签拥挤;保存参数dpi=200够答辩投影用,bbox_inches='tight'可以防止标题被截断。
4.2 时间维度:月度走势折线图与坐标轴密度控制
事故随时间的变化趋势是另一个必做维度。我用月份聚合画折线图,效果稳定,不需要任何额外库:
monthly = acc.groupby('month').size() fig, ax = plt.subplots(figsize=(10, 4)) ax.plot(monthly.index, monthly.values, marker='o', color='#C0392B') ax.set_xticks(range(1, 13)) ax.set_xticklabels([f'{m}月' for m in range(1, 13)]) ax.set_ylabel('事故数量') ax.set_title('各月份事故数量走势') ax.grid(True, linestyle='--', alpha=0.3) plt.tight_layout() plt.savefig('monthly_trend.png', dpi=200) plt.show()这里有一个容易踩的细节:直接用 Pandas 的.plot()也能画,但默认 x 轴标签常常是「1 2 3 ... 12」这种干巴巴的数字,不专业。set_xticks和set_xticklabels这两行的作用就是显式地指定刻度位置和刻度名称,把 1 变 1 月。很多新手画时间图时横坐标密集成一团毛线,原因就是忽略了这两个方法。marker='o'是为了在折线上标出数据点,论文中用黑白打印也能看清趋势。另外grid我习惯开后加虚线透明样式,能让读者更专注看数据。
4.3 交叉维度:天气与事故类型的热力图
单维度图虽然清晰,但热力图这种交叉分析才是答辩时的加分项。它能同时体现两个分组变量的关系,比如恶劣天气下追尾事故是不是明显增多。Seaborn 的heatmap可以直接吃 Pandas 的透视表:
import seaborn as sns crosstab = pd.crosstab(acc['weather'], acc['accident_type']) fig, ax = plt.subplots(figsize=(8, 6)) sns.heatmap(crosstab, annot=True, fmt='d', cmap='YlOrRd', linewidths=0.5, linecolor='white') ax.set_title('天气状况与事故类型交叉分析') ax.set_xlabel('事故类型') ax.set_ylabel('天气状况') plt.tight_layout() plt.savefig('weather_type_heatmap.png', dpi=200) plt.show()pd.crosstab是透视表函数,它生成行是天气、列是事故类型的计数矩阵。annot=True表示在格子中显示数字,fmt='d'表示用整数格式显示,不然你会看到1.0这种很业余的输出。配色cmap='YlOrRd'从浅黄到深红,深色自然代表高发组合。linewidths=0.5用于分隔每个小格子,视觉上更加整齐。这个图值得多花点时间观察:有时候某个格子的数值远超其他格子,那就说明天气和事故类型之间存在明显的相关关系,可以直接写进论文的分析结论。
4.4 多图组合输出:让 Notebook 的报告感更强
单张图保存成 PNG 当然没问题,但如果能做到在同一个 Notbook 里分区域展示,报告感会强很多。我用subplots做一张 1 行 2 列的组合图,左边是省份分布、右边是月度趋势,让对比更直观:
fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # 左边:月度趋势 axes[0].plot(monthly.index, monthly.values, marker='o') axes[0].set_title('月度事故数量') axes[0].set_xticks(range(1, 13)) # 右边:天气占比饼图 weather_cnt = acc['weather'].value_counts() axes[1].pie(weather_cnt.values, labels=weather_cnt.index, autopct='%.1f%%', startangle=90) axes[1].set_title('天气分布占比') plt.tight_layout() plt.savefig('combined_chart.png', dpi=200) plt.show()subplots(1, 2)生成左右两个子图,返回的axes是一个包含两个坐标轴对象的数组。value_counts()直接统计天气字段的类别频次,是画饼图前最简单高效的聚合工具。autopct='%.1f%%'在饼图中心显示保留一位小数的百分比,startangle=90让第一块扇区从 12 点方向开始画,符合直觉。这个组合图在答辩展示中非常实用,一张 PPT 能容纳两种信息。
5. 避坑与排查:我在复现这套项目时踩过的五个坑
5.1 图里的中文全部变方块
现象:代码运行无报错,但所有标题、轴标签的中文都显示成了无法辨认的方框。 原因:Matplotlib 默认字体库不包含中文字体,这是最常见且最常被忽略的问题。 解决:在绘图代码前显式指定中文字体。Windows 下最省事的方式是设置黑体或微软雅黑:
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei'] plt.rcParams['axes.unicode_minus'] = False注意第二行axes.unicode_minus要设置为False,否则坐标轴上的负号会显示成一个小方块或乱码。
5.2 Notebook 里ImportError: No module named pandas
现象:双击打开acc.ipynb,执行第一行import pandas as pd就报错,但自己单独写一个 .py 文件又能正常运行。 原因:电脑上装了多个 Python 环境,Notebook 的 kernel 指向的不是你安装了 pandas 的那个环境。 解决:我一般用 Anaconda 环境管理。在终端里检查当前环境:
conda activate base python -m pip install pandas matplotlib seaborn jupyter python -m ipykernel install --user --name base然后在 Notebook 右上角的 Kernel 菜单中选择刚才安装的base环境。这个坑在刚接触 Python 的新手里出现概率极高,不是代码问题,是环境串线问题。
5.3 清洗前后行数对不上,找不到原因
现象:明明只删了重复行,结果记录数少了上百行,仔细核对代码也没发现哪里多删除了一片。 原因:清洗过程中多次过滤,前一步用了isna()判断是否保留,后一步又用dropna()删掉整个行,几个动作叠加导致数据丢失。 解决:把清洗过程分段执行,每段打印一次行数,并记录每段删除的行数。我通常的做法是加一个计数器字典:
log = {} log['删除重复'] = acc.duplicated().sum() acc = acc.drop_duplicates() log['删除空时间'] = acc['datetime'].isna().sum() acc = acc.dropna(subset=['datetime']) print(log)这样每一步删掉多少行、是因为哪个规则删掉的,全都留痕,论文里直接可引用。
5.4 导入wwa.ipynb后代码能跑,但导出的 HTML 图表全部空白
现象:在 Jupyter 里点File → Download as → HTML,生成的网页打开后图表区域是一片空白,但代码输出区域还在。 原因:这个是你的浏览器缓存或 Notebook 未执行完造成的。如果 Notebook 里的图表是最后一次运行后显示的,那么导出时页面会尝试重新渲染输出流的图片数据;一旦内存中的图像数据被回收或 kernel 重启过,HTML 里就丢失了图。 解决:强制从上到下完整执行一次所有单元格,再导出:
jupyter nbconvert --to html --execute acc.ipynb --output accident.html--execute参数的作用是在转换前自动重启 kernel 并按顺序执行全部单元格,这能规避「没执行完就导出」的问题。
5.5 假数据分布太“均匀”,答辩被追问时解释不清楚
现象:画出来的省份分布图特别平均,每个省份的事故数量几乎一致,不符合真实业务里「东部省份明显高于西北」的常识。 原因:fake_accident_data.csv是模拟数据,生成时为了演示方便,默认均匀分布。它不是真实统计年鉴数据。 解决:最稳妥的做法是在论文「数据来源」一节明确写清「本研究使用模拟数据验证方法流程」,并在图表下方加一句脚注。如果你需要做更真实的分布,可以自己手动调整权重生成一份新数据,保留旧文件做对照。我的习惯是往下调province列的采样概率,用random.choices加weights参数跑一次,就能得到一条更有说服力的长尾分布。
6. 从 Notebook 到答辩材料:把演示过程做成闭环
6.1 用 nbconvert 输出一份导师能直接打开的报告
上面已经提过基础的导出命令,这里再给一个自己常用的组合。导出 HTML 只是其中一环,建议把每个 Notebook 的执行结果也保留下来,方便在论文附录里引用:
jupyter nbconvert --to html --execute acc.ipynb --output accident_report.html jupyter nbconvert --to html --execute wwa.ipynb --output weather_report.html如果导出结果太大,可以加--template classic参数,它会让页面变得比较朴素,但不会丢失任何图像内容。答辩前我会把这个 HTML 文件放到和preprocessed_accident_data.csv同级的目录下,用浏览器在投影上完整走一遍。
6.2 给图表补一个业务可读的「结论标注」
图像文件本身不会说话,答辩时老师盯着图看十秒,自己心里得有底。我的习惯是每张图保存后,在 Notebook 的 Markdown 单元格里写两句话:这图为什么存在、结论是什么。案例写法如下:
结论:月度趋势图显示 12 月事故数量明显上升。 可能原因:冬季天气复杂、道路结冰概率增加。 局限性:本数据为模拟数据,趋势结论仅在数据范围内有效。这种结构的好处是让整个 Notebook 有「发现问题 → 解释问题」的完整链路,而非图表罗列。老师翻到任何一张图都能看到对应的文字记录,这会直接拉高印象分。
6.3 自我验证:用抽样与汇总检查结果合理性
在正式提交前,我会做一次反向校验。具体做法是把清洗后的数据按省份分组汇总,再手工抽取一个省份核对明细记录数是否一致:
check = pd.read_csv('preprocessed_accident_data.csv', encoding='utf-8-sig') pivot = check.groupby('province').size().reset_index(name='counts') # 抽查一个省份:该省明细行数应等于分组数 sample_province = '广东' detail_count = (check['province'] == sample_province).sum() pivot_count = pivot.loc[pivot['province'] == sample_province, 'counts'].iloc[0] print(f'明细行数:{detail_count},分组计数:{pivot_count}')这个验证的意义在于,它能排查出清洗阶段常见的重复删除或索引错位问题。两个值相等,说明数据一致;不等,请回到上一节的分段日志去找丢失在哪一步。从那以后,我每次交付任何数据报告,都会强制自己走一遍「抽样 → 汇总 → 对照」的验证流程,这个习惯帮我挡掉了至少三次数据不一致的事故,希望也能帮到你。
本文还有配套的精品资源,点击获取