機器學習案例-汽車目的地智能預測大賽 附方案代碼

筆者介紹

曾擔任零售企業項目負責人,負責企業數據化轉型,數據化管理;曾服務中國移動,負責客服部門產品推薦模型組組長;現於某金融投資公司大數據中心,負責風控數據建設,風控建模工作。在除工作外,也喜歡在DC、DF、天池、Kaggle參加一些比賽。機器學習方面,有一定經驗,願與各位分享我的所見所聞所想,與各位共同進步。

比賽經歷

零零散散參加了不少比賽,因為個人時間安排原因,有時候很忙沒空,所以不能拖隊友後腿吧?就一直自己耍,經常玩到一半跑路。本次想分享一下前段時間參加的DC的比賽,雖然成績不咋地,參考交流學習。

比賽介紹

機器學習案例-汽車目的地智能預測大賽 附方案代碼

比賽地址:

http://www.dcjingsai.com/common/cmpt/汽車目的地智能預測大賽_競賽信息.html

比賽數據:

地址:https://pan.baidu.com/s/1Xrrzb6gGkHP0R0ERln94kA

密碼:gqx1

競賽背景

美國東北大學一個科研團隊的一項研究表明,人類93%的行為都是可以預測的,想一想我們的規律生活,就會覺得這個數字並不是那麼誇張。也正因如此, 交通才得以合理規劃,城市才得以有序發展。 在實際應用中,其實並沒有那麼容易預測,數據的缺乏是一個重要的原因。那麼在有限的數據下,我們能夠在多大程度上預測出人們的行為呢?

報名開始:

10月12日 11:00

比賽時間:

10月12日 11:00--12月7日 15:00

個人成績:

12/1411

參賽隊伍:

在水裡燒烤

參賽人員:

在水裡燒烤

機器學習案例-汽車目的地智能預測大賽 附方案代碼

評分標準

本次比賽考察預測目的地(經緯度)與實際目的地(經緯度)的球面距離,評分計算方法如下:

其中,di為第i個樣本的預測目的地與實際目的地的球面距離(單位:米),n為測試集樣本個數。注:score的極限最優值大約在0.01798(即當所有目的地的預測誤差均在0米時)。

機器學習案例-汽車目的地智能預測大賽 附方案代碼

機器學習案例-汽車目的地智能預測大賽 附方案代碼

距離與score的關係如下圖,橫軸為距離,縱軸為分值。

機器學習案例-汽車目的地智能預測大賽 附方案代碼

比賽要求

任務

通過學習一些車輛的歷史行程,訓練模型,在給定車輛id、時間和出發地點的情況下,預測該輛車在此次行程中的目的地。

數據

*注 : 報名參賽或加入隊伍後,可獲取數據下載權限。

10月27日更新的訓練集和測試集說明:

1.train_new.csv 訓練集數據,共1495814條,為2018年1月1日到2018年8月1日的數據(包括原測試集中的數據),字段如下:

2.test_new.csv 測試集數據,為抽取的當年9-10月的數據,除無end_time,end_lat,end_lon字段外,其它字段同訓練集數據。共58097條。

機器學習案例-汽車目的地智能預測大賽 附方案代碼

比賽分析

總結來說,給了用戶1-8月的數據,預測9-10月用戶出行時會去哪?

怎麼入手這類題目,首先要問問自己幾個問題?

1 . 我們可以從數據獲取用戶什麼信息?

這份數據比較簡單,就只有用戶行程始終點,對應的時間。

2 . 把這個問題定義成什麼問題更好解決?

我們知道用戶的起點,想知道用戶的終點,終點是一個經緯度,考慮迴歸還是分類?

考慮迴歸,效果應該還不錯,我個人認為決策樹迴歸應該還可以,但是還沒試過。

我個人嘗試的是分類問題,第一個想到的就是貝葉斯分類,用戶這個地點出發,這個時間出發,很大概率是和往常去的是同個地方。但是提供的是經緯度?怎麼進行分類?借鑑了一下摩拜單車的解決方法geohash編碼,把地圖分為好幾塊,每一塊一個點。

