ARTICLE DETAIL

资讯详情

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

Python+transbigdata出租车轨迹数据可视化实战:从清洗到热力图与OD分析

Python+transbigdata出租车轨迹数据可视化实战:从清洗到热力图与OD分析 简介面向交通数据分析与Python可视化学习者这份基于Python和transbigdata的出租车轨迹数据可视化分析项目提供从数据读取、清洗、处理到可视化展示的完整流程适合用于城市交通、热点区域及行驶效率研究。资源共26个文件包括15个Python脚本、2个Jupyter Notebook、2个CSV与2个JSON数据文件以及License、Git忽略规则、说明文档等脚本负责各环节处理Notebook承载交互式探索分析数据文件存储上海、深圳出租车GPS轨迹整体压缩包约70.26MB。目前已有471人学习下载。借助Jupyter环境代码支持对经纬度轨迹、OD起讫点、网格聚合等内容的可视化并通过geohash与可视化模块呈现空间分布可帮助读者快速复用出租车轨迹分析与制图流程节省从零搭建环境的时间。1. 出租车轨迹可视化这份源码包能帮你解决什么城市交通研究里最不缺的就是数据最缺的是把几百万行GPS坐标变成一张能讲故事的图。这份基于Python和transbigdata的出租车轨迹数据可视化分析源码把「原始CSV → 数据清洗 → 格网聚合 → 热力图与OD图」整条链路串成了两个可直接运行的Jupyter Notebook覆盖上海和深圳两份真实轨迹数据。我拆包后的感受是它不追求炫技而是把轨迹分析最常用的预处理、质量清洗、格网热力、起终点分析都沉淀成了可复用函数特别适合正在做交通方向课题的学生以及刚接触轨迹数据、想快速搭一条成熟可视化流水线的从业者。下面按数据读取、预处理、核心可视化、避坑排查的顺序展开。2. 搭好Jupyter环境并看懂源码包结构先跑通上海出租车数据2.1 为什么选transbigdata而不是自己写坐标聚合先聊选型。出租车GPS数据有典型的海量特征上海市区的出租车一天能产生上千万条记录这种量级下如果自己写geohash或H3聚合要处理的边界情况非常多坐标系转换、网格边界合并、底图偏移校正、聚合后Shapefile导出每一步都能消耗一下午。transbigdata把这一层封装成了tbd.gps_to_grid、tbd.grid_to_shape这类函数内部走numpy和pandas的向量化运算几百万行点数据做格网映射基本在秒级完成。它另一个设计优势是「一套参数走到底」。源码里的shanghai.json保存了研究区的四至边界你只需要在Notebook里加载它并补一个cell_size网格边长后续的格网化、绘图、OD聚合全部复用同一份params字典。这意味着改数据时不用改逻辑改研究区时不用改代码只要重新生成一份边界JSON就行。相比自己维护一套经纬度投影代码这种封装方式对探索性分析友好得多。我拆这份源码时的实际经验是预处理逻辑用脚本文件preprocess.py、taxigps.py探索性可视化用Notebook这样重跑全量数据时不用翻单元格写报告时又保留每一步的中间输出。项目里15个Python脚本和2个Notebook的分工本质上就是这个思路。2.2 源码包文件结构解读哪些文件值得你细读整个源码包共25个文件我按用途分成四类transbigdata目录下的15个Python脚本包括preprocess.py预处理、taxigps.py出租车GPS专用处理、odprocess.pyOD起终点处理、grids.py格网化、quality.py数据质量、plotmap.py地图绘制、ckdnearest.py最近邻匹配等是库的核心实现。2个Jupyter Notebookcode_shanghai_taxi_gps.ipynb和code_shenzhen_taxi_gps.ipynb是分析主战场建议先跑上海这个。数据文件2个CSVshanghai_taxi_gps.csv、shenzhen_taxi_gps.csv和2个JSONshanghai.json、shenzhen.json其中shanghai.json存放研究区边界参数frame.png是可视化结果参考图。工程文件LICENSE开源协议、.gitignore版本控制忽略规则、readme.txt使用说明。对普通使用者来说最值得精读的是taxigps.py里的状态清洗函数和grids.py里的格网映射函数因为后面所有可视化都建立在这两层之上。readme.txt和LICENSE建议先扫一眼里面一般会写明坐标系统约定比如数据用的是GCJ-02还是WGS84。这一点直接决定你能不能把高德底图和轨迹点对齐后面避坑章节会专门讲。2.3 上手第一个Notebook读取上海出租车CSV打开code_shanghai_taxi_gps.ipynb第一步通常是数据检视。下面这段代码是完整流程的起点import pandas as pd import transbigdata as tbd # 读入上海出租车GPS数据先取前5万行验证管道 df pd.read_csv(data/shanghai_taxi_gps.csv, nrows50000) print(字段列表, df.columns.tolist()) print(总行数, len(df)) print(df.head(3)) # 从研究区边界JSON读取四至参数 import json with open(data/shanghai.json, r, encodingutf-8) as f: bounds json.load(f) # 打印transbigdata版本用于排查依赖版本冲突 print(transbigdata版本, tbd.__version__)逻辑说明nrows50000是先抽样验证管道避免全量数据把内存撑爆shanghai.json里的四至边界会被tbd用来计算网格的行列索引所以必须和CSV数据来自同一坐标系。打印字段列表的目的是确认列名不同来源的出租车CSV列名差异很大有的叫OpenStatus有的叫State有的叫status后面清洗脚本都要按实际列名调整。参数说明json.load读出来的bounds是一个字典通常包含minLng、minLat、maxLng、maxLat这几个键对应研究区的经纬度范围。后续构造params时会用到它们。如果打印出来发现minLng大于maxLng说明边界文件写反了需要先修数据再往下走。提示Jupyter里重复跑单元格之前最好重启kernel避免上一次运行残留的变量污染结果。数据量大的时候内存里同时挂着原始DataFrame、清洗后DataFrame和多个中间结果很容易把Notebook拖到无响应。我一般会跑完一个阶段就del掉临时变量并调用gc.collect()。跑通这一步之后就可以进入预处理环节。需要特别注意的是read_csv默认把第一行当表头如果CSV本身没有表头你会损失第一行数据。判断方法很简单print(df.head(3))看第一行是不是一条正常的GPS记录如果第一条的经纬度接近0说明数据文件没有表头要用pd.read_csv(path, headerNone)再手动指定列名。3. 把GPS原始数据变成可用轨迹预处理与质量清洗的三个关键动作3.1 出租车GPS字段语义与载客状态位的含义出租车GPS数据常见的字段包括车辆编号、时间戳、经度、纬度、载客状态、速度、方向角。其中载客状态位是整个分析的生命线它决定了一条记录是「空车巡游」还是「载客行程」。很多初学者上来就直接对全部点做热力图画出来是一团均匀的噪点看不出任何交通结构。问题就出在没有区分状态位。在上海出租车数据里载客状态通常用OpenStatus或status字段表示常见取值是0和10代表空车1代表载客。要做上下客点分析OD分析还需要通过状态位的跳变来判断事件从0变1是上客点从1变0是下客点。transbigdata的taxigps.py里专门实现了这个逻辑。如果原始数据的字段名不叫status而叫State你需要自己做一个列名映射否则库函数取不到对应列。3.2 用clean_taxi_status做首次状态清洗直接拿原始数据做载客状态提取会踩一个坑GPS设备在乘客上下车的瞬间经常出现连续跳变比如1-0-1在几秒内反复切换如果不过滤一个真实行程会被切成好几段。transbigdata处理这个问题的思路是检查状态序列的连续性把持续时间过短的片段合并或丢弃。对应的调用方式如下import pandas as pd import transbigdata as tbd # 假设已读入原始数据字段含VehicleNum、Stime、Lng、Lat、OpenStatus df pd.read_csv(data/shanghai_taxi_gps.csv, nrows200000) # 先按车辆编号和时间排序保证状态序列是有序的 df df.sort_values([VehicleNum, Stime]).reset_index(dropTrue) # 调用库中的状态清洗函数剔除异常的载客状态跳变 df_clean tbd.clean_taxi_status( df, col[VehicleNum, Stime, Lng, Lat, OpenStatus] ) print(清洗前行数, len(df)) print(清洗后行数, len(df_clean))逻辑说明核心参数col是一个长度为5的列表顺序固定对应VehicleNum车辆唯一标识、Stime时间戳、Lng经度、Lat纬度、OpenStatus载客状态。函数内部先按车辆分组再对状态序列做平滑把时间上相邻且状态频繁翻转的记录折叠成一次有效状态变化。输出结果里每一段连续的1状态就是一段可用的载客行程。参数说明如果你的数据里时间列是Unix时间戳10位秒级或13位毫秒级Stime这一列会以数值形式传入清洗函数不会做时间单位假设但你在后续时间聚合时必须先统一成datetime类型。我一般会在清洗前先把Stime转成字符串或datetime避免排序时出现数值和字符串混排的幺蛾子。3.3 去重、去噪、时间排序轨迹重建三件套状态清洗之后数据里仍然存在三类脏数据GPS漂移点经纬度突然跳到几百米外、设备重复上报同一辆车同一时刻多条记录、时间乱序补传数据导致时间戳倒挂。这三类问题如果不处理后面画轨迹线时会画出跨越半个城区的斜线热力图也会出现孤立的红色噪点。我的处理习惯是写一个四步管道# 1. 剔除经纬度明显越界的记录 df_clean df_clean[ (df_clean[Lng] bounds[minLng]) (df_clean[Lng] bounds[maxLng]) (df_clean[Lat] bounds[minLat]) (df_clean[Lat] bounds[maxLat]) ] # 2. 去掉完全重复的记录同一车、同一时间、同一坐标 df_clean df_clean.drop_duplicates( subset[VehicleNum, Stime, Lng, Lat] ) # 3. 按车辆和时间排序为轨迹连线做准备 df_clean df_clean.sort_values( [VehicleNum, Stime] ).reset_index(dropTrue) # 4. 计算相邻点速度剔除超过120km/h的漂移点 df_clean[prev_lng] df_clean.groupby(VehicleNum)[Lng].shift(1) df_clean[prev_lat] df_clean.groupby(VehicleNum)[Lat].shift(1) df_clean[distance] tbd.geodistance( df_clean[Lng], df_clean[Lat], df_clean[prev_lng], df_clean[prev_lat] ) df_clean[time_diff] ( pd.to_datetime(df_clean[Stime]) - pd.to_datetime(df_clean[Stime].shift(1)) ).dt.total_seconds() df_clean df_clean[ (df_clean[distance] / df_clean[time_diff].clip(lower1) * 3.6) 120 ]逻辑说明第一步的bounds直接从shanghai.json读取把超出研究区的点先挡在门外。第二步drop_duplicates的subset参数只比对三个核心字段避免把同一辆车在不同时间经过同一坐标的有效记录误删。第四步是漂移点过滤的关键逻辑用geodistance算出相邻两点球面距离除以时间差得到速度超过120km/h的记录基本可以断定是GPS跳点。参数说明120这个阈值不是随便定的出租车在市区正常行驶速度很少超过80km/h高速上也就100出头留20%余量足够。clip(lower1)是为了防止time_diff为0时出现除零错误如果数据里同一秒有多条记录这些点在第三步去重时已经处理了一部分但不同车辆间仍可能出现极小的time_diff。速度阈值你可以根据自己的数据场景下调到80但不要低于60否则高峰期低速跟车会被误删。做完这三步数据才真正配得上「轨迹」两个字。无论是后面的格网热力图还是OD分析都是建立在这样一套清洗管道之上的。quality.py脚本里还有更细的质量评估函数比如按车辆统计有效记录数、计算时间覆盖率跑完清洗后我会顺手看一眼这些指标确认没有把某辆车的5万条记录洗成50条。4. 从轨迹点到热点格网transbigdata的可视化核心路径4.1 gps_to_grid把GPS点映射到网格的底层机制拿到干净的轨迹数据后下一步就是把离散的点变成有空间结构的网格。transbigdata的处理方式是三层先构造params参数再调用gps_to_grid把每个经纬度映射到网格编号最后用grid_to_shape还原网格图形。核心参数cell_size决定统计粒度也是整个可视化最敏感的参数。先看参数构造和格网映射import transbigdata as tbd import json # 读取研究区边界 with open(data/shanghai.json, r, encodingutf-8) as f: bounds json.load(f) # 构造格网参数500米一个格子 params tbd.grid_params( bounds, cell_size500, gridtyperect ) # 把清洗后的经纬度映射到网格行列号 grid_id, mapped tbd.gps_to_grid( df_clean[Lng], df_clean[Lat], params ) df_clean[grid_id] grid_id print(唯一网格数, df_clean[grid_id].nunique())逻辑说明grid_params根据bounds和cell_size算出研究区在经纬度坐标系下的行列划分输出一个params字典。gps_to_grid接收经纬度数组和params返回每个点所在网格的编号。gridtype有两个选项rect表示矩形网格geohash表示geohash网格。矩形网格的好处是边界整齐、适合热力图展示geohash网格的好处是编码自带空间邻近关系、适合做空间索引。参数说明cell_size的单位是米这里设成500意味着每格约500米见方。这个值的选择直接决定可视化效果200米会让热力图碎成噪点1000米以上又模糊掉街道级差异。我的经验是展示全市范围用800到1000米展示中心城区用300到500米。如果只关心局部商圈甚至可以压到100米。确定最优值的办法很简单从1000米开始往下降同时打印nunique网格数观察数量从缓慢爬升变成指数爆炸的那个拐点就是当前数据量下视觉最舒服的粒度。4.2 聚合计数与grid_to_shape还原网格边界有了grid_id之后统计每个网格的轨迹点数就是一次groupby的事。但直接画散点看不出网格边界需要把网格id还原成可绘制的几何形状# 按网格聚合统计点数 grid_counts df_clean.groupby(grid_id).size().reset_index(namecount) print(网格统计完成最大格点数, grid_counts[count].max()) # 将网格id映射为边界图形 plot_data tbd.grid_to_shape( grid_counts[grid_id], params, shp_typerect ) # 合并统计值 plot_data plot_data.merge(grid_counts, ongrid_id)逻辑说明grid_to_shape接收grid_id数组和params返回一个GeoDataFrame每条记录是一个格子的边界多边形。shp_type必须和前面gps_to_grid里的gridtype保持一致混用会导致行列号解析错位图形画出来位置全偏。merge把统计值挂到边界上这一步完成之后热力图的每个格子都自带一个count字段可以直接按数值赋色。这里有个容易被忽略的细节grid_to_shape生成的边界是基于经纬度的如果直接用matplotlib画横纵比不对地图会被拉变形。常规做法是在绘图前调用tbd.plot_map设置正确的投影参数并加载底图下面这段是完整的热力图绘制流程import matplotlib.pyplot as plt # 初始化绘图区域设置中文字体兜底 fig, ax plt.subplots(figsize(12, 8)) # 用plot_map加载研究区范围内的底图 plot_map tbd.plot_map( plt, bounds, zoom11, stylehighmap ) # 把网格边界和计数传给plotgrid做分级着色 tbd.plotgrid(plot_data, plot_map, params) plt.show()逻辑说明plot_map内部会拉取在线底图高德或Carto样式zoom参数控制缩放级别市区范围一般用10到12。tbd.plotgrid把plot_data里的网格多边形按count字段分级着色count越大颜色越深形成热力效果。style参数决定底图风格highmap偏深色适合突出亮色热力lightmap更清爽适合论文插图。参数说明zoom并不是越大越好出租车GPS存在定位误差zoom调到15以上会看到大量点落在楼宇轮廓外反而不利于宏观判断。我一般习惯zoom12配cell_size500整体看下来既能看到延安高架、内环线的走向又不至于被单个路口的噪声干扰。4.3 热点区域判读从可视化结果反推交通规律格网热力图跑出来之后城市交通结构会非常直观地呈现红色高亮区域通常集中在外滩、人民广场、陆家嘴、虹桥枢纽这些客流集散地。但热力图只能告诉你「哪里人多」要解释「为什么人多」还需要叠加时间维度或者做OD分析。我拆完这套源码后印象最深的一点是transbigdata的grids.py里还封装了网格级别的OD聚合接口也就是把每个网格视为一个交通小区通过车辆编号和时间把载客行程整理成「上客网格 — 下客网格」的起终点对。odprocess.py脚本处理的就是这件事。具体做法是先对每辆车按时间排序用状态位跳变切割出每段载客行程再提取行程首尾坐标并映射到网格# 将连续载客状态整理为OD行程 oddata tbd.taxi_gps_to_od( df_clean, col[VehicleNum, Stime, Lng, Lat, OpenStatus], paramsparams ) print(OD行程数量, len(oddata)) print(oddata.head())逻辑说明taxi_gps_to_od内部就是按车辆分组、按状态跳变切分行程、取起点终点坐标三个步骤的封装。输出结果每一行是一段载客行程包含起点网格、终点网格、起始时间、结束时间、行程时长。这一步把几十万条GPS点压缩成了几千条有效出行记录数据量瞬间降了几个数量级后续无论是做OD图还是做网络分析都轻松得多。提示OD分析对清洗质量极其敏感。如果第3章的状态清洗没做好这里切出来的“行程”会混杂大量虚假跳变段OD图的连线会像蜘蛛网一样杂乱看起来全是“全城任意点可达”的结论那基本可以确定是清洗环节出了问题而不是城市交通真的这么高效。5. 避坑手册轨迹数据分析常见的五个翻车现场5.1 底图和轨迹点错位坐标系不一致现象热力图格子分布和底图道路走向对不上明明应该在高架上的点落在河道里格子整体向北偏了几百米。原因大部分出租车GPS数据用的是WGS84坐标而在线底图尤其是高德底图用的是GCJ-02坐标两者在上海市区存在几百米的系统偏差。transbigdata自身不承担坐标转换因为它无法判断你的数据是哪种坐标系。解决先看readme.txt里有没有注明坐标系统。如果数据是WGS84用在线底图前需要先调tbd.set_gcj02()或手动做坐标纠偏。我常用的稳妥方案是直接改用WGS84底图服务Carto light样式或者用gcj02_to_wgs84类工具把底图反向偏移对齐轨迹数据。判断方法很简单跑一次全部点落在路网上的叠加图如果偏移方向一致且有固定距离基本可以断定是坐标系问题。5.2 清洗后数据量被砍掉一半以上现象clean_taxi_status跑完数据量直接腰斩某些车辆只剩原来20%的记录。原因多数情况是OpenStatus字段的取值定义与库函数假设不一致。有的数据源用0表示载客、1表示空车与库函数的默认约定相反有的数据源载客状态是2、3这样的多值编码混入了停运和签到状态。库函数按默认规则过滤后你的有效数据自然大量丢失。另一个原因是时间列Stime未统一格式字符串和datetime混排导致状态序列判断错乱。解决先做一次取值分布检查用df[OpenStatus].value_counts()打印所有枚举值和对应频数再对照readme判断哪个值代表载客。如果需要反转定义在调用clean_taxi_status前先把列值取反或者改写传入的列映射。从那以后我每次拿新数据源第一步永远是value_counts看字段分布而不是急着跑清洗函数。5.3 时间戳解析成object时间聚合全乱现象按小时聚合时groupby(pd.Grouper(freqH))报错或者画出来的24小时流量曲线全是锯齿。原因CSV里的Stime是2024-01-01 08:15:23这种带格式的字符串read_csv读进来默认是object类型如果部分行还混入2024/1/1 8:15这种变体格式pd.to_datetime部分转换失败后整列会保留object类型。解决读表时指定parse_dates[Stime]或者在清洗前强制统一格式。转换时不要只用简单的pd.to_datetime可以加format参数锁定模板比如format%Y-%m-%d %H:%M:%S解析速度能快一个量级还不会静默猜测错。转换完成后马上检查df[Stime].dtype看到datetime64[ns]再往下走。5.4 热力图全是孤立噪点没有结构现象cell_size设成200米后热力图上到处都是单个格子的亮点完全没有道路走向的趋势感调大cell_size到1000米亮点倒是少了但细节也全糊了。原因cell_size和数据量不匹配。数据量只有几万行时500米的格子里可能只有两三个点统计结果泊松噪声占主导视觉上就是随机噪点。另外一个常见原因是没有筛掉GPS漂移点漂移点散布在网格里形成假热点。解决先按第3章管道清洗再根据数据量调整cell_size。一个可复用的经验法则是热力图里单个网格平均点数集中在50到200之间最合适。你可以用总点数除以当前网格数估算平均密度如果均值小于20就把cell_size往大了调如果均值大于500说明聚类过度调小一点。网格平均密度比绝对点数更能反映可视化质量。5.5 Notebook跑久了内存溢出直接卡死现象Notebook连续跑完数据读取、清洗、热力图、OD分析后kernel无响应或者直接OOM被杀掉。原因Jupyter会把每个单元格的输出对象保存在内存里几百行DataFrame输出加上matplotlib图像对象再加上中间变量引用计数不释放内存就爆了。transbigdata的grid_to_shape返回的GeoDataFrame尤其占内存一个城市级网格可能生成几万个多边形对象。解决养成两个习惯。第一对大DataFrame只print形状和前五行不要print(df)整个输出第二跑完一个分析阶段后删除中间变量并主动回收del mapped, grid_counts再import gc; gc.collect()。多边形数据用完即弃需要保存结果时直接导出成GeoJSON或CSV不要一直挂在内存里。6. 进阶技巧把一天压缩成一张动态热力图静态热力图能回答「哪里热」但回答不了「早高峰的热区怎么从郊区往市中心扩散、晚高峰又怎么反向退潮」。这类时序问题需要把一天切成24个时间片逐小时生成热力帧再合成动态图。这个技巧在源码基础上加几十行代码就能实现效果却非常适合汇报展示。具体做法是在4.2节的热力图流水线外面套一层时间循环。先把Stime转成datetime并抽取出小时字段然后按小时分组循环执行格网映射、聚合、绘图三步。注意每一帧之间保持相同的单元格大小和颜色映射范围否则不同帧之间色标不一致动态图会让人误判热力强度的变化幅度。用vmin和vmax锁死色标上下界是所有时序热力图最容易忽略的细节。生成动态图后我还习惯把每一帧同时保存为PNG这样既可以在汇报里放静态关键帧又可以合成动态图。如果数据覆盖周期长可以按30分钟甚至15分钟切片颗粒度更细但切片越细单帧数据量越稀疏噪点问题会重新出现一般会配合更大的cell_size使用。比如15分钟切片配800米格网相比1小时切片配500米格网动态趋势更平滑。从那以后我接手任何一份出租车GPS数据都强制自己先跑一遍这套流程字段分布检查、状态清洗、去噪排序、格网热力、OD聚合。这套基于transbigdata的源码包把最繁琐的中间层都封装好了留给你的核心工作就是理解参数、控制参数、排查数据问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表