当前位置:   article > 正文

整理一下第一次参加华为大数据挑战赛自己的一些收获吧(正式赛篇阶段一)_船运到达时间预测 代码

船运到达时间预测 代码

写在前面

emmm,这篇文章就整理一下我处理正式赛题阶段一(读取数据的方式是分块读取,保存为三个csv文件)的过程吧

赛题介绍:

正式赛题——船运到达时间预测
在企业全球化业务体系中,海运物流作为其最重要的一项支撑。其中,船运公司会和数据供应公司进行合作,对运输用的船通过GPS进行定位以监控船的位置;在运输管理的过程中,货物到达目的港的时间是非常重要的一项数据,那么需要通过船运的历史数据构建模型,对目的港到达时间进行预测,预测时间简称为ETA(estimated time of arrival),目的港到达时间预测为ARRIVAL_ETA。
本次大赛提供历史运单GPS数据、历史运单事件数据、港口坐标数据,预测货物运单的到达时间,对应“历史运单事件”数据中EVENT_CODE字段值为ARRIVAL AT PORT时EVENT_CONVOLUTION_DATE的时间值。

一、比赛数据
大赛提供脱敏后的训练数据及测试数据,训练数据集包括:历史运单GPS数据、历史运单事件数据、港口坐标数据,这些数据主要用于参赛队伍训练模型,制定预估策略;测试运单数据为不同运单、运输过程中的不同位置所构成,供选手测试对应的ETA时间。
货物运单在船运过程中,会产生大量的GPS运单数据,记录为“历史运单GPS数据”;货物运单在船运过程中离开起运港、到达中转港、到达目的港等关键事件,记录为“历史运单事件数据”;“港口的坐标数据“为与运单船运相关的港口坐标信息。
允许选手合理增加与题目相关的外部数据进行纠正,如大赛提供的港口坐标数据存在偏差时可自行补充数据纠正。

  1. 历史运单GPS数据
    历史运单GPS数据描述每个运单在船运的过程中,所在船产生的GPS位置的相关信息。
    在这里插入图片描述
    数据说明:
    每个运单表示一次运输的运输单号,不会重复使用,一次运输过程中的多条GPS数据拥有相同的运输单号。船号为运单货物所在的船编号,会重复出现在不同次运输的GPS数据中。需要注意的是GPS数据中可能会有异常的GPS,可能且不限于如下问题:
    (1) GPS坐标在陆地,或者有些港口是内陆的港口。
    (2) GPS漂移:两点距离过大,超过船的行驶能力。
    (3) GPS在部分地区的比较稀疏(比如南半球、敏感海域)。
    (4) 最后的GPS点可能和港口的距离较远(比如塞港时,或者临近目的港时已无GPS数据)。
    (5) speed字段之后数据可能会有少量缺失(如GPS设备短暂异常)。

  2. 历史运单事件数据
    历史运单事件数据描述每个运单在船运的过程中,与港口相关的关键信息,如离开起运港、到达目的港等。
    在这里插入图片描述

  3. 港口坐标数据
    港口坐标数据描述每个运单在船运的过程中涉及的港口位置信息。
    在这里插入图片描述
    数据说明:
    部分地址信息可能已脱敏、缺失或有偏差,选手可自行补充或修正。

  4. 测试运单数据
    测试运单数据为运单运输过程中的不同位置点所构成,供选手测试对应的ETA时间。测试运单数据如下表描述。
    在这里插入图片描述
    二、选手提交结果
    所有参与竞赛的选手登录到大赛平台,提交结果数据,具体提交格式要求:
    在这里插入图片描述
    其中,ETA为选手评估的时间值;creatDate为该表或该CSV文件创建时间,用于区别多次提交数据。对于未提交的运单ETA,后台统一取timestamp时间计算。
    三、 评估标准
    选手提交结果的评估指标是MSE,即ARRIVAL AT PORT预测时间ETA与真实时间ATA的差距的平方和,计算如下:
    在这里插入图片描述

其中:
(1) hETA为同一个货物运单到达目的港口的预测所需时间。选手提供DATE时间,评测程序转换为单位所需时间,单位:小时。
(2) hATA为同一个货物运单到达目的港口的实际所需时间,大赛测评程序后台保存,用于测评运算。
(3) ETA_NUM为预测的ETA数量,测评程序后台运算,大赛测评程序后台保存,用于测评运算。
最终使用MSE值作为参赛选手得分,MSE值越小,排名越靠前。

示例说明:
如某一货物运单路由CNSHK-MYPKG-MYTPP,已离开起运港CNSHK,SHIPMENT ONBOARD DATE为2019/09/05 16:33:17,通过经纬度等信息判断船位置在CNSHK与MYPKG之间,根据,预测目的港口MYTPP的时间,提交的ETA:”2019/09/18 22:28:46”。
在这里插入图片描述

ARRIVAL AT PORT实际到达目的港MYTPP为2019/09/18 13:28:46;选手预测为2019/09/18 22:28:46;
目的港MYTPP的hETA1为(2019/09/18 22:28:46) – (2019/09/05 16:33:17) = 317.9925 (单位:小时),hATA1为(2019/09/18 13:28:4) – (2019/09/05 16:33:17) = 308.9925 (单位:小时)。
只算此条路径MSE:

在这里插入图片描述
刚开始看到赛题确实给我整晕了,“脱敏”“路由”“识别码”…啥玩意儿?

