最近平安车险的一个数据挖掘比赛,训练集2000w,属于数据不均衡的回归问题,正负例之比大约1:7。
比赛链接
http://www.datafountain.cn/?u=7612745&/competitions/284/intro
本赛题提供部分客户1分钟级驾驶行为数据及对应客户的赔付率作为训练集,包括经纬度定位及驾驶状态等(已脱敏),参赛队伍需要对其进行数据挖掘和必要的机器学习训练。另外,我们会提供同期其他部分客户的驾驶行为数据来做评测,检测您的算法是否能准确的识别出当时客户的驾驶风险。
整理一下这个比赛的思路:
- 特征工程
- 模型
特征工程
特征工程是机器学习/深度学习里面非常重要的一个环境,觉得的模型效果的上限。特征工程的好坏,对比赛的影响非常大。在这里用pandas来读取csv文件,来做基本的特征处理。
从左到右,分别是用户id,轨迹id,时间,经度,维度,方向,高度,速度,通话状态。Y表示当前用户的赔付率。
每一个用户,对应多个轨迹id,每一个轨迹id,对应多行数据。但是每一个用户,只对应一个赔付率。也就是说,特征工程的单位是基于用户,为每一个用户构建一个特征向量。
-
对于时间特征
原始数据给出的是unix时间戳,将它转换成正常的时间。然后把时间转换成一个one-hot的向量。
对于一个用户,如果时间的粒度是1小时,那就是构造一个长度为24的向量。(这个是我随便举的一个例子,在比赛里面这个粒度对分数影响非常大!!!)
如果一个用户有两条轨迹早上一点的轨迹,这个用户总共有有100条轨迹。那么 one-hot 的表示方式 是 : [2/100, xx , xx , ... ]alluser = data['TERMINALNO'].nunique() # Feature Engineer, 对每一个用户生成特征: for item in data['TERMINALNO'].unique(): #print('user NO:',item) temp = data.loc[data['TERMINALNO'] == item,:] temp['hour'] = temp['TIME'].apply(lambda x: (datetime.datetime.fromtimestamp(x).hour)) hour_state = np.zeros([24,1]) for i in range(24): hour_state[i] = temp[temp['hour']==i].shape[0]/temp.shape[0]
one-hot 是一个处理特征常用的套路,比如对于性别特征,性别有两个属性男和女,那么男可以表示为[1, 0],女可以表示为[0, 1]。
Pandas 可以使用get_dummies函数把字符特征转换成one hot形式:
dummies = pd.get_dummies(dframe[feat]) #获取特征 dummies = dummies.rename(columns=lambda x: feat+str(x)) # 改特征名 dframe = pd.concat([dframe,dummies],axis=1) #把新特征加入原始数据集
-
对于速度,高度,方向这三个数值特征
可以选择他们的均值,方差作为特征(即统计某一个用户,所有轨迹的速度值(高度/方向)的均值,方差等)
-
使用频率作为特征
对于一个用户,统计这个用户的所有的速度的分布的频率作为特征。比如将速度分成三个区间[0,5],[5,15],[15,50] ,然后统计速度落每个区间的数量/总数。这样就得出三个频率值。
在实际问题里面,我们使用分位点来划分区间。#选择6个分位点,将区间划分成7份 thresold_class1[i] = np.percentile(data2.values,1) thresold_class2[i] = np.percentile(data2.values,5) thresold_class3[i] = np.percentile(data2.values,90) thresold_class4[i] = np.percentile(data2.values,95) thresold_class5[i] = np.percentile(data2.values,99) thresold_class6[i] = np.percentile(data2.values,100) def character_class(x,thresold_class0,thresold_class1,thresold_class2,thresold_class3,thresold_class4,thresold_class5,thresold_class6): if thresold_class0 <= x < thresold_class1: return 0 if thresold_class1 < x <= thresold_class2: return 1 if thresold_class2 < x <= thresold_class3: return 2 if thresold_class3 < x <= thresold_class4: return 3 if thresold_class4 < x <= thresold_class5: return 4 if thresold_class5 < x <= thresold_class6: return 5 #把速度值根据分位点变成0~5的状态值 temp['speed'] = temp['SPEED'].apply(lambda x: character_class(x,thresold_class0[0],thresold_class1[0],thresold_class2[0],thresold_class3[0],thresold_class4[0] ,thresold_class5[0],thresold_class6[0])) ### 将速度按照10分级,分成14份; #转换成频率的one-hot speed_state = np.zeros([6,1]) for i in range(6): speed_state[i] = temp.loc[temp['speed']==i].shape[0]/float(nsh)
在比赛里面,这个分位点的选择很重要,对分数影响很大。尤其是对于特别大和特别小区间的时候,往往分的细一点会效果更好一些。
-
对于gps的处理
1.最早的plan是将gps栅格化,然后给每个方块编码,转换成类似于one-hot的特征。
但是这个的问题是gps的数据差距很大,并不是同一个城市,这样意味着栅格大了没有用(没有办法把点区分出来),栅格小了编码比较多,不一定有效果。2.一个神奇的trick
在比赛的讨论群里面,有人说给gps减去一个固定的坐标(随意的gps,然后必须是一个固定的值),使用这个距离差作为特征。尝试了一下,效果很好!我的理解是有一些地方是事故高发的地方,所以,这么搞一下。可以找到这个地方。
最后,返回生成的特征
feature = [item, num_of_trips, num_of_records,num_of_state_0,num_of_state_1,num_of_state_2,num_of_state_3,num_of_state_4
,float(speed_state[0]),float(speed_state[1]),float(speed_state[2]),float(speed_state[3]),float(speed_state[4]),float(speed_state[5]),float(height_state[0])
,float(height_state[1]),float(height_state[2]),float(height_state[3]),float(height_state[4]),float(height_state[5]),float(direction_state[0]),float(direction_state[1])
,float(direction_state[2]),float(direction_state[3]),float(direction_state[4]),float(direction_state[5]),float(hour_state[0]),float(hour_state[1])
,float(hour_state[2]),float(hour_state[3]),float(hour_state[4]),float(hour_state[5])
,float(hour_state[6]),float(hour_state[7]),float(hour_state[8]),float(hour_state[9]),float(hour_state[10]),float(hour_state[11])
,float(hour_state[12]),float(hour_state[13]),float(hour_state[14]),float(hour_state[15]),float(hour_state[16]),float(hour_state[17])
,float(hour_state[18]),float(hour_state[19]),float(hour_state[20]),float(hour_state[21]),float(hour_state[22]),float(hour_state[23])
,float(hour_state[24]), float(hour_state[25]), float(hour_state[26]), float(hour_state[27]),float(hour_state[28]), float(hour_state[29])
,float(hour_state[30]), float(hour_state[31]), float(hour_state[32]), float(hour_state[33]),float(hour_state[34]), float(hour_state[35])
,float(hour_state[36]), float(hour_state[37]), float(hour_state[38]), float(hour_state[39]),float(hour_state[40]), float(hour_state[41])
,float(hour_state[42]), float(hour_state[43]), float(hour_state[44]), float(hour_state[45]),float(hour_state[46]), float(hour_state[47])
,hdis1,target]
数据补全,异常处理 和 归一化
对于缺失数据,可以使用均值,中位数,或者众数填充。
归一化
2.1 Z-Score标准化
2.2 Min-Max归一化
min_max_scaler = preprocessing.MinMaxScaler()
minmax_train = min_max_scaler.fit_transform(train_x)
minmax_test = min_max_scaler.transform(test_x)
train_x, test_x = pd.DataFrame(minmax_train), pd.DataFrame(minmax_test)
- 不平衡数据的处理
一般是上采样或下采样。我们这次比赛是为0的数据和非0的数据是7:1的关系。我们尝试了上采样和下采样,但是效果没有明显变化。感觉这个也和测试集数据的分布有关系。可以尝试,当不一定有效果。
模型
我们尝试的模型有随机森林,lgb,线性回归,svm等。综合下来,最好的效果是lgb。对于模型的参数是一个很麻烦的事情,尤其是新添加了几个特征之后,模型的参数就全变了。没有办法判断是特征没有效果还是参数没有效果。我们的想法是把特征都推上去,然后调参。
特征选择
由于比赛每天只有三次的提交机会,而调参又是需要大量的尝试,当我们添加了一个特征的时候,往往用原来的参数是没有办法证明这个特征的有效性(换句话说,就是原来模型的参数无效了,有需要重新调参)。这个显然是一个无解的死循环。所以,我们的想到的解决办法是。当添加新的特征的时候,使用线性回归的模型进行提交,因为线性回归的模型是不需要很多参数,可以迅速的验证添加的特征是否可以提高分数。
如果添加特征之后,分数没有下降,那么可以基本上可以认为这个特征是一个有效的特征。这就是我们后期选择特征的方法。然后在把模型换成lgb,进行调参。-
调参
调参的思路是根据每一次提交的成绩得分,来估计模型是处于一个过拟合还是一个欠拟合的状态。(比如当前的分数是0.15 , 试着把迭代次数调高,再提交一次,如果分数下降了。那可以说明模型现在处于过拟合的状态。)如果是过拟合,采用的调参思路是:
- 提高迭代的次数
- 提升num_leaves
如果是过拟合,采用的办法是:
- L1 和 L2正则
- 更少的num_leaves
- 减少迭代次数
- 使用early_stop(ealry_stop的实现是,如果cost不再下降的时候,就提前结束,来避免过拟合)
比赛很遗憾,没有杀进决赛,最后的名次是初赛15名,复赛16名。领了3000元的奖金,作为伙食费散场了。