简介:本资源是一套基于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”……这不是数据,是“数据沼泽”。而毕业设计要的不是“能画图”,是让老师一眼看出你懂业务、会清洗、能归因、有工程意识。这个标题下的系统,本质是一个轻量级但可交付的 ETL+BI 工作流:用 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, errors='coerce') # 再尝试纯时间: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, errors='coerce').date() return pd.to_datetime(f"{base_date} {time_part}", errors='coerce') except: pass # 否则用今天日期(仅用于临时占位,后续需标注) return pd.to_datetime(f"{pd.Timestamp.today().date()} {time_part}", errors='coerce') except: continue return pd.NaT # 应用清洗 df['accident_datetime'] = df.apply(parse_accident_time, axis=1) 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(..., errors='coerce')配合手动拼接,确保时间语义可控。errors='coerce'是后悔药——宁可返回 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', encoding='utf-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, key=len) 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}, 深圳市, 广东省", timeout=10) # 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, key=len)确保“北环大道”优先于“北环”。
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(), key=lambda x: x[1], reverse=True): if keyword.lower() in text_lower: return weight return np.nan df['resp_level'] = df['responsibility'].apply(map_responsibility)逻辑说明:排序
sorted(..., reverse=True)确保“全责”(权重4)比“主责”(权重3)先匹配,避免“负全部责任”被误判为“主责”。text_lower统一小写,兼容“主要责任”和“主要责任”。返回np.nan而非 0,因为 0 是有效值(无责),必须区分“明确无责”和“未识别”。
3. 用 GeoPandas 构建空间分析层:为什么热力图不能只靠 plotly.scatter_geo?
很多毕设同学直接px.scatter_geo(df, lat='lat', lon='lon'),结果地图上一堆重叠小点,看不出哪里事故高发。问题在于:交通事故是空间聚集事件,单点不具统计意义,必须做空间聚合(Spatial Aggregation)。plotly 的scatter_geo是渲染层,不是分析层;真正的分析必须在 GeoPandas 中完成:将点数据栅格化(Rasterize)或聚类(Cluster),生成带统计值的面要素(GeoDataFrame),再传给 plotly 渲染。
3.1 用 H3 六边形网格替代传统矩形网格:解决“边界效应”问题
传统geopandas.overlay()切矩形网格,会导致:
- 南山区科技园的六边形网格 vs 罗湖区老城区的六边形网格,面积差异大 → 事故密度计算失真
- 网格边界切割道路,把一条路上的事故硬拆到两个网格 → 归因错误
H3 是 Uber 开源的球面六边形分级网格系统,同一层级(resolution)下所有六边形面积误差 <1%,且支持父子网格嵌套。我们用h3库将每个事故点落进 resolution=8 的六边形(平均边长约 780 米,适合城市级分析),再按六边形 ID 聚合事故数、平均伤亡、酒驾占比。
import h3 import geopandas as gpd from shapely.geometry import Polygon def point_to_h3(lat, lon, resolution=8): """将经纬度转为 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']), axis=1) # 按 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_json=True)) for h in h3_agg['h3_index']], crs="EPSG:4326" )参数说明:
resolution=8是经验值——res=7(边长≈2.4km)太粗,抓不住科技园内部差异;res=9(边长≈260m)太细,部分六边形事故数为0,噪声大。h3.h3_to_geo_boundary(..., geo_json=True)直接返回 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", zoom=12, source=ctx.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( geojson=h3_gdf.__geo_interface__, # GeoDataFrame 转 GeoJSON locations=h3_gdf.index, z=h3_gdf['accident_count'], colorscale="YlOrRd", zmin=0, zmax=h3_gdf['accident_count'].quantile(0.95), # 截断异常值 marker_opacity=0.7, marker_line_width=0.5, showscale=True, colorbar=dict(title="事故数") )) # 添加离线底图(关键参数) fig.update_layout( mapbox=dict( style="white-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}, zoom=11 ), margin={"r":0,"t":0,"l":0,"b":0} )避坑重点:
coordinates必须按顺时针顺序提供瓦片四角经纬度,否则图像翻转。zmax=h3_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, axis=1 ) 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()), errors='coerce') if pd.notna(record_date) and abs((row['accident_datetime'] - record_date).days) > 1: row['accident_date'] = row['accident_datetime'].date() - pd.Timedelta(days=1)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.exe; pip 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_plotlyjs='cdn'→'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_type='div', include_plotlyjs='directory', # 关键!改为 'directory',会生成 plotly.min.js 文件 filename='templates/plot.html' )然后在templates/目录下手动放入plotly.min.js(从 https://cdn.plot.ly/plotly-latest.min.js 下载),确保include_plotlyjs='directory'时能定位到。
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、分析报告 PDF)
from 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', encoding='utf-8') as f: summary = json.load(f) return render_template('index.html', summary=summary) @app.route('/time') def time_analysis(): # 读取已生成的 time_fig.html(由 analysis/time_analysis.py 预生成) with open('templates/time_fig.html', 'r', encoding='utf-8') as f: html_content = f.read() return render_template('base.html', title="时间分析", content=html_content) @app.route('/space') def space_analysis(): with open('templates/space_fig.html', 'r', encoding='utf-8') as f: html_content = f.read() return render_template('base.html', title="空间分析", content=html_content) @app.route('/cause') def cause_analysis(): with open('templates/cause_fig.html', 'r', encoding='utf-8') as f: html_content = f.read() return render_template('base.html', title="致因分析", content=html_content) @app.route('/export') def export_data(): return render_template('export.html') if __name__ == '__main__': app.run(debug=False, host='127.0.0.1', port=5000)逻辑说明:所有图表 HTML 都在
analysis/子目录下用独立脚本预生成(如analysis/time_analysis.py),app.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 lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{ title }} - 交通事故分析系统</title> <!-- Bootstrap 5 CSS(CDN,答辩现场可提前下载为本地) --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- Font Awesome 图标 --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> </head> <body> <!-- 顶部导航栏 --> <nav class="navbar navbar-expand-lg navbar-dark bg-primary shadow-sm"> <div class="container"> <a class="navbar-brand" href="/"> <i class="fas fa-car-crash me-2"></i>交通事故分析系统 </a> <div class="navbar-nav"> <a class="nav-link" href="/"><i class="fas fa-home"></i> 总览</a> <a class="nav-link" href="/time"><i class="fas fa-clock"></i> 时间分析</a> <a class="nav-link" href="/space"><i class="fas fa-globe-americas"></i> 空间分析</a> <a class="nav-link" href="/cause"><i class="fas fa-exclamation-triangle"></i> 致因分析</a> <a class="nav-link" href="/export"><i class="fas fa-file-export"></i> 导出</a> </div> </div> </nav> <!-- 主内容区 --> <div class="container mt-4 mb-4"> <h2 class="mb-4">{{ title }}</h2> {{ content | safe }} </div> <!-- 底部 --> <footer class="bg-light py-3 mt-5"> <div class="container text-center"> <small class="text-muted">基于 Python 的中国交通事故数据分析可视化系统 · 毕业设计作品</small> </div> </footer> <!-- Bootstrap JS --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> </html>技巧说明:
<i class="fas 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-import:geopandas和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; df=pd.read_csv('data/cleaned.csv'); print(df[df['location'].str.contains('世界之窗', na=False)][['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,就知道你是不是真干了活。希望帮到你。
本文还有配套的精品资源,点击获取