手把手教写出XGBoost实战程序

简单介绍:

这是一个真实的比赛。赛题来源是天池大数据的 "商场中精确定位用户所在店铺"。原数据有114万条,计算起来非常困难。为了让初学者有一个更好的学习体验,也更加基础,我将数据集缩小了之后放在这里,密码:ndfd。供大家下载。

在我的数据中,数据是这样子的: train.csv

user_id用户的idtime_stamp时间戳
latitude纬度wifi_strong 1-10十个wifi的信号强度
longitude经度wifi_id 1-10十个wifi的id
shop_id商店的idcon_sta 1-10十个wifi连接状态

test.csv

user_id用户的idtime_stamp时间戳
latitude纬度wifi_id 1-10十个wifi的id
longitude经度con_sta 1-10十个wifi连接状态
row_id行标wifi_strong 1-10十个wifi的信号强度
shop_id商店的id  

这个题目的意思是,我们在商场中,由于不同层数和GPS精度限制,我们并不能仅根据经纬度准确知道某用户具体在哪一家商店中。我们通过手机与附近10个wifi点的连接情况,来精准判断出用户在哪个商店中。方便公司根据用户的位置投放相应店家的广告。

开始实战

准备实战之前,当然要对整个XGBoost有一个基本了解,对这个模型不太熟悉的朋友,建议看我之前的文章《XGBoost》

实战的流程一般是先将数据预处理,成为我们模型可处理的数据,包括丢失值处理,数据拆解,类型转换等等。然后将其导入模型运行,最后根据结果正确率调整参数,反复调参数达到最优。

我们在机器学习实战的时候一定要脱离一个思维惯性————一切都得我们思考周全才可以运行。这是一个很有趣的思维惯性,怎么解释呢?比如这道赛题,我也是学通信出身的,看到十个wifi强度值,就想找这中间的关系,然后编程来求解人的确切位置。这本质上还是我们的思维停留在显式编程的层面上,觉得程序只有写清楚才可达到预定的目标。但其实大数据处理并不是这个原理。决策树不管遇到什么数据,无论是时间还是地理位置,都是一样的按照一定规则生成树,最后让新数据按照这个树走一遍得到预测的结果。也就是说我们不必花很多精力去考虑每个数据的具体物理意义,只要把他们放进模型里面就可以了。(调参需要简单地考虑物理意义来给各个数据以权重,这个以后再说)

分析一下数据

我们的数据的意义都在上面那张表里面,我们有用户的id、经纬度、时间戳、商店id、wifi信息。我们简单思考可以知道:

  1. user_id并没有什么实际意义,仅仅是一个代号而已
  2. shop_id是我们预测的目标,我们题目要求就是我们根据其他信息来预测出用户所在的shop_id,所以 shop_id 是我们的训练目标
  3. 经纬度跟我们的位置有关,是有用的信息
  4. wifi_id 让我们知道是哪个路由器,这个不同的路由器位置不一样,所以有用
  5. wifi_strong是信号强度,跟我们离路由器距离有关,有用
  6. con_sta是连接状态,也就是有没有连上。本来我看数据中基本都是没连上,以为没有用。后来得高人提醒,说如果有人自动连上某商店wifi,不是可以说明他常来么,这个对于判断顾客也是有一点用的。
  7. 我们看test.csv总体差不多,就多了个row_id,我们输出结果要注意对应上就可以

python库准备

import pandas as pd
import xgboost as xgb
from sklearn import preprocessing
复制代码

咱这个XGBoost比较简单,所以就使用了最必要的三个库,pandas数据处理库,xgboost库,从大名鼎鼎的机器学习库sklearn中导入了preprocessing库,这个pandas库对数据的基本处理有很多封装函数,用起来比较顺手。想看例子的戳这个链接,我写的pandas.Dataframe基本拆解数据的方法。

先进行数据预处理

咱得先导入一份数据:

train = pd.read_csv(r'D:\XGBoost_learn\mall_location\train2.csv')
tests = pd.read_csv(r'D:\XGBoost_learn\mall_location\test_pre.csv')
复制代码

我们使用pandas里面的read_csv函数直接读取csv文件。csv文件全名是Comma-Separated Values文件,就是每个数据之间都以逗号隔开,比较简洁,也是各个数据比赛常用的格式。 我们需要注意的是路径问题,windows下是\,linux下是/,这个有区别。并且我们写的路径经常会与库里的函数字段重合,所以在路径最前加一个r来禁止与库里匹配,重合报错。r是raw的意思,生的,大家根据名字自行理解一下。

