Python出租车GPS轨迹数据分析与可视化实战:从清洗到地图呈现
发布时间:2026/10/1 11:17:20来源:尧图网络
简介这是一份面向计算机相关专业学生与项目实战学习者的出租车轨迹数据分析与可视化高分项目源码评审分98分可作为课程设计、期末大作业或简历项目参考。资源以Python为核心结合transbigdata等工具围绕上海、深圳两地的出租车GPS数据展开涵盖数据清洗、轨迹处理、时空分析与可视化呈现等完整流程。压缩包共26个文件约13.49MB其中15个py脚本承担核心分析与绘图逻辑2个ipynb笔记便于分步运行与结果复现2个csv与2个json提供数据与配置支撑另有png图表、md说明等辅助文件目录结构清晰便于按模块查阅。目前已有83人学习读者可从中获得一套可运行的轨迹分析方案、数据处理与可视化思路以及项目组织与排错参考适合希望快速上手时空数据分析实战的学习者。1. 出租车轨迹数据分析与可视化从一堆 GPS 点到能讲故事的图手里拿到一份出租车 GPS 轨迹数据通常是几十万到上千万行、每行一个时间戳加经纬度打开 CSV 一看密密麻麻直接扔进 Excel 会卡死画个散点图也只是一团糊在一起的线。这个标题要解决的就是这件事用 Python 把原始轨迹点清洗成能用的行程段再算出速度、里程、载客状态这些指标最后落到地图和统计图上让数据能看出早晚高峰、热点区域、空驶率这些真正有价值的东西。适合谁做课程设计、毕业设计、数据分析入门练手的人以及需要快速把轨迹数据跑出一版可视化结论的从业者。核心链路就四步读数据、清洗分段、算指标、出图。下面按这条链路拆开讲每一步都给能直接抄的代码和参数说明。2. 轨迹数据读进来先别急着画图字段、坐标系与清洗2.1 先搞清楚你手里的轨迹长什么样出租车轨迹数据常见的字段结构是车辆 ID、时间戳、经度、纬度有的还带载客状态0 空车 / 1 载客、方向角、瞬时速度。不同来源字段名不一样但语义就这几类。拿到数据第一件事不是写分析代码而是用 pandas 把前几行和数据类型看清楚确认时间列是不是字符串、经纬度有没有缺失、有没有重复点。坐标系是新手最容易翻车的地方。国内常见的 GPS 原始数据是 WGS84但很多地图底图比如高德、百度用的是 GCJ02 或 BD09。如果你直接把 WGS84 的点画到高德底图上位置会整体偏移几百米看起来「差不多对」但就是贴不到路上。常见做法是先确认数据坐标系如果和底图不一致用坐标转换库转一次再画。这一步不做后面所有空间分析都是错的。import pandas as pd # 读取轨迹数据指定时间列解析为 datetime df pd.read_csv( taxi_traj.csv, parse_dates[timestamp], # 时间列直接解析省得后面再转 dtype{taxi_id: str} # 车辆ID用字符串避免前导零丢失 ) print(df.dtypes) print(df.head()) print(总行数:, len(df)) print(缺失值:\n, df.isnull().sum())这段代码的关键在parse_dates和dtype。时间列如果不解析后面做时间差、按小时分组都会报错或算错车辆 ID 如果是纯数字且带前导零用默认 int 会丢零导致同一辆车被拆成两个 ID。先跑这一遍心里有底再往下走。2.2 清洗去重、去漂移、按车按时间排序原始轨迹有三类脏数据必须处理。第一类是重复点同一秒同一位置出现多条直接drop_duplicates。第二类是漂移点GPS 信号抖动导致某个点突然跳到几公里外又跳回来判断方法是算相邻点之间的距离超过合理阈值城市出租车 1 秒最多移动几十米就标记为异常。第三类是时间乱序同一辆车的时间戳不是递增的分段时会出错必须按车 ID 和时间排序。import numpy as np def haversine(lat1, lon1, lat2, lon2): # 计算两点间球面距离单位米 R 6371000 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 df.drop_duplicates(subset[taxi_id, timestamp]) # 按车、按时间排序 df df.sort_values([taxi_id, timestamp]).reset_index(dropTrue) # 计算相邻点距离标记漂移 df[prev_lat] df.groupby(taxi_id)[lat].shift(1) df[prev_lon] df.groupby(taxi_id)[lon].shift(1) df[dist] haversine(df[lat], df[lon], df[prev_lat], df[prev_lon]) # 距离超过 2000 米视为漂移点阈值按采样频率调整 df[is_drift] df[dist] 2000 print(漂移点数量:, df[is_drift].sum())haversine是球面距离公式比直接算欧氏距离准确因为经纬度不是平面坐标。阈值 2000 米是个经验值如果采样间隔是 10 秒以上正常行驶也可能超过这个数需要按实际采样频率调。判断漂移更稳的做法是结合时间差算速度速度超过 150 km/h 在城市里基本就是异常。清洗完记得把中间列删掉别带到后面分析里。3. 把连续轨迹切成行程段载客状态与停留点判定3.1 用载客状态字段直接分段如果数据带载客状态0/1分段最简单状态从 0 变 1 是上车从 1 变 0 是下车中间就是一个载客行程。用diff找到状态变化的位置给每段打上行程 ID。# 只保留载客状态的行程段 df[status_change] df.groupby(taxi_id)[passenger].diff().fillna(0) # 状态变为1表示上车累计求和得到行程编号 df[trip_id] (df[status_change] 1).groupby(df[taxi_id]).cumsum() # 提取载客行程 trips df[df[passenger] 1].copy() trip_summary trips.groupby([taxi_id, trip_id]).agg( start_time(timestamp, min), end_time(timestamp, max), point_count(timestamp, count) ).reset_index() # 过滤掉点数太少的行程少于5个点可能是误判 trip_summary trip_summary[trip_summary[point_count] 5] print(有效行程数:, len(trip_summary))diff算状态变化cumsum给每段编号这是分段的标准套路。过滤点数少于 5 的行程是因为 GPS 抖动可能产生假的上下车信号这种短段没有分析价值。如果你的数据没有载客状态字段就得用停留点判定连续多个点位置变化小于阈值且持续一段时间视为停留两个停留点之间就是一段行程。3.2 没有载客字段时用停留点切分停留点判定的核心是滑动窗口在时间窗口内如果所有点之间的距离都小于阈值比如 50 米就认为车停着。实现时按车分组逐点判断与前一个点的距离和时间差。def split_by_stay(group, dist_thresh50, time_thresh120): # 距离小于50米且时间跨度超过120秒视为停留 group group.sort_values(timestamp).reset_index(dropTrue) group[dist] haversine( group[lat], group[lon], group[lat].shift(1), group[lon].shift(1) ).fillna(0) group[time_diff] group[timestamp].diff().dt.total_seconds().fillna(0) # 累计停留标记 group[is_stay] (group[dist] dist_thresh) (group[time_diff] time_thresh) group[seg_id] (~group[is_stay]).cumsum() return group df df.groupby(taxi_id, group_keysFalse).apply(split_by_stay)dist_thresh和time_thresh是两个必调参数。阈值太小会把等红灯也切成停留太大又会把慢速行驶误判成停留。城市出租车一般用 50 米 / 120 秒起步再根据数据采样频率微调。cumsum在布尔序列上会把 False 当 0、True 当 1 累加所以每次从停留变行驶就产生一个新段号逻辑上正好把行程切开。4. 指标计算速度、里程、空驶率怎么算才不出错4.1 逐点速度和行程平均速度速度有两种算法用相邻点距离除以时间差得到瞬时速度或者用行程总距离除以总时长得到平均速度。瞬时速度对 GPS 噪声敏感通常要做平滑滚动平均平均速度更稳适合做统计对比。# 逐点瞬时速度km/h df[time_diff] df.groupby(taxi_id)[timestamp].diff().dt.total_seconds() df[speed_kmh] (df[dist] / df[time_diff]) * 3.6 # 过滤掉时间差为0或速度异常的记录 df df[(df[time_diff] 0) (df[speed_kmh] 150)] # 行程级指标 trip_stats df.groupby([taxi_id, trip_id]).agg( total_dist(dist, sum), # 总距离米 duration(time_diff, sum), # 总时长秒 avg_speed(speed_kmh, mean), # 平均速度 max_speed(speed_kmh, max) # 最大速度 ).reset_index() trip_stats[total_dist_km] trip_stats[total_dist] / 1000 trip_stats[duration_min] trip_stats[duration] / 60* 3.6是米每秒转公里每小时。过滤速度大于 150 是为了去掉漂移点造成的假高速。groupby聚合时dist求和要注意第一行的dist是 NaN求和会自动跳过但如果你用fillna(0)处理过第一段距离会被算成 0影响不大但要知道这个细节。4.2 空驶率衡量出租车效率的核心指标空驶率 空驶时间 / 总运营时间。有载客状态字段时直接按状态分组算时长占比。这个指标能反映一个区域的出租车供需是否平衡空驶率高说明车多单少低说明供不应求。# 按车辆计算空驶率 total_time df.groupby(taxi_id)[time_diff].sum() empty_time df[df[passenger] 0].groupby(taxi_id)[time_diff].sum() empty_rate (empty_time / total_time).fillna(0) # 按小时统计空驶率看时间规律 df[hour] df[timestamp].dt.hour hourly df.groupby(hour).apply( lambda g: g[g[passenger] 0][time_diff].sum() / g[time_diff].sum() ) print(hourly)按小时聚合能看出早晚高峰空驶率低、凌晨空驶率高的规律这就是可视化要讲的故事。fillna(0)处理某些车全程载客没有空驶记录的情况。注意time_diff在跨车分组时第一行是 NaN聚合前最好统一fillna(0)否则总时长会偏小。5. 可视化落地地图轨迹、热力图与时间分布图5.1 用 folium 把轨迹画到地图上静态图看不出空间规律轨迹数据必须落到地图上。folium 基于 Leaflet能把经纬度点画成线、热力图、标记输出一个 HTML 文件浏览器直接打开就能交互缩放。import folium from folium.plugins import HeatMap # 取一辆车的一段行程画轨迹线 sample df[(df[taxi_id] df[taxi_id].iloc[0]) (df[trip_id] 1)] center [sample[lat].mean(), sample[lon].mean()] m folium.Map(locationcenter, zoom_start13, tilesOpenStreetMap) # 轨迹线 points list(zip(sample[lat], sample[lon])) folium.PolyLine(points, colorblue, weight3, opacity0.7).add_to(m) # 起点终点标记 folium.Marker(points[0], popup起点, iconfolium.Icon(colorgreen)).add_to(m) folium.Marker(points[-1], popup终点, iconfolium.Icon(colorred)).add_to(m) m.save(trip_map.html)tiles参数决定底图OpenStreetMap不需要密钥适合本地跑。如果要换成国内底图注意坐标系要匹配否则轨迹会偏移。PolyLine的points必须是(lat, lon)顺序写反了线会跑到南极去这是最常见的翻车点。5.2 热力图看热点区域ECharts 看时间分布热力图适合展示所有轨迹点的空间聚集一眼看出哪些区域打车需求密集。folium 的HeatMap直接接收经纬度列表。# 全量轨迹点热力图数据量大时抽样 sample_heat df.sample(nmin(50000, len(df)), random_state42) heat_data sample_heat[[lat, lon]].values.tolist() m2 folium.Map(location[df[lat].mean(), df[lon].mean()], zoom_start12) HeatMap(heat_data, radius8, blur6).add_to(m2) m2.save(heatmap.html)radius控制热力点半径blur控制模糊程度数据密集时调小这两个值避免糊成一片。抽样 5 万点是性能折中全量画浏览器会卡。时间分布用 ECharts 更合适按小时统计订单量柱状图能直接看出早晚高峰。from pyecharts.charts import Bar from pyecharts import options as opts hourly_count df[df[passenger] 1].groupby(hour).size() bar ( Bar() .add_xaxis([str(h) for h in hourly_count.index]) .add_yaxis(订单量, hourly_count.values.tolist()) .set_global_opts( title_optsopts.TitleOpts(title各小时订单量分布), xaxis_optsopts.AxisOpts(name小时), yaxis_optsopts.AxisOpts(name订单数) ) ) bar.render(hourly_orders.html)pyecharts 输出 HTML和 folium 一样浏览器打开即可。add_xaxis要传字符串列表传 int 有时渲染不出来。这套组合下来一张地图看空间、一张柱状图看时间轨迹数据的核心结论就出来了。6. 避坑与排查轨迹分析里最容易翻车的五件事6.1 坐标系不匹配导致轨迹整体偏移现象轨迹线画在地图上形状对但整体偏离道路几百米。原因数据是 WGS84底图是 GCJ02两者差一个非线性偏移。解决确认数据坐标系用坐标转换库统一转成底图坐标系再画别靠手动平移凑。6.2 时间戳没解析导致分组全乱现象按小时分组报错或者diff算出来是 NaN。原因时间列读进来是字符串pandas 不认识。解决read_csv时加parse_dates或者读完后pd.to_datetime转一次转完检查dtypes确认是 datetime64。6.3 漂移点没过滤平均速度高得离谱现象算出来平均速度 300 km/h。原因GPS 漂移点造成相邻点距离巨大除以很小的时间差就爆表。解决先按距离或速度阈值过滤异常点再做统计。阈值按采样频率定别照搬别人的数。6.4 groupby 后索引丢失导致合并出错现象groupby().apply()之后想和原表合并发现对不上。原因apply返回的索引结构和原表不同。解决加group_keysFalse保留原索引或者reset_index后按明确字段合并别依赖隐式索引。6.5 数据量大时全量画图卡死浏览器现象folium 画几十万点HTML 打开转圈半天。原因每个点都是一个 DOM 元素浏览器扛不住。解决热力图抽样轨迹线只画单辆车单段统计图用聚合后的数据别把原始点全塞进去。7. 让分析更稳的两个进阶习惯第一个习惯是给清洗流程加断言。每做完一步清洗用assert检查关键条件比如时间递增、经纬度在合理范围、行程段数不为零。这样数据换一批时能立刻发现哪里不对而不是等图画出来才发现是错的。# 清洗后加断言换数据时快速定位问题 assert df[timestamp].is_monotonic_increasing or True # 分组内递增 assert df[lat].between(-90, 90).all(), 纬度越界 assert df[lon].between(-180, 180).all(), 经度越界 assert len(trip_summary) 0, 没有有效行程检查分段逻辑第二个习惯是把参数集中到配置字典里别散落在代码各处。阈值、采样频率、坐标系这些一变就要改好几个地方集中管理省得漏改。CONFIG { dist_thresh_m: 50, # 停留判定距离阈值 time_thresh_s: 120, # 停留判定时间阈值 max_speed_kmh: 150, # 速度过滤上限 drift_dist_m: 2000, # 漂移点距离阈值 coord_system: WGS84, # 数据坐标系 }我自己的习惯是每换一份数据先跑一遍字段检查和断言确认坐标系和采样频率再动分析代码。轨迹数据这东西清洗占七成时间画图只占三成但大部分人反着来先写画图代码结果被脏数据折腾到怀疑人生。把清洗和参数管理做扎实后面换数据、加指标都是顺手的事。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网