还好官方给了赛题讲解直播,看完直播再结合赛题,总算让我对题目有了一个大概的认知以及初期处理的思路。

官方给的数据有三个维度,历史运单GPS数据、历史运单事件数据、港口坐标数据,一共二十六个字段,着实看得人眼花缭乱。但是仔细分析测试运单数据之后,瞬间就理清了思绪。测试数据中一共十个字段,分别是
loadingOrder、timestamp、longitude、latitude、speed、direction、carrierName、vesselMMSI、onboardDate、TRANSPORT_TRACE
对比历史运单GPS数据,其中vesselNextportETA、vesselStatus、vesselDatasource,这三个字段是测试运单数据没有的,那么也就意味着这三个字段的数据以及历史运单事件数据、港口坐标数据的作用不是用作特征工程,而是用于清洗数据。
(测试运单数据中的onboardDate字段指:船舶离开起运港时间,官方未给直接给出,后续需要自己从历史运单GPS数据挖掘)

1.用合适的方法从给出的数据里提取需要的数据(数据清洗等)

由于这次比赛的数据量很大,历史运单GPS数据大概有七亿条(记不太清了),脏数据会给预测结果带来很大的偏差,所以清洗数据是重中之重,听说有某大佬只清洗数据使用姜大佬的baseline就打到了三位数tql!
由于近二十个G的历史运单GPS数据都在一个csv文件中,一般的机器是无法一次性读取出来的,我使用的是pandas库的read_csv()函数,通过设置chunksize参数分块读取并处理数据:

为了降低如此庞大的数据所占的内存,在读取数据时对数据进行了字典压缩以及通过改变数据格式来降低内存的操作:

  • 1.以下是通过改变数据格式来降低内存的函数:
#降内存函数
def reduce_mem_usage(props):
    # 计算当前内存
    start_mem_usg = props.memory_usage().sum() / 1024 ** 2
    print("Memory usage of the dataframe is :", start_mem_usg, "MB")
    
    # 哪些列包含空值,空值用-999填充。why:因为np.nan当做float处理
    NAlist = []
    for col in props.columns:
        # 这里只过滤了objectd格式,如果你的代码中还包含其他类型,请一并过滤
        if (props[col].dtypes != object):
            
            print("**************************")
            print("columns: ", col)
            print("dtype before", props[col].dtype)
            
            # 判断是否是int类型
            isInt = False
            mmax = props[col].max()
            mmin = props[col].min()
            
            # # Integer does not support NA, therefore Na needs to be filled
            # if not np.isfinite(props[col]).all():
            #     NAlist.append(col)
            #     props[col].fillna(-999, inplace=True) # 用-999填充
                
            # test if column can be converted to an integer
            asint = props[col].fillna(0).astype(np.int64)
            result = np.fabs(props[col] - asint)
            result = result.sum()
            if result < 0.01: # 绝对误差和小于0.01认为可以转换的,要根据task修改
                isInt = True
            
            # make interger / unsigned Integer datatypes
            if isInt:
                if mmin >= 0: # 最小值大于0,转换成无符号整型
                    if mmax <= 255:
                        props[col] = props[col].astype(np.uint8)
                    elif mmax <= 65535:
                        props[col] = props[col].astype(np.uint16)
                    elif mmax <= 4294967295:
                        props[col] = props[col].astype(np.uint32)
                    else:
                        props[col] = props[col].astype(np.uint64)
                else: # 转换成有符号整型
                    if mmin > np.iinfo(np.int8).min and mmax < np.iinfo(np.int8).max:
                        props[col] = props[col].astype(np.int8)
                    elif mmin > np.iinfo(np.int16).min and mmax < np.iinfo(np.int16).max:
                        props[col] = props[col].astype(np.int16)
                    elif mmin > np.iinfo(np.int32).min and mmax < np.iinfo(np.int32).max:
                        props[col] = props[col].astype(np.int32)
                    elif mmin > np.iinfo(np.int64).min and mmax < np.iinfo(np.int64).max:
                        props[col] = props[col].astype(np.int64)  
            else: # 注意:这里对于float都转换成float16,需要根据你的情况自己更改
                props[col] = props[col].astype(np.float16)
            
            print("dtype after", props[col].dtype)
            print("********************************")
    print("___MEMORY USAGE AFTER COMPLETION:___")
    mem_usg = props.memory_usage().sum() / 1024**2 
    print("Memory usage is: ",mem_usg," MB")
    print("This is ",100*mem_usg/start_mem_usg,"% of the initial size")
    return props#, NAlist
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 2.获取数据字典
import pandas as pd
from tqdm import tqdm
import numpy as np

from sklearn.metrics import mean_squared_error,explained_variance_score
from sklearn.model_selection import KFold
import lightgbm as lgb

import warnings
warnings.filterwarnings('ignore')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
train_gps_path = 'D:/datalab/train0523/train0523.csv'

dir_loadingOrder={}
loadingOrder_flux=pd.read_csv(train_gps_path,chunksize=1000000,header=None,usecols=[0])
for data in tqdm(loadingOrder_flux):
    for i,element in enumerate(data[0].unique()):
        if(element not in list(dir_loadingOrder.keys())):
            dir_loadingOrder[element]=len(dir_loadingOrder)+1

#保存为csv文件
data_loadingOrder={'key':list(dir_loadingOrder.keys()),'value':list(dir_loadingOrder.values())}
df_loadingOrder=pd.DataFrame(data_loadingOrder)
df_loadingOrder.to_csv('df_loadingOrder.csv', index=False)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

