尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Python的车辆驾驶行为分析与司机聚类评分实战

基于Python的车辆驾驶行为分析与司机聚类评分实战 简介以运输车辆驾驶行为分析为案例的Python数据分析实战教程面向物流与交通行业数据分析人员也适合正在学习Pandas、NumPy、Matplotlib、Seaborn等库的Python初学者。资料从环境配置讲起说明如何采集车辆GPS定位、速度、加速度、急加速急减速事件并结合气象与道路状况数据随后系统讲解Pandas读取CSV/Excel数据、缺失值与异常值处理、时间戳转换等预处理。特征工程部分重点演示急加速急减速事件识别、每公里急加速急减速次数、速度标准差、加速度标准差等特征构建提供可直接运行的示例代码。分析环节涵盖描述性统计、速度分布直方图、按车辆ID的驾驶行为箱线图、加速度变化曲线和相关性热力图并结合结果讨论驾驶员安全培训与路线优化应用。资源为单个PDF文档体积仅126KB内容精炼适合作为课程案例、实训项目或毕业设计参考。目前已有458人学习浏览读者可从中获得完整数据分析流程、业务分析思路和可迁移到实际项目的代码模板。1. 货车怎么开最费油数据说了算车辆驾驶行为分析在物流和运输行业一直有个老问题同样的路线、同样的车型不同司机开出来的油耗和事故率能差出 20% 以上。靠老司机带新人、靠安全员盯监控既慢又覆盖不全。我的做法是把车载 GPS 终端和 CAN 总线里埋了几年的原始数据翻出来用 Python 做一轮完整的「采集—清洗—特征工程—建模—可视化」分析用聚类算法把司机分成激进型、平稳型、经济型再结合超速、急加速、急刹、急转弯等事件给每个司机出驾驶行为评分。这篇文章就把这条链路里能直接照抄的代码、参数和坑按实操顺序拆开讲。适合手里已有车辆轨迹 CSV 或 数据库表、但还没跑通完整分析流程的 Python 数据分析工程师。2. 驾驶行为分析的原始数据长什么样先做清洗再说2.1 GPS 轨迹表和 CAN 报文表的常见字段做车辆驾驶行为分析数据来源无非两路OBD 终端回传的 GPS 轨迹和 CAN 总线解析出的车辆状态报文。GPS 轨迹表里通常有车辆编号、定位时间、经度、纬度、瞬时速度、方向角CAN 报文表里则有发动机转速、油门踏板开度、制动踏板状态、横向加速度这些高频字段。两张表通过车辆编号和时间戳关联但时间精度往往不一致——GPS 是秒级CAN 是毫秒级第一步就是统一时间基准。SELECT vehicle_no, DATE_FORMAT(gps_time, %Y-%m-%d %H:%i:%s) AS ts_sec, lng, lat, speed_kmh, heading FROM gps_log WHERE gps_date 2024-06-01 AND lng BETWEEN 73 AND 135 AND lat BETWEEN 18 AND 54;这段 SQL 在做两件事截断毫秒时间戳方便后续和 CAN 表做关联同时把经纬度限制在中国范围内过滤掉漂移点。如果车辆去了边境或园区外说明定位模块可能被遮挡或天线故障。还需要说明的是lng、lat 越界的直接丢弃因为轨迹漂移点会在后面算里程和转角时放大误差宁可丢不能留。2.2 pandas 处理缺失值和重复轨迹实际拿到的 CSV 用 pandas 读进来之后第一件要检查的事情是重复 GPS 点——很多终端在信号弱或停车时会原地刷新产生同一时间戳、同一经纬度的多条记录。重复点不处理算出来的单日里程会偏大急加速事件次数也会被误判。import pandas as pd import numpy as np df pd.read_csv(vehicle_gps_2024.csv, parse_dates[gps_time]) df df.drop_duplicates(subset[vehicle_no, gps_time]) df df.sort_values([vehicle_no, gps_time]).reset_index(dropTrue) # 停车状态标记速度接近0且超过3分钟视为停车 df[is_stop] (df[speed_kmh] 0.5).astype(int) df[stop_group] (df[is_stop].diff() ! 0).cumsum() stop_mask df[is_stop] 1 df.loc[stop_mask, stop_duration_min] ( df.loc[stop_mask, gps_time].groupby(df.loc[stop_mask, stop_group]).diff().dt.total_seconds() / 60 ) df[stop_duration_min] df[stop_duration_min].fillna(0) df df[df[stop_duration_min] 30]去重时保留第一条即可因为同一秒内的坐标差异对速度计算没有意义is_stop用 0.5 km/h 的阈值是为了兼容低速蠕行工况比如在停车场挪车或堵车缓行这类点如果在后续用于计算转角或加速度会制造大量伪急转弯事件。stop_duration_min超过 30 分钟的高速公路停车可能是事故或严重故障这类轨迹段会显著影响作息判断直接切掉比修复更稳妥。3. 驾驶行为特征工程从原始轨迹算出超速、急刹车和急转弯3.1 加速度计算与急加速急减速识别驾驶行为分析里的急加速、急减速、急转弯都没有一个全国统一标准物流公司通常参照 JT/T 883 营运车辆安全运行标准或者自己企业制定的阈值。常见做法是用相邻两个 GPS 点的速度差除以时间差得到纵向加速度再进行滑窗平滑消除定位噪声带来的毛刺。df df.sort_values([vehicle_no, gps_time]).reset_index(dropTrue) df[dt_sec] df.groupby(vehicle_no)[gps_time].diff().dt.total_seconds() df[v_prev] df.groupby(vehicle_no)[speed_kmh].shift(1) df[accel_ms2] (df[speed_kmh] - df[v_prev]) * 1000 / 3600 / df[dt_sec].clip(lower1) # 用滚动窗口去噪窗口内取中位数 df[accel_smooth] df[accel_ms2].rolling(5, min_periods2, centerTrue).median() hard_accel (df[accel_smooth] 2.5) (df[speed_kmh] 20) hard_brake (df[accel_smooth] -2.5) (df[speed_kmh] 20) # 聚合到每次行程 trip_id (df[vehicle_no].ne(df[vehicle_no].shift()) | (df[gps_time].diff().dt.total_seconds() 1800)).cumsum() df[trip_id] trip_id event_cnt df.groupby(trip_id).agg( 急加速次数(accel_smooth, lambda s: ((s 2.5) (df.loc[s.index, speed_kmh] 20)).sum()), 急刹车次数(accel_smooth, lambda s: ((s -2.5) (df.loc[s.index, speed_kmh] 20)).sum()) ).reset_index()加速度阈值 2.5 m/s² 对应约 9 km/h 每秒的变化率比一般家用车的日常起步略大比紧急制动小。低于 20 km/h 时的急加速不统计因为红绿灯起步频繁、速度本就低容易把正常启动误判为激进驾驶这也是在做规则提取时最容易搞错的细节——只设加速度阈值、不设下限速度会让市区司机的不良驾驶事件数量虚高。dt_sec.clip(lower1)防止设备重复上报瞬间时间差为 0 导致除零。3.2 用转向角变化率识别急转弯和变道横向行为的判定仅靠 GPS 不够最好叠加陀螺仪 z 轴角速度。如果没有陀螺仪可以用方向角 heading 的差分近似横向角速度但 GPS 方向角在低速时抖动很大需要先平滑。df[heading_smooth] df.groupby(vehicle_no)[heading].transform( lambda s: s.rolling(5, min_periods2, centerTrue).apply( lambda x: np.degrees(np.arctan2(np.sin(np.radians(x)).mean(), np.cos(np.radians(x)).mean())) ) ) df[heading_diff] df.groupby(vehicle_no)[heading_smooth].diff() df[heading_diff] ((df[heading_diff] 180) % 360) - 180 # 映射到[-180, 180] df[yaw_rate_dps] df[heading_diff] / df[dt_sec].clip(lower1) sharp_turn (df[yaw_rate_dps].abs() 30) (df[speed_kmh] 30)方向角平滑这里用了圆周均值法直接用普通rolling().mean()会在 0° 和 360° 交叉处产生跳变比如 350° 和 10° 的平均值会算出 180° 这种完全错误的结果。圆周均值先转成 sin、cos 再求均值再转回角度能天然处理角度环绕。角速度阈值给到 30°/s约等于 0.52 rad/s对应半径比较小的弯道或紧急变道速度阈值取 30 km/h 是为了排除停车场里原地打轮的情况——低速时方向角本来就会快速变化但车身并没有发生真正的横向失稳。3.3 分箱统计驾驶行为评分有了事件明细后最终要落到每个司机的可对比评分。我用的是分箱加权法先按每百公里的急加速/急刹车/急转弯次数分档再给不同档位赋分综合得到 0-100 的驾驶评分。这里不直接用原始次数做归一化是因为不同司机跑的城市、山区、高速占比不一样绝对值不可比。trip_stats event_cnt.merge(trip_km, ontrip_id) trip_stats[rate_accel] trip_stats[急加速次数] / trip_stats[里程_km] * 100 trip_stats[rate_brake] trip_stats[急刹车次数] / trip_stats[里程_km] * 100 trip_stats[rate_turn] trip_stats[急转弯次数] / trip_stats[里程_km] * 100 def score_by_rate(rate, breakpoints, scores): # breakpoints 如 [2, 4, 6, 8]scores 如 [100, 85, 70, 50, 30] for bp, sc in zip(breakpoints, scores): if rate bp: return sc return scores[-1] trip_stats[驾驶评分] ( trip_stats[rate_accel].apply(lambda r: score_by_rate(r, [2,4,6,8], [100,85,70,50,30])) * 0.3 trip_stats[rate_brake].apply(lambda r: score_by_rate(r, [1.5,3,5,7], [100,85,70,50,30])) * 0.5 trip_stats[rate_turn].apply(lambda r: score_by_rate(r, [0.5,1,2,3], [100,85,70,50,30])) * 0.2 )权重分配急刹车最高是因为急刹车不仅费刹车片还和追尾事故相关性最强急加速次之对油耗和传动系统冲击大急转弯只占 20%因为它的样本量最小、噪声也最大。分箱阈值要随车队实际水平迭代我见过油耗极高但事件次数并不突出的车队说明他们的驾驶行为问题是长时间高速巡航或怠速而不是激烈的加减速这时需要再加一个超速时长占比特征。行为类型推荐触发阈值最低车速统计单位急加速纵向加速度 2.5 m/s²20 km/h次/百公里急减速纵向加速度 -2.5 m/s²20 km/h次/百公里急转弯横摆角速度 30°/s30 km/h次/百公里超速行驶路段限速 10 km/h持续 30s分钟/百公里4. 用聚类算法给司机分型KMeans 和 DBSCAN 怎么选4.1 特征归一化与 PCA 降维事件统计做完之后每个司机就变成了一行特征向量。我通常会挑 6 个特征进聚类模型平均车速、超速时长占比、急加速率、急刹率、急转弯率、夜间行驶占比。这六个特征不在一个量纲上——平均车速可能是 60急转弯率可能只有 0.3如果不做标准化聚类结果会被平均车速主导其他特征等于没喂进去。from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.cluster import KMeans, DBSCAN import matplotlib.pyplot as plt feature_cols [平均车速_kmh, 超速时长占比, 急加速率, 急刹率, 急转弯率, 夜间行驶占比] X driver_feat[feature_cols].fillna(0) scaler StandardScaler() X_scaled scaler.fit_transform(X) pca PCA(n_components2, random_state42) X_pca pca.fit_transform(X_scaled) print(前两个主成分解释方差比, pca.explained_variance_ratio_.sum())PCA 降维主要不是为了聚类本身而是为了在二维平面上画散点图验证聚类效果。如果前两个主成分累计解释方差不到 70%说明特征维度间的相关性不强或者信息被摊得很散这时可以考虑剔除某些冗余特征再跑一次。有些资料会直接把原始高维特征丢进 KMeans再用 TSEN 降维画图但 TSNE 的布局受困惑度参数影响很大同一份数据两次运行的结果形状可能不一样用于内部验证可以用于给管理层汇报很容易被质疑。4.2 肘部法则和轮廓系数定聚类数量KMeans 的 K 值不能拍脑袋定 3 或 4同一个数据集在 3 类和 5 类下的业务解释完全不同。我用的是两个指标交叉验证肘部法则看簇内误差平方和下降的拐点轮廓系数看同一类内的紧凑度和不同类间的分离度两者结合再决定最终 K 值。from sklearn.metrics import silhouette_score inertia_list, sil_list [], [] K_range range(2, 7) for k in K_range: km KMeans(n_clustersk, random_state42, n_init10) labels km.fit_predict(X_scaled) inertia_list.append(km.inertia_) sil_list.append(silhouette_score(X_scaled, labels)) for k, inert, sil in zip(K_range, inertia_list, sil_list): print(fK{k}, 簇内SSE{inert:.2f}, 轮廓系数{sil:.4f})轮廓系数的取值范围是 -1 到 1大于 0.5 说明聚类结构合理0.3 到 0.5 说明有重叠但仍可用低于 0.25 就基本等于随机划分可以考虑换特征或换距离度量。注意 silhouette_score 对样本量敏感司机总数少于 50 人时轮廓系数的波动会很大这时我更倾向直接看每个簇的特征均值手工验证业务上是否分得开——毕竟给管理层解释「轮廓系数 0.42」远不如「激进型司机百公里急刹 3 次经济型司机 0.4 次」来得直观。4.3 DBSCAN 处理异常驾驶风格KMeans 的问题在于它强迫每个司机都进一个簇哪怕这个司机的驾驶数据完全不可信——比如 GPS 信号丢失后补传的伪轨迹导致车速在 0 到 90 之间反复跳变。这种数据在特征空间里就是个离群点KMeans 会硬把它塞进最近的某个簇从而拉偏那个簇的质心。DBSCAN 的好处是不需要预设类别数并且能把噪声点单独标成 -1。db DBSCAN(eps1.8, min_samples5, metriceuclidean) db_labels db.fit_predict(X_scaled) noise_mask db_labels -1 print(f噪声样本数{noise_mask.sum()}占比 {noise_mask.mean():.1%}) driver_feat[聚类标签] db_labels driver_feat.loc[noise_mask, 聚类标签] 异常数据DBSCAN 的两个参数中min_samples 推荐取特征维数的 2 倍再加 1即 6 个特征就设 13 左右但如果司机样本总数少min_samples 设 13 会导致几乎所有点都变成噪声要结合样本量缩小到 5。eps 我一般从 1.5 开始迭代每次加 0.1观察噪声占比曲线的变化——噪声占比突然下降的那段往往对应合适的 eps。聚类完之后异常数据单独建一张表不参与车队平均值的计算同时标记给设备维护人员去查终端状态这比直接删掉更严谨。5. 用聚类结果反查事件明细定位需要干预的具体路段和时间段聚类模型输出的只是司机层面的结论真正要落地的安全管理和油耗管理必须能下钻到具体的时间段和地点。我一般会再把每个簇的司机名单回放到原始 GPS 表里按路段分组统计事件密集区域画核密度热力图。这种可视化用 folium 生成交互页面比 matplotlib 静态图更实用可以直接发给车队长去对应路线上实地核查。import folium from folium.plugins import HeatMap agg_df df[df[急加速标志] 1][[lat, lng]].dropna() m folium.Map(location[agg_df[lat].mean(), agg_df[lng].mean()], zoom_start12) HeatMap(agg_df[[lat, lng]].values.tolist(), radius12, blur18, max_zoom10).add_to(m) m.save(hard_accel_hotmap.html)HeatMap 的 radius 参数控制单个点的影响半径值太大会让热点连成一片看不出具体路口我习惯先设 12缩放级别到 12 左右时基本能精确到路口尺度。这里还需要注意一个常见误区直接用所有点画热力图会被高频通行的路段淹没正确做法是只取事件标志为 1 的点做核密度而不是用全部轨迹做底图。另外 folium 依赖网络加载 OpenStreetMap 底图在内网部署的服务器上出不来图解决方案是本地下载底图瓦片目录或者改用 pyecharts 的地图组件。夜间驾驶行为的识别也是这里值得单独做的一块。我判断夜间时段不是用固定 22 点到 6 点而是用日出日落时间动态计算因为新疆和内地的时差导致同一标准时间下的光照条件完全不同。可以用astral或suntime这个库传入经纬度和日期计算日出日落再判断每条定位记录是否为夜行。from astral import LocationInfo from astral.sun import sun import datetime city LocationInfo(乌鲁木齐, China, Asia/Shanghai, 43.82, 87.62) s sun(city.observer, datedatetime.date(2024, 6, 1)) sunset_ms s[sunset].hour * 60 s[sunset].minute df[夜间] (df[gps_time].dt.hour * 60 df[gps_time].dt.minute sunset_ms).astype(int)用动态日落时间算夜行占比比用固定 18:00 判定的好处在于夏季新疆 21:30 天还是亮的用固定阈值会错判大量正常驾驶为夜行驾驶导致夜行疲劳评分失真。得出的夜间驾驶特征喂回第 4 章的特征矩阵里就能把「白天正常、夜晚激进」这类混合型司机从纯激进群里分出来干预方式是完全不同的——前者要调整排班后者才需要培训。这套「聚类分群—回查事件—时间地点定位—差异干预」的流程是整个驾驶行为分析项目最有交付价值的部分比单出一份司机排名表有用得多。本文还有配套的精品资源点击获取
返回列表