ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于Python的武汉出租车轨迹数据挖掘:预处理、聚类与OD网络

2026/10/3 8:54:14 拓冰建站 浏览量
基于Python的武汉出租车轨迹数据挖掘:预处理、聚类与OD网络 简介这是一份基于Python实现的武汉市出租车轨迹数据挖掘与分析项目面向计算机相关专业的在校学生、教师或企业开发者可支撑毕业设计、课程设计、作业及项目初期演示等场景。资源包共36个文件以10个Python脚本为核心配合Shapefile空间数据文件shp/shx等、属性数据库文件及说明文档压缩包约11.58MB目录组织清晰便于按步骤复现。项目覆盖出租车轨迹处理的完整链路读取原始轨迹文件、轨迹可视化、插值平滑与路网匹配、上下车点热点分析、轨迹聚类、出租车OD热点空间交互网络构建、目的地预测及异常轨迹分析并附带武汉行政区划数据可直接运行验证。代码经测试可稳定运行可直接用于学习或二次开发。已有213人学习下载适合作为数据挖掘、时空分析与GIS综合应用的项目参考。1. 基于Python实现的武汉市出租车轨迹数据挖掘先过数据这关再做挖掘拿到“基于Python实现的武汉市出租车轨迹的数据挖掘与分析”这份资源时我最直观的判断是这不是一个“跑完出图”的演示项目而是一条从原始 GPS 文件一路干到 OD 空间交互网络、目的地预测和异常轨迹分析的完整数据挖掘链路。武汉出租车轨迹数据量大、时间戳乱、坐标噪声明显直接把数据丢进聚类算法或热力图出来的一定是垃圾。这个项目把轨迹数据处理最关键的排序、插值、平滑、路网匹配、上下车点识别、热区聚类、轨迹相似度、聚类、预测、OD网络构建按脚本编号排好了顺序适合要做毕设、课程设计或者想快速上手车辆轨迹数据挖掘的人。我拆完一遍后确认代码能跑通但参数和踩坑点都在脚本细节里。2. 轨迹数据预处理链路排序、插值、平滑与路网匹配怎么搭2.1 先排序再挖掘轨迹文件不是按时间组织好的武汉市出租车轨迹原始数据通常是按采集块切分成的多个文本文件每个文件内部的时间戳不一定有序甚至同一辆车的轨迹会分散在好几个文件里。如果不先做全局读取和排序后面的插值、路网匹配、OD提取全都会错位。包里的第一个脚本“读取文件夹中的所有轨迹原始文件并进行排序.py”就是在解决这件事。我一般会先把所有文件读进来统一列名再按车辆ID和时间排序import pandas as pd import glob raw_files glob.glob(data/raw/*.txt) frames [] for f in raw_files: df pd.read_csv(f, headerNone, names[car_id, time, lon, lat, speed, dir, status]) frames.append(df) traj pd.concat(frames, ignore_indexTrue) traj[time] pd.to_datetime(traj[time]) traj traj.sort_values([car_id, time]).reset_index(dropTrue) print(traj.shape, traj[car_id].nunique())这段代码会把所有轨迹片段拼接成一张大表按car_id和time做双重排序。关键在于排序键的顺序必须先car_id再time否则多辆车的轨迹会交叉穿插后续按车分组处理时直接错乱。如果你的原始文件里time是字符串直接to_datetime就能统一格式这一步省不了。常见误用是只按time排序或者读文件时不指定列名导致首行被当成表头。另外武汉出租车原始数据某些文件里会有空行和重复表头读的时候用comment或skiprows处理一下比事后清洗省事得多。按我处理这类数据的习惯排序之后会立刻检查一下每辆车的点数分布如果某辆车只有两三个点后面做插值也是白做直接过滤掉。2.2 插值和平滑参数决定轨迹“看起来”和“算起来”的区别GPS 采样间隔不固定信号丢失会导致轨迹点之间出现几十秒的空白。直接连线看起来是直线但计算行驶距离、速度曲线时误差很大。项目里的第三个脚本“轨迹数据插值平滑路网匹配.py”把这两步放在一起做顺序不能反先插值补齐时间轴再平滑去除抖动。插值的核心思想是按固定频率重建时间序列。以目标频率 10 秒为例import numpy as np from scipy.interpolate import interp1d def interpolate_track(sub, freq10s, max_gap30s): sub sub.set_index(time).sort_index() sub sub[~sub.index.duplicated(keepfirst)] gap_mask sub.index.to_series().diff().dt.total_seconds() 30 sub sub.loc[gap_mask] new_index pd.date_range(sub.index.min(), sub.index.max(), freqfreq) interpolated pd.DataFrame(indexnew_index) for col in [lon, lat, speed]: interp interp1d(sub.index.astype(np.int64), sub[col], kindlinear, fill_valueextrapolate) interpolated[col] interp(new_index.astype(np.int64)) return interpolated.reset_index()这里最关键的不是插值函数而是max_gap30这个阈值。GPS 信号丢失超过 30 秒的片段意味着车辆可能进隧道、地下停车场或者设备断电这时候强行插值会凭空造出一段“穿越”轨迹。正确做法是超过阈值就切断轨迹而不是补点。我就是在这个参数上翻过车一开始不设限结果某辆车在一座桥下丢失信号两分钟插值出来的轨迹直接穿过汉江后续路网匹配全部报错。平滑我用的是 Savitzky-Golay 滤波器它比简单滑动平均更不容易削掉轨迹拐弯处的真实特征from scipy.signal import savgol_filter def smooth_track(lon_lat, window5, polyorder2): lon savgol_filter(lon_lat[:, 0], window_lengthwindow, polyorderpolyorder) lat savgol_filter(lon_lat[:, 1], window_lengthwindow, polyorderpolyorder) return np.column_stack([lon, lat])窗口大小直接对应采样频率如果目标是 10 秒一个点window5意味着用前后各 50 秒的数据做平滑如果采样变成 30 秒窗口必须相应调大否则平滑没有意义。polyorder一般取 2 或 3取太大会把噪声当特征取太小则只能拟合直线。平滑后的轨迹要拿回来看一眼尤其是转弯半径小的路段过度平滑会让车辆“漂移”到对向车道。2.3 路网匹配把经纬度压到武汉的道路上插值和平滑之后轨迹点仍然是一堆散落的经纬度坐标它们在不在武汉道路上、在路的哪一侧都不知道。路网匹配的作用是把 GPS 点“贴”到最近的武汉路网线段上为后续轨迹距离、OD 分析提供道路语义。这个步骤在项目里也是单独成脚本的可见它的独立性。如果只是做粗粒度热区分析可以不做严格的隐马尔可夫匹配用几何最近邻就够from shapely.geometry import Point, LineString from shapely.ops import nearest_points def match_to_road(point, road_lines, max_dist30): nearest_line min(road_lines, keylambda line: line.distance(point)) dist nearest_line.distance(point) if dist max_dist: return None proj nearest_points(point, nearest_line)[1] return proj.x, proj.y, dist, nearest_line这个函数的思路很直接逐点计算 GPS 点到武汉路网所有道路线段的距离取最近的那条并投影到线上。max_dist30的单位是米因为出租车正常行驶时 GPS 偏移一般不会超过 30 米超过这个值说明点可能是漂移点或者掉线重连后的异常点直接返回None后续就不用它了。但要提醒一点武汉的出租车轨迹数据如果来自网约车平台坐标大概率是 GCJ-02 加密坐标而你拿到的道路 shp 可能是 WGS-84 或 GCJ-02两个坐标系不统一匹配结果会产生系统性偏移。碰到这种情况先做坐标转换再匹配。我拆这个包时发现数据和路网数据放在同一个data目录下但坐标系是否一致还得自己确认别默认一样。路网匹配完成后顺便统计一下匹配成功率低于 90% 就说明要么坐标没统一要么最大距离阈值定太紧。3. 上下车点可视化与热区聚类从散点图到可读的OD3.1 上下车点提取状态字段变化是最可靠的信号出租车轨迹里最有业务价值的点就是上下车点。资源包里的第 2 个脚本负责可视化第 4 个和第 5 个脚本则分别处理上下车点聚类和热区分析。提取上下车点的前提是轨迹表里要有载客状态字段一般用status或occupancy表示1 代表载客0 代表空车。状态从 0 变 1 是上车点从 1 变 0 是下车点。def extract_pickup_dropoff(traj): traj traj.sort_values([car_id, time]).reset_index(dropTrue) traj[prev_status] traj.groupby(car_id)[status].shift(1) pickup traj[(traj[status] 1) (traj[prev_status] 0)] dropoff traj[(traj[status] 0) (traj[prev_status] 1)] return pickup[[car_id, time, lon, lat]], dropoff[[car_id, time, lon, lat]]按car_id分组做shift(1)才能拿到每辆车自己的上一个状态而不是和别的车混在一起。这里最容易翻车的是文件切分边界一辆车在某天某个文件的最后一条记录是载客状态在下一个文件的首条记录变成空车如果直接跨文件拼接后做groupby(car_id)状态变化会被误判成一次下车。所以我一般会在每个文件读取时就保留原始顺序并且检查有没有车辆跨文件重复。另一个更隐蔽的问题是上下车点经常会连续出现多个重复坐标点车辆在等红灯时状态切换GPS 原地不动这时候直接取第一点或者最后一点都有偏差我的做法是对 30 秒内的连续上下车事件只保留一次。可视化时我会把上下车点画到武汉城区底图上。脚本里给的散点图只是粗看真正要用来做分析的是把上下车点按网格聚合后再叠加到行政边界上。社区划分的数据也放在了data road 武汉行政区划数据里这一步能明显提升图的业务感。3.2 热区分析的核密度参数带宽选不好热区全是平的上下车点的热区分析常用核密度估计KDE而不是单纯画散点图。散点图在点密度高的地方会糊成一团核密度则能把密集区域转成连续的“热度面”。项目里的第 5 个脚本“上下车点热区分析.py”本质上就是解决“哪里上下车最集中”这个问题。KDE 的核心参数是带宽bandwidth它直接决定热区数量。带宽太大会把整个汉口站和江汉路挤成一个热区带宽太小则每个路口的几个点都能形成一个热点视觉上一片碎斑。对武汉这种城市尺度我通常把经纬度先转成弧度再传给KernelDensityimport numpy as np from sklearn.neighbors import KernelDensity points np.radians(pickup[[lon, lat]].values) kde KernelDensity(bandwidth0.005, kernelgaussian, metrichaversine) kde.fit(points) grid_lon, grid_lat np.meshgrid(np.linspace(114.0, 114.6, 500), np.linspace(30.4, 30.8, 500)) grid np.column_stack([grid_lon.ravel(), grid_lat.ravel()]) density np.exp(kde.score_samples(np.radians(grid)))这里metrichaversine会按球面距离计算比直接用平面经纬度更符合真实地理距离。bandwidth0.005弧度约等于 0.55 公里在武汉市域范围内是一个“看得到主干道片区又不至于碎片化”的尺度。如果你想要更粗的片区级热区可以放大到 0.01如果想要路口级分析缩到 0.003 也可以。这个值必须根据你实际筛选的上下车点数量来回试多试几次再定不要用默认值。聚类这块同样有参数选择问题。第 4 个脚本里做了上下车点聚类常见做法是选 KMeans但 k 值要先跑一遍肘部法则from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler X StandardScaler().fit_transform(pickup[[lon, lat]].values) inertia [KMeans(n_clustersk, random_state42).fit(X).inertia_ for k in range(2, 21)] # 选拐点处的 k比如 8 km KMeans(n_clusters8, random_state42).fit(X) pickup[cluster] km.labels_注意这里我先把经纬度做了标准化。经纬度的量纲虽然都是“度”但数值范围不同不做归一化的话聚类会被经度方向的长跨度带偏。更重要的是random_state42一定要固定否则每次运行聚类结果都不一样写实验报告时你根本没有办法复现自己的图。3.3 聚类结果怎么验证不能只看轮廓系数很多人调完聚类就只看一张散点图觉得“看起来分开了”就算成功。实际上对于武汉出租车这种上下车点分布极不均匀的数据KMeans 很容易把点密集的区域切出几个大小悬殊的簇而轮廓系数对这种不均衡聚类的评价并不可靠。所以我在跑完聚类后还会叠加一层验证比较每个簇的上下车数量占比和实际地标位置。一种更实用的验证方式是回到热区图把聚类中心画到 KDE 热区图上检查每个簇中心是否落在热区峰值附近。如果某个簇中心明显地落在道路外、甚至落在长江里大概率是坐标漂移点没清洗干净而不是聚类算法的问题。反过来如果两个相邻的簇中心距离不足 500 米说明 k 取大了两个簇其实是一个热区的两侧。第 7 个脚本“轨迹聚类.py”处理的是轨迹线级别的聚类和点聚类不一样它的验证也要靠“同一簇轨迹在地理空间上是否靠近”来人工抽查而不是只信距离矩阵的数字。4. 轨迹数据挖掘的五个常见问题排查清单4.1 高频坑位现象、原因与解决我把拆这个包里会碰到的典型问题整理成了一张表每一条都对应“现象—原因—解决”你要是卡住了按这个顺序查最快。现象原因解决轨迹点画出来横跨长江车辆“飞”过江面原始 GPS 漂移或坐标系混用WGS-84 与 GCJ-02 混在一起先统一坐标系再用路网匹配过滤投影距离大于 30 米的点插值后出现车辆原地转圈或倒退对超过 30 秒的信号丢失窗口也做了插值插值前先按max_gap切分轨迹只对短时间缺失补点聚类结果每次运行都不一样KMeans 的初始中心随机导致固定random_state并选择稳定的 k上下车点数量明显偏多文件边界处状态字段误判乘客上下车瞬间连续跳变按car_id分组做前后比较对 30 秒内连续事件去重路网匹配后大量点落在楼顶或空地道路 shp 坐标系与轨迹坐标系不一致或道路线太稀疏检查 shp 的投影信息必要时对路网做缓冲区扩展第一行里的坐标漂移是轨迹数据的老大难。武汉主城区高楼多、高架桥多GPS 反射严重肉眼看到轨迹穿江、穿楼不要惊讶这就是原始数据质量不是代码写错了。解决方法是先把坐标统一再用路网匹配做物理约束。第二行插值问题我自己踩得最狠因为我一开始是想尽量多补点把max_gap设成了 5 分钟结果轨迹数量显得很完整但后续计算 OD 和轨迹距离时误差大到离谱。第三行聚类随机性问题是老生常谈但每次复现实验时还是会有人忘。第四行和第五行更像“业务常识坑”。上下车点识别看起来简单但状态位的含义你得分清楚有的数据用 0/1有的用 0/1/2空车、载客、停运停运状态和空车状态混在一起会把交接班的停车误判成上下车。路网匹配则要确认道路 shp 是线还是面如果是面要素还要先提中心线。4.2 排查顺序与代码埋点习惯我处理轨迹数据养成的一个习惯是“先画图再跑指标”。很多同学一上来就打印轮廓系数、均方误差数字好看但图上一片乱。有效的排查顺序是先按单辆车画一条轨迹线肉眼确认排序、插值和平滑是否正常再画所有上下车点散点图确认坐标范围和分布是否合理接着画热区图确认参数是否合适最后再跑聚类和预测模型。为了让排查更快我会在管线里加几个简单的断言assert traj[lon].between(113.8, 114.8).all(), 经度越界 assert traj[lat].between(30.0, 31.2).all(), 纬度越界 assert traj[speed].between(0, 20).all(), 速度超过 72km/h这三条断言不是什么高深的东西但能在预处理阶段就拦下明显的脏数据。经度范围我用的是武汉市域的大致边界如果你的数据覆盖武汉全市加郊区范围可以再放宽。速度上限用20米每秒换算下来是 72 km/h出租车在市区一般不会持续超过这个速度偶尔超速可以但连续多个点超速大概率是 GPS 跳变。把这些查完再往下走后面的坑会少一半。5. 进阶OD 空间交互网络、目的地预测与异常轨迹的验证到这里前面的预处理和聚类只是把轨迹变成“能看的”数据真正能被拿来写报告、讲故事的是 OD起点—终点空间交互网络和目的地预测。这个包里的第 9 个脚本把上下车点聚类结果聚合成了 OD 网络我的做法是先把每个上下车点打上簇标签再统计簇与簇之间的转移次数最后用阈值过滤低频 OD 对od_pairs pickup.merge(dropoff, on[car_id], suffixes(_pickup, _dropoff)) od_flow od_pairs.groupby([cluster_pickup, cluster_dropoff]).size() threshold od_flow.quantile(0.8) significant_od od_flow[od_flow threshold]这个quantile(0.8)的阈值很关键。如果取中位数网络里全是低频噪声如果取 0.95又只剩汉口站到各大商圈这种超级热点看不出城市内部的通勤规律。先画一次不同分位数的网络看节点的度分布再定最终阈值比一开始就拍脑袋选 100 次好得多。目的地预测我用的策略是把时间段、上车点聚类标签、上车点经纬度作为特征用随机森林预测下车点聚类。这里要多加一个交叉验证不能只在同一天的数据上训练和测试。武汉周末和通勤日的出行模式差异极大正确做法是按天分训练集和测试集。至于异常轨迹分析我判断“异常”的标准是连续多个点速度为零但距离在变化、轨迹点投影到路网上距离超过 30 米且持续时间长、或者单段轨迹的平均速度超过 80 km/h 且没有走高速。这几个规则可以组合成一个异常得分再看得分分布选阈值。最后说个小习惯每跑一次 OD 网络我都会随机挑五条显著 OD 对去地图上核对起点和终点区域是否真的和那个簇的语义一致。有一次聚类结果把武昌站和汉口站归成同一个簇原因是两处的上下车点密度太高KMeans 中心落在两站之间的长江上。从那以后我每次做轨迹聚类都会强制走一遍“先单条轨迹肉眼查、再聚类、再回到地图核对中心点”的流程宁可多花半小时也不出一个让人反直觉的假热点。这条路子虽然不是最炫的算法但对你手头这份武汉出租车轨迹数据它足够扎实。希望帮到你。本文还有配套的精品资源点击获取