对carrierName、vesselMMSI、TRANSPORT_TRACE进行同样操作

  • 接下来开始分三次读取数据:
    以下是获取0-4chunk的数据,对于5-9chunk、10-15chunk以同样方式处理
import pandas as pd
from tqdm import tqdm
import numpy as np

from sklearn.metrics import mean_squared_error,explained_variance_score
from sklearn.model_selection import KFold
import lightgbm as lgb

import warnings
warnings.filterwarnings('ignore')

#获取数据字典
df_loadingOrder=pd.read_csv('./df_loadingOrder.csv')
dict_loadingOrder={}
for key,value in enumerate(df_loadingOrder.key):
    dict_loadingOrder[value]=key

df_carrierName=pd.read_csv('./df_carrierName.csv')
dict_carrierName={}
for key,value in enumerate(df_carrierName.key):
    dict_carrierName[value]=key

df_vesselMMSI=pd.read_csv('./df_vesselMMSI.csv')
dict_vesselMMSI={}
for key,value in enumerate(df_vesselMMSI.key):
    dict_vesselMMSI[value]=key

df_TRANSPORT_TRACE=pd.read_csv('./df_TRANSPORT_TRACE.csv')
dict_TRANSPORT_TRACE={}
for key,value in enumerate(df_TRANSPORT_TRACE.key):
    dict_TRANSPORT_TRACE[value]=key

#字典压缩
def dict_compress(data):
    data['loadingOrder_factorize']=data.loadingOrder.map(dict_loadingOrder)
    data['carrierName_factorize']=data.carrierName.map(dict_carrierName)
    data['vesselMMSI_factorize']=data.vesselMMSI.map(dict_vesselMMSI)
    data['TRANSPORT_TRACE_factorize']=data.TRANSPORT_TRACE.map(dict_TRANSPORT_TRACE)
    data.drop(['loadingOrder','carrierName','vesselMMSI','TRANSPORT_TRACE'],axis=1,inplace=True)
    return data

train_gps_path = 'D:/datalab/train0523/train0523.csv'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
#开始处理数据0-4chunk
train_flux=pd.read_csv(train_gps_path,chunksize=10000000,header=None)
## 注意 train里面的运单不是按顺序放好的,所以要读取完整的loadingOrder数据最好从头到尾扫描一遍
count=0
loadingOrders=[] # 可以用来记录读取了多少个loadingOrders
train_data_raw=pd.DataFrame(columns=['loadingOrder_factorize','carrierName_factorize','timestamp','longitude',
                  'latitude','vesselMMSI_factorize','speed','direction','TRANSPORT_TRACE_factorize'])
for data in tqdm(train_flux):
    if(count<5):
        data.columns = ['loadingOrder','carrierName','timestamp','longitude',
                  'latitude','vesselMMSI','speed','direction','vesselNextport',
                  'vesselNextportETA','vesselStatus','vesselDatasource','TRANSPORT_TRACE']
        data=reduce_mem_usage(data)#转换数据格式
        data=dict_compress(data)#字典压缩
        data=data.drop_duplicates(['loadingOrder_factorize','timestamp'])#根据运单号和时间戳去重
        train_data_raw=train_data_raw.append(data)
        count=count+1
    else:
        break

train_data_raw=train_data_raw.drop_duplicates(['loadingOrder_factorize','timestamp'])#再次去重
train_data_raw.info()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
0it [00:00, ?it/s]
Memory usage of the dataframe is : 991.8214111328125 MB
**************************
columns:  longitude
dtype before float64
dtype after float16
********************************
**************************
columns:  latitude
dtype before float64
dtype after float16
********************************
**************************
columns:  speed
dtype before int64
dtype after uint8
********************************
**************************
columns:  direction
dtype before int64
dtype after int32
********************************
___MEMORY USAGE AFTER COMPLETION:___
Memory usage is:  772.476318359375  MB
This is  77.88461810650854 % of the initial size
1it [00:24, 24.13s/it]
Memory usage of the dataframe is : 991.8214149475098 MB
**************************
columns:  longitude
dtype before float64
dtype after float16
********************************
**************************
columns:  latitude
dtype before float64
dtype after float16
********************************
**************************
columns:  speed
dtype before int64
dtype after uint8
********************************
**************************
columns:  direction
dtype before int64
dtype after int32
********************************
___MEMORY USAGE AFTER COMPLETION:___
Memory usage is:  772.4763221740723  MB
This is  77.8846181915677 % of the initial size
2it [00:50, 24.89s/it]
Memory usage of the dataframe is : 991.8214149475098 MB
**************************
columns:  longitude
dtype before float64
dtype after float16
********************************
**************************
columns:  latitude
dtype before float64
dtype after float16
********************************
**************************
columns:  speed
dtype before int64
dtype after uint8
********************************
**************************
columns:  direction
dtype before int64
dtype after int32
********************************
___MEMORY USAGE AFTER COMPLETION:___
Memory usage is:  772.4763221740723  MB
This is  77.8846181915677 % of the initial size
3it [01:20, 26.40s/it]
Memory usage of the dataframe is : 991.8214149475098 MB
**************************
columns:  longitude
dtype before float64
dtype after float16
********************************
**************************
columns:  latitude
dtype before float64
dtype after float16
********************************
**************************
columns:  speed
dtype before int64
dtype after uint8
********************************
**************************
columns:  direction
dtype before int64
dtype after int32
********************************
___MEMORY USAGE AFTER COMPLETION:___
Memory usage is:  772.4763221740723  MB
This is  77.8846181915677 % of the initial size
4it [01:59, 30.02s/it]
Memory usage of the dataframe is : 991.8214149475098 MB
**************************
columns:  longitude
dtype before float64
dtype after float16
********************************
**************************
columns:  latitude
dtype before float64
dtype after float16
********************************
**************************
columns:  speed
dtype before int64
dtype after uint8
********************************
**************************
columns:  direction
dtype before int64
dtype after int32
********************************
___MEMORY USAGE AFTER COMPLETION:___
Memory usage is:  772.4763221740723  MB
This is  77.8846181915677 % of the initial size
5it [02:58, 38.85s/it]
<class 'pandas.core.frame.DataFrame'>
Int64Index: 26073281 entries, 0 to 49999999
Data columns (total 13 columns):
TRANSPORT_TRACE_factorize    object
carrierName_factorize        object
direction                    object
latitude                     float16
loadingOrder_factorize       object
longitude                    float16
speed                        object
timestamp                    object
vesselDatasource             object
vesselMMSI_factorize         object
vesselNextport               object
vesselNextportETA            object
vesselStatus                 object
dtypes: float16(2), object(11)
memory usage: 2.4+ GB
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144

