2026/10/11 1:13:47

出租车GPS数据分析深圳交通:清洗、匹配与时空聚合实战

出租车GPS数据分析深圳交通:清洗、匹配与时空聚合实战 简介基于出租车GPS数据分析城市道路交通的数学建模完整方案适合准备数学建模竞赛、学习空间聚类与交通流分析的高校学生和数据分析人员。资源围绕深圳市交通小区划分、OD时空分布计算与推断、拥堵路段及时段识别四个核心问题展开详细展示了DBSCAN密度聚类、带约束二次规划模型与内点障碍罚函数法的应用过程并利用统筹产业结构和用地布局的方式推断OD分布。资料为1个PDF文件共972KB内容包含问题重述、模型假设、符号说明、模型建立与求解以及模型改进步骤其中对面积较大的交通小区进行了细分能帮助读者理解从海量GPS数据到聚类分区、OD统计、拥堵识别、规划建议的完整建模链条。现有145人学习下载配套博文资源适合需要系统掌握数学建模全流程的人群。1. 用出租车GPS分析深圳交通这题到底考什么每年的数学建模竞赛里“用出租车GPS数据分析城市道路交通情况”几乎是长盛不衰的命题方向深圳、北京、成都都被出成过题目。拿到这份PDF的人多半不是想在理论层面聊轨迹数据挖掘而是要在限定时间内完成一套“从原始GPS经纬度到交通规律结论”的分析链条。这个链条的核心并不是某个高深的机器学习模型而是三件看起来基础却最容易翻车的事GPS数据清洗、地图匹配、时空聚合建模。适合参赛队伍、交通数据分析入门者以及想用公开GPS数据做城市研究的从业者。读完这篇文章你能照着复现一条完整的深圳出租车GPS分析路径并且知道每个关键步骤的坑在哪里。2. 拿到出租车GPS数据先做三件事字段理解、清洗、坐标系对齐出租车GPS数据看起来就是一张“时间、经度、纬度、速度、状态”的大表但真正动手做分析时很多人第一周都耗在“数据怎么这么乱”上。其实只要把字段含义、清洗规则、坐标系这三个基本功打牢后面所有分析都会顺很多。2.1 看懂原始字段出租车GPS记录有哪些关键列深圳出租车GPS最典型的输出格式是每一辆车每隔10到30秒上报一条记录。常见字段如下字段名示例值含义VEHICLE_ID粤B1234车辆唯一标识TIME2023-06-01 07:59:32UTC或本地时间戳LON / LAT114.0654 / 22.5389经纬度注意坐标系SPEED35.2瞬时速度单位km/hDIRECTION258方向角0-360度STATE1载客状态1载客0空车用Pandas读进来之后我建议先做一次df.dtypes检查重点看时间字段是不是已经被解析成了datetime对象。很多竞赛数据给的时间是字符串后面做时间切片时会报错。另外要确认经纬度是十进制度数还是度分秒格式深圳地区正常范围是纬度22.4到22.9经度113.7到114.6超出这个范围的基本都是脏数据。State字段是整个分析里最重要的一个字段它直接决定了你能否做OD分析起讫点分析和载客速度与空驶速度的拆分。有的数据集用occupancy、空重车标志等名字先看清楚说明文档别想当然认为1就是载客。2.2 数据清洗掉线点、漂移点、速度异常的过滤规则GPS数据天生包含噪声定位漂移、高楼遮挡导致的跳变、车辆熄火后的原地抖动。这些噪声如果不过滤直接算平均速度或画热力图结果会非常离谱。我常用的清洗顺序是先范围过滤、再速度过滤、再加速度过滤。先看一段最基础的清洗逻辑import pandas as pd import numpy as np df pd.read_csv(shenzhen_taxi.csv, parse_dates[TIME]) # 深圳行政区域粗略经纬度范围 lon_min, lon_max 113.7, 114.6 lat_min, lat_max 22.4, 22.9 df df[(df[LON] lon_min) (df[LON] lon_max)] df df[(df[LAT] lat_min) (df[LAT] lat_max)] df df[df[SPEED].between(0, 120)] # 城市出租车极速不可能超过120 # 按车辆排序后计算两点间的实际距离和加速度 df df.sort_values([VEHICLE_ID, TIME]) df[prev_lon] df.groupby(VEHICLE_ID)[LON].shift(1) df[prev_lat] df.groupby(VEHICLE_ID)[LAT].shift(1) # haversine距离 - 判断相邻两点速度 def haversine(lon1, lat1, lon2, lat2): R 6371.0 # 简化直接用经纬度差值估算 return ((lon2 - lon1) * 111.0) ** 2 ((lat2 - lat1) * 111.0) ** 2 df[dist_km] np.sqrt(haversine(df[prev_lon], df[prev_lat], df[LON], df[LAT])) df[dt_h] (df[TIME] - df.groupby(VEHICLE_ID)[TIME].shift(1)).dt.total_seconds() / 3600 df[calc_speed_kmh] df[dist_km] / df[dt_h].replace(0, np.nan) # 过滤掉瞬时计算速度超过150km/h或加速度异常的点 df df[df[calc_speed_kmh] 150]这段代码里有几个参数值得注意经纬度范围一定要先看数据本身的分布不要照抄我的数值不同来源的数据可能只有深圳中心城区。速度过滤阈值120km/h要结合出租车业务实际空车深夜在空旷路段开快一点是可能的但超过120基本就是漂移。最容易被忽略的是加速度如果相邻两条记录间隔10秒两点距离算出速度是120km/h但上一秒车速只有20km/h说明是跳变点而非真实行驶应该再按加速度阈值比如超过 10 m/s²剔掉。2.3 坐标系与深圳路网WGS84还是GCJ-02如果你要拿GPS数据和深圳路网做匹配坐标系就是第一道坎。国内民用GPS设备输出的坐标绝大多数是GCJ-02加密坐标也就是俗称的火星坐标系而OpenStreetMap下载的路网是WGS84坐标。直接拿这两者叠加会出现每条道路都要偏移几百米的问题。判断数据属于哪个坐标系有一个土办法把轨迹点画出来再叠加一个深圳海岸线或主要道路图。如果轨迹整体在道路上往东南方向偏移了约200到600米基本可以断定原数据是WGS84而路网是GCJ-02。反过来也一样。我一般用下面这个近似转换来把WGS84转成GCJ-02import math def wgs84_to_gcj02(lon, lat): a 6378245.0 ee 0.00669342162296594323 d_lon 300.1 lon 2.0 * lat 3.0 * math.sqrt(abs(lon)) d_lat 300.0 lat 0.2 * lat 3.0 * math.sqrt(abs(lon)) # 实际工程中用完整火星坐标转换公式 # 这里仅示意竞赛中可直接调用 coord_convert 库 return lon d_lon * 0.00001, lat d_lat * 0.00001真正要落生产环境直接使用pyproj或gcj02库里的标准算法。坐标系统一后建议把路网数据里的主干道提取出来为下一步的网格化或路段匹配做准备。3. 从轨迹到道路状态地图匹配与速度计算只要做交通分析绕不开一个问题这些GPS点到底落在深圳哪条路上地图匹配就是把离散的轨迹点投射到最可能的道路上的过程。竞赛场景不需要工业级的隐马尔可夫模型一个轻量级网格或最近路段的做法能让你两小时内跑通流程。3.1 地图匹配的轻量做法网格绑定与路网最近点最稳定的轻量匹配方式是把深圳市划分成规则网格每个网格面积约500米×500米再把GPS点归入网格。这种做法的好处是不依赖高质量路网数据计算量小而且天然适合做空间聚合。import geopandas as gpd # 设置网格起点深圳西南角和网格大小 GRID_SIZE 0.005 # 约500米 min_lon, min_lat 113.7, 22.4 def to_grid(lon, lat): gx int((lon - min_lon) / GRID_SIZE) gy int((lat - min_lat) / GRID_SIZE) return gx, gy df[grid_x] df[LON].apply(lambda x: to_grid(x, 0)[0]) df[grid_y] df[LAT].apply(lambda y: to_grid(113.7, y)[1])如果你需要把GPS点匹配到具体道路就用路网文件做一个空间连接。我通常用GeoPandas的within和nearest操作先读入深圳路网shapefile给每个GPS点找最近的路段中心线距离阈值设为60米超过阈值就标记为“无匹配”这类点往往是小区内部道路或停车场。地图匹配这一步没有绝对正确的结果关键是别让错误匹配毁了后续速度计算。常见的错误是让轨迹点匹配到了高架桥旁边的地面道路导致速度被高架桥的80km/h拉高。所以如果数据里有方向角做最近点匹配时要把方向角夹角超过30度的候选路段排除掉。3.2 路段速度与拥堵指数怎么算才稳定路段平均速度算出来并不难难在“稳定”两个字。出租车GPS每10秒一个点车辆在红绿灯路口停车瞬时速度接近0如果直接把所有点平均路段速度会显著偏低。而且载客状态不同司机行为差异很大空驶出租车喜欢在路边慢速等客载客出租车更接近真实交通流。我用的算法是先对每个网格或路段分时段统计同时只取载客状态下的行程时间这样可以剔除空车慢速巡游的干扰。另一个细节是必须删除单次载客行程中持续时间太短比如少于3分钟的记录那往往是司机交接班或开错路掉头会制造大量低速样本。def road_speed_by_hour(df): # 只保留载客状态计算每小时、每网格的平均速度 df_loaded df[df[STATE] 1].copy() df_loaded[HOUR] df_loaded[TIME].dt.hour speed ( df_loaded.groupby([grid_x, grid_y, HOUR])[SPEED] .mean() .reset_index() ) return speed # 拥堵指数 自由流速度 / 当前时段平均速度 # 自由流速度取该网格全时段90%分位速度 free_speed speed.groupby([grid_x, grid_y])[SPEED].quantile(0.9) speed[free] speed.set_index([grid_x, grid_y]).index.map(free_speed) speed[c_index] speed[free] / speed[SPEED]这里的核心参数是90%分位自由流速度而不是最高速。原因是里程表里偶然出现的120km/h漂移点即使清洗了也还会残留用90%分位能抵御离群值。如果某个网格样本数太少比如一天只有10个点算出的拥堵指数没有意义最少要保证该网格每个小时有30个以上样本才纳入分析。3.3 时空聚合早晚高峰热力图切片速度算完之后下一步是可视化深圳交通在时间上的演变。常见做法是把全天按小时切片分别绘制早高峰7点到9点、午平峰12点到14点、晚高峰17点到19点的路段平均速度热力图。用matplotlib的scatter或hexbin就能做不必一上来就用地图引擎。import matplotlib.pyplot as plt def plot_heatmap(speed, hour_range): sub speed[speed[HOUR].between(hour_range[0], hour_range[1])] plt.figure(figsize(8, 6)) plt.scatter( sub[grid_x] * GRID_SIZE min_lon, sub[grid_y] * GRID_SIZE min_lat, csub[SPEED], cmapRdYlGn, s5, alpha0.6 ) plt.colorbar(labelAverage Speed (km/h)) plt.title(fHour {hour_range[0]}-{hour_range[1]} Taxi Speed) plt.xlabel(Longitude) plt.ylabel(Latitude) plt.show() plot_heatmap(speed, (7, 9))热力图只是观察工具真正写进论文的是每个区域的速度变化曲线。比较合理的方式是提取全市10个核心区域比如福田中心区、华强北、科技园、宝安中心区的逐小时速度序列做成时序折线图。这样既能看出早高峰和晚高峰的双峰特征也能对比不同区域拥堵起止的先后顺序。4. 深挖交通规律OD分析、热点识别与建模选型算完速度只能回答“哪里堵、什么时候堵”但竞赛题通常还要追问“这个拥堵是怎么发生的、乘客从哪里到哪里”。这时需要OD分析来刻画出行需求用密度聚类找拥堵热点再根据问题目标选择合适的数学建模方法。4.1 OD起讫点提取与区域流量矩阵OD即Origin-Destination出行起点和终点。出租车载客状态字段每一次从0跳变到1就是一个行程开始从1跳变到0就是一个行程结束。处理时要防止状态字段在短时间内反复抖动比如司机在等人时频繁切换空重车标志会被误判成多次短途行程。def extract_od(df): df df.sort_values([VEHICLE_ID, TIME]).copy() df[state_change] df.groupby(VEHICLE_ID)[STATE].diff() # 记录状态从0变为1的时刻为起点从1变为0的时刻为终点 trip_start df[df[state_change] 1] trip_end df[df[state_change] -1] od pd.DataFrame({ start_lon: trip_start[LON].values, start_lat: trip_start[LAT].values, end_lon: trip_end[LON].values, end_lat: trip_end[LAT].values, start_time: trip_start[TIME].values, end_time: trip_end[TIME].values, }) # 过滤掉持续时间过短(3分钟)或距离过短(500米)的行程 od[noise] (od[end_time] - od[start_time]).dt.total_seconds() 180 od od[~od[noise]] return od为什么过滤掉500米以内的短行程因为很多出租车空车状态下司机用计价器“刷单”制造虚假行程或者乘客在路边上车又因为行程取消立刻下车。这类短行程对OD分析毫无意义还会让区域流量矩阵出现异常尖峰。有了OD表之后按行政区划分组就能得到深圳各区域之间的OD矩阵。这里我习惯用福田、南山、罗湖、宝安、龙岗、龙华六个区的边界做空间连接统计每个区之间的到发量再画桑基图或弦图直观展示跨区通勤的流量方向。4.2 用DBSCAN识别拥堵热点路段拥堵热点不是看平均速度低就行还得看拥堵是否持续聚集。出租车速度低于15km/h的点往往聚集在几个固定区域比如华强北商圈、深圳湾口岸、学校附近。用DBSCAN对低速点聚类能把这些空间聚集区挖出来。from sklearn.cluster import DBSCAN import numpy as np low_speed df[df[SPEED] 15][[LON, LAT]].values # 经纬度转成平面近似坐标深圳地区1度约111km X np.column_stack([ low_speed[:, 0] * 111.0, low_speed[:, 1] * 111.0 * np.cos(np.deg2rad(22.5)) ]) clustering DBSCAN(eps0.5, min_samples20).fit(X) print(聚类簇数量:, clustering.labels_.max() 1)DBSCAN里最需要调的是eps和min_samples。eps代表邻域半径单位是公里0.5表示半径500米内聚成一类min_samples表示一个簇至少要20个点。深圳出租车晚高峰的低速点非常密集如果min_samples太小整个商圈会连成一片如果太大小型拥堵点又会被忽略。先跑一遍不同参数看聚类结果的轮廓系数选曲线拐点处的值最稳。聚类出来的每个簇再回头计算簇内点的平均速度、平均停留时间、出现时段就形成了“热点档案”可以直接对应当前新闻报道里的“常发堵点”做验证。4.3 数学建模怎么选模型从统计回归到时空扫描统计分析类题目和预测类题目的建模路线完全不同。如果题目问“分析深圳交通拥堵的时空特征”那是描述型问题用聚类加统计检验就够了不需要上深度学习。如果题目问“预测明天早高峰哪里会堵”才是真正的预测型问题可以考虑时间序列、随机森林回归甚至图神经网络。我整理了一条选型路径供参考问题关注“哪些因素影响拥堵”用多元线性回归或随机森林把天气、学校放假、节假日、道路等级、路段长度作为特征速度作为目标变量。问题关注“异常拥堵事件检测”用时空扫描统计如SaTScan或基于密度的离群点检测找短时聚集且速度骤降的异常区域。问题关注“通勤供需关系”用OD矩阵结合重力模型或辐射模型估算区域间的期望出行量再和实际OD流量做残差分析。问题关注“短期拥堵预测”先把数据按15分钟时间窗重采样构建每个网格的时序特征用LightGBM或Prophet做回归不建议一上来就堆LSTM出租车GPS数据噪声大时序样本短复杂模型容易过拟合。竞赛中常见的翻车是为了追求模型复杂度把所有方法都堆在结果里却没有回答题目真正要求的“道路交通情况分析”。建模之前先把你要回答的问题用一句话写在本子上然后只选一个最贴合问题的主模型其他方法作为辅助验证。5. 出租车GPS数据避坑五个常见翻车点与排查方法这部分是血泪经验也是团队协作时最容易互相甩锅的地方。以下五个坑是我在完完整整跑过三次出租车GPS交通分析之后总结出来的每条都是“现象 → 原因 → 解决”的结构。5.1 轨迹在建筑物外乱飞热力图长满刺现象画出出租车轨迹后发现大量点位跑到深圳湾海面上或高楼群中间道路热力图看起来像长了毛刺。原因GPS漂移尤其在城市峡谷地带高楼对卫星信号反射导致经纬度跳变。清洗阶段只过滤了速度和经纬度范围但没过滤相邻点距离突变。解决在清洗阶段增加“相邻点速度校验”两点之间的距离除以时间间隔如果超过120km/h直接删除后一个点。更稳妥的做法是后面接一步“将偏离最近道路超过80米的点标记为不可用”不参与速度统计。5.2 凌晨平均速度异常偏高半夜反而成为全天最畅通时段现象凌晨2点到4点全市平均车速80km/h以上明显不符合实际交通状况。原因出租车在凌晨空驶率高且很多车在机场、火车站排队等待这部分车辆处于怠速或低速状态但GPS上报点大量集中在停车区域。空驶出租车在快速路狂飙也拉高了平均速度。解决速度分析严格限制在载客状态同时把每个网格的样本量低于50的点剔除。另一个有效手段是把速度指标改成“中位数”而不是“均值”抗噪声能力更强。5.3 叠加深圳路网图后轨迹整体偏离道路几百米现象把GPS点叠加在下载的深圳路网图层上发现所有轨迹点都在道路的东南方向偏移量大致恒定。原因坐标系不一致。GPS数据是WGS84路网是高德/腾讯的GCJ-02火星坐标或者反过来。解决先通过少量特征点判断坐标系然后统一转换成GCJ-02或WGS84。注意深圳本地有些竞赛数据已经做过坐标偏移处理拿到数据后先随机抽100个点手工对比高德地图坐标不要盲目转换。5.4 OD矩阵里出现大量“隔壁小区起步、隔壁街结束”的1分钟短单现象OD分析得到的行程数量比出租车实际客运量高出30%以上且大量行程时长不足1分钟。原因载客状态字段在车辆短时停车时有抖动比如乘客支付后司机没有立即翻牌、空车等待时误触计价器导致状态在0和1之间快速变化。解决在提取行程时不仅过滤时长小于3分钟的行程还要检查行程距离。特别要看状态变化瞬间前后10秒的经纬度总位移小于200米就判定为无效行程。这里不能只按时间过滤因为出租车在爬坡缓行时3分钟可能只走了300米。5.5 热力图颜色集中在市中心关外区域一片空白现象可视化后福田、南山红成一片宝安、龙岗几乎没有颜色但从其他数据源看关外明明有严重拥堵。原因出租车GPS数据本身的时空分布不均。关外区域出租车密度低样本量少平均速度本来就不稳定。热力图如果直接按绝对数值着色低样本区域就会被压成白色。解决改用等值图时先做空间平滑或者把每个网格的速度值除以该网格自身的全天样本量做归一化。更常用的做法是设定最低样本阈值少于阈值就用灰色显示并在论文里明确说明这是数据覆盖不足不代表不堵。6. 落地验证从一份PDF到可复现的建模流程到这一步你已经可以跑通从原始GPS数据到深圳交通分析的全流程。但竞赛拿奖和工程落地之间还差一道工序验证结果是否可信。我给自己定了一个固定动作写一个验证脚本输出全市主要道路的全天平均速度曲线并且和公开的深圳交通指数做相关性比较。如果两条曲线的趋势差异很大问题大概率出在数据处理阶段而不是建模阶段。# 验证脚本对比出租车计算速度与公开指数 import pandas as pd daily_speed df[df[STATE] 1].groupby(df[TIME].dt.hour)[SPEED].mean() daily_volume df[df[STATE] 1].groupby(df[TIME].dt.hour).size() plt.figure(figsize(10, 4)) plt.plot(daily_speed.index, daily_speed.values, labelTaxi Avg Speed) plt.plot(daily_speed.index, daily_volume.values / daily_volume.max() * 60, labelTraffic Volume (norm)) plt.xlabel(Hour) plt.ylabel(Speed km/h / Norm Volume) plt.legend() plt.show()这个脚本同时绘制速度和流量正常情况应该是速度画出早晚双谷流量画出早晚双峰且相位基本同步。如果高峰时段速度反而上升那一定是样本污染了。最后说一个我的个人习惯每次做这类数据分析我都会保留一份“脏数据样本”文件夹把过滤掉的典型异常点导出成CSV放在项目目录的data/debug下。论文写作时不需要它们但评委如果质疑数据真实性这些样本能证明你确实认真地做了清洗。用出租车GPS数据做交通分析永远不是模型越厉害越好而是数据处理得越扎实结果越经得起推敲。希望这篇实战笔记能在你准备数学建模竞赛或做城市数据分析的时候帮到你。本文还有配套的精品资源点击获取