我们的time_stamp原来是一个str类型的数据,计算机是不会知道它是什么东西的,只知道是一串字符串。所以我们进行转化成datetime处理:

train['time_stamp'] = pd.to_datetime(pd.Series(train['time_stamp']))
tests['time_stamp'] = pd.to_datetime(pd.Series(tests['time_stamp']))
复制代码

train和tests都要处理。这也体现了pandas的强大。接下来我们看time_stamp数据的样子:2017/8/6 21:20,看数据集可知,是一个十分钟为精确度(粒度)的数据,感觉这个数据包含太多信息了呢,放一起很浪费(其实是容易过拟合,因为一个结点会被分的很细),我们就将其拆开吧:

train['Year'] = train['time_stamp'].apply(lambda x: x.year)
train['Month'] = train['time_stamp'].apply(lambda x: x.month)
train['weekday'] = train['time_stamp'].dt.dayofweek
train['time'] = train['time_stamp'].dt.time
tests['Year'] = tests['time_stamp'].apply(lambda x: x.year)
tests['Month'] = tests['time_stamp'].apply(lambda x: x.month)
tests['weekday'] = tests['time_stamp'].dt.dayofweek
tests['time'] = tests['time_stamp'].dt.time
复制代码

细心的朋友可能会发现,这里采用了两种写法,一种是.apply(lambda x: x.year),这是什么意思呢?这其实是采用了一种叫匿名函数的写法.匿名函数就是我们相要写一个函数,但并不想费神去思考这个函数该如何命名,这时候我们就需要一个匿名函数,来实现一些小功能。我们这里采用的是.apply(lambda x: x.year)实际上是调用了apply函数,是加这一列的意思,加的列的内容就是x.year。我们要是觉得这样写不直观的话,也可以这样写:

YearApply(x):
   return x.year
   
train['Year'] = train['time_stamp'].apply(YearApply)
复制代码

这两种写法意义都是一样的。 在调用weekday和datetime的时候,我们使用的是numpy里面的函数dt,用法如代码所示。其实这weekday也可以这样写: train['weekday'] = train['time_stamp'].apply(lambda x: x.weekday()),注意多了个括号,因为weekday需要计算一下才可以得到,所以还调用了一下内部的函数。 为什么采用weekday呢,因为星期几比几号对于购物来说更加有特征性。 接下来我们将这个time_stamp丢掉,因为已经有了year、month那些:

train = train.drop('time_stamp', axis=1)
tests = tests.drop('time_stamp', axis=1)
复制代码

再丢掉缺失值,或者补上缺失值。

train = train.dropna(axis=0)
tests = tests.fillna(method='pad')
复制代码

我们看到我对训练集和测试集做了两种不同方式的处理。训练集数据比较多,而且缺失值比例比较少,于是就将所有缺失值使用dropna函数,tests文件因为是测试集,不能丢失一个信息,哪怕数据很多缺失值很少,所以我们用各种方法来补上,这里采用前一个非nan值补充的方式(method=“pad”),当然也有其他方式,比如用这一列出现频率最高的值来补充。

class DataFrameImputer(TransformerMixin):
   def fit(self, X, y=None):
       for c in X:
           if X[c].dtype == np.dtype('O'):
               fill_number = X[c].value_counts().index[0]
               self.fill = pd.Series(fill_number, index=X.columns)
           else:
               fill_number = X[c].median()
               self.fill = pd.Series(fill_number, index=X.columns)
       return self
       
       def transform(self, X, y=None):
           return X.fillna(self.fill)
       
train = DataFrameImputer().fit_transform(train)
复制代码