保存0-4chunk

train_data_raw.to_csv('D:/datalab/train_propress_bychunk/train_propress_0-4chunk.csv', index=False)
  • 1

经过上述操作后,20G+的数据被压缩成了8G+,还是很有效果的哈哈
得到的数据中包含以下字段:

'loadingOrder_factorize','carrierName_factorize','timestamp','longitude',
'latitude','vesselMMSI_factorize','speed','direction','vesselNextport',
 'vesselNextportETA','vesselStatus','vesselDatasource','TRANSPORT_TRACE_factorize'
  • 1
  • 2
  • 3

我的清洗数据的处理思路是尽可能保留完整的数据,这样后续如果要用到的话不至于再花时间重新从原始csv文件中读,以上代码大概花了超过二十四小时运行。。

以上是清洗历史运单GPS数据,历史运单事件数据、港口坐标数据是用于清洗历史运单GPS数据中的错误数据,但由于这两个数据文件是人工录入的,所以有很多数据是有误的。所以用之前还得自己纠正,比较耗费时间,当时我就没有考虑这块(据说用了跟没用差不多)

2.根据需要进行特征工程

特征工程这块,当时正式赛开赛第二天好像,姜大佬在群里分享了他做的baseline(十分感谢)。这块可以说我是站在“巨人”的肩膀上进行的哈哈

先进行字典解压缩:

#获取数据字典
df_loadingOrder=pd.read_csv('./df_loadingOrder.csv')
dict_loadingOrder={}
for key,value in enumerate(df_loadingOrder.key):
    dict_loadingOrder[key]=value
    
df_carrierName=pd.read_csv('./df_carrierName.csv')
dict_carrierName={}
for key,value in enumerate(df_carrierName.key):
    dict_carrierName[key]=value

df_vesselMMSI=pd.read_csv('./df_vesselMMSI.csv')
dict_vesselMMSI={}
for key,value in enumerate(df_vesselMMSI.key):
    dict_vesselMMSI[key]=value

df_TRANSPORT_TRACE=pd.read_csv('./df_TRANSPORT_TRACE.csv')
dict_TRANSPORT_TRACE={}
for key,value in enumerate(df_TRANSPORT_TRACE.key):
    dict_TRANSPORT_TRACE[key]=value

#字典压缩
def dict_discompress(data):
    data['loadingOrder']=data.loadingOrder_factorize.map(dict_loadingOrder)
    data['carrierName']=data.carrierName_factorize.map(dict_carrierName)
    data['vesselMMSI']=data.vesselMMSI_factorize.map(dict_vesselMMSI)
    data['TRANSPORT_TRACE']=data.TRANSPORT_TRACE_factorize.map(dict_TRANSPORT_TRACE)
    data.drop(['loadingOrder_factorize','carrierName_factorize','vesselMMSI_factorize','TRANSPORT_TRACE_factorize'],axis=1,inplace=True)
    return data

#train数据集字典解压缩
train_data=dict_discompress(train_data)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  1. loadingOrder

loadingOrder订单编号,官方给出的解释是类似于快递单号,是字母与数字的组合
在这里插入图片描述
我的猜想是不同的字母指的是不同的海运公司或是不一样的航线(网上也没有明确的解释),数字的部分应该是仅作区别,字母部分可以作为一个特征。但是这样做的效果不好,原因是测试集的数据里的loadingOrder字母部分与训练集极少重叠,所以这个想法只能作罢。
最终我选择在feature数组中删除loadingOrder。

  1. carrierName_factorize

carrierName,承运商名称,类似快递公司名称。同loadingOrder,这种方法只有在测试集字段的unique值是训练集的字段的unique值的子集时才会有很好效果,但是此题不符合这个要求。

  1. vesselMMSI

vesselMMSI,唯一标识每一艘船,同上,删除。

  1. TRANSPORT_TRACE

