【从零学算法之阿里天池智慧交通挑战赛优胜方案复现】

0.写在前面

作者从22年11月份开始学习机器学习与深度学习,目前为广东某大学非计算机专业研0,第一次写博客,有点紧张~!! 博客中主要写明思路,某些结果没有列出来(呜–不是不想放,俺滴小破电脑算起来有丢丢慢)。

1.复现赛题介绍

复现比赛为阿里天池—智慧交通预测挑战赛,旨在通过机器学习和数据科学技术预测城市道路的通行时间和路况。比赛方给了三个txt文件,分别为“new_gy_contest_traveltime_training_data_s econd.txt”、“gy_contest_link_info.txt”和“gy_contest_link_top.txt”,包含了北京市和天津市的道路的交通状况数据,时间跨度为2016年1月1日到2016年7月31日。“new_gy_contest_traveltime_tra ining_data_second.txt”数据集中的每条记录包括了路段的ID、道路起点和终点、时间戳以及该路段在该时间点的路况指数等信息,数据总量为百万级别。

1.1 编译环境

Jupyter notebook(python 3.9)

1.2 Dataframe解析之new_gy_contest_traveltime_training_data_second.txt

属性 类型 表示含义
link_ID object 每条路段(Link)唯一标识 ,eg:4377906281969500514
date object 日期,eg: ‘2017-05-06’
time_interval object 时间区间,eg:[2017-05-06 11:04:00,2017-05-06 11:06:00)
travel_time float64 车辆在道路上的通行时间\s,eg:19.9

1.3 Dataframe解析之gy_contest_link_info.txt

属性 类型 表示含义
link_ID object 每条路段(Link)唯一标识 ,eg:4377906281969500514
length int64 道路长度/m,eg:57代表道路长度57m
width int64 道路宽度/m,eg:3代表道路宽度3m
link_class int64 道路等级,eg:1代表主干道

1.4 Dataframe解析之gy_contest_link_top.txt

属性 类型 表示含义
link_ID object 每条路段(Link)唯一标识 ,eg:4377906281969500514
in_links object 道路起始段所连接的道路数量
out_links object 道路末尾段所连接的道路数量

2.导入工具包

前三行代码导入机器学习所普遍使用的三大工具包—numpy、pandas和matplotlib.pyplot;而 import UnivariateSpline 是为了进行曲线拟合和数据插值,它基于样条插值的原理,可以帮助我们从不连续的数据中得出平滑的函数形式;import warning可以不打印警告信息(看着清爽!!)
代码如下:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 插值处理所用工具包
from scipy.interpolate import UnivariateSpline 
## 忽略警告
import warnings
warnings.filterwarnings('ignore')

3.导入数据集

利用pandas中读入工具read_csv导入三个数据集分别命令为df_time_interval、df_info 、df_top 。注:txt文件里面是用“;”进行分列的,因此用delimiter命令让其形成的Dataframe分列。其次,亲测如果不加命令dtype={‘link_ID’:object},导入的link_ID为uint64,建议写上。

代码如下:

# 导入数据集1
df_time_interval = pd.read_csv('new_gy_contest_traveltime_training_data_second.txt',
                delimiter=';',dtype={
    
    'link_ID':object})
df_time_interval
# 导入数据集2
df_info = pd.read_csv('gy_contest_link_info.txt',delimiter=';',
                dtype={
    
    'link_ID':object}) 
df_info
# 导入数据集3
df_top  = pd.read_csv(r'gy_contest_link_top.txt',delimiter=','
                ,dtype={
    
    'link_ID':object}) 
df_top

所得三个Dataframe分别为:

  1. df_time_interval (shape=7705175x4)
    数据集1

  2. df_info(shape=132x4)
    数据集2

  3. df_top(shape=132x3)
    在这里插入图片描述

4.标签转换

观察数据集df_time_interval, ‘travel_time’相当于y,‘link_ID’为道路的id,对预测‘travel_time’有决定性作用的标签有‘time_interval’和‘date’。然而,很难直接用 ‘time_interval’进行建模预测,在这里对‘time_interval’标签进行一个转换,仅保留时段区间的起始时间,并将其转换为pandas时间序列,取名为‘time_interval_begin’。

df_time_interval['time_interval_begin'] = pd.to_datetime(df_time_interval['time_interval'].map(lambda x:x[1:20]))

所得结果如下:
time_interval_begin

5.去除离群点

很简单,画一个直方图,bins值可设大一点儿,作者最后选择的是bins=200,若看不清可先调整figsize再调整x和y轴的取值范围。