这一段代码有一点拗口,意思是对于X中的每一个c,如果X[c]的类型是object‘O’表示object)的话就将[X[c].value_counts().index[0]传给空值,[X[c].value_counts().index[0]表示的是重复出现最多的那个数,如果不是object类型的话,就传回去X[c].median(),也就是这些数的中位数。

在这里我们可以使用print来输出一下我们的数据是什么样子的。

print(train.info())
复制代码
<class 'pandas.core.frame.DataFrame' at 0x0000024527C50D08>
Int64Index: 467 entries, 0 to 499
Data columns (total 38 columns):
user_id          467 non-null object
shop_id          467 non-null object
longitude        467 non-null float64
latitude         467 non-null float64
wifi_id1         467 non-null object
wifi_strong1     467 non-null int64
con_sta1         467 non-null bool
wifi_id2         467 non-null object
wifi_strong2     467 non-null int64
con_sta2         467 non-null object
wifi_id3         467 non-null object
wifi_strong3     467 non-null float64
con_sta3         467 non-null object
wifi_id4         467 non-null object
wifi_strong4     467 non-null float64
con_sta4         467 non-null object
wifi_id5         467 non-null object
wifi_strong5     467 non-null float64
con_sta5         467 non-null object
wifi_id6         467 non-null object
wifi_strong6     467 non-null float64
con_sta6         467 non-null object
wifi_id7         467 non-null object
wifi_strong7     467 non-null float64
con_sta7         467 non-null object
wifi_id8         467 non-null object
wifi_strong8     467 non-null float64
con_sta8         467 non-null object
wifi_id9         467 non-null object
wifi_strong9     467 non-null float64
con_sta9         467 non-null object
wifi_id10        467 non-null object
wifi_strong10    467 non-null float64
con_sta10        467 non-null object
Year             467 non-null int64
Month            467 non-null int64
weekday          467 non-null int64
time             467 non-null object
dtypes: bool(1), float64(10), int64(5), object(22)
memory usage: 139.1+ KB
None
复制代码

我们可以清晰地看出我们代码的结构,有多少列,每一列下有多少个值等等,有没有空值我们可以根据值的数量来判断。 我们在缺失值处理之前加入这个print(train.info())就会得到:

<class 'pandas.core.frame.DataFrame' at 0x000001ECFA6D6718>
RangeIndex: 500 entries, 0 to 499
复制代码

这里面就有500个值,处理后就只剩467个值了,可见丢弃了不少。同样的我们也可以将test的信息输出一下:

<class 'pandas.core.frame.DataFrame' at 0x0000019E13A96F48>
RangeIndex: 500 entries, 0 to 499
复制代码

500个值一个没少。都给补上了。这里我只取了输出信息的标题,没有全贴过来,因为全信息篇幅很长。 我们注意到这个数据中有boolfloatintobject四种类型,我们XGBoost是一种回归树,只能处理数字类的数据,所以我们要转化。对于那些字符串类型的数据我们该如何处理呢?我们采用LabelEncoder方法:

for f in train.columns:
    if train[f].dtype=='object':
        if f != 'shop_id':
            print(f)
            lbl = preprocessing.LabelEncoder()
            train[f] = lbl.fit_transform(list(train[f].values))
for f in tests.columns:
    if tests[f].dtype == 'object':
        print(f)
        lbl = preprocessing.LabelEncoder()
        tests[f] = lbl.fit_transform(list(tests[f].values))
复制代码

这段代码的意思是调用sklearn中preprocessing里面的LabelEncoder方法,对数据进行标签编码,作用主要就是使其变成数字类数据,有的进行归一化处理,使其运行更快等等。 我们看这段代码,lbl只是LabelEncoder的简写,lbl = preprocessing.LabelEncoder(),这段代码只有一个代换显得一行不那么长而已,没有实际运行什么。第二句lbl.fit_transform(list(train[f].values))是将train里面的每一个值进行编码,我们在其前后输出一下train[f].values就可以看出来:

print(train[f].values)
train[f] = lbl.fit_transform(list(train[f].values))
print(train[f].values)
复制代码

我加上那一串0/的目的是分隔开输出数据。我们得到:

user_id
['u_376' 'u_376' 'u_1041' 'u_1158' 'u_1654' 'u_2733' 'u_2848' 'u_3063'
 'u_3063' 'u_3063' 'u_3604' 'u_4250' 'u_4508' 'u_5026' 'u_5488' 'u_5488'
 'u_5602' 'u_5602' 'u_5602' 'u_5870' 'u_6429' 'u_6429' 'u_6870' 'u_6910'
 'u_7037' 'u_7079' 'u_7869' 'u_8045' 'u_8209']
[ 7  7  0  1  2  3  4  5  5  5  6  8  9 10 11 11 12 12 12 13 14 14 15 16 17
 18 19 20 21]
复制代码

我们可以看出,LabelEncoder将我们的str类型的数据转换成数字了。按照它自己的一套标准。 对于tests数据,我们可以看到,我单独将shop_id给避开了。这样处理的原因就是shop_id是我们要提交的数据,不能有任何编码行为,一定要保持这种str状态。

接下来需要将train和tests转化成matrix类型,方便XGBoost运算:

feature_columns_to_use = ['Year', 'Month', 'weekday',
'time', 'longitude', 'latitude',
'wifi_id1', 'wifi_strong1', 'con_sta1',
 'wifi_id2', 'wifi_strong2', 'con_sta2',
'wifi_id3', 'wifi_strong3', 'con_sta3',
'wifi_id4', 'wifi_strong4', 'con_sta4',
'wifi_id5', 'wifi_strong5', 'con_sta5',
'wifi_id6', 'wifi_strong6', 'con_sta6',
'wifi_id7', 'wifi_strong7', 'con_sta7',
'wifi_id8', 'wifi_strong8', 'con_sta8',
'wifi_id9', 'wifi_strong9', 'con_sta9',
'wifi_id10', 'wifi_strong10', 'con_sta10',]
train_for_matrix = train[feature_columns_to_use]
test_for_matrix = tests[feature_columns_to_use]
train_X = train_for_matrix.as_matrix()
test_X = test_for_matrix.as_matrix()
train_y = train['shop_id']
复制代码

待训练目标是我们的shop_id,所以train_yshop_id

导入模型生成决策树

gbm = xgb.XGBClassifier(silent=1, max_depth=10, n_estimators=1000, learning_rate=0.05)
gbm.fit(train_X, train_y)
复制代码

这两句其实可以合并成一句,我们也就是在XGBClassifier里面设定好参数,其所有参数以及其默认值(缺省值)我写在这,内容来自XGBoost源代码

  • max_depth=3, 这代表的是树的最大深度,默认值为三层。max_depth越大,模型会学到更具体更局部的样本。
  • learning_rate=0.1,学习率,也就是梯度提升中乘以的系数,越小,使得下降越慢,但也是下降的越精确。
  • n_estimators=100,也就是弱学习器的最大迭代次数,或者说最大的弱学习器的个数。一般来说n_estimators太小,容易欠拟合,n_estimators太大,计算量会太大,并且n_estimators到一定的数量后,再增大n_estimators获得的模型提升会很小,所以一般选择一个适中的数值。默认是100。
  • silent=True,是我们训练xgboost树的时候后台要不要输出信息,True代表将生成树的信息都输出。
  • objective="binary:logistic",这个参数定义需要被最小化的损失函数。最常用的值有:
  1. binary:logistic 二分类的逻辑回归,返回预测的概率(不是类别)。
  2. multi:softmax 使用softmax的多分类器,返回预测的类别(不是概率)。在这种情况下,你还需要多设一个参数:num_class(类别数目)。
  3. multi:softprob和multi:softmax参数一样,但是返回的是每个数据属于各个类别的概率。
  • nthread=-1, 多线程控制,根据自己电脑核心设,想用几个线程就可以设定几个,如果你想用全部核心,就不要设定,算法会自动识别
  • `gamma=0,在节点分裂时,只有分裂后损失函数的值下降了,才会分裂这个节点。Gamma指定了节点分裂所需的最小损失函数下降值。 这个参数的值越大,算法越保守。这个参数的值和损失函数息息相关,所以是需要调整的。
  • min_child_weight=1,决定最小叶子节点样本权重和。 和GBM的 min_child_leaf 参数类似,但不完全一样。XGBoost的这个参数是最小样本权重的和,而GBM参数是最小样本总数。这个参数用于避免过拟合。当它的值较大时,可以避免模型学习到局部的特殊样本。 但是如果这个值过高,会导致欠拟合。这个参数需要使用CV来调整
  • max_delta_step=0, 决定最小叶子节点样本权重和。 和GBM的 min_child_leaf 参数类似,但不完全一样。XGBoost的这个参数是最小样本权重的和,而GBM参数是最小样本总数。这个参数用于避免过拟合。当它的值较大时,可以避免模型学习到局部的特殊样本。 但是如果这个值过高,会导致欠拟合。这个参数需要使用CV来调整。
  • subsample=1, 和GBM中的subsample参数一模一样。这个参数控制对于每棵树,随机采样的比例。减小这个参数的值,算法会更加保守,避免过拟合。但是,如果这个值设置得过小,它可能会导致欠拟合。典型值:0.5-1
  • colsample_bytree=1, 用来控制每棵随机采样的列数的占比(每一列是一个特征)。典型值:0.5-1
  • colsample_bylevel=1,用来控制树的每一级的每一次分裂,对列数的采样的占比。其实subsample参数和colsample_bytree参数可以起到相似的作用。
  • reg_alpha=0,权重的L1正则化项。(和Lasso regression类似)。可以应用在很高维度的情况下,使得算法的速度更快。
  • reg_lambda=1, 权重的L2正则化项这个参数是用来控制XGBoost的正则化部分的。这个参数越大就越可以惩罚树的复杂度
  • scale_pos_weight=1,在各类别样本十分不平衡时,把这个参数设定为一个正值,可以使
  • base_score=0.5, 所有实例的初始化预测分数,全局偏置;为了足够的迭代次数,改变这个值将不会有太大的影响。
  • seed=0, 随机数的种子设置它可以复现随机数据的结果,也可以用于调整参数

数据通过树生成预测结果

predictions = gbm.predict(test_X)
复制代码

将tests里面的数据通过这生成好的模型,得出预测结果。

submission = pd.DataFrame({'row_id': tests['row_id'],
                            'shop_id': predictions})
print(submission)
submission.to_csv("submission.csv", index=False)
复制代码

将预测结果写入到csv文件里。我们注意写入文件的格式,row_id在前,shop_id在后。index=False的意思是不写入行的名称。改成True就把每一行的行标也写入了。



附录

参考资料

  1. 机器学习系列(12)_XGBoost参数调优完全指南(附Python代码)http://blog.csdn.net/han_xiaoyang/article/details/52665396
  2. Kaggle比赛:泰坦尼克之灾: https://www.kaggle.com/c/titanic

完整代码

import pandas as pd
import xgboost as xgb
from sklearn import preprocessing


train = pd.read_csv(r'D:\mall_location\train.csv')
tests = pd.read_csv(r'D:\mall_location\test.csv')

train['time_stamp'] = pd.to_datetime(pd.Series(train['time_stamp']))
tests['time_stamp'] = pd.to_datetime(pd.Series(tests['time_stamp']))

print(train.info())

train['Year'] = train['time_stamp'].apply(lambda x:x.year)
train['Month'] = train['time_stamp'].apply(lambda x: x.month)
train['weekday'] = train['time_stamp'].apply(lambda x: x.weekday())
train['time'] = train['time_stamp'].dt.time
tests['Year'] = tests['time_stamp'].apply(lambda x: x.year)
tests['Month'] = tests['time_stamp'].apply(lambda x: x.month)
tests['weekday'] = tests['time_stamp'].dt.dayofweek
tests['time'] = tests['time_stamp'].dt.time
train = train.drop('time_stamp', axis=1)
train = train.dropna(axis=0)
tests = tests.drop('time_stamp', axis=1)
tests = tests.fillna(method='pad')
for f in train.columns:
    if train[f].dtype=='object':
        if f != 'shop_id':
            print(f)
            lbl = preprocessing.LabelEncoder()
            train[f] = lbl.fit_transform(list(train[f].values))
for f in tests.columns:
    if tests[f].dtype == 'object':
        print(f)
        lbl = preprocessing.LabelEncoder()
        lbl.fit(list(tests[f].values))
        tests[f] = lbl.transform(list(tests[f].values))


feature_columns_to_use = ['Year', 'Month', 'weekday',
'time', 'longitude', 'latitude',
'wifi_id1', 'wifi_strong1', 'con_sta1',
 'wifi_id2', 'wifi_strong2', 'con_sta2',
'wifi_id3', 'wifi_strong3', 'con_sta3',
'wifi_id4', 'wifi_strong4', 'con_sta4',
'wifi_id5', 'wifi_strong5', 'con_sta5',
'wifi_id6', 'wifi_strong6', 'con_sta6',
'wifi_id7', 'wifi_strong7', 'con_sta7',
'wifi_id8', 'wifi_strong8', 'con_sta8',
'wifi_id9', 'wifi_strong9', 'con_sta9',
'wifi_id10', 'wifi_strong10', 'con_sta10',]

big_train = train[feature_columns_to_use]
big_test = tests[feature_columns_to_use]
train_X = big_train.as_matrix()
test_X = big_test.as_matrix()
train_y = train['shop_id']

gbm = xgb.XGBClassifier(silent=1, max_depth=10,
                    n_estimators=1000, learning_rate=0.05)
gbm.fit(train_X, train_y)
predictions = gbm.predict(test_X)

submission = pd.DataFrame({'row_id': tests['row_id'],
                            'shop_id': predictions})
print(submission)
submission.to_csv("submission.csv",index=False)


作者:香橙云子
链接:https://juejin.im/post/5a1bb29e51882531ba10aa49
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

转载:https://juejin.im/post/5a1bb29e51882531ba10aa49

  • 12
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值