简介:第七届泰迪杯数据挖掘竞赛“车辆驾驶行为分析”完整项目,含源码、文档说明与比赛总结,面向数据挖掘学习者、竞赛选手及车辆网联相关毕设学生。项目在常规驾驶行为分析基础上,引入省、市、县级温度、天气、湿度等环境数据,建立环境评测模型,探究环境因素对行驶节能的影响,并将环境维度融入驾驶行为评测体系。资源共10个文件,压缩包约1.82MB。核心为5个Jupyter分析笔记,覆盖数据预处理、经纬度与路程速度探索、450辆车特征整合、聚类及汽车评价体系打分等关键环节;另有2个Python脚本用于异常值剔除和绘图,2个Markdown文档与1个Word文档提供说明和比赛总结,便于理顺全流程。已有167人学习。下载后可按笔记顺序复现赛题解法,借鉴环境融合思路与建模流程,也可作为课程设计或项目演示基础。作者表示运行遇到问题可私聊远程教学,适合需要完整参考实现的学习者。
1. 泰迪杯车辆驾驶行为分析:一套能直接复现的比赛源码与建模全流程
450辆车的GPS轨迹放进KMeans之前,真正决定比赛名次的是数据清洗。这套第七届泰迪杯车辆驾驶行为分析资源,最值钱的不是最后的聚类代码,而是把“经纬度→路程→速度→驾驶行为画像→环境模型”整条链路拆成了五个可运行的ipynb、一个drop_outlier.py和一份比赛总结。它解决的是数据挖掘比赛里最现实的问题:拿到轨迹、天气、地区数据后,怎么快速做出有业务解释的驾驶行为评分,再把温度、湿度这些环境因素量化到节能分析里。适合参加数据挖掘比赛、做课设毕设,以及想入行车联网数据分析的从业者。
2. 数据预处理与路程速度特征:把450辆车的GPS轨迹洗成可用特征宽表
2.1 异常值剔除:drop_outlier.py 剔的是哪些“怪点”
车辆轨迹数据里最常见的问题不是缺数据,而是“多出来的假数据”。停车场里GPS信号来回跳,经过高架桥下卫星丢失,急刹车瞬间速度先冲到120又跌回0,这些点如果不处理,平均速度会被拉高,加速度特征会失真。整车数据挖掘项目里,清洗所花的时间通常占一半以上。
drop_outlier.py这种脚本一般是用MAD(中位数绝对偏差)而不是3σ来做离群点剔除。因为速度序列不是正态分布,有大量怠速0值和高速巡航值,平均值本身就不稳,用均值加几个标准差去卡阈值很容易把正常的城市快速路数据也剔掉。MAD只依赖中位数,对尖峰和重尾分布稳健得多。参考实现可以写成一个独立函数:
import numpy as np import pandas as pd def drop_outlier(df, col='speed', k=5.2): # 用MAD(中位数绝对偏差)识别速度离群点,比3σ更抗干扰 median = df[col].median() mad = (df[col] - median).abs().median() # 1.4826 是把MAD换算成正态分布标准差尺度的常数 lower = median - k * 1.4826 * mad upper = median + k * 1.4826 * mad return df[(df[col] >= lower) & (df[col] <= upper)]这段代码的关键在k的取值。k取5.2是我常用的起点,相当于给GPS轨迹留出足够的正常波动空间,只把“物理上几乎不可能”的速度点删掉。如果你想更激进,k取3到4,会把更多急加速峰值也抹掉,适合做平稳型驾驶行为分析;如果只是想清掉明显的漂移点,k取5到6更安全。运行后要看剔除比例,控制在1%~3%是健康的,超过5%就要怀疑是速度单位问题或者数据本身有重复采样。
2.2 经纬度与路程:Haversine 公式和速度列怎么算出来的
资源里的经纬度.ipynb解决的是一个很基础但绕不开的问题:两个GPS点之间的距离,不能直接用经纬度差当公里数算。纬度1度约111公里,经度1度在赤道约111公里,但在北纬40度只有85公里左右,必须用球面距离公式。Haversine公式是车联网轨迹处理里最通用的方案:
import math def haversine(lon1, lat1, lon2, lat2): # 输入两组经纬度,输出球面距离,单位公里 R = 6371.0 lon1, lat1, lon2, lat2 = map(math.radians, [lon1, lat1, lon2, lat2]) dlon = lon2 - lon1 dlat = lat2 - lat1 a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2 return 2 * R * math.asin(math.sqrt(a))注意传入的是经纬度的小数形式,比如114.30、30.59,而不是度分秒。R取6371公里是地球平均半径,实际在极地和赤道差几十公里,对驾驶行为分析这个精度完全够用。两个采样点之间的瞬时速度,用距离除以时间差再乘3.6换算成km/h即可。常见做法是先把所有点按车辆和时间排序,再对每个相邻点对算距离,最后把距离序列除以时间差序列。
2.3 特征窗口:3 秒平滑、30 秒统计为什么是这个参数
原始速度序列是1Hz甚至更低频率的,直接拿来算加速度会非常抖。我一般会先做3秒滑动平均,把GPS本身的噪声滤掉,再对平滑后的速度做差分得到加速度。加速度阈值方面,急加速一般取大于1.5 m/s²,急减速取小于-1.5 m/s²,这两个阈值来自《驾驶行为评分标准》这类行业参考,也和资源里汽车评价体系得分的口径接近。
# 3秒平滑后再算加速度,避免GPS跳变造成假急加速 df['speed_smooth'] = df['speed'].rolling(window=3, min_periods=1).mean() df['acc'] = df['speed_smooth'].diff() / 3.6 / 3 # 急加速和急减速标记 df['is_rapid_acc'] = (df['acc'] > 1.5).astype(int) df['is_rapid_dec'] = (df['acc'] < -1.5).astype(int)这里的除以3.6是把km/h换成m/s,再除以3是因为diff是3秒的差分。窗口选3秒而不是1秒,是为了滤掉GPS定位噪声带来的虚假加速度;30秒统计窗口则是对应城市道路的一个完整驾驶片段,短于30秒会有太多空窗口,长于5分钟又会把加减速细节平均掉。统计内容一般是平均速度、速度标准差、急加速次数、急减速次数、怠速时长占比。
2.4 450 辆车的预处理流水线:一个 for 循环串起全流程
资源里450辆车预处理.ipynb的组织方式,就是典型的逐车清洗再合并。每辆车单独读取、单独做离群点剔除和路程速度计算,最后拼成一张以“车辆ID”为行的特征宽表。这一步之所以要逐车做,是因为每辆车的采样频率和传感器精度可能不同,异常值比例也不一样,统一跑一个全局阈值反而会误伤。
import pandas as pd all_features = [] for car_id in range(1, 451): # 按车循环:清洗、算路程速度、滑窗统计 df = pd.read_csv(f'data/car_{car_id}.csv') df = drop_outlier(df, col='speed', k=5.2) df = calc_distance_speed(df) # 内部调用haversine feat = window_features(df) # 30秒滑窗统计 feat['car_id'] = car_id all_features.append(feat) feature_table = pd.concat(all_features, ignore_index=True) feature_table.to_csv('features_450.csv', index=False)这样处理完的特征表才是后续KMeans聚类的输入,每一行是一辆车,列是这辆车在30秒窗口上的聚合值。这里有个经验:多辆车合并阶段不要直接concat原始轨迹,而是concat特征,否则内存压力大且难以定位是哪辆车引入的脏数据。
3. 驾驶行为画像:KMeans聚类与汽车评价体系得分
3.1 聚类特征:别把原始速度直接喂进KMeans
KMeans对特征尺度极其敏感,这是老生常谈但依然频繁翻车的地方。如果把平均速度、速度标准差、急加速次数直接拼在一起跑,平均速度动辄几十,急加速次数只有个位数,距离计算基本被速度主导,聚出来的簇本质上是“跑得快还是跑得慢”,而不是驾驶行为差异。所以特征选择这一步要刻意加入行为维度:急加速频次、急减速频次、夜间行驶占比、怠速时长占比,这些才能真正区分激进型和稳健型驾驶风格。
from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans features = ['avg_speed', 'speed_std', 'rapid_acc_cnt', 'rapid_dec_cnt', 'night_ratio', 'idle_ratio'] X = feature_table[features].values X_scaled = StandardScaler().fit_transform(X)StandardScaler对每个特征减去均值除以标准差,让所有特征在同一尺度下参与距离计算。注意必须用fit_transform在训练集上学习均值和标准差,之后对测试集只能用同一个scaler的transform方法,不能重新fit。很多人在这个位置少写一行,导致训练和预测用了两套尺度,聚类中心完全错位。
3.2 聚类数 K:轮廓系数和业务解释哪个优先
聚类数K是驾驶行为分析里最玄学的一个参数。技术层面可以用轮廓系数帮忙筛选,它会同时评估簇内紧密度和簇间分离度,取值越接近1说明聚类效果越好。但跑出来的轮廓系数会随着K增大单调变化,不能只看最高分。
from sklearn.metrics import silhouette_score sil_scores = [] K_range = range(2, 9) for k in K_range: km = KMeans(n_clusters=k, random_state=42, n_init=10) labels = km.fit_predict(X_scaled) sil_scores.append(silhouette_score(X_scaled, labels))在实际项目里,KMeans的n_init参数建议从默认的10提高到20甚至50,避免随机初始化带来的不稳定性。K的选择,我的习惯是轮廓系数曲线先找一个明显的拐点,再回来看每个簇的车均特征是否讲得通。比如K=4时有一个簇平均速度28km/h、急加速6.2次/百公里,可以解释为拥堵路况下的激进驾驶;K=5时这个簇又被劈成两半且特征几乎重叠,那就说明K取大了。数值指标只是参考,业务解释才是最终标准。
3.3 评价体系得分:从簇标签到百分制的映射
聚类的产出是簇标签,但比赛和业务方要的是“这辆车能得多少分”。资源里汽车评价体系得分.ipynb做的事,就是把每个簇按特征均值排序后映射成百分制得分。常见做法是用簇中心的平均速度排序,速度均值最低、急加减速最少的经济型簇给高分,平均速度和急加减速都高的激进型簇给低分。
centers = pd.DataFrame(km.cluster_centers_, columns=features) # 按平均速度升序排列簇中心,越省油平稳的簇得分越高 order = centers['avg_speed'].sort_values().index score_map = {order[0]: 90, order[1]: 78, order[2]: 65, order[3]: 55} feature_table['behavior_score'] = feature_table['cluster'].map(score_map)这里要留一个心眼:直接固定打分很容易让所有得分集中在几个离散值上,导致最终分布没法区分同簇内个体差异。更好的做法是在簇得分基础上,再按该车在簇内的急加速次数排名做加减分修正。比如同样是经济型簇,急加速次数在簇内排名前20%的扣3分,排名后20%的加2分。这样得分连续,后续做环境模型时也更好用。
4. 环境评测模型:把温度、天气、湿度接进驾驶行为得分
4.1 环境数据对齐:城市字段和时间粒度是主要阻力
赛题要求结合省、市、县级地区的温度、天气、湿度等环境特征。这类数据通常来自气象站或公开天气API,格式是“某城市某日”或者“某城市某小时”,而车辆轨迹是经纬度加时间戳。对齐的第一步是把轨迹数据映射到城市或区县,这一步资源里一般已经做了,但如果要自己复现,可以用逆地理编码库把经纬度翻译成城市名,或者按轨迹起点所属城市近似代替。
# 天气表里的城市可能带“市”字,轨迹表里的城市没有,先清洗再合并 trip['city'] = trip['city'].str.replace('市', '', regex=False) weather['city'] = weather['city'].str.replace('市', '', regex=False) # 以城市和日期为键做左连接,保留全部行程记录 merged = pd.merge(trip, weather, on=['city', 'date'], how='left')时间粒度的选择会影响模型效果。如果轨迹数据有小时级时间戳,建议把天气也按小时对齐,因为温度和湿度的日内波动很大,早晚温差对能耗的影响比日平均温度更显著。天气API一般一小时一条记录,直接按城市加小时merge即可。如果只有日粒度天气,那就退而求其次用日平均温度,但要接受信息损失。
4.2 环境评测模型:线性回归还是树模型
环境评测模型的核心目标是量化“温度、天气、湿度对车辆行驶节能的影响”,这时候线性回归比随机森林更好用。因为树模型的输出是特征重要性,只能告诉你哪个变量重要,不能告诉你温度每升高1度能耗变化多少;线性回归的系数可以直接读成边际影响。比赛总结里如果要写“低温环境下能耗显著上升”,线性回归系数的说服力最强。
from sklearn.linear_model import LinearRegression # 节能指标用百公里等效能耗,若原始表无能耗字段可用行为得分做代理 merged['energy_per_100km'] = merged.get( 'energy_per_100km', merged['behavior_score'] * -1 ) X_env = merged[['temp', 'humidity', 'is_rain', 'is_snow']] y = merged['energy_per_100km'] model = LinearRegression().fit(X_env, y)is_rain和is_snow需要提前从天气现象字段里拆出来,用one-hot或二值标记都行。线性回归的系数解释要注意单位:temp的单位是摄氏度,humidity是百分比,若湿度系数是0.05,就意味着湿度每增加10个百分点,百公里能耗上升0.5,这个量级是合理且可汇报的。
4.3 节能影响量化:不同温度区间的能耗差异
回归系数给出的是全局平均趋势,但实际驾驶行为受温度影响往往不是线性的,低温到常温区间下降很快,常温以上基本平稳。比赛评审更愿意看到这种分段差异,而不是一个孤立系数。分组聚合是最直接的方法:
# 把温度切成5个区间,看每个区间下百公里能耗均值 import pandas as pd bins = [-20, 0, 10, 20, 30, 40] labels = ['<-20', '0~10', '10~20', '20~30', '30+'] merged['temp_bin'] = pd.cut(merged['temp'], bins=bins, labels=labels) impact = merged.groupby('temp_bin', observed=False)['energy_per_100km'].mean()这段代码跑完,一般能看到低温段能耗比常温段高出10%~20%的结果。要注意观察每个温度区间的样本量,如果0度以下的行程只有几十条,均值很容易被少数极端天气驾驶行为带偏,汇报时要补一个样本量说明。这是环境模型里最容易忽略的统计陷阱。
5. 避坑指南:跑这套驾驶行为源码最容易翻车的五个位置
5.1 GPS 漂移点把速度推到 180,清洗阈值怎么定
现象:某辆车的瞬时速度列里出现181 km/h,但看它的轨迹只是在市区绕圈,位置点来回跳跃。
原因:GPS在立交桥下或高楼密集区丢失卫星信号,接收机输出错误坐标,相邻两点距离被算成几公里,速度自然爆表。如果不处理,平均速度直接失真,聚类时会被分到“高速型”,而实际上司机根本没开那么快。
解决:不要只设一个绝对速度上限,而是用2.1节里的MAD方法按车剔除。如果单是几个尖峰,先删除速度大于120km/h且持续时间小于5秒的点段;如果是整段信号丢失,还要看时间间隔,超过10秒的相邻点直接断开,不计算这段路程。
5.2 时间格式不统一,merge 直接少一半数据
现象:轨迹表里时间列是“2017/1/1 8:30”,天气表里是“2017-01-01 08:30:00”,直接用字符串merge,原本能匹配上的记录全变成NaN,后面模型样本量直接砍半。
原因:Excel导出、API返回、原始CSV三个来源各用各的格式,字符串匹配严格按字符比对,斜杠和横杠不相等。
解决:在任何merge操作前,先统一时间列格式,然后单独提取date字段作为关联键。处理完必须检查merge后的非空比例,低于90%就要回头查是格式问题还是城市字段问题。常见做法是把所有时间字段强制解析成datetime类型再格式化。
5.3 聚类结果无法解释:只按速度分组不是驾驶行为
现象:KMeans跑出来三个簇,簇1全是平均速度80以上的,簇2全是40~60的,簇3全是20以下的。各簇急加速次数没有明显差别,报告写出来像“路况分类”而不是“驾驶行为分类”。
原因:特征列里速度相关变量占比过高,行为特征太少或量纲太小,被速度特征淹没。这是3.1节提到的尺度问题,再加一个隐患:如果只用平均速度和行驶时长,KMeans聚的就是出行场景而非人的行为。
解决:检查特征相关性矩阵,速度均值和速度标准差相关度高于0.6时保留一个;强制加入急加速频次、夜间占比、怠速占比等行为特征,并确认标准化之后它们在距离计算中的实际贡献。特征数量控制在5到7个,太多反而让业务解释困难。
5.4 天气数据 join 不上:城市字段差一个字
现象:天气表里城市是“武汉市”,轨迹表里是“武汉”;天气表用省份+地级市两列,轨迹表只有一列“湖北武汉”。merge之后大部分行没有天气数据。
原因:行政区划的字段在清洗时没做归一,来源系统不一样,有的带行政后缀有的不带。
解决:字符清洗是第一步,把“市”“省”“自治区”等后缀统一去掉;如果还匹配不上,就改用轨迹起点经纬度去匹配最近气象站,每个气象站有经纬度和城市属性,按Haversine距离取最近的一个。这个方法省事且能兼容到县级数据,也符合赛题“省、市、县级”的要求。
5.5 训练集和测试集分布不一致:模型要固定不能重训
现象:比赛给了一部分车辆的预处理结果,你在训练集上聚出4个簇,测试集来了之后又对所有数据重新跑了一遍KMeans,结果簇的中心和业务含义全变了,评分结果对不上。
原因:KMeans是无监督算法,重新fit就会得到新簇中心。很多人习惯性地对新数据再跑一次fit,这是无监督模型落地时最典型的错误。
解决:训练集上fit好KMeans之后保存模型,测试集只用transform或predict把样本映射到最近的簇,绝不要重新fit。同样的原则也适用于StandardScaler。比赛提交前,把保存模型的pickle文件和评分脚本一起打包,能省掉很多麻烦。
6. 验证技巧与复盘写法:让驾驶行为分析结果能经得起问
模型做完只是第一步,答辩和比赛总结里评审一定会问:“你这个驾驶行为评分有什么实际意义?怎么证明它是准的?”下面三个验证技巧是我的习惯,都写进了资源里的比赛总结文档。
第一个技巧是同路段对比验证。找同一个司机在同一个路段、同一时段的早晚高峰行程,提取平均速度、急加速次数。如果评分模型合理,拥堵晚高峰的急加速次数应该比畅通早高峰少,而急减速次数应该更多。这个验证不用额外数据,一张分组统计表就能说明问题,也最容易让外行听懂。
第二个技巧是密度分布图代替均值柱状图。聚类结果对比时,不要只画各簇的平均速度柱状图,那是把个体差异抹平了。用seaborn的kdeplot画出不同簇的速度分布曲线,如果一个簇的分布明显呈双峰,说明这个簇里混入了两种驾驶风格,K取值可能偏小;如果两个簇的分布大面积重叠,只有均值略有不同,说明聚类边界不清晰。这个可视化在答辩时很加分,因为一眼能看出问题在哪里。
第三个技巧是把回归系数和特征重要性写进总结文档,而不是只写结论。环境模型里温度系数是0.12,意味着温度每下降10度,百公里能耗上升1.2左右,这个数字比“温度影响显著”有说服力得多。还要配上样本量和置信区间,防止被质疑数据量太小。
从那次比赛之后,我每次拿到GPS轨迹数据,不管目标是什么,都强制先跑一遍离群点剔除和时间格式统一,再谈后面的聚类和建模。这套流程看起来不起眼,但至少帮我避开了之后很多项目里80%的返工。希望这套源码和复盘文档也能帮到你。
本文还有配套的精品资源,点击获取