TRANSPORT_TRACE,船的路由,由“-”连接组成,例如CNSHK-MYPKG-MYTPP。
相当于是船的路线图,很容易联想到经过的地方越多,路由越长,那么一般来讲船的到达时间会越长。但是很可惜,测试数据中的TRANSPORT_TRACE字段只保留了重点和起点。
无法提取特征,那么还可以用来清洗数据。
这块我的思路是只在训练集中选择TRANSPORT_TRACE字段值包含在测试集的TRANSPORT_TRACE中的记录,在正式赛阶段二中实现了

  1. timestamp

这道题并没有直接给出label需要自己计算。
我使用的是姜大佬的baseline中确定label的方法,对训练集的loadingOrder进行分组,然后根据timestamp进行排序,再用最后一条记录的timestamp减去第一条记录的timestamp,从而得到label。当然此方法是有缺陷的,因为无法排除塞港等等其他的影响,所以后续我有做一些改进,在正式赛篇阶段二中补充。

  1. latitude/longitude/direction/speed

latitude:纬度坐标
longitude:经度坐标
direction:当前船舶的行驶方向,正北是0度,31480代表西北方向314.80度,900代表正北偏东9度
speed:单位km/h,货物在运输过程中,当前船舶的瞬时速度
这块我的处理是把同一订单的经纬度、速度、方向的min,max,mean, median作为特征

def get_feature(df):
    group_df = df.groupby('loadingOrder_factorize')['timestamp'].agg(mmax='max', count='count', mmin='min').reset_index()
    # 读取数据的最大值-最小值,即确认时间间隔为label
    group_df['label'] = (group_df['mmax'] - group_df['mmin']).dt.total_seconds()
    agg_function = ['min', 'max', 'mean', 'median']
    agg_col = ['latitude', 'longitude', 'speed', 'direction']

    group = df.groupby('loadingOrder_factorize')[agg_col].agg(agg_function).reset_index()#根据loadingOrder对agg_col分组,应用agg_function聚合
    group.columns = ['loadingOrder_factorize'] + ['{}_{}'.format(i, j) for i in agg_col for j in agg_function]#生成新列,共十七列
    temp=df[['loadingOrder_factorize','TRANSPORT_TRACE_factorize','carrierName_factorize','vesselMMSI_factorize']]
    group_df=group_df.merge(temp,on='loadingOrder_factorize',how='left')
    group_df = group_df.merge(group, on='loadingOrder_factorize', how='left')#连接表
    group_df.drop_duplicates(inplace=True)#去重
    return group_df
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

或许可以试试用latitude_max-latitude_min得到一个船只航行路线所跨的经度值,这样或许可以更好的描述航行路线,longitude也是一样(为什么不早点想到o(╥﹏╥)o)

  1. vesselNextportETA/vesselNextport

vesselNextportETA:船运公司给出的到“下一个港口”预计到达时间
vesselNextport:船舶将要到达的下一港口
这两个字段测试数据并没有包含,所以无法作为特征,作为清洗数据的依据我也没有想出什么可行的方法,能力有限,就先删除吧,嘻嘻

  1. vesselStatus

vesselStatus,当前船舶航行状态,主要包括:moored(系泊:靠于码头、浮筒或他船),under way using engine(动力机在航),not under command(不受指挥/控制),at anchor(抛锚:指投锚于水中使船泊定),under way sailing(帆船在航 ),constrained by her draught(受吃水的限制)

很明显这个字段的数据是用于清洗数据的 ,我尝试过删除包含moored、not under command、at anchor的记录,但是效果不明显,与别人交流之后得知大家似乎都对这个字段束手无策,没有什么好的方法利用起来,最终我也是选择了删除。

  1. vesselDatasource

vesselDatasource,船舶数据来源(岸基/卫星):Coastal AIS,Satellite
这块我的想法是可以删除数据不准确的记录,但是网上找不到岸基和卫星数据准确性比较的资料,所以这个字段我选择从feature数组中删除。

  1. disease

考虑到这次新冠疫情对海运的影响,我增加了一个disease特征,数据类型为category。在网上查阅了资料,我选定二月一号为时间分割点,二月一号以前的disease置0,二月一号以后的disease置1

#增加disease特征
def get_d_test(x):
    timeindex=pd.to_datetime('2019-02-01 00:00:00', infer_datetime_format=True)
    if x<timeindex:
        return 0
    else:
        return 1
train_data['mmin']=pd.to_datetime(train_data['mmin'], infer_datetime_format=True)
train_data['disease']=train_data.apply(lambda row:get_d_test(row['mmin']),axis=1)

temp=test_data[['loadingOrder','onboardDate']]
temp.drop_duplicates(subset=['loadingOrder'],inplace=True)
test=test.merge(temp,on='loadingOrder', how='left')
test['disease']=test.apply(lambda row:get_d_test(row['onboardDate']),axis=1)

train['disease']=train['disease'].astype('category')
test['disease']=test['disease'].astype('category')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

当然二月一号这个时间肯定是不准确的,后续可以尝试不同的时间分界线

3.模型融合

这块大概分三步走:

  • 1.特征筛选
    贴一下特征筛选的函数吧(参考大树先生的博客):
