
简介《北京市出租车GPS轨迹数据统计特征分析》是一份学术PDF文档面向测绘科学与技术、GIS及交通数据分析领域的研究者与学习者可作为GPS定位系统、轨迹数据挖掘及统计特征分析方向的参考文献与专业指导。资源压缩包内仅含1个PDF文件大小约2.56MB已有479人学习下载。文档以2015年5月11日至17日北京市3万多辆出租车的3.85亿条轨迹点数据为对象剔除非载客段数据后运用R语言从行驶距离、轨迹段持续时间、分时段出行量、出行方向与平均速度等角度展开统计特征分析并拟合出浮动车行驶距离与持续时长遵循的指数分布。据此进一步识别出距离衰减效应以及工作日与周末的日律性有助于深入理解乘客出行距离与出行时间的潜在规律可为城市规划、交通预测等实践提供数据与决策支持。1. 一份 PDF 标题背后北京市出租车 GPS 轨迹数据统计特征分析到底在分析什么拿到一份名为“北京市出租车GPS轨迹数据统计特征分析.pdf”的文档外行看到的是几十张热力图和曲线干这行的人会先翻它处理数据的口径。出租车 GPS 轨迹数据是城市计算里最常见的数据源之一原始数据就是一堆带时间戳的经纬度点而“统计特征分析”要回答的问题是城市里每天有多少车在跑、什么时候空驶、乘客在哪上下车、一次行程到底多远。这类分析能直接支撑出租车调度、网约车需求预测和公交线网评估。适合读这类内容的人是准备拿轨迹数据做落地项目的工程师、做时空数据挖掘的学生以及想评估数据质量再决定要不要投入采集的人。需要提醒的是同一批轨迹数据统计口径不同结论可能完全相反。2. 先看懂原始数据北京市出租车 GPS 轨迹的字段、采样间隔与坐标系2.1 出租车 GPS 数据的六个常见字段哪些字段能直接信一份出租车轨迹数据集的原始表结构常见是 CSV 或 Parquet每条记录对应一个 GPS 采样点。北京市出租车数据的字段一般包括车辆标识、时间戳、经度、纬度、载客状态和速度部分版本还会带方向角。下面的字段映射是我处理这类数据的默认起点不依赖具体某份文件而是所有同类数据集的公共骨架。字段常见类型统计用途可信度车辆IDstring/int按车聚合、单车运营画像高时间戳datetime排序、行程时长、速度计算高但要确认时区和是否含毫秒经度/纬度float空间网格、距离计算中容易出漂移点载客状态0/1 或 int切分行程、载客率低需要做迟到修正速度float异常过滤、拥堵判断低和位移反推速度经常对不上方向角float可选用于转弯和停车判断低硬件校准差异大载客状态字段可信度低这一点做分析前就要有预期。计价器状态翻转依赖车辆硬件时钟GPS 采样是另一个时钟两者不同步状态翻转帧经常比实际上下客晚一个采样周期甚至更长。直接拿翻转点当上下客点行程终点会整体偏移几十到几百米后面统计上下客热点时这个误差会被网格放大。速度字段同样不能直接信。不少设备的 GPS 速度输出的是瞬时速度在城市里经常跳变和相邻点位移反推出来的平均速度差出一大截。我在清洗阶段一般把“速度字段”当成参考值以位移反推速度为主判据两个速度一起看才能判断一个点是真的在行驶还是漂移。2.2 采样间隔与数据规模30 秒一条记录意味着什么北京市出租车 GPS 数据的常见采样间隔是 30 秒或 1 分钟部分企业自建平台能做到 10 秒。按 1 万辆运营车、30 秒一次估算单日全量约 2880 万条记录一个月接近 9 亿条。这批数据用 pandas 直接全量读进内存不现实常规做法是先把原始 CSV 转成 Parquet 列存按日期分区再按“车辆ID 日期”分批处理。采样间隔的另一个坑是不均匀。GPS 模块在隧道、地下车库、高架下方丢星时间隔会从 30 秒拉到几分钟甚至更长。处理之前先算一版时间戳间隔分布这一步花不了几分钟但能直接暴露丢星程度也决定后面停留阈值怎么设。import pandas as pd df pd.read_parquet(beijing_taxi_2024.parquet) df df.sort_values([vehicle_id, timestamp]) # 按车计算相邻点时间间隔单位秒 df[interval_sec] ( df.groupby(vehicle_id)[timestamp].diff().dt.total_seconds() ) # 看间隔分布重点看 90 分位和最大值 print(df[interval_sec].describe(percentiles[0.5, 0.9, 0.99]))这段代码的核心是 groupby 后做 diff它算出的间隔分布直接反映数据质量。如果 90 分位在 35 秒左右说明大部分点正常如果 90 分位跑到 120 秒以上说明丢星严重后续切分行程时要把“间隔大于 5 分钟”作为行程中断的判据之一而不是只依赖载客状态。提示先看时间戳间隔分布再做任何统计。这一步能同时暴露休眠、丢星和文件拼接错误比看任何质量报告都直接。2.3 GPS 误差与坐标系丢星、漂移和坐标偏移在数据里长什么样GPS 误差这个词在轨迹分析里有三种典型表现处理方式完全不同。第一种是丢星后的长间隔定位模块在信号遮挡区失去锁定恢复后时间戳间隔明显变大表现是轨迹上出现一条“直线”连接两个相距很远的点。第二种是漂移点单个点看似在道路上实际横穿建筑落到另一个街区。第三种是坐标系偏移原始数据标注的是 WGS84 还是 GCJ-02决定了它能不能直接叠加在线地图底图。判断数据落在哪个坐标系不要看文档描述直接实测。取一个北京已知地标的经纬度和轨迹数据里同一个地点的点做对比差值在百米量级基本就是坐标系偏移。处理原则是轨迹距离计算统一用 WGS84如果最后要和国内在线地图服务叠加做可视化再做一次坐标纠偏。坐标系的坑做完一次就有血泪经验我第一次处理时没验证坐标系热力图叠到底图上整体偏移了约 300 米所有热点都落在街道另一侧看起来像导航导到河中央其实就是坐标系没统一。漂移点的量化方法更简单用相邻两个点的位移除以时间间隔得到“位移反推速度”超过物理上限的点就是可疑点。城市出租车正常行驶速度上限可以放宽到 120 km/h超过这个值不是飙车是 GPS 在飘。3. 数据清洗与行程切分把上万个 GPS 点还原成一次真实出行3.1 清洗规则重复点、越界点、速度跳变点怎么判清洗阶段的目标不是删得越多越好而是把“不可信的点”删掉“可信但需要修正的点”保留。我一般按四条规则依次处理先做按车去重再过滤经纬度边界然后做速度双重校验最后标记短期停车点。顺序不能乱边界过滤必须在速度校验之前否则漂移点算出来的速度会把正常点误删。import pandas as pd import numpy as np def haversine_speed(df): 由相邻点位推算速度单位 km/h R 6371.0 lat1 np.radians(df[lat].shift()) lat2 np.radians(df[lat]) dlat np.radians(df[lat] - df[lat].shift()) dlon np.radians(df[lon] - df[lon].shift()) a np.sin(dlat / 2) ** 2 np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2) ** 2 dist_km 2 * R * np.arcsin(np.sqrt(a)) interval_h df[interval_sec] / 3600.0 return dist_km / interval_h.replace(0, np.nan) def clean_gps(df): # 第一步按车和时间戳去重 df ( df.drop_duplicates(subset[vehicle_id, timestamp]) .sort_values([vehicle_id, timestamp]) .reset_index(dropTrue) ) # 第二步经纬度边界过滤北京行政范围外扩约 0.5 度 df df[ (df[lon] 115.5) (df[lon] 117.5) (df[lat] 39.0) (df[lat] 42.0) ] # 第三步重叠两个速度判据物理上限 120 km/h df[speed_geodetic] haversine_speed(df) df df[df[speed_geodetic].fillna(0) 120.0] return df这段清洗代码有三个关键点。第一去重时必须按“车 时间戳”双键只看时间戳会把不同车的点误删。第二边界过滤用的是经纬度框而不是一个圆形缓冲区原因是网格化统计阶段需要规则的经纬度边界矩形框更方便对齐。第三speed_geodetic是由相邻点位移和时间反推的和车辆自带的速度字段互不信任只要一个超限就删。速度上限 120 km/h 看起来宽松实际是故意留的余量。城市出租车一般跑不到这个速度但高架上瞬时冲到 80-100 正常如果阈值收紧到 80会把正常高速路段都删掉。真正常见的异常不是超速而是 0 到 120 的瞬间跳变这种要靠下一节的行程切分处理。3.2 用载客状态翻转切分行程迟到修正法载客状态从 0 变 1表示一次载客行程开始从 1 变 0表示结束。理论上这样切分就够了但前面提过状态翻转和实际上下车之间有时间差。直接按翻转点切切出来的行程会整体滞后一两个采样周期。我的做法是“迟到修正”找到状态翻转点之后向前回溯若干个采样点把行程起点往前挪。修正是有条件的不是无脑前移——回溯点必须满足“正在行驶”且“位置连续”否则会把一次起步停车误判成新行程。def split_trips(df, lookback3, min_speed_kmh5.0): df df.copy() df[trip_id] np.nan df[is_start] 0 df[is_end] 0 trip_counter 0 for vid, group in df.groupby(vehicle_id, sortFalse): group group.reset_index(dropTrue) status group[passenger_status].values start_idx None for i in range(1, len(group)): if status[i - 1] 0 and status[i] 1: # 状态翻转向前回溯修正起点 cand i for j in range(i - 1, max(-1, i - lookback - 1), -1): if group.loc[j, speed_geodetic] min_speed_kmh: cand j else: break start_idx cand trip_counter 1 group.loc[start_idx, is_start] 1 if status[i - 1] 1 and status[i] 0 and start_idx is not None: group.loc[i, is_end] 1 group.loc[start_idx:i, trip_id] trip_counter start_idx None # 处理跨文件截断的行程到文件末尾状态仍为 1 if start_idx is not None: group.loc[start_idx:, trip_id] trip_counter df.loc[group.index, trip_id] group[trip_id] df.loc[group.index, is_start] group[is_start] df.loc[group.index, is_end] group[is_end] return df.dropna(subset[trip_id])这里lookback3表示最多回溯 3 个采样点对应约 90 秒的迟到窗口。回溯要判断“速度 5 km/h”是因为停车等红灯时的状态翻转不具备行程意义。切分完成后再按trip_id聚合就能得到每次行程的起点时间、终点时间、起点经纬度、终点经纬度。这个修正法不完美但比直接按翻转点切稳定得多。回溯窗口设太大也不行会把前一单的末尾误并入下一单一般 2-3 个点最常用。3.3 轨迹距离计算用 Haversine 而不是欧氏距离轨迹距离是后续所有里程类统计的基础这里有一个高频翻车点把经纬度当成平面坐标直接算欧氏距离。在北京的纬度上经度 1 度约等于 85 公里纬度 1 度约等于 111 公里直接平方开根算出来的距离是错的数量级。正确做法是用球面距离常用的就是 Haversine 公式。def haversine(lon1, lat1, lon2, lat2): 两点球面距离返回公里 R 6371.0 p1 np.radians(lat1) p2 np.radians(lat2) dp np.radians(lat2 - lat1) dl np.radians(lon2 - lon1) a np.sin(dp / 2) ** 2 np.cos(p1) * np.cos(p2) * np.sin(dl / 2) ** 2 return 2 * R * np.arcsin(np.sqrt(a)) # 按 trip_id 聚合计算每次行程里程 trip_dist ( df.groupby(trip_id) .apply(lambda g: haversine( g[lon].iloc[0], g[lat].iloc[0], g[lon].iloc[-1], g[lat].iloc[-1] )) .rename(trip_km) )这段代码用行程起终点直接算球面距离适合快速统计。但要注意它算的是“起终点直线距离”不是“实际行驶里程”。如果要做运营里程统计应该对行程内相邻点逐一算 Haversine 再累加那个值更接近计价器里程。两种口径差多少北京市区短途订单直线距离和实际行驶距离差距普遍在 10%-30%高架绕行路段差距更大。写报告时要标明用的是哪种口径否则数字会被质疑。4. 统计特征计算从单车运营指标到城市级出行画像4.1 单车运营统计指标出车时长、载客率、空驶率的定义与口径单车运营指标是整个分析的中层产物它回答“一辆出租车一天怎么运营”的问题。常见指标包括出车时长、运营时长、载客时长、载客率、空驶率、平均单次载客里程、日均出车次数。这些指标看起来简单口径却容易打架。指标推荐口径说明出车时长当日第一个点到最后一个点的时间跨度包含停车休息运营时长排除长静默后的行驶等待时间长静默指超过 30 分钟不动载客时长载客状态1 的采样点时长合计要乘采样间隔并做迟到修正载客率载客时长 / 运营时长按时间算不按点数算空驶率1 - 载客率空驶率不是越低越好平均单次载客里程行程里程合计 / 行程数用累加里程不用直线里程按“采样点数”算载客率和按“时间”算载客率结果差异很大。GPS 采样是等间隔的但丢星会拉长间隔如果按点数算丢星少的车权重偏高按时间算需要用时间戳差值聚合更接近真实运营。空驶率这个指标尤其容易误读空驶率低说明车辆几乎不停但在高峰期可能意味着司机在路边长时间等待接单并不一定是高效。4.2 城市级时间分布24 小时载客曲线为什么有两个峰把全量行程的起点时刻按小时聚合能得到一条 24 小时载客曲线。北京市的典型曲线呈现双峰结构早高峰 7-9 点有一个峰晚高峰 17-19 点有一个更高的峰凌晨 3-5 点跌到谷底。这个曲线是城市出行需求的最直观表达也是调度系统做运力配置的基础。曲线形状对统计口径非常敏感。用“行程起点”聚合和用“载客状态点数占比”聚合峰的位置和高度完全不同。按行程起点统计短途订单多的时段会被放大按载客点数占比统计长行程多的时段会突出。我一般同时算两条曲线一条做需求分析一条做运力评估报告里只提其中一条时一定标注口径。这里还有一个容易被忽略的操作细节时间要按照“本地时区”聚合不要用 UTC。不少原始数据集时间戳存的是 UTC北京在东八区直接用 UTC 小时聚合会把早高峰移到下午结论直接反掉。4.3 空间统计按 500 米网格统计上下客热点空间统计的落点是热点网格。常用做法是把北京划分成规则的经纬度网格网格边长取 0.005 度约等于 500 米。对每个行程的起点和终点分别落格统计每个网格内的上客量、下客量、平均等待时长输出成一份网格级 CSV 供后续可视化或建模使用。import numpy as np GRID_SIZE 0.005 # 约 500 米 def to_grid(lon, lat): 经纬度映射到网格编号返回 (gx, gy) gx int(np.floor(lon / GRID_SIZE)) gy int(np.floor(lat / GRID_SIZE)) return gx, gy # 只取行程起点做上客热点 starts df[df[is_start] 1].copy() starts[gx], starts[gy] zip(*starts.apply( lambda r: to_grid(r[lon], r[lat]), axis1 )) pickup_hot ( starts.groupby([gy, gx]) .size() .reset_index(namepickup_cnt) ) pickup_hot.to_parquet(pickup_grid.parquet)GRID_SIZE这里直接作用在经度和纬度上不做投影转换原因是分析只需要相对密度不是精确面积。如果要做面积相关的统计比如每平方公里单量需要先做投影再网格化那是另一套流程。网格大小 0.005 度适合城市尺度分析。网格越大热点越平滑但空间分辨率下降网格越小越能看出街道级差异但统计稀疏区域会出现大量零值网格。选多大没有绝对标准看分析目的是宏观评估还是微观选址。5. 参数选定与避坑同样一批数据为什么能统计出两种结论5.1 网格大小、速度阈值、停留时长参数怎么定才有可比性统计特征分析里最容易出分歧的就是参数。网格大小、速度上限、停留阈值、载客状态回溯窗口每个参数都会显著影响最终数字。参数怎么定才科学我的原则是先看数据分布再定阈值不拍脑袋。下面是一组经过多轮验证的默认参数。参数默认值影响面调整依据网格大小0.005 度约 500 米热点图颗粒度缩小到 0.002 看街道级放大到 0.01 看城区级速度上限120 km/h漂移过滤强度观察速度分布 99 分位超过物理上限再截断停留阈值5 分钟区分“等待”和“收车”看间隔分布长尾起点一般在几分钟量级载客回溯窗口3 个采样点行程切分准确度取决于采样间隔30 秒采样用 31 分钟采样用 2早高峰时段7-10 点时段统计对比不同城市峰谷不同先画曲线再定边界这些参数要写进报告的方法部分。很多 PDF 分析结论无法复现原因就是参数没写全——网格大小差一倍热点排名完全不同。我见过两个团队用同一份北京数据一个用 1 公里网格一个用 300 米网格最后产出的“重点区域”名单重合度不到一半。这不是谁对谁错而是分析粒度定义不同。5.2 避坑记录四类必须处理的数据质量坑第一坑凌晨轨迹大面积消失。现象是 0 点到 5 点车辆数骤降曲线看起来像“半夜没车跑”。原因是不少出租车夜间停在固定停车场GPS 断电或进入休眠不是真的没车。解决方法是先做长静默识别按车统计时间戳间隔间隔超过 30 分钟的点段标记为“休整”不参与运营统计。这个坑不处理凌晨时段的空驶率会被严重高估。第二坑速度从 0 瞬间跳到 120 km/h。现象是单条轨迹上出现一个孤立的远点和前后点形成锐角折线。原因是 GPS 接收机在遮挡环境下多路径效应导致定位跳变。解决方法是按位移反推速度和原始速度字段双重校验任意一个超限就删如果是连续多个点异常还要配合滑动窗口做中值滤波。第三坑载客状态翻转和实际上下车点对不上。现象是某单行程的起点落在一条河中间或者终点离路口还有 200 米。原因是计价器状态改变滞后于 GPS 时间戳。解决方法是前面写的回溯修正但要注意修正窗口不是越大越好我一般限制在 3 个采样点以内避免把上一单的尾巴并入新单。第四坑跨天行程被切成两半。现象是 23:50 开始的行程在次日文件里出现另一半单独看两天数据会统计出两段短途订单。原因是按日期分文件存储时零点附近的行程被截断。解决方法是处理每日文件前先把前一天最后一单的尾部点拼到当天开头再按“行驶中断间隔大于 5 分钟”重新切分。拼接逻辑要在清洗之前做否则去重和速度计算都会受边界影响。第五坑热力图整体偏移几百米。现象是所有热点看起来都落在街道另一侧和真实地标对不上。原因是坐标系没统一原始数据是 WGS84可视化底图用 GCJ-02或者反过来。解决方法是先取几个已知地标点验证坐标系再决定要不要做偏移纠正。做轨迹计算用 WGS84做可视化按底图要求处理。6. 让统计特征产生价值用 24 小时上下客量做区域需求预测统计特征的终点不是报告是能喂给下游模型的特征矩阵。这里给一个最直接的落地技巧把前面的网格和时段统计结果转成“区域需求预测”的输入。特征矩阵按“网格 × 小时 × 是否工作日”构造目标值是下一小时该网格的上客量。这个预测对调度系统意义很大能提前一小时知道哪里会叫不到车而不是事后看热点图。# 构造特征矩阵每个 (网格, 小时) 一行的历史需求 feat starts.copy() feat[hour] starts[timestamp].dt.hour feat[is_weekday] (starts[timestamp].dt.dayofweek 5).astype(int) X ( feat.groupby([gy, gx, hour, is_weekday]) .size() .reset_index(namepickup_cnt) ) # 滞后特征前 3 小时同一网格的上客量 X X.sort_values([gy, gx, is_weekday, hour]) for lag in [1, 2, 3]: X[flag_{lag}] X.groupby([gy, gx, is_weekday])[pickup_cnt].shift(lag) X X.dropna()做需求预测的验证方式也和普通机器学习不同不能用随机切分。时间序列数据按时间顺序切分才有意义用随机切分会把未来信息泄露到训练集里。我一般用 scikit-learn 的TimeSeriesSplit或者手动留出最后 7 天做验证。特征工程里最有用的往往不是天气或 POI而是滞后值——前 1 小时的上客量本身就是最强预测因子。我习惯在跑全量统计前先单独抽查一个字段的可信度再决定要不要信任后续结论。这个习惯帮我避免过好几次“统计做完才发现数据本身有问题”的返工希望帮到你。本文还有配套的精品资源点击获取