df_time_interval['travel_time'].plot(kind='hist',bins=200)
plt.show()

在这里插入图片描述
上图感觉不太直观,且和正态分布相差较大,将其+1取对数试试

如下:

df_time_interval['travel_time'] = np.log1p(df_time_interval['travel_time'])
df_time_interval['travel_time'].plot(kind='hist',bins =200)
plt.show()

在这里插入图片描述
这下好多了!

6.补全完整时间序列

next,数据表中所给时间序列不太完整,我们需要对其进一步处理将其填充成一个完成的时间序列,再用UnivariateSpline 进行插值。

假设小于5%分位数与大于99%分位数的数值为离群点,将其全部用5%分位数或99%分位数代替,可用函数quantile。

代码如下:

def outlier_movement(group):
    group[group < group.quantile(0.05)] = group.quantile(0.05)
    group[group > group.quantile(0.99)] = group.quantile(0.99)
    return group

df_time_interval['travel_time'] = df_time_interval.groupby(['link_ID','date'])['travel_time'].transform(outlier_movement)

6.1 构建时间集

接下来就开始补全完整时间序列的第一步,补全时间区间,没有数值暂时用nan代替。此过程需要用到

min_time = time_interval['time_interval_begin'].min()
max_time = time_interval['time_interval_begin'].max()

# 为保证时间区间完整性,右区间选择取大一点儿
date_range = pd.date_range(min_time,'2017-07-31 23:58:00',freq='2min')
new_index = pd.MultiIndex.from_product([time_interval['link_ID'].unique(),date_range],names=['link_ID','time_interval_begin'])

df_time_interval = pd.DataFrame(index=new_index).reset_index()
df_time_interval_merge = pd.concat([df_time_interval,time_interval])

# 再次转换为pandas时间序列
df_time_interval_merge['time_interval_begin'] = pd.to_datetime(df_time_interval_merge['time_interval_begin'])
df_time_interval_merge['date'] = df_time_interval_merge['time_interval_begin'].dt.strftime('%Y-%m-%d')

6.2 插值

补全完整时间序列的第二步就是将nan值转换为具体的一个数值,该数值可用回归得到。本文中所用中心思想如下:travel_time与所在年、月、日、小时、分钟有关,将其分为年、月、日、小时、分钟影响分部与残差,分别回归得到各影响分部的值,最后将其相加即可得travel_time的预测值。

限于此次比赛所给数据集的体量(我觉得很大了~),选择小时与分钟两个影响分部和残差进行预测。

  1. 小时影响分部

对影响分部回归之前需选择赛题指定的时间范围,代码如下:

df_time_interval_merge = df_time_interval_merge.loc[(df_time_interval_merge['time_interval_begin'].dt.hour.isin([6, 7, 8, 13, 14, 15, 16, 17, 18]))]
df_time_interval_merge = df_time_interval_merge.loc[~((df_time_interval_merge['time_interval_begin'].dt.year == 2017) & (df_time_interval_merge['time_interval_begin'].dt.month == 7) & (
    df_time_interval_merge['time_interval_begin'].dt.hour.isin([8, 15, 18])))]
df_time_interval_merge = df_time_interval_merge.loc[~((df_time_interval_merge['time_interval_begin'].dt.year == 2017) & (df_time_interval_merge['time_interval_begin'].dt.month == 3) & (
    df_time_interval_merge['time_interval_begin'].dt.day == 31))]

df_time_interval_merge['date'] = df_time_interval_merge['time_interval_begin'].dt.strftime('%Y-%m-%d')

线性回归建模,并predict得预测值

df_time_interval_merge['travel_time2'] = df_time_interval_merge['travel_time']
df_time_interval_merge['date_hour'] = df_time_interval_merge['time_interval_begin'].map(lambda x:x.strftime('%Y-%M-%d-%H'))

# 导入工具包
from sklearn.linear_model import LinearRegression

# 回归得到小时影响分部数值
def date_trend(group):
    #按小时进行排序
    tmp = group.groupby('date_hour').mean().reset_index()
    print(tmp)
    
    #得到nan值所在位置与索引
    def nan_helper(y):
        return np.isnan(y),lambda z:z.nonzero()[0]
    y = tmp['travel_time'].values
    nans,x = nan_helper(y)
    
    lr = LinearRegression()
    lr.fit(x(~nans).reshape(-1,1),y[~nans].reshape(-1,1)) # 掩码操作用小括号
    tmp['date_trend'] = lr.predict(tmp.index.values.reshape(-1,1)).ravel() # ravel拉成一长列
    group = pd.merge(group,tmp[['date_hour','date_trend']],on='date_hour',how='left')
    return group