3 . 怎麼構建數據進行挖掘,是全數據挖掘?還是分類建模怎麼處理?

考慮到數據量大,分類也很細,我的小電腦跑不動,所以我考慮了分類建模,怎麼分類?用車輛id作為唯一用戶,對每個用戶進行建模。

建模流程

機器學習案例-汽車目的地智能預測大賽 附方案代碼

建模流程

預處理及構建特徵

數據預處理做了如下事情:

  1. 剔除r_key字段;(無用id)
  2. 時間變量衍生;(起始點時間進行特徵生成,生成星期、工作日、休息日、上中下旬等)
  3. 起終點經緯度geohash編碼;(使用geohash這個包)
  4. 統計最常出現的地點作為Home標記,生成出每個出行點離家距離特徵;
  5. 剔除出現次數少於10次的地點;(通過遍歷,得到小於10次的地點為臨時出行點,認為是干擾數據,進行剔除)

模型選擇

二分類多分類?因為是多個地點,所以選擇用多分類器。

第一次使用是貝葉斯分類器、效果一般,線上大概0.45,之後用lightgbm、xgboost,其中xgboost效果較好。

代碼示例

# 加載包
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
import sys, time
import geohash
import copy
import urllib.request
from bs4 import BeautifulSoup
import json
warnings.filterwarnings('ignore')
class ShowProcess():
"""
顯示處理進度的類
調用該類相關函數即可實現處理進度的顯示
"""
i = 0
max_steps = 0
max_arrow = 50
infoDone = 'done'
def __init__(self, max_steps, infoDone = 'Done'):
self.max_steps = max_steps
self.i = 0
self.infoDone = infoDone
def show_process(self, i=None):
if i is not None:
self.i = i
else:
self.i += 1
num_arrow = int(self.i * self.max_arrow / self.max_steps) #計算顯示多少個'>'
num_line = self.max_arrow - num_arrow #計算顯示多少個'-'
percent = self.i * 100.0 / self.max_steps #計算完成進度,格式為xx.xx%
process_bar = '[' + '>' * num_arrow + '-' * num_line + ']'\
+ '%.2f' % percent + '%' + '\r' #帶輸出的字符串,'\r'表示不換行回到最左邊
sys.stdout.write(process_bar) #這兩句打印字符到終端
sys.stdout.flush()

if self.i >= self.max_steps:
self.close()
def close(self):
print('')
print(self.infoDone)
self.i = 0

# - 計算兩點距離
from math import radians, atan, tan, sin, acos, cos
def getDistance(latA, lonA, latB, lonB):
ra = 6378140 # radius of equator: meter
rb = 6356755 # radius of polar: meter
flatten = (ra - rb) / ra # Partial rate of the earth
# change angle to radians
radLatA = radians(latA)
radLonA = radians(lonA)
radLatB = radians(latB)
radLonB = radians(lonB)

try:
pA = atan(rb / ra * tan(radLatA))
pB = atan(rb / ra * tan(radLatB))
x = acos(sin(pA) * sin(pB) + cos(pA) * cos(pB) * cos(radLonA - radLonB))
c1 = (sin(x) - x) * (sin(pA) + sin(pB))**2 / cos(x / 2)**2
c2 = (sin(x) + x) * (sin(pA) - sin(pB))**2 / sin(x / 2)**2
dr = flatten / 8 * (c1 - c2)
distance = ra * (x + dr)
return distance# meter
except:
return 0.0000001
# - 分解時間特徵
import time,datetime
def time_feature(data,col):
data[str(col)+'tm_hour'] = data[col].map(lambda x:time.strptime(x, "%Y-%m-%d %H:%M:%S").tm_hour)
data[str(col)+'tm_min'] = data[col].map(lambda x:time.strptime(x, "%Y-%m-%d %H:%M:%S").tm_min)
data[str(col)+'tm_wday'] = data[col].map(lambda x:time.strptime(x, "%Y-%m-%d %H:%M:%S").tm_wday)
return data


