데이터 분석/파이썬

[Kaggle/캐글]PUBG 승률예측_1

22rodnjf 2021. 5. 31. 21:15

캐글 Competitions를 보다 보니 게임에 관련된 내용이 몇 가지 있었습니다.

그중에서 오픈베타때부터 꾸준히 즐겨오다 현재는 하고 있지 않은 PUBG가 눈에 들어왔습니다.

https://www.kaggle.com/c/pubg-finish-placement-prediction/overview

 

PUBG Finish Placement Prediction (Kernels Only)

Can you predict the battle royale finish of PUBG Players?

www.kaggle.com

PUBG의 캐글 목표는 승률 예측입니다.

현재는 Competiton이 종료되었지만 그래도 제출은 가능하기 때문에 한번 작업을 해봤습니다.

1차 결과가 생각보다 실망스러웠기 때문에 Tensorflow의 사용에 조금 더 익숙해지면 Tensorflow로 한번 더 작업을 해보는 게 좋을 것 같습니다.

결과는 아래에서 보여드리는 것처럼 간신히 평균을 조금 넘겼습니다.

게다가 전체 1528건의 Competition 중에서 1453~1454 사이이니까 하위 5%입니다. 형편없는 성적이네요.

sample_submission의 값이 0.52662 였으니 그보단 높지만, 많이 아쉽습니다.


먼저 데이터를 불러온 뒤 필요하지 않다고 판단한 데이터를 drop 했습니다.

아마 여기서 첫 번째 실수가 있었던 것 같습니다.

 

groupId 같은 그룹과 식별자를 분류하는 코드를 날려버린 것은 크게 문제가 없다고 판단했지만

지금 와서 되짚어 봤을 때는 KillStreaks(짧은 시간 죽인 적 플레이어의 최대수), KillPlace(죽인 적 기반 순위), rankPoints(Elo순위) 등을 가져오지 않은 것이 큰 실수 같습니다.

제가 판단했던 아이디어는 조금 더 단순했습니다.

df = pd.read_csv('/kaggle/input/pubg-finish-placement-prediction/train_V2.csv')
df = df.drop(["groupId","matchId","roadKills","swimDistance","killStreaks",
              "rankPoints","vehicleDestroys","winPoints","killPoints","longestKill",
              "matchDuration","numGroups","maxPlace","killPlace","teamKills"], axis=1)
df.head(10)

우선 남은 값들의 null 값을 확인했습니다.

df.isnull().sum()

19만 개의 데이터에서 한건의 null 값이 있었기 때문에 이는 dorpna()로 삭제 처리했습니다.

여기서 보이듯 저는 입힌 대미지, 적을 죽인 수, 녹다운시킨 수, 살린 수 정도만의 데이터로 충분히 순위를 예측할 수 있다고 판단했습니다.

이게 가장 큰 실수였던 것 같습니다.

해보신 분들은 아시겠지만 PUBG는 단순하게 상대를 많이 죽이고, 이동을 많이 한다고 해서 승리하는 게임이 아닙니다.

매우 다양한 요소들이 합쳐져서 결과가 만들어지는 게임입니다.

그렇다 보니 지금 생각해 봤을 때는 전반적인 요소들에 대한 가중치를 별도로 만들어서 승리 예측에 가중치를 주는 형태로 만드는 것이 좋을 것 같습니다.

이는 결과론적 이야기이고, 우선 마저 설명드리도록 하겠습니다.


matchType이 종류가 다양하게 나뉘어 있었기 때문에 우선 matchType의 정리부터 하는 것이 필요합니다.

df['matchType'].value_counts()

1인칭과 3인칭간의 차이는 크지 않다고 생각했기 때문에

크게 솔로 / 듀오 / 스쿼드로 분류하고 nomal과 rank의 차이는 분류하기 위해서 nomal - 솔로 / 듀오 / 스쿼드 로 한 번 더 분류하고 남은 flare와 crash는 전부 7번으로 묶었습니다.

df['matchType'] = df['matchType'].replace({'squad':1, 'squad-fpp':1, 'duo':2, 'duo-fpp':2, 'solo':3,
                                           'solo-fpp':3, 'normal-squad-fpp':4, 'normal-squad':4,
                                           'normal-duo-fpp':5, 'normal-duo':5, 'normal-solo-fpp':6,
                                           'normal-solo':6, 'crashfpp':7, 'flaretpp':7, 'flarefpp':7, 'crashtpp':7 })

df['matchType'].value_counts()


이후 각 매치별로 별도의 DataFrame을 만들어 줬습니다.

df_1 = df[(df.matchType == 1)]
df_2 = df[(df.matchType == 2)]
df_3 = df[(df.matchType == 3)]
df_4 = df[(df.matchType == 4)]
df_5 = df[(df.matchType == 5)]
df_6 = df[(df.matchType == 6)]
df_7 = df[(df.matchType == 7)]