df_time_interval_merge = date_trend(df_time_interval_merge)

结果如下:
在这里插入图片描述
2. 分钟影响分部

同样进行转换,代码如下:

def minute_trend(group):
    tmp = group.groupby('hour_minute').mean().reset_index()
    spl = UnivariateSpline(tmp.index,tmp['travel_time'].values,s=0.5)
    tmp.loc[tmp['travel_time'].isnull(), 'travel_time'] = spl(tmp.loc[tmp['travel_time'].isnull()].index)
    tmp['minute_trend'] = tmp['travel_time']
    group = pd.merge(group,tmp[['minute_trend','hour_minute']],on='hour_minute',how='left')
    return group

df_time_interval_merge['hour_minute'] = df_time_interval_merge['time_interval_begin']
    .map(lambda x:x.strftime('%H-%m'))

df = minute_trend(df_time_interval_merge)

结果如下:
在这里插入图片描述
到此,date_trend和minute_trend均计算完毕,将travel_time减去小时和分钟影响分部可以得到残差

df = df.drop(['hour_minute'],axis=1)
df.reset_index()
df['travel_time'] = df['travel_time'] - df['minute_trend']

#及时保存
df.to_csv('com_training.txt',sep=';',header=True,mode='w',index=None)

数据量有点儿大,先保存下

df = pd.read_csv('com_training.txt',delimiter=';',dtype={
    
    'link_ID':object})

6.3 构建特征

从剩下的df_info 、df_top中提取相关特征

df_top_info = pd.merge(df_info,df_top,on='link_ID',how='left')

df_top_info['links_num'] = df_top_info['in_links'] + df_top_info['out_links']
df_top_info['area'] = df_top_info['length']*df_top_info['width']
df = pd.merge(df, df_top_info[['link_ID', 'length', 'width', 'links_num', 'area']], on=['link_ID'], how='left') #组合特征
df.head()

提取起始时间中的月份、天、小时、分钟

df['time_interval_begin'] = pd.to_datetime(df['time_interval_begin'])
df['minute'] = df['time_interval_begin'].dt.minute
df['hour'] = df['time_interval_begin'].dt.hour
df['day'] = df['time_interval_begin'].dt.day
df['month'] = df['time_interval_begin'].dt.month
df.head() 

6.4 残差标准化

将travel_time列除以std(travel_time)可将其标准化,代码如下:

def standardization(group):
    group['travel_time_std'] = np.std(group['travel_time'])
    return group

df_std = df.groupby('link_ID').apply(standardization)
df_std['travel_time_std'] = df_std['travel_time']/df_std['travel_time_std']
df_std.head()

6.5 缺失值预测

采用XGBoost对缺失值进行预测,params 设置参考迪哥课堂,预测之后将其整合到原Dataframe中,代码如下:

df_std_train = df_std.loc[~df_std['travel_time'].isnull()]
df_std_test = df_std.loc[df_std['travel_time'].isnull()]

train_feature = [x for x in df_std.columns.values.tolist() if x not in ['link_ID','time_interval_begin','travel_time','travel_time2', 
    'date_trend','minute_trend','date','travel_time_std']] 

from sklearn.model_selection import train_test_split
x = df_std_train[train_feature].values
y = df_std_train['travel_time'].values
x_train,x_vaild,y_train,y_valid = train_test_split(x,y,test_size=0.2,random_state=929)
import lightgbm as lgb
params = {
    
    
    'task': 'train',
    'boosting_type': 'gbdt',  # 提升树类型
    'objective': 'regression',  # 损失函数
    'metric': {
    
    'rmse'},  # 评估指标
    'num_leaves': 2,  # 叶子节点数目
    'learning_rate': 0.05,  # 学习率
    'feature_fraction': 0.9,  # 每次迭代随机选择特征的比例
    'bagging_fraction': 0.8,  # 每次迭代随机选择数据的比例
    'bagging_freq': 1,  # bagging 操作执行的频率
    'verbose': 0  # 控制输出的级别,0 为不输出
}
train_data = lgb.Dataset(x_train, label=y_train)
lgb_reg = lgb.train(params, train_data, num_boost_round=5)
df_std_test['predtion'] = lgb_reg.predict(df_std_test[train_feature].values) 

df_std = pd.merge(df_std,df_std_test[['link_ID','predtion','time_interval_begin']],on=['link_ID','time_interval_begin'],how='left')                                                                

6.6 重新计算travel_time值