def get_top_n_features(train_data_X, train_data_Y, top_n_features):
#训练集特征:train_data_X,训练集label:train_data_Y,限定特征个数:top_n_features
    # random forest
    rf_est = RandomForestClassifier(random_state=0)
    rf_param_grid = {'n_estimators': [500], 'min_samples_split': [2, 3], 'max_depth': [20]}
    rf_grid = model_selection.GridSearchCV(rf_est, rf_param_grid, n_jobs=25, cv=10, verbose=1)
    rf_grid.fit(train_data_X, train_data_Y)
    print('Top N Features Best RF Params:' + str(rf_grid.best_params_))
    print('Top N Features Best RF Score:' + str(rf_grid.best_score_))
    print('Top N Features RF Train Score:' + str(rf_grid.score(train_data_X, train_data_Y)))
    feature_imp_sorted_rf = pd.DataFrame({'feature': list(train_data_X),
                                          'importance': rf_grid.best_estimator_.feature_importances_}).sort_values('importance', ascending=False)
    features_top_n_rf = feature_imp_sorted_rf.head(top_n_features)['feature']
    print('Sample 10 Features from RF Classifier')
    print(str(features_top_n_rf[:10]))

    # AdaBoost
    ada_est =AdaBoostClassifier(random_state=0)
    ada_param_grid = {'n_estimators': [500], 'learning_rate': [0.01, 0.1]}
    ada_grid = model_selection.GridSearchCV(ada_est, ada_param_grid, n_jobs=25, cv=10, verbose=1)
    ada_grid.fit(train_data_X, train_data_Y)
    print('Top N Features Best Ada Params:' + str(ada_grid.best_params_))
    print('Top N Features Best Ada Score:' + str(ada_grid.best_score_))
    print('Top N Features Ada Train Score:' + str(ada_grid.score(train_data_X, train_data_Y)))
    feature_imp_sorted_ada = pd.DataFrame({'feature': list(train_data_X),
                                           'importance': ada_grid.best_estimator_.feature_importances_}).sort_values('importance', ascending=False)
    features_top_n_ada = feature_imp_sorted_ada.head(top_n_features)['feature']
    print('Sample 10 Feature from Ada Classifier:')
    print(str(features_top_n_ada[:10]))

    # ExtraTree
    et_est = ExtraTreesClassifier(random_state=0)
    et_param_grid = {'n_estimators': [500], 'min_samples_split': [3, 4], 'max_depth': [20]}
    et_grid = model_selection.GridSearchCV(et_est, et_param_grid, n_jobs=25, cv=10, verbose=1)
    et_grid.fit(train_data_X, train_data_Y)
    print('Top N Features Best ET Params:' + str(et_grid.best_params_))
    print('Top N Features Best ET Score:' + str(et_grid.best_score_))
    print('Top N Features ET Train Score:' + str(et_grid.score(train_data_X, train_data_Y)))
    feature_imp_sorted_et = pd.DataFrame({'feature': list(train_data_X),
                                          'importance': et_grid.best_estimator_.feature_importances_}).sort_values('importance', ascending=False)
    features_top_n_et = feature_imp_sorted_et.head(top_n_features)['feature']
    print('Sample 10 Features from ET Classifier:')
    print(str(features_top_n_et[:10]))
    
    # GradientBoosting
    gb_est =GradientBoostingClassifier(random_state=0)
    gb_param_grid = {'n_estimators': [500], 'learning_rate': [0.01, 0.1], 'max_depth': [20]}
    gb_grid = model_selection.GridSearchCV(gb_est, gb_param_grid, n_jobs=25, cv=10, verbose=1)
    gb_grid.fit(train_data_X, train_data_Y)
    print('Top N Features Best GB Params:' + str(gb_grid.best_params_))
    print('Top N Features Best GB Score:' + str(gb_grid.best_score_))
    print('Top N Features GB Train Score:' + str(gb_grid.score(train_data_X, train_data_Y)))
    feature_imp_sorted_gb = pd.DataFrame({'feature': list(train_data_X),
                                           'importance': gb_grid.best_estimator_.feature_importances_}).sort_values('importance', ascending=False)
    features_top_n_gb = feature_imp_sorted_gb.head(top_n_features)['feature']
    print('Sample 10 Feature from GB Classifier:')
    print(str(features_top_n_gb[:10]))
    
    # DecisionTree
    dt_est = DecisionTreeClassifier(random_state=0)
    dt_param_grid = {'min_samples_split': [2, 4], 'max_depth': [20]}
    dt_grid = model_selection.GridSearchCV(dt_est, dt_param_grid, n_jobs=25, cv=10, verbose=1)
    dt_grid.fit(train_data_X, train_data_Y)
    print('Top N Features Best DT Params:' + str(dt_grid.best_params_))
    print('Top N Features Best DT Score:' + str(dt_grid.best_score_))
    print('Top N Features DT Train Score:' + str(dt_grid.score(train_data_X, train_data_Y)))
    feature_imp_sorted_dt = pd.DataFrame({'feature': list(train_data_X),
                                          'importance': dt_grid.best_estimator_.feature_importances_}).sort_values('importance', ascending=False)
    features_top_n_dt = feature_imp_sorted_dt.head(top_n_features)['feature']
    print('Sample 10 Features from DT Classifier:')
    print(str(features_top_n_dt[:10]))
    
    # merge the three models
    features_top_n = pd.concat([features_top_n_rf, features_top_n_ada, features_top_n_et, features_top_n_gb, features_top_n_dt], 
                               ignore_index=True).drop_duplicates()
    
    features_importance = pd.concat([feature_imp_sorted_rf, feature_imp_sorted_ada, feature_imp_sorted_et, 
                                   feature_imp_sorted_gb, feature_imp_sorted_dt],ignore_index=True)
    
    return features_top_n , features_importance
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 2.依据筛选出的特征构建训练集和测试集
feature_to_pick = 15#根据需要调整
train_data_X=train[features]#feature为前面特征工程处理好的特征数组
train_data_Y=train['label']
feature_top_n, feature_importance = get_top_n_features(train_data_X, train_data_Y, feature_to_pick)
  • 1
  • 2
  • 3
  • 4