우선 강 항목별 상관 분석을 먼저 진행하였습니다.

import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.tree import DecisionTreeClassifier
from sklearn.tree import plot_tree

plt.figure(figsize=(10,10))
sns.heatmap(df_2.corr(),cmap='coolwarm',annot=True)
plt.show()

이 항목을 봤을 때 DBNOs와 kills가 damageDealt와 유사도가 높은 것은 아무래도 딜을 많이 했다는 것 자체가 kill과 knockdown과 이어질 가능성이 높습니다.

그런데 의외인 점은 boosts를 많이 사용한 것과 walkDistance(이동거리), weaponAcquired(픽업 무기수)가 많을 경우 승률과의 상관도가 높아지지만 킬과 다운 숫자는 승률과 상관도가 크지 않다는 것입니다.


이후 winPlacePerc와 string 값인 Id는 drop 하고 y값으로 winPlacePerc를 옮겨준 뒤 test 데이터를 50% 수준으로 만들었습니다.

X_1=df_1.drop(["winPlacePerc", "Id"],axis=1)
X_2=df_2.drop(["winPlacePerc", "Id"],axis=1)
X_3=df_3.drop(["winPlacePerc", "Id"],axis=1)
X_4=df_4.drop(["winPlacePerc", "Id"],axis=1)
X_5=df_5.drop(["winPlacePerc", "Id"],axis=1)
X_6=df_6.drop(["winPlacePerc", "Id"],axis=1)
X_7=df_7.drop(["winPlacePerc", "Id"],axis=1)

y_1=df_1["winPlacePerc"]
y_2=df_2["winPlacePerc"]
y_3=df_3["winPlacePerc"]
y_4=df_4["winPlacePerc"]
y_5=df_5["winPlacePerc"]
y_6=df_6["winPlacePerc"]
y_7=df_7["winPlacePerc"]

from sklearn.model_selection import train_test_split
X_train1, X_test1, y_train1, y_test1 = train_test_split(X_1, y_1, test_size=0.5, random_state=0)
X_train2, X_test2, y_train2, y_test2 = train_test_split(X_2, y_2, test_size=0.5, random_state=0)
X_train3, X_test3, y_train3, y_test3 = train_test_split(X_3, y_3, test_size=0.5, random_state=0)
X_train4, X_test4, y_train4, y_test4 = train_test_split(X_4, y_4, test_size=0.5, random_state=0)
X_train5, X_test5, y_train5, y_test5 = train_test_split(X_5, y_5, test_size=0.5, random_state=0)
X_train6, X_test6, y_train6, y_test6 = train_test_split(X_6, y_6, test_size=0.5, random_state=0)
X_train7, X_test7, y_train7, y_test7 = train_test_split(X_7, y_7, test_size=0.5, random_state=0)

이후 X_1 값 만을 이용하여 3개의 각기 다른 머신러닝 알고리즘을 사용하였습니다.

가장 범용성 있게 쓰이는 LinearRegression(선형 회귀)와  DecisionTreeRegressor(의사결정 나무), neural_network(인공신경망) 3개를 사용하였습니다.

 

from sklearn.linear_model import LinearRegression
r_linear = LinearRegression(fit_intercept = True)
r_linear.fit(X_train1 ,y_train1)
linear_pred= r_linear.predict(X_test1)

from sklearn.tree import DecisionTreeRegressor
r_tree = DecisionTreeRegressor(ccp_alpha=0.0,min_impurity_decrease=0.007,min_samples_split=2,random_state=0)
r_tree.fit(X_train1, y_train1)
tree_pred= r_tree.predict(X_test1)

from sklearn.neural_network import MLPRegressor
from sklearn.preprocessing import StandardScaler
scaler1 = StandardScaler()
X_train_scaled1 = scaler1.fit_transform(X_train1)
X_test_scaled1 = scaler1.transform(X_test1)
r_nn1 = MLPRegressor(hidden_layer_sizes=(5), random_state=0, max_iter = 1000)
r_nn1.fit(X_train_scaled1, y_train1)

이에 대해 적합도 판단을 위해 R2(결정계수), mae(평균 절대 오차), mape(평균 절대 비오차), mse(평균 제곱 오차)

네 가지를 사용하여 적합도를 판단하였습니다.

결정계수에서 Neural Network가 가장 높은 0.77이 나왔습니다.

from sklearn.metrics import r2_score as r2
print('Linear Regression:',np.round(r2(y_test1,r_linear.predict(X_test1)),2))
print('Pruned Tree      :',np.round(r2(y_test1,r_tree.predict(X_test1)),2))
print('Neural Network   :',np.round(r2(y_test1,r_nn1.predict(X_test_scaled1)),2))

 

mae에서 역시 Neural Network가 가장 낮은 0.11이 나왔습니다.

이후 mape, mse 모두 Neural Network가 가장 낮은 것으로 나왔기 때문에 Neural Network를 채용하였습니다.

from sklearn.metrics import mean_absolute_error as mae
print('Linear Regression:',np.round(mae(y_test1,r_linear.predict(X_test1)),2))
print('Pruned Tree      :',np.round(mae(y_test1,r_tree.predict(X_test1)),2))
print('Neural Network   :',np.round(mae(y_test1,r_nn1.predict(X_test_scaled1)),2))

from sklearn.metrics import mean_absolute_percentage_error as mape
print('Linear Regression:',np.round(mape(y_test1,r_linear.predict(X_test1)),2))
print('Pruned Tree      :',np.round(mape(y_test1,r_tree.predict(X_test1)),2))
print('Neural Network   :',np.round(mape(y_test1,r_nn1.predict(X_test_scaled1)),2))

from sklearn.metrics import mean_squared_error as mse
print('Linear Regression:',np.round(mse(y_test1,r_linear.predict(X_test1)),2))
print('Pruned Tree      :',np.round(mse(y_test1,r_tree.predict(X_test1)),2))
print('Neural Nerwork   :',np.round(mse(y_test1,r_nn1.predict(X_test_scaled1)),2))

이후 위에서 만들어둔 1~7번의 각 데이터를 모두 Neural Network로 학습을 시켰습니다.

그 후 학습을 통해서 만들어진 r_nn 예측값을 테스트 데이터에 적용했습니다.

x_01 = df1.drop(["winPlacePerc","Id"], axis=1)
x_02 = df2.drop(["winPlacePerc","Id"], axis=1)
x_03 = df3.drop(["winPlacePerc","Id"], axis=1)
x_04 = df4.drop(["winPlacePerc","Id"], axis=1)
x_05 = df5.drop(["winPlacePerc","Id"], axis=1)
x_06 = df6.drop(["winPlacePerc","Id"], axis=1)
x_07 = df7.drop(["winPlacePerc","Id"], axis=1)

df1.loc[:,'winPlacePerc'] = r_nn1.predict(x_01)
df2.loc[:,'winPlacePerc'] = r_nn2.predict(x_02)
df3.loc[:,'winPlacePerc'] = r_nn3.predict(x_03)
df4.loc[:,'winPlacePerc'] = r_nn4.predict(x_04)
df5.loc[:,'winPlacePerc'] = r_nn5.predict(x_05)
df6.loc[:,'winPlacePerc'] = r_nn6.predict(x_06)
df7.loc[:,'winPlacePerc'] = r_nn7.predict(x_07)

이제 마지막으로 reslut에 정답을 입력할 테이블을 불러왔습니다.

reslut = pd.read_csv('/kaggle/input/pubg-finish-placement-prediction/sample_submission_V2.csv')
reslut.head()

이후 각각의 값들을 모두 merge로 불러온 뒤 sum을 통해서 합쳐줬습니다.

여기서 잘못된 점은 winPlacePerc 점수 자체가 1~0.0001 사이에서 나와야 하는데 기본적으로 10 이상의 숫자가 나온 부분입니다.

이에 대해 /100을 해서 소수점 아래로 내리긴 했지만 다른 근본적 원인이 있는 것 같기 때문에 원론적으로 다시 확인을 해봐야 할 것 같습니다.

reslut = pd.merge(left = reslut, right = df1, how = "left", on = "Id")
reslut = pd.merge(left = reslut, right = df2, how = "left", on = "Id")
reslut = pd.merge(left = reslut, right = df3, how = "left", on = "Id")
reslut = pd.merge(left = reslut, right = df4, how = "left", on = "Id")
reslut = pd.merge(left = reslut, right = df5, how = "left", on = "Id")
reslut = pd.merge(left = reslut, right = df6, how = "left", on = "Id")
reslut = pd.merge(left = reslut, right = df7, how = "left", on = "Id")

reslut = reslut.fillna(0)
reslut.columns = ['Id','winPlacePerc','winPlacePerc1','winPlacePerc2','winPlacePerc3','winPlacePerc4','winPlacePerc5','winPlacePerc6','winPlacePerc7']
reslut = reslut.astype({'winPlacePerc':'float'})

for i, row in reslut.iterrows():
    reslut.at[i,'winPlacePerc'] = (reslut.at[i,'winPlacePerc1'] + reslut.at[i,'winPlacePerc2'] + reslut.at[i,'winPlacePerc3'] + reslut.at[i,'winPlacePerc4'] + reslut.at[i,'winPlacePerc5'] + reslut.at[i,'winPlacePerc6'] + reslut.at[i,'winPlacePerc7'])/100
    
    
reslut = reslut.drop(["winPlacePerc1","winPlacePerc2","winPlacePerc3","winPlacePerc4","winPlacePerc5","winPlacePerc6","winPlacePerc7"], axis=1)
reslut.head()