这是参加的第二个kaggle的比赛:facebook V:Predicting Check ins,其与前一阵子Expedia比赛很相似,其预测目标集合都是非常大的。这是比赛入口:https://www.kaggle.com/c/facebook-v-predicting-check-ins。本文可以当做一个简单粗糙的数据挖掘tutorial。
比赛题目要求是预测登入用户的地点id,数据集是10km * 10km的方形区域(facebook团队创造的虚拟人工世界)中100,000个地点id的用户相关信息,其中的数据带有程度不定的噪声。提交的文件要求预测test.csv中每一个row_id(8,607,230 个)对应的地点id预测,选手可为每个row_id提供三个预测地点。结果的评估公式如下:
如上所示,评估的公式采用MAP公式,即要求推荐的三个place_id 中没有一个预测正确则不得分。在三个place_id中,有次序关系,若预测正确的place_id次序越前,则得分越高。
在对于该数据挖掘问题的方案制定前,需要先对数据进行探索。数据探索有助于对数据有个初步的了解。其中训练数据为1.24G、测试数据为0.27G。机器为8G RAM。故将训练数据数据抽取10,000,000个样本读入内存,了解其概况信息。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
df_train = pd.read_csv('../train_sample_10000000.csv')
# 读入文件
## 画分布图
statis_values = df_train['place_id'].value_counts().values
# 统计地点id
place_statis = Series(statis_values, index = range(len(statis_values)))
# 为地点id重新编号
place_statis.plot()
# 绘图
plt.show()
# 显示
## 数据统计描述
print(descirbe(df_train))
以下是抽样数据大致的概貌情况:
图1:100,000个地点的频数分布
如上图所示:由于需要预测的类别集合元素有100,000个,故这种情况不适合使用回归、SVM、神经网络以及决策树等算法直接构建分类器。因为这些算法仅仅在类别数目比较少时才能有效工作。
由以上观察的数据结果,我们可以对训练数据进行抽样。鉴于训练样本集有大致2千多万条条目,故抽样数据不能够太少,否则会使得抽样后的样本分布对比原分布改变太多。
以下是抽样函数的实现代码:
## 抽样数据集
def pickSample(filename, nSample = 1000000):
df = pd.read_csv(filename)
len_df = len(df.index)
samp_id = sorted(random.sample(range(len_df), nSample))
# 得到随机的抽样id
outputFilename = filename[len(PATH_SAVE) : -4] + '_' + 'sample' + '_' + str(nSample) + '.csv'
# 生成样本数据的文件名
samps_train = df[df.row_id.isin(samp_id)]
samps_train.to_csv(PATH_SAVE + outputFilename, index = False, mode = 'w')
pickSample('../train.csv', 1000000)
# 抽取1000000个训练样本
由于训练数据与预测数据的条目非常多,对于任何一个机器学习算法都是一个不小的负担。特别对于在这个地方,尝试通过将x,y地点切分为40*40网格的方型区域,随机其中选取中一个网格进行数据探索。
## 将数据分到相应的网格中去
n_cell_x = 40
n_cell_Y = 40
# x, y尺度归一
size_x = 10. / n_cell_x
size_y = 10. / n_cell_y
## 去除0值
eps = 0.00001 # 设置精度到eps,
xs = np.where(df.x.values < eps, 0 , df.x.values - eps)
ys = np.where(df.y.values < eps, 0 , df.y.values - eps)
## 生成网格id
pos_x = (xs / size_x).astype(np.int)
# normilze and change it to int type(整数)
pos_y = (ys / size_y).astype(np.int)
df['grid_cell'] = pos_y * n_cell_x + pos_y
## 试验
th = 5 # 地点频数阈值
test = getDataFromGrid(df_train, 0, th)
## 网格内数据截取
def getDataFromGrid(df, grid_id, th):
df = df.loc[df.grid_cell == grid_id]
# 与 temp = df[df.grid_cell == grid_id] 等价
# 截取满足条件的train样本
place_counts = df.place_id.value_counts()
# 做每个网格里的地点统计
mask = (place_counts[df.place_id.values] >= th).values
# 将数目少的地点当做噪声排除掉
# 这里dfInCell_train.place_id.values作为下标输入,输出的该地点下标
# 的统计值
df = df.loc[mask]
return df
可以看出,随机选取的网格中的地点数量降至2600多。可以发现100,000个地点确实是按一定地理位置分布的。故选择合适的网格大小在按网格划定建立每个网格对应的预测系统是可行的。同时地点中有许多频数非常小的地点,这些地点在预测中通常对被预测系统的选择推荐的可能性不大,故可将低于一定频度的地点滤去。上面的做法对于使用如knn、贝叶斯以及其他ML算法而言,较小的数据量能大幅减少算法的训练时间与空间存储大小。
给定的数据中有四个特征:x,y,accuracy与time:以下依次为四个特征的频率分布图,前两个图为time与accuracy,后三个图为想x,y的联合分布图。
图4:某个网格内的time频数分布
图6:某个网格内x,y按20 X 20、40X40与80X80的样本频数分布
由上图可以观察出几个特征之间的量程范围差距太多,其中时间特征需要具体切分细化。accuracy这个特征取值的拖尾很严重,如果直接使用该特征作为输入,大多数ML算法都会大打折扣。而且从网格化的x,y联合分布来看,其频数分布随机性比较大,且其分布并不是稠密的。故使用ML算法去训练数据时,需要考虑一些网格本身的样本稀疏性,才能够提供很好的地点预测。
其中绘图的源代码参考来自以下kaggle选手的公开代码:
以下是随机截取5个样本数据,由于特征有限,故可以考虑人工新增一些特征:
图7:样本示例
## 特征生成函数
def featEngineering(df, n_cell_x, n_cell_y):
## 网格位置特征
size_x = 10. / n_cell_x
size_y = 10. / n_cell_y
eps = 0.00001 # 设置精度到eps,
xs = np.where(df.x.values < eps, 0 , df.x.values - eps)
ys = np.where(df.y.values < eps, 0 , df.y.values - eps)
pos_x = (xs / size_x).astype(np.int)
# normilze and change it to int type(整数)
pos_y = (ys / size_y).astype(np.int)
df['grid_cell'] = pos_y * n_cell_x + pos_y
## 修正地理特征
fw_1 = [500, 1000]
# 特征加权参数
df.x = df.x.values * fw_1[0]
df.y = df.y.values * fw_1[1]
## 时间特征
fw_2 = [4, 3, 1./22., 2, 10]
initial_date = np.datetime64('2014-01-01T01:01', dtype='datetime64[m]')
d_times = pd.DatetimeIndex(initial_date + np.timedelta64(int(mn), 'm')
for mn in df.time.values)
df['hour'] = d_times.hour * fw_2[0]
df['weekday'] = d_times.weekday * fw_2[1]
df['day'] = (d_times.dayofyear * fw_2[2]).astype(int)
df['month'] = d_times.month * fw_2[3]
df['year'] = (d_times.year - 2013) * fw_2[4]
df = df.drop(['time'], axis=1)
return df
从样例数据中可以看出time采用的不是传统的记录形式,可将其转化为年月日-时分秒的形式,再依次得到year,month,weekday,day,hour等更精确的时间特征;对于各种精确时间可将其相对化,可生成hour_of_day,day_or_week,month_of_year等特征;同时为了将样本进行网格化,可以根据x,y的取值划定其处于哪个网格,并设为新特征grid_id;accuracy特征由于其分布呈现拖尾情况,故可以添加其对数特征作为新特征。
数据切分为20*40个网格区域,使用网格化下的最近邻算法为每个预测样本选取3个最有可能的地点。KNN算法在每个网格中使用,以下是使用25个k邻近点、采用距离加权,且距离度量为曼哈顿距离的knn代码(KNN的算法来自scikit-learn的python机器学习包):
nNeighbors = 25
clf = KNeighborsClassifier(n_neighbors = nNeighbors, weights='distance',
metric='manhattan')
clf.fit(X, y)
y_pred = clf.predict_proba(X_test)
def knn_inGrid(df_train, df_test, grid_id, th, mpps):
grid_train = getDataFromGrid(df_train, grid_id, th)
grid_test = df_test.loc[df_test.grid_cell == grid_id]
canPass = len(grid_train) != 0 and len(grid_test) != 0
if canPass == True:
print('one grid use knn')
## 提炼训练与测试样本数据
row_ids = grid_test.index
# 获得id
## 机器学习模块
le = LabelEncoder()
# 生成一个标签编码器,利用标签编码器给多个类做编码
y = le.fit_transform(grid_train.place_id.values)
# 转换成输出y
X = grid_train.drop(['row_id', 'place_id', 'grid_cell'], axis=1).values.astype(int)
# 一步到位转化成ndarry,可以作为机器学习算法输入
X_test = grid_test.drop(['row_id', 'grid_cell'], axis = 1).values.astype(int)
## 算法
nNeighbors = 25
clf = KNeighborsClassifier(n_neighbors = nNeighbors, weights='distance',
metric='manhattan', n_jobs = -1)
clf.fit(X, y)
y_pred = clf.predict_proba(X_test)
pred_labels = le.inverse_transform(np.argsort(y_pred, axis=1)[:,::-1][:,:3])
# 排序可能性由高到低,转化标签
# [:,::-1]是取反序(两个分号),[:,:3]是截取前3个
## 如果网格内无训练数据,但有测试数据。使用最频繁的地点id填充
elif len(grid_test) != 0:
print('one grid use mpps')
row_ids = grid_test.index
pred_labels = np.array([np.array(mpps) for i in range(len(row_ids))])
## 若果网格内训练集与测试集都没有
else:
pred_labels = False
row_ids = False
print('skip one grid')
pdb.set_trace()
return pred_labels, row_ids
def knn(df_train, df_test, th, n_cells):
preds = np.zeros((df_test.shape[0], 3), dtype=np.int64)
mpps = getMostProbPlaces(df_train)
for grid_id in range(n_cells):
if grid_id % 100 == 0:
print('finish: %s grids' %(grid_id))
pred_labels, row_ids = knn_inGrid(df_train, df_test, grid_id, th, mpps)
if isinstance(pred_labels, np.ndarray) == False:
continue
else:
preds[row_ids] = pred_labels
# 保留下测试集的标签
return preds
df_train = featEngineering_ver1(df_train, n_cell_x, n_cell_y)
df_test = featEngineering_ver1(df_test, n_cell_x, n_cell_y)
## training and predicting
preds = knn(df_train, df_test, th, n_cell_x * n_cell_y)
使用KNN算法需要考虑的是特征之间的量程问题,太大量程范围的特征会对算法起主导作用。故使用需要改变各个特征的加权参数,使用加权参数可以将各个特征量程调节到大致的水平上,但是这样忽略了不同特征的重要性。为了发掘不同特征并做加权参数的确定,我们使用逻辑回归的方法可以大致估算knn算法的权重,具体思路如下:
由于采用的是曼哈顿距离,且采用距离加权,可以想象KNN此时的分类的决定因素是样本点与邻近点的距离。故可以考虑一个机器学习问题:对于一个给定的地点ID,设计一个二分类器,分类输出结果为是此地点和非此地点,而输入的特征为邻近点的特征与样本点特征的差值。使用常用机器学习方法可解决此机器学习问题,该方案中采用逻辑回归,而得到的权值即可作为KNN算法的特征加权参考值。
输入特征为x,y,hour,day_of_week,month_of_year,year,下面为计算代码块:
df['hour'] = d_times.hour
df['weekday'] = d_times.weekday
df['month'] = d_times.month
df['hour'] = (d_times.hour%24+1)*fw[2]
df['weekday'] = (df['weekday']%7+1)*fw[3]
df['month'] = (df['month']%12+1)*fw[4]
df['year'] = (d_times.year - 2013) * fw[5]
加权参数计算代码:<待补充>
得到的KNN加权依次为:
knn_w = [500., 1000., 3., 4., 3., 11.]
以上的方案的评估结果分数为:0.56829
再加入了3维特征,accuracy的对数值特征,sine与cos值特征
df['sine'] = np.sin(2*np.pi*df["hour_of_day"]/24)
df['cos'] = np.cos(2*np.pi*df["hour_of_day"]/24)
df['accuracy'] = np.log(df['accuracy']+1)
得到的KNN加权依次为:
knn_w = [500., 1000., 4., 3., 2., 11., 10., 12., 9.]
得到的结果为:(待补充),提升了一些。
网格化的KNN没有考虑到如下的问题,当要预测的网格边界附近的样本点时,其附近的近邻点有可能很多在网格边界之外,而网格化的KNN算法并没有考虑到这些点,从而导致网格边界附近的样本点预测效果比较差。
这里的方案参考kaggle选手 David 的方案,采用网格松弛增量的方式缓解这个问题。具体方案:
对训练数据进行网格化数据截取时,定义x,y两个方向上的松弛增量。在截取长宽均比原网格较大的网格,再数据输入KNN算法模块训练。这样做减少如左图所示的边界问题出现的可能情况。对于测试数据采用非松弛增量的方式截取数据即可,并利用上面得到的KNN模块进行预测。
由于太多的特征超出了8G RAM,机器跑不了,故仅选择如下的特征:
x,y,hour,day_of_week,month_of_year,year
计算得到的相应权值为:
knn_w = [500., 1000., 3., 4., 3., 11.]
截取的代码如下:
x_border_augment = 0.02
y_border_augment = 0.02
#Working on df_train
df_cell_train = df_train[(df_train['x'] >= x_min-x_border_augment) & (df_train['x'] < x_max+x_border_augment) &
(df_train['y'] >= y_min-y_border_augment) & (df_train['y'] < y_max+y_border_augment)]
#Working on df_test
df_cell_test = df_test[(df_test['x'] >= x_min) & (df_test['x'] < x_max) &
(df_test['y'] >= y_min) & (df_test['y'] < y_max)]
计算结果为:0.57187
<未完>
以上的代码和思路参考以下选手的代码与资料: