☰
出租车GPS数据分析深圳交通:清洗、匹配与时空聚合实战
2026/10/11 1:13:43 网站建设 项目流程

简介:基于出租车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/h
DIRECTION258方向角,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-02:

import 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, c=sub["SPEED"], cmap="RdYlGn", s=5, alpha=0.6 ) plt.colorbar(label="Average Speed (km/h)") plt.title(f"Hour {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(eps=0.5, min_samples=20).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, label="Taxi Avg Speed") plt.plot(daily_speed.index, daily_volume.values / daily_volume.max() * 60, label="Traffic Volume (norm)") plt.xlabel("Hour") plt.ylabel("Speed km/h / Norm Volume") plt.legend() plt.show()

这个脚本同时绘制速度和流量,正常情况应该是速度画出早晚双谷,流量画出早晚双峰,且相位基本同步。如果高峰时段速度反而上升,那一定是样本污染了。

最后说一个我的个人习惯:每次做这类数据分析,我都会保留一份“脏数据样本”文件夹,把过滤掉的典型异常点导出成CSV,放在项目目录的data/debug下。论文写作时不需要它们,但评委如果质疑数据真实性,这些样本能证明你确实认真地做了清洗。用出租车GPS数据做交通分析,永远不是模型越厉害越好,而是数据处理得越扎实,结果越经得起推敲。希望这篇实战笔记能在你准备数学建模竞赛或做城市数据分析的时候帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询