简单介绍:
这是一个真实的比赛。赛题来源是天池大数据的 “商场中精确定位用户所在店铺”。原数据有114万条,计算起来非常困难。为了让初学者有一个更好的学习体验,也更加基础,我将数据集缩小了之后放在这里,密码:ndfd。供大家下载。
在我的数据中,数据是这样子的: train.csv
user_id | 用户的id | time_stamp | 时间戳 |
---|---|---|---|
latitude | 纬度 | wifi_strong 1-10 | 十个wifi的信号强度 |
longitude | 经度 | wifi_id 1-10 | 十个wifi的id |
shop_id | 商店的id | con_sta 1-10 | 十个wifi连接状态 |
test.csv
user_id | 用户的id | time_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信息。我们简单思考可以知道:
- user_id并没有什么实际意义,仅仅是一个代号而已
- shop_id是我们预测的目标,我们题目要求就是我们根据其他信息来预测出用户所在的shop_id,所以 shop_id 是我们的训练目标
- 经纬度跟我们的位置有关,是有用的信息
- wifi_id 让我们知道是哪个路由器,这个不同的路由器位置不一样,所以有用
- wifi_strong是信号强度,跟我们离路由器距离有关,有用
- con_sta是连接状态,也就是有没有连上。本来我看数据中基本都是没连上,以为没有用。后来得高人提醒,说如果有人自动连上某商店wifi,不是可以说明他常来么,这个对于判断顾客也是有一点用的。
- 我们看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个值一个没少。都给补上了。这里我只取了输出信息的标题,没有全贴过来,因为全信息篇幅很长。 我们注意到这个数据中有bool
、float
、int
、object
四种类型,我们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_y
是shop_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”,这个参数定义需要被最小化的损失函数。最常用的值有:
binary:logistic
二分类的逻辑回归,返回预测的概率(不是类别)。multi:softmax
使用softmax的多分类器,返回预测的类别(不是概率)。在这种情况下,你还需要多设一个参数:num_class(类别数目)。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就把每一行的行标也写入了。
附录
参考资料
- 机器学习系列(12)_XGBoost参数调优完全指南(附Python代码)http://blog.csdn.net/han_xiaoyang/article/details/52665396
- 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)
复制代码