# - 計算得分
def distinct_score(start,end):
train = pd.merge(start,end,left_index=True, right_index=True)
train.columns=['true_lat','ture_lon','pred_lat','pred_lon']
distinct = train.apply(lambda train:getDistance(train['true_lat'],train['ture_lon'],train['pred_lat'],train['pred_lon']),axis=1)
fdi = 1/(1+np.exp(-(distinct-1000)/250))
score = fdi.sum()/fdi.shape[0]
print("Score:",score)
# - 輸出結果

def get_submit(data,path_csv):
data['end_code'] = data.end_encode.map(range_dict)
data['end_lat'] = data.end_code.map(lat_dict)
data['end_lon'] = data.end_code.map(lon_dict)
data[['r_key','end_lat','end_lon']].to_csv(path_csv,index=False)
# - 讀取並清洗數據
def get_data():
print('Get Data start ...')

train = pd.read_csv('train_new.csv')
train['out_id'] = train.out_id.astype(str)
del train['r_key']
test = pd.read_csv('test_new.csv')
test['out_id'] = test.out_id.astype(str)

train = time_feature(train,'start_time')
test = time_feature(test,'start_time')

train['start_encode'] = train.apply(lambda train:geohash.encode(train['start_lat'],train['start_lon'],6),axis=1)
train['end_encode'] = train.apply(lambda train:geohash.encode(train['end_lat'],train['end_lon'],6),axis=1)
test['start_encode'] = test.apply(lambda test:geohash.encode(test['start_lat'],test['start_lon'],6),axis=1)

train['date_time'] = pd.to_datetime(train['start_time']).map(lambda x:x.strftime('20%y%m%d'))
test['date_time'] = pd.to_datetime(test['start_time']).map(lambda x:x.strftime('20%y%m%d'))

print('Get Data over ...')
return train,test
def get_dict():
encode_list = pd.concat([train.start_encode,train.end_encode,test.start_encode],axis=0)
encode_dict = dict(zip(set(encode_list),list(range(0,len(set(encode_list))))))
out_id_dict = dict(zip(set(train.out_id),list(range(0,len(set(train.out_id))))))
range_dict = dict(zip(list(range(0,len(set(encode_list)))),set(encode_list)))
start_ = train[['start_lat','start_lon','start_encode']]
end_ = train[['end_lat','end_lon','end_encode']]
end_.columns=['start_lat','start_lon','start_encode']
encode2 = pd.concat([start_,end_],axis=0)
lat_dict = dict(encode2.groupby('start_encode').start_lat.mean())
lon_dict = dict(encode2.groupby('start_encode').start_lon.mean())

# - 構建節假日字典(後面再考慮假日時間長短)
set_date = set(pd.concat([train['date_time'],test['date_time']]))
max_steps = len(set_date)
process_bar = ShowProcess(max_steps, 'Get dict OK')
date_lable = []
for date_s in set_date:
date_lable.append(Getdatatype(date_s))
process_bar.show_process()
time.sleep(0.01)

date_dict = dict(zip(set_date,date_lable))

return encode_dict,range_dict,out_id_dict,lat_dict,lon_dict,date_dict
# - 獲取是否節假日及工作日
def Getdatatype(datetime_):
url = 'http://api.goseek.cn/Tools/holiday?date='+datetime_
urllib.request.urlopen(url) # 打開url
html = urllib.request.urlopen(url).read() # 讀取內容
return json.loads(html)['data']
# - 進行編碼
def FeatureEncode(data,istrain=True):
encodeData = copy.deepcopy(data)
encodeData['date_time'] = encodeData['date_time'].map(date_dict)
encodeData['out_id'] = encodeData.out_id.map(out_id_dict)
encodeData['start_encode'] = encodeData.start_encode.map(encode_dict)
if istrain == True :
encodeData['end_encode'] = encodeData.end_encode.map(encode_dict)
return encodeData
# - 獲得家庭地址
def Gethomedict(data,col1,col2):
Fdata = copy.deepcopy(data)