train_data_X=train[feature_top_n]
test_data_X=test[feature_top_n]
train_data_Y=train['label']
  • 1
  • 2
  • 3
  • 3.模型融合
    转自大树先生-Kaggle_Titanic生存预测 – 详细流程吐血梳理
    常见的模型融合方法有:Bagging、Boosting、Stacking、Blending。
    (3-1):Bagging
    Bagging 将多个模型,也就是多个基学习器的预测结果进行简单的加权平均或者投票。它的好处是可以并行地训练基学习器。Random Forest就用到了Bagging的思想。
    (3-2): Boosting
    Boosting 的思想有点像知错能改,每个基学习器是在上一个基学习器学习的基础上,对上一个基学习器的错误进行弥补。我们将会用到的 AdaBoost,Gradient Boost 就用到了这种思想。
    (3-3): Stacking
    Stacking是用新的次学习器去学习如何组合上一层的基学习器。如果把 Bagging 看作是多个基分类器的线性组合,那么Stacking就是多个基分类器的非线性组合。Stacking可以将学习器一层一层地堆砌起来,形成一个网状的结构。
    相比来说Stacking的融合框架相对前面的二者来说在精度上确实有一定的提升,所以在下面的模型融合上,我们也使用Stacking方法。
    (3-4): Blending
    Blending 和 Stacking 很相似,但同时它可以防止信息泄露的问题。

Stacking框架融合:
热身赛我使用了两层的模型融合,Level 1使用了:RandomForestRegressor、AdaBoostRegressor、GradientBoostingRegressor、DecisionTreeRegressor、DecisionTree、SVM ,一共7个模型,Level 2使用了XGBoost使用第一层预测的结果作为特征对最终的结果进行预测。

Level 1:
Stacking框架是堆叠使用基础分类器的预测作为对二级模型的训练的输入。 然而,不能简单地在全部训练数据上训练基本模型,产生预测,输出用于第二层的训练。
如果在Train Data上训练,然后在Train Data上预测,就会造成标签。为了避免标签,需要对每个基学习器使用K-fold,将K个模型对Valid Set的预测结果拼起来,作为下一层学习器的输入。

这里是建立输出fold预测方法:

from sklearn.model_selection import KFold

# Some useful parameters which will come in handy later on
ntrain = train_data_X.shape[0]
ntest = text_data_X.shape[0]
SEED = 0 # for reproducibility
NFOLDS = 7 # set folds for out-of-fold prediction
kf = KFold(n_splits = NFOLDS, random_state=SEED, shuffle=False)
#n_splits:分成几份,shuffle:在每次划分时,是否洗牌

def get_out_fold(clf, x_train, y_train, x_test):
    oof_train = np.zeros((ntrain,))#np.zeros返回来一个给定形状和类型的用0填充的数组
    oof_test = np.zeros((ntest,))
    oof_test_skf = np.empty((NFOLDS, ntest))#np.empty依据给定形状和类型(shape,[dtype, order])返回一个新的随机数数组
    for i, (train_index, test_index) in enumerate(kf.split(x_train)):#https://www.runoob.com/python/python-func-enumerate.html
        x_tr = x_train[train_index]
        y_tr = y_train[train_index]
        x_te = x_train[test_index]

        clf.fit(x_tr, y_tr)

        oof_train[test_index] = clf.predict(x_te)
        oof_test_skf[i, :] = clf.predict(x_test)

    oof_test[:] = oof_test_skf.mean(axis=0)
    return oof_train.reshape(-1, 1), oof_test.reshape(-1, 1)#数据变成了一列
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

构建不同的基学习器:

from sklearn import ensemble
from sklearn import model_selection 
from sklearn.ensemble import RandomForestRegressor
from sklearn.ensemble import AdaBoostRegressor
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import ExtraTreesRegressor
rf = RandomForestRegressor(n_estimators=500, warm_start=True, max_features='sqrt',max_depth=6, 
                            min_samples_split=3, min_samples_leaf=2, n_jobs=-1, verbose=0)
ada = AdaBoostRegressor(n_estimators=500, learning_rate=0.1)
gb = GradientBoostingRegressor(n_estimators=500, learning_rate=0.008, min_samples_split=3, min_samples_leaf=2, max_depth=5, verbose=0)
dt = DecisionTreeRegressor(max_depth=8)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

将pandas转换为arrays:

# Create Numpy arrays of train, test and target (Survived) dataframes to feed into our models
x_train = train_data_X.values # Creates an array of the train data
x_test = test_data_X.values # Creats an array of the test data
y_train = train_data_Y.values
  • 1
  • 2
  • 3
  • 4

使用KFOLD:

rf_oof_test = get_out_fold(rf, x_train, y_train, x_test) # Random Forest
ada_oof_test = get_out_fold(ada, x_train, y_train, x_test) # AdaBoost 
gb_oof_test = get_out_fold(gb, x_train, y_train, x_test) # Gradient Boost
dt_oof_test = get_out_fold(dt, x_train, y_train, x_test) # Decision Tree
  • 1
  • 2
  • 3
  • 4

得到训练集和测试集:

