简介:一份以深圳市出租车GPS数据为对象、研究城市道路交通状况的数学建模PDF资料,适合数学建模竞赛备赛者、交通数据分析学习者及城市规划相关专业学生。压缩包内共含一个PDF文件,大小约972KB,正文涵盖问题分析、模型假设、符号说明、模型建立与求解、模型改进等完整结构。该资料在CSDN已有145人浏览学习。内容围绕交通小区划分、OD时空分布计算与推断、拥堵路段/路口识别等实际问题展开,演示了DBSCAN聚类、带约束二次规划、内点障碍罚函数等方法的运用,并给出了数据库SQL处理海量GPS数据的思路,对理解数学建模从数据处理到模型求解的完整流程有帮助。通过阅读可掌握利用GPS时空数据研究交通流动态的方法,也可借鉴其建模与论文写作框架。
1. 出租车 GPS 数据:数学建模里最容易被低估的“路况黑匣子”
数学建模竞赛里,凡是沾上“交通”二字的题目,十有八九会给你一份 GPS 数据。2025 年研究生数学建模竞赛 E 题就能看到这类影子——给的往往是某个城市的出租车轨迹,让你从里面积分出路况、拥堵、甚至是城市职住结构。很多人拿到数据的第一反应是画散点图,画完就卡住了,因为轨迹点连起来不是路,是一条条横七竖八的乱线。实际上,出租车 GPS 数据是一份被低估的交通感知网络:一辆车就是一个移动传感器,几万台车同时跑,就把一座城市的路况“采样”出来了。
这份《数学建模资料-用出租车GPS数据分析深圳道路交通情况》解决的正是在这种场景下的完整流程问题:从原始 CSV 轨迹点,到清洗、地图匹配、路段速度计算,再到拥堵评价和 OD 分析。适合三类人——准备数学建模竞赛的学生、刚接触交通数据分析的从业者、以及想用开源方式做城市分析的研究者。新手能跟着把流程跑通,熟手可以跳过常规步骤,直接看边界条件和参数陷阱。
2. 出租车轨迹的数据结构与深圳场景:先搞懂点在记录什么
2.1 一条 GPS 记录里的字段,远不止经纬度
出租车 GPS 数据最常见的格式是 CSV,一行一条记录,深圳这类城市的开放数据集中,核心字段一般逃不出这七列:车辆 ID、时间戳、经度、纬度、速度、方向角、载客状态。有些数据集还会带车牌类型、公司编号、甚至当班司机 ID。很多参赛队拿到数据就开始算距离,结果忽略了一个关键事实:载客状态(通常用 0/1 表示,0 是空车,1 是载客)直接把轨迹切成了两类——载客时的轨迹反映的是乘客的出行路径,空车时的轨迹反映的是司机的寻客逻辑。做路况分析,两类都能用;做 OD 分析,只取载客段。
时间戳在深圳这份数据里常见的是 Unix 秒或毫秒级整数,少数是字符串格式“yyyy-MM-dd HH:mm:ss”。如果是整数,第一步先转换成可读时间,再按车辆 ID 和时间做排序,否则轨迹是乱的。排序这件事看似简单,实际是后续所有计算的地基——不排序,算出来的速度可能是个负值。
还有个容易忽略的点是数据采样率。深圳出租车 GPS 的采样间隔常见是 10 秒到 30 秒不等,部分公司设备是 5 秒。这意味着相邻两个点之间的距离可能在 100 到 500 米之间(市区拥堵时短,高速上长)。采样率直接决定了你能做什么分析——10 秒间隔能做路段速度,但做不到车道级精细度。
2.2 坐标系的坑:WGS84 与 GCJ-02 的偏移
国内出品的 GPS 数据,十份里八份是 GCJ-02 坐标系,也就是俗称的“火星坐标”。深圳的出租车数据大多也不例外。GCJ-02 是在 WGS84 基础上做了一次非线性偏移,偏移量大约在 100 到 700 米之间,而且城市不同偏移不同。如果你把 GCJ-02 数据直接丢到天地图或者某些国际底图上叠绘,会发现轨迹整体偏到路对面甚至偏移到另一条路上。
判断坐标系是哪个,有一个土办法:取一个已知地标的经纬度,比如深圳市民中心,然后跟地图软件上点的坐标对比。如果粗略一致,大概率是 GCJ-02;然后你用 WGS84 的标准值做差,如果差值在百米量级,基本确认是火星坐标。从 WGS84 转 GCJ-02 有公开的转换算法,反向也有,网上能搜到一份流传很广的 Python 实现,核心是查表加多项式逼近。
坐标系不统一的后果很致命:后续做地图匹配时,轨迹点和路网不重合,偏移几百米后可能匹配到隔壁平行路上,算出来的车速偏差直接到 30% 以上。数据清洗做得再漂亮,坐标错,全盘皆输。
3. 数据清洗三步走:漂移点、驻车段、异常速度
3.1 漂移点的识别与剔除
GPS 设备在城市峡谷里容易收到多路径反射信号,导致点位瞬间跳出几百米。漂移点的典型特征是:相邻两点距离异常大(比如 5 秒内跳出 1 公里),且计算出的瞬时速度远超道路限速。有一类漂移暂时算不出来——设备自主上报的状态位错误——这部分属于系统性噪声,后面用滤波处理。
处理漂移点的常用做法是:遍历每辆车的轨迹点,计算相邻点的距离和速度,如果速度超过某个阈值(比如 120 km/h,出租车在城市里极少超过这个值),就标记为疑似漂移点,再结合方向角变化率判断。方向角在短时间内剧烈变化,同时伴随高速移动,基本可以确认是设备受干扰,直接剔除。
import pandas as pd import numpy as np def haversine(lon1, lat1, lon2, lat2): R = 6371000 lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2]) dlon = lon2 - lon1 dlat = lat2 - lat1 a = np.sin(dlat/2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2 return 2 * R * np.arcsin(np.sqrt(a)) df = pd.read_csv('shenzhen_taxi.csv') df['timestamp'] = pd.to_datetime(df['timestamp']) df = df.sort_values(['vehicle_id', 'timestamp']) for vid, group in df.groupby('vehicle_id'): lon = group['longitude'].values lat = group['latitude'].values time_diff = group['timestamp'].diff().dt.total_seconds().values dist = haversine(lon[:-1], lat[:-1], lon[1:], lat[1:]) speed = dist / time_diff[1:] * 3.6 # m/s -> km/h invalid = np.zeros(len(group), dtype=bool) invalid[1:] = speed > 120 # 方向角突变检测 heading_diff = np.abs(np.diff(group['heading'].values)) heading_diff[heading_diff > 180] = 360 - heading_diff[heading_diff > 180] invalid[1:] |= (heading_diff > 60) & (speed > 60) df.loc[group.index[invalid], 'is_invalid'] = True df = df[df['is_invalid'] != True]这段代码里 haversine 函数算的是球面距离,输入经度、纬度,输出单位为米。时间差用 pandas 的 diff 函数直接算,得到的是秒数。速度单位换算成 km/h 是为了跟阈值 120 对比更直观。方向角突变条件有两个:方向变化超过 60 度,同时当前速度大于 60 km/h——满足这两个条件才标记,单方向变化剧烈可能只是正常转弯,不能一刀切。
3.2 驻车段与空转段的处理
出租车有大量的路边等待和红绿灯停车,这类点位的速度接近零,但不是数据错误,不能直接删。真正要处理的是两类:一类是长时间原地不动(超过 3 分钟,且经纬度几乎不变),这大概率是停车休息或排队充电,不是红绿灯;另一类是数据里出现的重复点——同一时刻上报了多条相同记录,这在很多城市的原始数据集里都存在。
我的处理习惯是:先做完全相同记录的去重,再按车辆 ID 分组,计算相邻点经纬度距离。如果连续 5 个以上的点都在 10 米范围内移动,且持续超过 3 分钟,就判定为驻车段,整体标记出来但不删除。路况计算时排除这些段,OD 分析时它们反而是线索——空车长时间停在某个区域,说明那里有接单机会或者司机在等人。
这里有个容易被忽略的细节:驻车段的判断不能只看时间阈值,要看“连续静止”。出租车在拥堵路段也会出现走走停停的情况,每个点之间间隔 20 秒,速度 5 km/h,位移 30 米——这跟驻车状态完全不同。如果简单粗暴地按“速度 < 1 km/h 就剔除”,你会把拥堵路段的数据全部删掉,算出来的路况直接失真。
3.3 时间戳与轨迹排序
数据处理过程中,时间戳问题的优先级比想象中高。个别车辆上的设备可能出现时钟漂移,导致时间戳比实际时间快几分钟或慢几分钟。这个问题肉眼很难发现,但会在 OD 分析里露出马脚——某辆车的载客段出现“负时长”。处理方式是逐车检查时间戳的时间间隔分布,正常情况下的间隔应当是采样间隔的整数倍,出现大量小于 1 秒的异常间隔,就要标记该车为可疑设备。
time_gaps = df.groupby('vehicle_id')['timestamp'].diff().dt.total_seconds() gap_summary = time_gaps.groupby(df['vehicle_id']).agg(['min', 'max', 'median']) abnormal_vehicles = gap_summary[gap_summary['min'] < 0].index.tolist()这段代码做的事情很直白:先按车辆分组,计算每辆车内部相邻时间戳的间隔秒数,然后做摘要统计。出现min < 0,说明时间戳逆序,车辆的设备时钟出了问题。把这些车辆单独隔离出来,检查是否是少数几辆车的问题,如果占比很小(少于 0.5%),直接剔除即可,不影响整体分析结论。
4. 从轨迹点到路段路况:地图匹配与速度反演
4.1 网格化匹配:不做完整地图匹配也能算出路况
完整的地图匹配算法(比如隐马尔可夫模型)在论文里很好看,但在数学建模场景下,复杂度高、调参难,而且对路网数据的要求高——你得先有深圳完整的道路拓扑。实际竞赛中,很多队伍没拿到路网数据就开干了,这时候网格化就是一个务实的替代方案。
网格化的思路很简单:把深圳按经纬度切成均匀的格子(比如 500 米 × 500 米),每个格子作为一个“路段代理”。把 GPS 点落入对应的网格格子里,然后按网格汇总速度。这个方法的代价是精度受格子大小影响,好处是不依赖任何外部路网数据,拿一份经纬度边界就能跑通全流程。
grid_size = 0.005 # 约500米 df['grid_lon'] = (df['longitude'] / grid_size).astype(int) df['grid_lat'] = (df['latitude'] / grid_size).astype(int) def calc_grid_speed(group): speeds = group['speed'].values speeds = speeds[(speeds > 2) & (speeds < 80)] return np.mean(speeds), len(speeds) grid_stats = df.groupby(['grid_lon', 'grid_lat']).apply(calc_grid_speed)这段代码的网格划分用了整数取整的方式,0.005度大约对应 500 米在赤道附近的距离,深圳维度接近北回归线,实际约 540 米,对路况分析反正是粗粒度,够用。速度过滤2 ~ 80 km/h是为了排除驻车点和异常高速点。统计结果里的len(speeds)是样本量,后面评估拥堵指数可靠性时要用到——一个网格里只有两条记录的均速没有任何参考价值。
网格粒度怎么选?我一般建议做两轮实验:第一轮用 1 公里粒度跑一遍,看整体路况分布是否合理;第二轮切到 300 米粒度细看核心城区。市中心的经纬度网格天然会出现一个格子横跨三条路的情况,这点后面专门讲。
4.2 拥堵指数计算:从平均速度到路况等级
平均速度本身不能直接当拥堵指标——同一条路,限速 80 和限速 40,跑 30 km/h 的含义完全不同。深圳的快速路(北环大道、滨海大道)限速普遍 80,市区主干道限速 60,支路限速 40。为了可比,业内常用 TSI(Traffic State Index)的思路:实际运行速度与自由流速度的比值映射到 0 到 100 的指数。
自由流速度怎么取?没有官方参考值时,可以用该网格/路段在一天内速度分布的 85 分位数作为近似。比如某路段一天内所有经过车辆的速度 85% 都在 55 km/h 以下,那 55 就当作自由流速度的估计值。思路是:夜间的出租车速度更接近道路设计速度,取一天的高位分位数,能天然消除早晚高峰的拥堵影响。
def calc_tsi(avg_speed, free_flow_speed): ratio = avg_speed / free_flow_speed if ratio >= 0.8: return 1 elif ratio >= 0.6: return 2 elif ratio >= 0.4: return 3 elif ratio >= 0.2: return 4 else: return 5 grid_speed = grid_stats[0] # 平均速度 grid_free_flow = df.groupby(['grid_lon', 'grid_lat'])['speed'].apply( lambda x: np.percentile(x, 85) ) grid_tsi = pd.DataFrame({ 'avg_speed': grid_speed, 'free_flow_speed': grid_free_flow }).apply(lambda row: calc_tsi(row['avg_speed'], row['free_flow_speed']), axis=1)TSI 等级从 1 到 5,1 表示畅通,5 表示严重拥堵。这个五级分类法跟很多城市的交通运行指数公布口径类似,竞赛论文里用这套表述,评委一看就知道你做了标准化的工作。需要注意的是自由流速度按天算还是分时段算——我建议分时段,凌晨的 85 分位数速度会高于晚高峰的 85 分位数速度,如果统一用全天数据,高峰期的“相对畅通”会被误判成拥堵。
4.3 早晚高峰的切片分析
做完网格速度计算,下一步就是时间维度上的切片。深圳的交通有两个明显的窗口:早高峰 7:30 到 9:30,晚高峰 17:30 到 19:30。把一天 24 小时切成 5 分钟窗口,每个窗口内统计一次网格平均速度,就能得到“一天内拥堵演化”的时间序列。
这个切片的参数敏感性很高。窗口太短(1 分钟),部分网格可能一个样本都没有;窗口太长(30 分钟),高峰期的拥堵细节全被抹平。5 分钟窗口配合 500 米网格,在深圳出租车数据量下通常每个格子有几十个到几百个样本,是比较合适的平衡点。
有了时间序列后,可以做的事就多了:定位“常发拥堵点”(连续三天同一时段 TSI 为 4 以上的网格)、分析拥堵在空间上的扩散方向(从关口向市中心蔓延)、甚至做拥堵预测的模型输入。论文里画一张热力图——横轴时间,纵轴网格 ID,颜色表示 TSI——比整页的文字更有说服力。
5. 出租车 GPS 分析的避坑记录:这些坑我都踩过
5.1 坐标系混淆导致的路网偏移
现象:轨迹点画出来和深圳地图的路网对不上,整体偏移大约 200 到 500 米,跟道路平行但不重合。
原因:数据用的是 GCJ-02,但你拿的是 WGS84 底图或者反过来。深圳的数据大多偏火星坐标,直接画图就是这样。
解决:先用已知地标做坐标验证,确认坐标系后做统一转换。GCJ-02 转 WGS84 网上有标准算法,转换后必须抽样复检——取几个路口点位,跟地图交叉核对误差在 20 米内,才说明转换参数没问题。
5.2 重复记录导致的负时间间隔
现象:部分车辆的轨迹里面出现连续两条完全相同的记录,时间间隔为 0,个别甚至出现负间隔。代码跑速度计算时报出离谱的 2000 km/h。
原因:设备的重复上报,以及车辆熄火后数据补传导致的乱序。这不是你代码的问题,是设备行为。
解决:做全字段去重,再按车辆 ID 和时间戳排序,排序后如果还有负间隔,直接标记该车辆并剔除。血泪经验:负间隔如果只出现在个别车辆上,剔除是最省事的做法;不要试图修正,因为你不知道是时钟问题还是补传问题。
5.3 网格划分造成的虚假拥堵
现象:某条快速路在热力图上呈现深红色“严重拥堵”,但你抽样看原始轨迹,车速其实 60 以上。
原因:快速路和辅路并行在同一网格内,辅路的低速样本拉低了平均值。网格是沙盘不是解剖刀——它分不开并行道路。
解决:评估网格内样本量的分布,如果某个网格里速度分布出现双峰(一个峰值在 60+,一个在 20 以下),说明网格内混了不同限速的道路。处理方式是缩小网格尺寸到 200 米重新计算,或者在论文里明确网格方法的精度边界,承认它识别的是片区拥堵而非路段拥堵。竞赛里,承认方法边界不比硬扛减分。
5.4 载客状态字段不规范
现象:OD 分析里出现单次行程超过 3 小时、里程超过 200 公里的记录,明显不合理。
原因:部分车辆载客状态切换存在延迟——乘客下车后 1 到 2 分钟才上报空车,导致上一次行程被拉长。
解决:在提取出行段时,设定最大时长阈值(比如 120 分钟)和最大距离阈值(比如 100 公里),超出即切断。还要检查状态切换是否交替出现——连续两次载客之间没有空车状态,就是字段异常,直接丢弃这一段。
6. 进阶:把路况分析做成决策依据
路况分析做到网格和 TSI 这层,已经是很多竞赛队伍的终点了。但这份深圳出租车数据还能再往下挖一层。
一个值得投入的方向是把 OD 分析与道路运行状态叠起来看。提取载客段后,聚合出每个网格的出行发生量和吸引量,再结合白天/夜晚的时段划分,能看到深圳的职住分离程度——越过梧桐山隧道去盐田上班的人有多少,福田到南山通勤的潮汐方向是什么。这些结论放在竞赛论文里,是从“描述现象”升到“解释机制”的关键一步。
另一个实用技巧是数据存储的优化。深圳一天的出租车轨迹大约千万行级,pandas 处理起来还不至于卡死,但如果要跑全月数据,建议用列式存储格式,同时按日期分区。把清洗后的数据转成 parquet 格式,文件体积能压缩到原来的三成以下,后续加载速度提升一个量级。代码上只需在清洗完成后加一行转换。
可视化层面,我用过比较顺手的是 folium 做采样点快速核查,正式出图用 pyecharts 的热力图。动态时序图能清楚地看到晚高峰拥堵从罗湖口岸向外扩散的过程——这种图在答辩时放出来,比静态地图有感染力得多,而且做起来就是逐帧渲染热力图而已。
从头跑完这套流程后,我的一个习惯是从不直接拿网格分析结果下结论。每次都会先抽出几个网格,把原始轨迹点画出来,人眼确认路况判断是否真实。从那以后,不管用模型还是数据,我都会强制过一遍“抽点看原始数据”的环节,这个动作在竞赛和实际项目里都帮我拦住了不少错误结论。希望这份资料里的流程和踩坑记录能帮你少走一段弯路,把重点放在真正有说服力的交通分析上。
本文还有配套的精品资源,点击获取