df_std2 = df_std.copy()
df_std2['travel_time'] = df_std2['travel_time'].fillna(value=df_std2['predtion'])
df_std2['travel_time_std'].fillna(df_std2['travel_time_std'].median(), inplace=True)
df_std2['travel_time'] = df_std2['travel_time']*df_std2['travel_time_std']+df_std2['minute_trend']+df_std2['date_trend']
df_std2['imputation1'] = df_std2['travel_time'].isnull()
df_std2.head()

The end,travel_time一列全部填充完毕
在这里插入图片描述
好习惯,保存一下。很多列不需要,可不保存,减少后面内存消耗。

df_std2[['link_ID','time_interval_begin','travel_time','imputation1']].to_csv('com_training.txt',header=True,index=None,sep=';',mode='w')

7.构建特征

  1. 老规矩,先读入数据
df = pd.read_csv('data/com_training.txt', delimiter=';', parse_dates=['time_interval_begin'], dtype={
    
    'link_ID': object})
  1. 时间序列预测,数据集中需要有与该点时间以前区段的相关数据信息,因此构件lagging1-lagging5特征,分别代表该时间段前2*i分钟的travel_time,i取1-5,也就是将Dataframe表格平移5格,代码如下:
#每次导入数据都得重新转换
df ['time_interval_begin'] = pd.to_datetime(df ['time_interval_begin'])

def create_lagging(df,original_df,i):
    df1 = original_df.copy()
    df1['time_interval_begin'] = df1['time_interval_begin'] + pd.DateOffset(minutes=2*i)
    df1 = df1.rename(columns={
    
    'travel_time':'lagging'+str(i)})
    df2 = pd.merge(df,df1[['link_ID','time_interval_begin','lagging'+str(i)]],on=['link_ID','time_interval_begin'],how='left')
    return df2

#lagging1需要单独转换
df1 = create_lagging(df , df , 1)

#lagging2-lagging5可以采用循环
lagging = 5
for i in range(2, lagging + 1):
    df1 = create_lagging(df1, df , i)
  1. 重新提取df_info 、df_top中的特征。此外,特征time_interval_begin无法直接使用,将其转换为距离某起始时间的分钟数量,该数量可用来建模。
df_info_top = pd.merge(df_info,df_top,on='link_ID',how='left')
df_info_top['links_num'] = df_info_top['in_links'] + df_info_top['out_links']
df_info_top['area'] = df_info_top['length']*df_info_top['width']

df1 = pd.merge(df1, df_info_top[['link_ID', 'length', 'width', 'links_num', 'area']], on=['link_ID'], how='left')

#起始分钟特征
df1 ['time_interval_begin'] = pd.to_datetime(df1 ['time_interval_begin'])
# 起始分钟特征
df1 .loc[df1 ['time_interval_begin'].dt.hour.isin([6, 7, 8]), 'minute_series'] = \
    df1 ['time_interval_begin'].dt.minute + (df1 ['time_interval_begin'].dt.hour - 6) * 60

df1 .loc[df1 ['time_interval_begin'].dt.hour.isin([13, 14, 15]), 'minute_series'] = \
    df1 ['time_interval_begin'].dt.minute + (df1 ['time_interval_begin'].dt.hour - 13) * 60

df1 .loc[df1 ['time_interval_begin'].dt.hour.isin([16, 17, 18]), 'minute_series'] = \
    df1 ['time_interval_begin'].dt.minute + (df1 ['time_interval_begin'].dt.hour - 16) * 60

还有些其他特征,如星期特征、时间段特征等等,其他的各位可根据情况加

# 星期特征
df1['day_of_week'] = df1['time_interval_begin'].map(lambda x: x.weekday() + 1)
df1.loc[df2['day_of_week'].isin([1, 2, 3]), 'day_of_week_en'] = 1
df1.loc[df2['day_of_week'].isin([4, 5]), 'day_of_week_en'] = 2
df1.loc[df2['day_of_week'].isin([6, 7]), 'day_of_week_en'] = 3

# 时间段特征
df1.loc[df['time_interval_begin'].dt.hour.isin([6, 7, 8]), 'hour_en'] = 1
df1.loc[df['time_interval_begin'].dt.hour.isin([13, 14, 15]), 'hour_en'] = 2
df1.loc[df['time_interval_begin'].dt.hour.isin([16, 17, 18]), 'hour_en'] = 3

!!数据终于处理完了,最后在保存至txt文件中以备后面建模。

df_5.to_csv('com_training.txt',header=True,index=None,sep=';',mode='w')

未完待续…

猜你喜欢

转载自blog.csdn.net/fly_ddaa/article/details/129835141