ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python出租车GPS轨迹数据分析实战:从经纬度点到城市脉搏

Python出租车GPS轨迹数据分析实战:从经纬度点到城市脉搏 简介这份资源面向具备一定Python基础、希望入门时空数据分析的学习者与开发者围绕出租车GPS轨迹的清洗、统计与地图可视化展开可解决轨迹数据杂乱、难以直观呈现载客规律的问题。压缩包共26个文件约13.13MB以15个py脚本和2个ipynb笔记本为核心辅以2个csv数据、2个json配置、1个png示意图及README、LICENSE等说明文件结构清晰便于按模块阅读。项目基于transbigdata第三方包分别处理上海与深圳两地的出租车GPS数据通过数据清洗后统计各栅格内的出租车数量并进一步分析OD路径与一天中的载客轨迹最终在地图上完成可视化呈现。已有823人学习下载读者可借此掌握轨迹数据预处理、栅格聚合、OD分析与地理可视化的完整流程理解城市交通时空特征的挖掘思路并可直接复用脚本与笔记本进行二次实验。1. 从一堆经纬度点到城市脉搏出租车轨迹数据分析到底在做什么手里拿到一份出租车 GPS 轨迹数据通常是几十万到上亿行、每行一个时间戳加一对经纬度外加车牌和载客状态。直接扔进 Excel 会卡死直接画散点图是一团糊。这个标题要解决的核心问题是把这种「原始点流」变成能回答业务问题的东西哪里堵、什么时段打车难、空驶率多高、热门上下客点在哪。Python 在这条链路里扮演的角色是胶水加引擎——pandas 清洗、numpy 算距离、matplotlib 或 pyecharts 出图必要时上 Dask 或 Spark 扛大数据量。适合谁做交通分析、网约车运营、城市数据开放平台、以及想拿一个真实数据集练手数据分析与可视化的工程师。它不需要你懂深度学习但需要你对坐标系、时间窗口、采样噪声有基本敬畏。下面按「数据长什么样 → 怎么清洗 → 怎么算指标 → 怎么画图 → 坑在哪」一路推下去每一步都给能跑的代码和参数。2. 轨迹数据到手先别急着画图字段、坐标系与采样噪声2.1 一份典型出租车轨迹数据的字段结构不同城市开放的数据格式略有差异但核心字段高度一致。常见的是 CSV 或 Parquet列名可能是taxi_id、timestamp、lon、lat、status也可能是VEHICLE_NO、GPS_TIME、LONGITUDE、LATITUDE、PASSENGER。先做字段映射别急着改列名用一层字典把原始列名映射到统一 schema这样换数据集时只改映射表。字段含义常见类型注意点taxi_id车辆唯一标识string可能是脱敏哈希别当数字timestamp采样时间string/int可能是秒级或毫秒级需统一lon经度float国内多为 GCJ-02不是 WGS-84lat纬度float同上status载客状态int/string0/1 或 空车/重车含义要确认坐标系是第一个大坑。国内出租车数据绝大多数是 GCJ-02火星坐标如果你直接拿去和 WGS-84 的底图叠加会有几百米偏移。常见做法是用coord-convert或自己写 GCJ-02 转 WGS-84 的函数但注意如果只做相对分析比如聚类、热力图不叠加外部底图其实可以不转省一步。只有需要和路网、POI 精确匹配时才必须转。2.2 用 pandas 做第一轮体检缺失、重复、越界拿到数据先跑体检脚本不要凭感觉。下面这段代码做四件事看形状、看缺失、看时间范围、看经纬度是否落在合理城市范围内。import pandas as pd import numpy as np # 读取低内存模式先看结构 df pd.read_csv(taxi_sample.csv, nrows100000) print(shape:, df.shape) print(missing:\n, df.isna().sum()) print(dtypes:\n, df.dtypes) # 时间字段统一转 datetime假设是秒级时间戳 df[ts] pd.to_datetime(df[timestamp], units, errorscoerce) print(time range:, df[ts].min(), -, df[ts].max()) # 经纬度合理范围以某城市为例按实际城市改 lon_min, lon_max 116.0, 116.8 lat_min, lat_max 39.6, 40.2 mask ( df[lon].between(lon_min, lon_max) df[lat].between(lat_min, lat_max) ) print(out of bound ratio:, 1 - mask.mean()) # 去重同一辆车同一秒只保留一条 df df.drop_duplicates(subset[taxi_id, ts]).sort_values([taxi_id, ts])逻辑说明nrows先抽样是为了避免一上来就吃满内存errorscoerce把无法解析的时间变成 NaT方便后续统计坏数据比例越界比例如果超过 5%说明坐标系或城市范围设错了先别往下走。参数上units和unitms要按实际数据确认搞错会让时间范围差出几十年这是血泪经验。2.3 采样间隔不均匀轨迹分析里最容易被忽略的噪声出租车 GPS 采样间隔通常不是固定的可能 10 秒、30 秒、甚至几分钟。间隔太大时两点之间直接连线会穿过建筑物和河流算出来的距离和速度全是错的。处理方式有两种一是设阈值间隔超过 120 秒的段直接切断不参与速度计算二是插值但插值会引入虚假点只适合做可视化平滑不适合做统计。df[dt] df.groupby(taxi_id)[ts].diff().dt.total_seconds() # 标记断点间隔过大或为负时间倒流 df[break] (df[dt].isna()) | (df[dt] 120) | (df[dt] 0) print(break ratio:, df[break].mean())dt 0这条别省真实数据里时间倒流并不罕见可能是设备时钟漂移或数据拼接错误。断点比例如果超过 20%这份数据的可用性就要打问号先和 data owner 确认采集策略。3. 从点到段用 Python 算出行程、速度与空驶率3.1 相邻点距离计算Haversine 与投影的取舍算两点距离最常用的是 Haversine 公式球面近似误差在百米级以内对城市尺度够用。如果要做精确的路网匹配得先投影到平面坐标如 UTM再用欧氏距离。下面给 Haversine 的向量化实现比逐行循环快几个数量级。def haversine(lon1, lat1, lon2, lat2): R 6371000.0 # 地球半径米 phi1, phi2 np.radians(lat1), np.radians(lat2) dphi np.radians(lat2 - lat1) dlambda np.radians(lon2 - lon1) a np.sin(dphi / 2) ** 2 np.cos(phi1) * np.cos(phi2) * np.sin(dlambda / 2) ** 2 return 2 * R * np.arcsin(np.sqrt(a)) # 按车分组后取相邻点 df[lon_next] df.groupby(taxi_id)[lon].shift(-1) df[lat_next] df.groupby(taxi_id)[lat].shift(-1) df[dist_m] haversine(df[lon], df[lat], df[lon_next], df[lat_next])参数说明R用 6371000 米是通用值精度要求高可用 WGS-84 椭球公式但城市分析没必要。shift(-1)生成下一行配合groupby保证不跨车。算完距离后dist_m里会出现 NaN每辆车最后一行和异常大值断点处下一步要过滤。3.2 速度与空驶率指标定义比代码更重要速度 距离 / 时间间隔但要用过滤后的段。空驶率 空车状态下的行驶距离 / 总行驶距离。这两个指标的定义在不同业务里不一样先和需求方对齐再写代码。# 过滤断点、距离过大5km 视为异常、时间间隔为 0 valid (~df[break]) (df[dist_m] 5000) (df[dt] 0) df[speed_kmh] np.where(valid, df[dist_m] / df[dt] * 3.6, np.nan) # 空驶率假设 status0 为空车 df[is_empty] df[status] 0 empty_dist df.loc[valid df[is_empty], dist_m].sum() total_dist df.loc[valid, dist_m].sum() print(empty ratio:, empty_dist / total_dist)逻辑说明dist_m 5000是经验阈值城市里两个采样点之间超过 5 公里基本是断点或漂移speed_kmh用np.where一次性赋值避免循环。空驶率如果算出来超过 0.6要么数据 status 字段含义反了要么这个城市出租车确实空驶严重需要交叉验证。3.3 用 groupby 做每车每日汇总单点指标看完要落到「每辆车每天」的粒度才能做排名和趋势。pandas 的groupby加agg是标准做法但要注意时间字段先取日期。df[date] df[ts].dt.date daily df.groupby([taxi_id, date]).agg( total_dist_m(dist_m, sum), avg_speed(speed_kmh, mean), empty_ratio(is_empty, mean), points(ts, count), ).reset_index() # 过滤掉点数太少的车日避免统计噪声 daily daily[daily[points] 50] print(daily.describe())参数说明points 50是过滤采样过少的车日具体阈值按采样频率定10 秒采样一天正常有几千个点。empty_ratio用mean是因为is_empty是布尔值均值就是比例。这一步的输出可以直接喂给可视化。4. 可视化不是画着好看热力图、OD 图与时间序列的正确打开方式4.1 用 pyecharts 做城市热力图数据量降采样是前提几十万个点直接画热力图浏览器会卡死。常见做法是先做网格聚合把城市切成 500m×500m 的格子统计每格点数再画。pyecharts 的HeatMap接受[lon, lat, value]列表配合BMap或Geo底图。import pyecharts.options as opts from pyecharts.charts import HeatMap # 网格聚合0.005 度约 500 米 grid 0.005 df[grid_lon] (df[lon] / grid).round() * grid df[grid_lat] (df[lat] / grid).round() * grid heat_data ( df.groupby([grid_lon, grid_lat]) .size() .reset_index(namecnt) ) # 只保留点数最多的前 5000 个格子避免渲染压力 heat_data heat_data.nlargest(5000, cnt) data heat_data[[grid_lon, grid_lat, cnt]].values.tolist() hm ( HeatMap() .add_xaxis([d[0] for d in data]) .add_yaxis(taxi, [d[1] for d in data]) .set_global_opts( visualmap_optsopts.VisualMapOpts(max_heat_data[cnt].max()), title_optsopts.TitleOpts(title出租车轨迹热力图), ) ) hm.render(heatmap.html)逻辑说明grid参数控制分辨率0.005 度在纬度 40 度附近约 550 米太小格子稀疏太大丢失细节。nlargest(5000)是渲染保护实际分析可以保留全部。pyecharts 输出 HTML适合嵌入看板不适合直接进论文。4.2 OD 图上下客点提取与流向可视化ODOrigin-Destination分析是出租车数据的核心价值。关键是从 status 变化里提取上下客事件status 从 0 变 1 是上客从 1 变 0 是下客。df[status_prev] df.groupby(taxi_id)[status].shift(1) pickup df[(df[status] 1) (df[status_prev] 0)] dropoff df[(df[status] 0) (df[status_prev] 1)] print(pickup events:, len(pickup), dropoff events:, len(dropoff))拿到上下客点后可以做聚类DBSCAN 或 KMeans找热点区域再用流向图展示区域间流量。DBSCAN 的eps参数按地理距离设500 米左右比较合理min_samples至少 10否则全是噪声。4.3 时间序列用 resample 看早晚高峰把数据按小时聚合画 24 小时曲线早晚高峰一目了然。pandas 的resample比手动 groupby 更顺手。ts df.set_index(ts) hourly ts.resample(1h).agg( trips(status, lambda x: ((x 1) (x.shift(1) 0)).sum()), avg_speed(speed_kmh, mean), ) hourly.plot(subplotsTrue, figsize(12, 6))参数说明resample(1h)按小时也可以15min看更细。trips用 lambda 统计上客次数注意shift(1)在 resample 后语义会变更稳妥的做法是先算好 pickup 标记再 resample。这一步的图直接能放进运营日报。5. 避坑与排查轨迹分析里那些让人返工的细节5.1 坐标系没对齐热力图整体偏移几百米现象热力图叠加在地图上所有点都往一个方向偏。原因数据是 GCJ-02底图是 WGS-84或者反过来。解决先确认数据坐标系用coord-convert统一转换或者干脆不叠加外部底图只做相对分析。转换代码网上很多但要注意转换方向转反了偏移更大。5.2 时间戳单位搞错早晚高峰画到凌晨现象24 小时曲线的高峰出现在凌晨 3 点。原因时间戳是毫秒级但用了units整体时间被放大 1000 倍。解决先看时间戳位数13 位是毫秒10 位是秒用pd.to_datetime时加unit参数转换后打印min()和max()确认年份合理。5.3 采样间隔过大导致速度爆表现象算出来的速度有 300 km/h。原因相邻两点间隔几分钟但距离是直线中间可能跨了城区。解决设dt阈值如 120 秒切断或对速度做分位数过滤保留 1% 到 99% 之间的值。别直接删先看比例比例高说明数据质量问题。5.4 status 字段含义反了空驶率算成 0.9现象空驶率超过 0.9明显不合理。原因status1 可能是空车不是重车。解决抽样看几辆车的 status 序列结合速度判断——载客时速度通常更稳定空驶时可能走走停停。确认后再改is_empty的定义。5.5 大数据量下 pandas 内存爆掉现象读 5000 万行 CSV 时 MemoryError。原因pandas 默认用 float64 和 object内存占用是实际数据的几倍。解决读的时候指定dtype经纬度用 float32id 用 category或者分块读chunksize逐块聚合再不行上 Dask 或 Spark但大多数城市开放数据在千万行级别pandas 加 dtype 优化够用。6. 进阶技巧用 H3 六边形网格做可复现的空间聚合前面用方形网格做热力图简单但有个问题不同纬度格子实际面积不一样跨城市对比会失真。更专业的做法是用 Uber 开源的 H3 六边形网格全球统一每个格子的面积和形状一致适合做可复现的空间分析。Python 里用h3库把经纬度映射到六边形索引再聚合。import h3 # 分辨率 9 约 0.1 平方公里城市分析常用 8 或 9 df[h3_index] df.apply( lambda r: h3.geo_to_h3(r[lat], r[lon], 9), axis1 ) hex_stat df.groupby(h3_index).agg( cnt(taxi_id, count), avg_speed(speed_kmh, mean), ).reset_index() # 取中心点用于画图 hex_stat[center] hex_stat[h3_index].apply( lambda h: h3.h3_to_geo(h) ) print(hex_stat.nlargest(10, cnt))参数说明resolution9对应平均边长约 200 米8约 500 米按分析尺度选。apply逐行慢数据量大时用h3.vectorized或先转 numpy 数组。H3 的好处是索引本身可排序、可做邻域查询比如找某个六边形的相邻格子直接h3.k_ring做流量扩散分析很方便。验证方法上我一般会做两件事一是拿一小段已知行程的数据手动算距离和速度和代码结果对二是把聚合结果导出 CSV随机抽几个格子用原始点核对数量。这两步能挡住大部分低级错误。最后说个习惯每次拿到新数据集先写一个inspect.py只做体检不做分析把字段、范围、缺失、断点比例打印出来存成报告。这个脚本我攒了七八个版本换城市换年份直接改配置就能跑。轨迹分析没有银弹把数据摸清楚后面的事就顺了。希望帮到你。本文还有配套的精品资源点击获取
返回列表