x_train = np.concatenate((rf_oof_train, ada_oof_train, gb_oof_train, dt_oof_train), axis=1)
x_test = np.concatenate((rf_oof_train, ada_oof_train, gb_oof_train, dt_oof_train), axis=1)
y_train=train_data_Y
  • 1
  • 2
  • 3

模型融合的方式有很多种,我还尝试了用五个算法(RandomForestRegressor、AdaBoostRegressor、GradientBoostingRegressor、DecisionTreeRegressor、ExtraTreesRegressor)预测的label的均值做为result,不过这个方法效果不太好哈哈

4.划分训练集和验证集,得到预测结果

def mse_score_eval(preds, valid):
    labels = valid.get_label()
    scores = mean_squared_error(y_true=labels, y_pred=preds)
    return 'mse_score', scores, True


train_pred = np.zeros((x_train.shape[0], ))
test_pred = np.zeros((x_test.shape[0], ))
n_splits = 5
# Kfold
fold = KFold(n_splits=n_splits, shuffle=True, random_state=1080)
kf_way = fold.split(x_train)#pred为除去['loadingOrder', 'label', 'mmin', 'mmax', 'count']的特征
# params
params = {
    'learning_rate': 0.01,
    'boosting_type': 'gbdt',
    'objective': 'regression',
    'num_leaves': 36,
    'feature_fraction': 0.6,
    'bagging_fraction': 0.7,
    'bagging_freq': 6,
    'seed': 8,
    'bagging_seed': 1,
    'feature_fraction_seed': 7,
    'min_data_in_leaf': 20,
    'nthread': 8,
    'verbose': 1,
}
# train
for n_fold, (train_idx, valid_idx) in enumerate(kf_way, start=1):#kf_way:返回样本切分之后训练集和验证集的index,即索引
    train_x, train_y = x_train[train_idx], train['label'].iloc[train_idx]#生成训练集_X,训练集_Y
    valid_x, valid_y = x_train[valid_idx], train['label'].iloc[valid_idx]#生成验证集_X,验证集_Y
    # 数据加载
    n_train = lgb.Dataset(train_x, label=train_y)
    n_valid = lgb.Dataset(valid_x, label=valid_y)

    clf = lgb.train(
        params=params,
        train_set=n_train,#训练
        num_boost_round=3000,
        valid_sets=[n_valid],
        early_stopping_rounds=100,
        verbose_eval=100,
        feval=mse_score_eval
    )
    train_pred[valid_idx] = clf.predict(valid_x, num_iteration=clf.best_iteration)#交叉验证之后得到的训练集的label

    test_pred += clf.predict(x_test, num_iteration=clf.best_iteration)/fold.n_splits#每一次迭代(共十次)由n_train训练的clf对test_pred预测得到的label/10,并累加

test['label'] = test_pred

result = test[['loadingOrder', 'label']]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
Training until validation scores don't improve for 100 rounds
[100]	valid_0's l2: 2.04768e+11	valid_0's mse_score: 2.04768e+11
Early stopping, best iteration is:
[1]	valid_0's l2: 8.22707e+11	valid_0's mse_score: 8.22707e+11
Training until validation scores don't improve for 100 rounds
[100]	valid_0's l2: 4.60331e+11	valid_0's mse_score: 4.60331e+11
Early stopping, best iteration is:
[1]	valid_0's l2: 1.27532e+12	valid_0's mse_score: 1.27532e+12
Training until validation scores don't improve for 100 rounds
[100]	valid_0's l2: 2.72004e+11	valid_0's mse_score: 2.72004e+11
Early stopping, best iteration is:
[1]	valid_0's l2: 8.924e+11	valid_0's mse_score: 8.924e+11
Training until validation scores don't improve for 100 rounds
[100]	valid_0's l2: 5.88297e+11	valid_0's mse_score: 5.88297e+11
Early stopping, best iteration is:
[1]	valid_0's l2: 1.49435e+12	valid_0's mse_score: 1.49435e+12
Training until validation scores don't improve for 100 rounds
[100]	valid_0's l2: 3.07061e+11	valid_0's mse_score: 3.07061e+11
Early stopping, best iteration is:
[1]	valid_0's l2: 9.27033e+11	valid_0's mse_score: 9.27033e+11

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

再一次使用KFOLD,这样就得到了最终的预测结果,最后按照比赛要求的格式生成result文件就好咯

test_data = test_data.merge(result, on='loadingOrder', how='left')
test_data['ETA'] = (test_data['onboardDate'] + test_data['label'].apply(lambda x:pd.Timedelta(seconds=x))).apply(lambda x:x.strftime('%Y/%m/%d  %H:%M:%S'))
test_data.drop(['direction','TRANSPORT_TRACE'],axis=1,inplace=True)
test_data['onboardDate'] = test_data['onboardDate'].apply(lambda x:x.strftime('%Y/%m/%d  %H:%M:%S'))
test_data['creatDate'] = pd.datetime.now().strftime('%Y/%m/%d  %H:%M:%S')
test_data['timestamp'] = test_data['temp_timestamp']
# 整理columns顺序
result = test_data[['loadingOrder', 'timestamp', 'longitude', 'latitude', 'carrierName', 'vesselMMSI', 'onboardDate', 'ETA', 'creatDate']]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

保存为.csv文件

result.to_csv('result.csv', index=False)
  • 1

这是我在初赛A阶段的排名
**加粗样式
**
B阶段因为一些个人原因没有打,也就直接失去了进入复赛的机会了

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/549011
推荐阅读
相关标签
  

闽ICP备14008679号