《绝地求生》玩家排名预测
- 1. 项目背景
- 2. 数据集介绍
- 3. 项目评估方式
- 3.1 评估方式
- 3.2 MAE(Mean Absolute Error)介绍
- 4. 项目实现(数据分析+RF+lightGBM)
- 4.1 获取数据、基本数据信息查看
- 4.2 数据基本处理
- 4.2.1 数据缺失值处理
- 4.2.2 特征数据规范化处理
- 4.2.2.1 查看每场比赛参加的人数
- 4.2.2.2 规范化输出部分数据
- 4.2.3 部分变量合成
- 4.2.4 异常值处理
- 4.2.4.1 异常值处理:删除有击杀,但是完全没有移动的玩家
- 4.2.4.2 异常值处理:删除驾车杀敌数异常的数据
- 4.2.4.3 异常值处理:删除玩家在一局中杀敌数超过30人的数据
- 4.2.4.4 异常值处理:删除爆头率异常数据
- 4.2.4.5 异常值处理:删除最远杀敌距离异常数据
- 4.2.4.6 异常值处理:删除关于运动距离的异常值
- 4.2.4.7 异常值处理:武器收集异常值处理
- 4.2.4.8 异常值处理:删除使用治疗药品数量异常值
- 4.2.5 类别型数据处理
- 4.2.5.1 比赛类型one-hot处理
- 4.2.5.2 对groupId,matchId等数据进行处理
- 4.2.6 数据截取
- 4.2.6.1 取部分数据进行使用(1000000)
- 4.2.7 确定特征值和目标值
- 4.2.8 分割训练集和测试集
- 4.3 机器学习(模型训练)和评估
- 4.3.1 使用随机森林对模型进行训练
- 4.3.1.1 初步使用随机森林进行模型训练
- 4.3.1.2 再次使用随机森林,进行模型训练
- 4.3.2 使用lightGBM对模型进行训练
- 4.3.2.1 模型初次尝试
- 4.3.2.2 模型二次调优
- 4.3.2.3 模型三次调优[思路]
1. 项目背景
绝地求生(Player unknown’s Battlegrounds),俗称吃鸡,是一款战术竞技型射击类沙盒游戏。
这款游戏是一款大逃杀类型的游戏,每一局游戏将有最多100名玩家参与,他们将被投放在绝地岛(battlegrounds)上,在游戏的开始时所有人都一无所有。玩家需要在岛上收集各种资源,在不断缩小的安全区域内对抗其他玩家,让自己生存到最后。
该游戏拥有很高的自由度,玩家可以体验飞机跳伞、开越野车、丛林射击、抢夺战利品等玩法,小心四周埋伏的敌人,尽可能成为最后1个存活的人。
该游戏中,玩家需要在游戏地图上收集各种资源,并在不断缩小的安全区域内对抗其他玩家,让自己生存到最后。
2. 数据集介绍
本项目中,将为您提供大量匿名的《绝地求生》游戏统计数据。
其格式为每行包含一个玩家的游戏后统计数据,列为数据的特征值。
数据来自所有类型的比赛:单排,双排,四排;不保证每场比赛有100名人员,每组最多4名成员。
文件说明:
train_V2.csv - 训练集test_V2.csv - 测试集
数据集局部图如下图所示:
数据集中字段解释:
3. 项目评估方式
3.1 评估方式
你必须创建一个模型,根据他们的最终统计数据预测玩家的排名,从1(第一名)到0(最后一名)。
最后结果通过平均绝对误差(MAE)进行评估,即通过预测的winPlacePerc和真实的winPlacePerc之间的平均绝对误差
3.2 MAE(Mean Absolute Error)介绍
4. 项目实现(数据分析+RF+lightGBM)
在接下来的分析中,我们将分析数据集,检测异常值。
然后我们通过随机森林模型对其训练,并对对该模型进行了优化。
# 导入数据基本处理阶段需要用到的api
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
4.1 获取数据、基本数据信息查看
导入数据,且查看数据的基本信息
train = pd.read_csv("./data/train_V2.csv")
train.tail()
train.describe()
train.info()
可以看到数据一共有4446966条,
train.shape
(4446966, 29)
# 查看一共有多少场比赛
np.unique(train["matchId"]).shape
(47965,)
# 查看一共有多少组
np.unique(train["groupId"]).shape
(2026745,)
4.2 数据基本处理
4.2.1 数据缺失值处理
查看目标值,我们发现有一条样本,比较特殊,其“winplaceperc”的值为NaN,也就是目标值是缺失值,
因为只有一个玩家是这样,直接进行删除处理。
# 查看缺失值
# np.any(train.isnull())
# 通过上面方法查看,发现只有"'winPlacePerc"列有缺失值;# 寻找缺失值行
train[train['winPlacePerc'].isnull()]
# 删除缺失值
train.drop(2744604, inplace=True)
train.shape
4.2.2 特征数据规范化处理
4.2.2.1 查看每场比赛参加的人数
处理完缺失值之后,我们看一下每场参加的人数会有多少呢,是每次都会匹配100个人,才开始游戏吗?
# 显示每场比赛参加人数
# transform的作用类似实现了一个一对多的映射功能,把统计数量映射到对应的每个样本上
count = train.groupby('matchId')['matchId'].transform('count')train['playersJoined'] = countcount.count()train.head()
# 通过每场参加人数进行,按值升序排列
train["playersJoined"].sort_values().head()
通过结果发现,最少的一局,竟然只有两个人,wtf!!!
# 通过绘制图像,查看每局开始人数
# 通过seaborn下的countplot方法,可以直接绘制统计过数量之后的直方图
plt.figure(figsize=(20,10))
sns.countplot(train['playersJoined'])
plt.title('playersJoined')
plt.grid()
plt.show()
通过观察,发现一局游戏少于75个玩家,就开始的还是比较少
同时大部分游戏都是在接近100人的时候才开始
限制每局开始人数大于等于75,再进行绘制。
猜想:把这些数据在后期加入数据处理,应该会得到的结果更加准确一些
# 再次绘制每局参加人数的直方图
plt.figure(figsize=(20,10))
sns.countplot(train[train['playersJoined']>=75]['playersJoined'])
plt.title('playersJoined')
plt.grid()
plt.show()
4.2.2.2 规范化输出部分数据
现在我们统计了“每局玩家数量”,那么我们就可以通过“每局玩家数量”来进一步考证其它特征,同时对其规范化设置
试想:一局只有70个玩家的杀敌数,和一局有100个玩家的杀敌数,应该是不可以同时比较的
可以考虑的特征值包括
-
1.kills(杀敌数)
-
2.damageDealt(总伤害)
-
3.maxPlace(本局最差名次)
-
4.matchDuration(比赛时长)
# 对部分特征值进行规范化
train['killsNorm'] = train['kills']*((100-train['playersJoined'])/100 + 1)
train['damageDealtNorm'] = train['damageDealt']*((100-train['playersJoined'])/100 + 1)
train['maxPlaceNorm'] = train['maxPlace']*((100-train['playersJoined'])/100 + 1)
train['matchDurationNorm'] = train['matchDuration']*((100-train['playersJoined'])/100 + 1)# 比较经过规范化的特征值和原始特征值的值
to_show = ['Id', 'kills','killsNorm','damageDealt', 'damageDealtNorm', 'maxPlace', 'maxPlaceNorm', 'matchDuration', 'matchDurationNorm']
train[to_show][0:11]
4.2.3 部分变量合成
此处我们把特征:heals(使用治疗药品数量)和boosts(能量、道具使用数量)合并成一个新的变量,命名:”healsandboosts“, 这是一个探索性过程,最后结果不一定有用,如果没有实际用处,最后再把它删除。
# 创建新变量“healsandboosts”
train['healsandboosts'] = train['heals'] + train['boosts']
train[["heals", "boosts", "healsandboosts"]].tail()
4.2.4 异常值处理
4.2.4.1 异常值处理:删除有击杀,但是完全没有移动的玩家
异常数据处理:
一些行中的数据统计出来的结果非常反常规,那么这些玩家肯定有问题,为了训练模型的准确性,我们会把这些异常数据剔除
通过以下操作,识别出玩家在游戏中有击杀数,但是全局没有移动;
这类型玩家肯定是存在异常情况(挂**),我们把这些玩家删除。
# 创建新变量,统计玩家移动距离
train['totalDistance'] = train['rideDistance'] + train['walkDistance'] + train['swimDistance']
# 创建新变量,统计玩家是否在游戏中,有击杀,但是没有移动,如果是返回True, 否则返回false
train['killsWithoutMoving'] = ((train['kills'] > 0) & (train['totalDistance'] == 0))
train["killsWithoutMoving"].head()
train["killsWithoutMoving"].describe()
# 检查是否存在有击杀但是没有移动的数据
train[train['killsWithoutMoving'] == True].shape
(1535, 37)
train[train['killsWithoutMoving'] == True].head()
# 删除这些数据
train.drop(train[train['killsWithoutMoving'] == True].index, inplace=True)
4.2.4.2 异常值处理:删除驾车杀敌数异常的数据
# 查看载具杀敌数超过十个的玩家
train[train['roadKills'] > 10]
# 删除这些数据
train.drop(train[train['roadKills'] > 10].index, inplace=True)
4.2.4.3 异常值处理:删除玩家在一局中杀敌数超过30人的数据
# 首先绘制玩家杀敌数的条形图
plt.figure(figsize=(10,4))
sns.countplot(train["kills"])
plt.show()
train[train['kills'] > 30].shape
(95, 37)
train[train['kills'] > 30].head()
4.2.4.4 异常值处理:删除爆头率异常数据
如果一个玩家的击杀爆头率过高,也说明其有问题
# 创建变量爆头率
train['headshot_rate'] = train['headshotKills'] / train['kills']
train['headshot_rate'] = train['headshot_rate'].fillna(0)
train["headshot_rate"].tail()
# 绘制爆头率图像
plt.figure(figsize=(12,4))
sns.distplot(train['headshot_rate'], bins=10, kde=False)
plt.show()
train[(train['headshot_rate'] == 1) & (train['kills'] > 9)].shape
(24, 38)
train[(train['headshot_rate'] == 1) & (train['kills'] > 9)].head()
train.drop(train[(train['headshot_rate'] == 1) & (train['kills'] > 9)].index, inplace=True)
4.2.4.5 异常值处理:删除最远杀敌距离异常数据
# 绘制图像
plt.figure(figsize=(12,4))
sns.distplot(train['longestKill'], bins=10, kde=False)
plt.show()
# 找出最远杀敌距离大于等于1km的玩家
train[train['longestKill'] >= 1000].shape
(20, 38)
train[train['longestKill'] >= 1000]["longestKill"].head()
train.drop(train[train['longestKill'] >= 1000].index, inplace=True)
train.shape
(4445287, 38)
4.2.4.6 异常值处理:删除关于运动距离的异常值
# 距离整体描述
train[['walkDistance', 'rideDistance', 'swimDistance', 'totalDistance']].describe()
a)行走距离处理
train[train['walkDistance'] >= 10000].shape
(219, 38)
train[train['walkDistance'] >= 10000].head()
train.drop(train[train['walkDistance'] >= 10000].index, inplace=True)
b)载具行驶距离处理
train[train['rideDistance'] >= 20000].shape
(150, 38)
train[train['rideDistance'] >= 20000].head()
train.drop(train[train['rideDistance'] >= 20000].index, inplace=True)
c)游泳距离处理
train[train['swimDistance'] >= 2000].shape
train[train['swimDistance'] >= 2000][["swimDistance"]]
train.drop(train[train['swimDistance'] >= 2000].index, inplace=True)
4.2.4.7 异常值处理:武器收集异常值处理
train[train['weaponsAcquired'] >= 80].shape
(19, 38)
train[train['weaponsAcquired'] >= 80][['weaponsAcquired']].head()
train.drop(train[train['weaponsAcquired'] >= 80].index, inplace=True)
4.2.4.8 异常值处理:删除使用治疗药品数量异常值
train[train['heals'] >= 40].shape
(135, 38)
train[train['heals'] >= 40][["heals"]].head()
train.drop(train[train['heals'] >= 40].index, inplace=True)
4.2.5 类别型数据处理
4.2.5.1 比赛类型one-hot处理
# 关于比赛类型,共有16种方式
train['matchType'].unique()
# 对matchType进行one_hot编码
# 通过在后面添加的方式,实现,赋值并不是替换
train = pd.get_dummies(train, columns=['matchType'])
train.shape
# 通过正则匹配查看具体内容
matchType_encoding = train.filter(regex='matchType')
matchType_encoding.head()
4.2.5.2 对groupId,matchId等数据进行处理
关于groupId,matchId这类型数据,也是类别型数据。但是它们的数据量特别多,如果你使用one-hot编码,无异于自杀。
在这儿我们把它们变成用数字统计的类别型数据依旧不影响我们正常使用。
# 把groupId 和 match Id 转换成类别类型 categorical types
# 就是把一堆不怎么好识别的内容转换成数字# 转换group_id
train["groupId"].head()
train['groupId'] = train['groupId'].astype('category')
train["groupId_cat"] = train["groupId"].cat.codes
train["groupId_cat"].head()
# 转换match_id
train['matchId'] = train['matchId'].astype('category')train['matchId_cat'] = train['matchId'].cat.codes# 删除之前列
train.drop(['groupId', 'matchId'], axis=1, inplace=True)# 查看新产生列
train[['groupId_cat', 'matchId_cat']].head()
4.2.6 数据截取
4.2.6.1 取部分数据进行使用(1000000)
# 取前100万条数据,进行训练
sample = 1000000
df_sample = train.sample(sample)
4.2.7 确定特征值和目标值
# 确定特征值和目标值
df = df_sample.drop(["winPlacePerc", "Id"], axis=1) #all columns except targety = df_sample['winPlacePerc'] # Only target variable
4.2.8 分割训练集和测试集
from sklearn.model_selection import train_test_splitX_train, X_valid, y_train, y_valid = train_test_split(df, y, test_size=0.2)
4.3 机器学习(模型训练)和评估
# 导入需要训练和评估api
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
4.3.1 使用随机森林对模型进行训练
4.3.1.1 初步使用随机森林进行模型训练
# 模型训练
m1 = RandomForestRegressor(n_estimators=40, min_samples_leaf=3, max_features='sqrt',n_jobs=-1)
# n_jobs=-1 表示训练的时候,并行数和cpu的核数一样,如果传入具体的值,表示用几个核去跑m1.fit(X_train, y_train)
y_pre = m1.predict(X_valid)
m1.score(X_valid, y_valid)
mean_absolute_error(y_true=y_valid, y_pred=y_pre)
经过第一次计算,得出准确率为:0.92, mae=0.061
4.3.1.2 再次使用随机森林,进行模型训练
减少特征值,提高模型训练效率
# 查看特征值在当前模型中的重要程度
m1.feature_importances_
imp_df = pd.DataFrame({"cols":df.columns, "imp":m1.feature_importances_})
imp_df = imp_df.sort_values("imp", ascending=False)
# 绘制特征重要性程度图,仅展示排名前二十的特征
imp_df[:20].plot('cols', 'imp', figsize=(20,8), kind = 'barh')
plt.show()
# 保留比较重要的特征
to_keep = imp_df[imp_df.imp>0.005].cols
print('Significant features: ', len(to_keep))
to_keep
# 由这些比较重要的特征值,生成新的df
df[to_keep].head()
# 重新制定训练集和测试集
df_keep = df[to_keep]
X_train, X_valid, y_train, y_valid = train_test_split(df_keep, y, test_size=0.2)
# 模型训练
m2 = RandomForestRegressor(n_estimators=40, min_samples_leaf=3, max_features='sqrt',n_jobs=-1)
# n_jobs=-1 表示训练的时候,并行数和cpu的核数一样,如果传入具体的值,表示用几个核去跑m2.fit(X_train, y_train)
# 模型评分
y_pre = m2.predict(X_valid)
m2.score(X_valid, y_valid)
# mae评估
mean_absolute_error(y_true=y_valid, y_pred=y_pre)# 使用该种方法,最后得到的结果,反而更差,停止优化!
4.3.2 使用lightGBM对模型进行训练
4.3.2.1 模型初次尝试
import lightgbm as lgb# 创建模型,训练模型
gbm = lgb.LGBMRegressor(objective='regression', num_leaves=31, learning_rate=0.05, n_estimators=20)gbm.fit(X_train, y_train, eval_set=[(X_valid, y_valid)], eval_metric='l1', early_stopping_rounds=5)
# ps:最后几个参数必须一起使用.
# 测试机预测
y_pred = gbm.predict(X_valid, num_iteration=gbm.best_iteration_)
mean_absolute_error(y_valid, y_pred)
4.3.2.2 模型二次调优
from sklearn.model_selection import GridSearchCV
# 网格搜索,参数优化
estimator = lgb.LGBMRegressor(num_leaves=31)
param_grid = {'learning_rate': [0.01, 0.1, 1],'n_estimators': [20, 40, 60, 100, 200, 300]
}
gbm = GridSearchCV(estimator, param_grid, cv=5, n_jobs=-1) # 此处原来有个警告,通过此添加,解决掉!!!
gbm.fit(X_train, y_train)
# 测试机预测
y_pred = gbm.predict(X_valid)
mean_absolute_error(y_valid, y_pred)
print('交叉验证\网格搜索调试后,最好的参数模型是:', gbm.best_params_)
交叉验证\网格搜索调试后,最好的参数模型是: {‘learning_rate’: 0.1, ‘n_estimators’: 300}
4.3.2.3 模型三次调优[思路]
# n_estimators
scores = []
n_estimators = [100, 500, 1000]for nes in n_estimators:lgbm = lgb.LGBMRegressor(boosting_type='gbdt', num_leaves=31,max_depth=5,learning_rate=0.1,n_estimators=nes,min_child_samples=20,n_jobs=-1)lgbm.fit(X_train, y_train, eval_set=[(X_valid, y_valid)], eval_metric='l1', early_stopping_rounds=5)y_pre = lgbm.predict(X_valid)mae = mean_absolute_error(y_valid, y_pre)scores.append(mae)print("test data mae eval : {}".format(mae))
plt.plot(n_estimators,scores,'o-')
plt.ylabel("mae")
plt.xlabel("n_estimator")
print("best n_estimator {}".format(n_estimators[np.argmin(scores)]))
best n_estimator 500
# max_depths
scores = []
max_depths = [1, 5, 7, 9]for nes in max_depths:lgbm = lgb.LGBMRegressor(boosting_type='gbdt', num_leaves=31,max_depth=nes,learning_rate=0.1,n_estimators=1000,min_child_samples=20,n_jobs=-1)lgbm.fit(X_train, y_train, eval_set=[(X_valid, y_valid)], eval_metric='l1', early_stopping_rounds=5)y_pre = lgbm.predict(X_valid)mae = mean_absolute_error(y_valid, y_pre)scores.append(mae)print("test data mae eval : {}".format(mae))
scores
plt.plot(max_depths,scores,'o-')
plt.ylabel("mae")
plt.xlabel("max_depths")
print("best max_depths {}".format(max_depths[np.argmin(scores)]))
best max_depths 7
其他参数可以依次如上调优。
ML入门学习暂且告一段落!感谢老师的悉心讲解!希望我可以慢慢去懂这些知识。
加油!
感谢!
努力!