Fdata[str(col1)+str(col2)] = Fdata[col1].map(lambda x:str(x)).str.cat(Fdata[col2].map(lambda x:str(x)))
place_dict = dict(Fdata[str(col1)+str(col2)].value_counts())
Fdata['place_time'] = Fdata[str(col1)+str(col2)].map(place_dict)

Fdata['rank_'] = Fdata['place_time'].groupby(Fdata[col1]).rank(ascending=0,method='dense') # - 排序打標籤
unually_dist = dict(zip(Fdata[Fdata.rank_==1][[col1,col2]].drop_duplicates()[col1], Fdata[Fdata.rank_==1][[col1,col2]].drop_duplicates()[col2]))
return unually_dist
# - 計算出行點離家距離
def FeatureHomeDist(data,dict_):
Fdata = copy.deepcopy(data)
Fdata['usual_dist'] = Fdata['out_id'].map(dict_)
Fdata['D_lat'] = Fdata['usual_dist'].map(range_dict).map(lat_dict)
Fdata['D_lon'] = Fdata['usual_dist'].map(range_dict).map(lon_dict)
Fdata['usual_dist'] = Fdata.apply(lambda Fdata:getDistance(Fdata['start_lat'],Fdata['start_lon'],Fdata['D_lat'],Fdata['D_lon']),axis=1)
return Fdata
def main():
# - 讀取數據並構建特徵
train,test = get_data()
encode_dict,range_dict,out_id_dict,lat_dict,lon_dict,date_dict = get_dict()
train_vaid = FeatureEncode(train,istrain=True)
test_vaid = FeatureEncode(test,istrain=False)
train_vaid['out_id_endcode'] = train_vaid['out_id'].map(lambda x:str(x)).str.cat(train_vaid['end_encode'].map(lambda x:str(x)))
unually_dist = Gethomedict(train_vaid,'out_id','start_encode')

train_vaid = FeatureHomeDist(train_vaid,unually_dist)
test_vaid = FeatureHomeDist(test_vaid,unually_dist)
ecut_rule = dict(train_vaid.out_id_endcode.value_counts())
from sklearn.model_selection import train_test_split
X_train,X_test,y_train,y_test = train_test_split(train_vaid[['out_id', 'start_timetm_hour','start_timetm_wday','date_time', 'start_encode', 'usual_dist','out_id_endcode']],train_vaid.end_encode,test_size=0.3,stratify=train_vaid.out_id,random_state=3)
np.random.seed(20)
test_ind = np.random.choice(np.array(list(set(X_test.out_id))),size=200)
X_test = X_test[X_test.out_id.isin(test_ind)][['out_id', 'start_timetm_hour','start_timetm_wday','date_time', 'start_encode', 'usual_dist']]
y_test = y_test[y_test.index.isin(X_test.index)]
X_train = X_train[X_train.out_id_endcode.map(ecut_rule)>=9][['out_id', 'start_timetm_hour','start_timetm_wday','date_time', 'start_encode', 'usual_dist']]
y_train = y_train[y_train.index.isin(X_train.index)]
for_id = set(X_test.out_id)
max_steps = len(for_id)
process_bar = ShowProcess(max_steps, 'Train data OK')
prediction = pd.DataFrame()
for out_id_ in for_id:
try:
train_vaid_x = X_train[X_train.out_id == out_id_]
train_vaid_y = y_train[X_train.out_id == out_id_]
test_vaid_ = X_test[X_test.out_id == out_id_]
r_key_list = np.array(test_vaid_.index)
from xgboost.sklearn import XGBClassifier
xgbc = XGBClassifier()
xgbc.fit(np.array(train_vaid_x),np.array(train_vaid_y))
predict_detail = pd.DataFrame({'index_':r_key_list,'end_encode':xgbc.predict(np.array(test_vaid_))})
prediction = pd.concat([prediction,predict_detail])
except:
pass
process_bar.show_process()
time.sleep(0.01)
prediction.index = prediction.index_.values
prediction['end_code'] = prediction.end_encode.map(range_dict)
prediction['end_lat'] = prediction.end_code.map(lat_dict)
prediction['end_lon'] = prediction.end_code.map(lon_dict)
distinct_score(prediction.sort_index()[['end_lat','end_lon']],train[train.index.isin(X_test.index)][['end_lat','end_lon']])
# - 切分臨時及非臨時點
ecut_rule = dict(train_vaid.out_id_endcode.value_counts())
train_vaid = train_vaid[(train_vaid.out_id_endcode.map(ecut_rule)>=10)]
# - 提交結果訓練
for_id = set(test_vaid.out_id)
max_steps = len(for_id)
process_bar = ShowProcess(max_steps, 'OK')
fail_out_id = []
prediction = pd.DataFrame()
for out_id_ in for_id:
try:
train_vaid_x = train_vaid[train_vaid.out_id == out_id_][X_train.columns]
train_vaid_y = train_vaid[train_vaid.out_id == out_id_]['end_encode']
test_vaid_ = test_vaid[test_vaid.out_id == out_id_][X_train.columns]

r_key_list = np.array(test_vaid[test_vaid.out_id == out_id_].r_key)
from xgboost.sklearn import XGBClassifier
xgbc = XGBClassifier(200)
xgbc.fit(np.array(train_vaid_x),np.array(train_vaid_y))
predict_detail = pd.DataFrame({'r_key':r_key_list,'end_encode':xgbc.predict(np.array(test_vaid_))})
prediction = pd.concat([prediction,predict_detail])
except:
fail_out_id.append(r_key_list)
process_bar.show_process()
time.sleep(0.01)
# - 數據還原,將geohash換成經緯度
sumb = pd.merge(prediction[['r_key','end_encode']],test[['r_key','out_id']],on='r_key')
sumb['oi'] = sumb['out_id'].map(out_id_dict).map(lambda x:str(x)).str.cat(sumb['end_encode'].map(lambda x:str(x)))
lat_d = dict(zip(train_vaid['out_id_endcode'],train_vaid['end_lat']))
lon_d = dict(zip(train_vaid['out_id_endcode'],train_vaid['end_lon']))
sumb['end_lat'] = sumb.oi.map(lat_d)
sumb['end_lon'] = sumb.oi.map(lon_d)
sumb2 = test[~test.r_key.isin(prediction.r_key)][['r_key','start_lat','start_lon']]
sumb2.columns = ['r_key','end_lat','end_lon']
pd.concat([sumb[['r_key','end_lat','end_lon']],sumb2]).to_csv('sumbit_1108v5_10.csv',index=False)
if __name__== '__main__':
main()

代碼可私信索要

提升方向

1、進行簡單分析,出錯較多的還是週六日和節假日,對於這些特殊時間點的出行。

對於該類型,可以考慮通過時間和空間特徵:

做一些比如假期剩餘時間、離家地點,當離家地點變化趨勢等,理由如下:

假期剩餘時間:當假期剩餘時間較少,則傾向回家

假期離家距離趨勢:當離家地點越來越近,則代表正在回家路上

節假日週六日出遊傾向:構建用戶附近的人群密集點,加上時間判斷作為出遊景點,構建用戶離景點距離特徵

2、參數調整,交叉驗證

模型參數還沒怎麼調整過,交叉驗證也還沒做,也能夠從這個方面輕微提高模型準確率。

總結

這次比賽過程中,就做了兩個多星期,最近才開始閒下來。因為工作原因當時沒法繼續。還有很多提升的方向有idea但是沒時間去做。另外就是代碼薄弱,我是學統計出身,有機會還是需要惡補代碼。若有什麼意見,可以私信或留言交流,謝謝。


分享到:


相關文章: