kaggle

[kaggle] Scrabble Player Rating 예측 - XGBoost 알고리즘

weweGH 2024. 7. 24. 18:03
반응형

kaggle xgboost
kaggle xgboost


Scrabble Player Rating 예측 - XGBoost 알고리즘


OUTLINE

  • Scrabble Player Rating 대회 개요
  • scrabble 게임이란?
  • 분석 데이터 
  • 분석 방법

Scrabble Player Rating 대회 개요

이 대회는 2023년 1월쯤 개최된 대회이다. Woogles.io에서 데이터를 받아서 Scrabble 게임에서 플레이어들의 등급을 예측하는 것이 대회의 목표다. 평가 방법은 RMSE를 사용한다.


Scarbble 게임이란?

알파벳 철자를 활용하여 단어를 만드는 게임. 각 알파벳 별로 점수가 다르고, 단어 별로 합산된 점수에 따라 승패가 결정된다.

scrabble
scrabble


분석 데이터

이 대회에 사용되는 데이터는 총 4개로, 학습데이터셋인 train.csv, 테스트 데이터셋인 test.csv 외에 games.csv, turns.csv가 있다.


train.csv

train  = pd.read_csv('train.csv')
print('len(train):',len(train))
train.head()

train
train

학습 데이터셋인 train.csv 는 game_id, nickname, score, rating 칼럼으로 이루어져 있다. train 데이터는 총 100,820 rows이다.

- game_id : 게임에 대한 고유한 id 키값

- nickname : 플레이어의 유저네임

- score : 각 게임에 대한 각 플레이어별 최종 점수

- rating은 게임이 시작되기 전의 플레이어에 대한 등급 


test.csv

test = pd.read_csv('test.csv')
print('len(test):',len(test))
test.head()

test
test

테스트 데이터셋도 train.csv와 동일한 컬럼으로 이루어져 있고, 총 44,726 rows이다.


games.csv

games = pd.read_csv('games.csv')
print('len(games):',len(games))
games.head()

games
games

게임에 대한 메타데이터이다. 예를 들어, game_id별로 언제 게임을 시작했고, 얼마나 게임이 진행됐는가에 대한 정보가 담겨있다. 총 72,773 rows이다.

- game_id : 게임에 대한 고유한 id 키값

- first : 처음 시작한 플레이어

- time_control_name : 사용된 time control 이름('regular', 'rapid', 'blitz')

- game_end_reason : 게임 종료 사유

- winner : 게임을 이긴 플레이어

- created_at : 게임 시작 시점

- lexicon : 사용된 English lexicon

- initial_time_seconds : 게임에서 각 플레이어의 시간제한

- increment_seconds : 각 플레이어가 교대할 때마다 얻는 시간 이득

- rating_mode : 게임이 플레이어 등급에 포함되는지 여부

- max_overtime_minutes : 플레이어가 타임아웃을 하기 전에 초과된 시간

- game_duration_seconds : 게임이 지속된 시간


turns.csv

turns = pd.read_csv('turns.csv')
print('len(turns):',len(turns))
turns.head()

turns
turns

 각 게임별로 모든 순서에 대하여 몇 점을 얻었고, 어떤 알파벳이 남아 있는지에 대한 데이터이다. 총 2,005,498 rows이다.

- game_id : 게임에 대한 고유한 id 키값

- turn_number : 게임에서 turn number

- nickname : 플레이어의 유저네임

- rack : 플레이어의 현재 rack

- location : 보드에서 플레이어가 타일을 놓은 위치

- move : 플레이어가 놓은 타일

- points : 플레이어가 각 순서에서 얻은 점수

- score : 플레이어가 각 순서에서 얻은 총 점수

- turn_type : 교대 type('Play', 'Exchange', 'Pass', 'Six-Zero Rule', 'Challenge')


분석방법

Data preprocessing → Select variables → train, test split → XGBoost 알고리즘 적용


Data Preprocessing

플레이어들의 등급을 예측하는 대회이기 때문에, 이에 필요한 칼럼이 더 있는지 우선 확인해 보았다. 첫 번째, 플레이어의 등급은 시간이 지남에 따라 변화가 있을 수 있다는 가설을 세웠다. 그리고 games 데이터에서 created_at 칼럼을 활용하여 연도 변수(created_at_yyyy), 월 변수(created_at_mm), 시간 변수(created_at_hh), 요일 변수(created_at_weekday), 평일/주말 구분 변수(created_at_weekday_gubun)를 생성했다. 

games['created_at_yyyy'] = games.created_at.str[0:4]
games['created_at_mm'] = games.created_at.str[5:7]
games['created_at_hh'] = games.created_at.str[11:13]
games['created_at_weekday'] = pd.to_datetime(games.created_at).dt.weekday
games['created_at_weekday_gubun'] = 0
games.loc[games.created_at_weekday == 5,'created_at_weekday_gubun'] = 1
games.loc[games.created_at_weekday == 6,'created_at_weekday_gubun'] = 1

games 전처리
games 전처리


두 번째, 닉네임별로 게임의 길이가 영향을 미칠 수 있다는 가설을 세웠다. turns에서 game_id별 game_length를 구해준 뒤, turns에서 game_id, nickname을 중복제거한 데이터프레임과 merge를 통해 nickname별 game_length_nickname 변수를 생성했다.

imsi = turns.groupby(['game_id'])['cnt'].sum().reset_index()
imsi.rename(columns={'cnt':'game_length'},inplace=True) # game_id별 game_length
imsi2 = pd.merge(turns[['game_id','nickname']].drop_duplicates(),imsi,on='game_id',how='left')
imsi2 = imsi2.groupby(['nickname'])['game_length'].mean().reset_index()
imsi2.rename(columns={'game_length':'game_length_nickname'},inplace=True)

imsi2
imsi2


세 번째, nickname_gubun 컬럼을 생성하여, nickname이 로봇인지 아닌지를 구별한다. nickname 구분을 위해 upper를 통해 대문자로 변환 후 'BOT'를 포함하면 1 아니면 0으로 진행했다.

train['nickname_gubun'] = 0
train['nickname2'] = train['nickname'].str.upper()
train.loc[train.nickname2.str.contains('BOT'),'nickname_gubun'] = 1

train 전처리
train 전처리


네 번째, nickname별 평균 point를 계산하여 avg_points 컬럼을 생성했다.

imsi = turns.groupby(['nickname'])['points'].mean().reset_index()
imsi.rename(columns={'points':'avg_points'},inplace=True)
imsi.head()

imsi head
imsi head


다섯 번째, nickname별 평균 rack_len, move_len, move_prop을 계산하여 컬럼을 생성했다.

imsi = turns.groupby(['nickname'])[['rack_len','move_len','move_prop']].mean().reset_index()
imsi.rename(columns={'rack_len':'rack_len_nickname','move_len':'move_len_nickname','move_prop':'move_prop_nickname'},inplace=True)
imsi.head()

계산 컬럼 생성
계산 컬럼 생성


여섯 번째, nickname별 승리하는 확률을 계산하여 win_prob 변수를 생성했다.

# nickname별 이긴 횟수
imsi1 = turns[['game_id','nickname','score']].sort_values(['game_id','score'],ascending=[True,False])
imsi1 = imsi1.drop_duplicates(['game_id'])
imsi1['cnt'] = 1
imsi1 = imsi1.groupby(['nickname'])['cnt'].sum().reset_index()
imsi1.rename(columns={'cnt':'이긴횟수'},inplace=True)

imsi2 = turns[['game_id','nickname']].drop_duplicates()
imsi2['cnt'] = 1
# nickname별 게임 횟수
imsi2 = imsi2.groupby(['nickname'])['cnt'].sum().reset_index()
imsi2.rename(columns={'cnt':'게임횟수'}, inplace=True)

print(len(imsi1))
print(len(imsi2))
imsi = pd.merge(imsi1,imsi2,on='nickname',how='outer')
print(len(imsi))
imsi['win_prob'] = round(imsi.이긴횟수 / imsi.게임횟수 * 100,2)
imsi = imsi[['nickname','win_prob']]
imsi.head()

승리 확률 win_prob
승리 확률 win_prob


일곱 번째, nickname별 평균적으로 얻는 점수를 계산하여 avg_score_nickname 변수를 생성했다.

imsi = turns.groupby(['game_id','nickname'])['score'].max().reset_index()
imsi = imsi.groupby(['nickname'])['score'].mean().reset_index()
imsi.rename(columns={'score':'avg_score_nickname'},inplace=True)
imsi.head()

avg_score_nickname
avg_score_nickname


Select variables

앞의 과정들을 통해 추가한 변수들과 기존의 train 변수들을 독립변수로 선택했다. 그리고 그중 범주형 변수들은 label encoding을 통해 숫자형 변수로 변환했다.

col_nm = ['nickname','score','first','time_control_name','lexicon','initial_time_seconds',
          'game_duration_seconds_nickname','created_at_mm','created_at_hh',
          'game_length_nickname','points_per_games','avg_points','rack_len_nickname','move_len_nickname','move_prop_nickname','rating_mode',
          'max_overtime_minutes','Play','win_prob','avg_score_nickname','nickname_gubun','created_at_weekday_gubun']

# label encoding
str_col = ['nickname','first','time_control_name','lexicon','rating_mode']
for i in str_col:
    le = LabelEncoder()
    le=le.fit(train[i])
    train[i]=le.transform(train[i])
    #
    for label in (test[i].unique()):
        if label not in le.classes_: 
            le.classes_ = np.append(le.classes_, label)
    test[i]=le.transform(test[i])

train, test split

train_test_split 함수를 통해 train 데이터셋을 8:2 비율로 나누었다.

x_train, x_valid, y_train, y_valid = train_test_split(train[col_nm], train['rating'], test_size=0.2, shuffle=True, random_state=34)

XGBoost 알고리즘 적용

예측을 위한 알고리즘은 XGBoost를 선택했다. 별도의 파라미터 튜닝을 하지 않은 상태로 예측을 진행했다. 결정계수는 약 97%, 정확도 RMSE는 39.08이 나왔다. 

model = XGBRegressor(n_estimators=500)
model.fit(x_train,y_train) 
y_pred = model.predict(x_valid)

print('R2:', model.score(x_valid, y_valid)) 
print('rmse:',mean_squared_error(y_valid, y_pred, squared=False))
print('mape:', np.mean(np.abs((y_valid - y_pred) / y_valid)) * 100)

preds = model.predict(test[col_nm])
test['preds'] = preds
final_preds = test[test.rating.isna()][['game_id','preds']]
final_preds.rename(columns={'preds':'rating'},inplace=True)
final_preds.to_csv('submission.csv',index=False)

plot_importance를 통해 변수 중요도를 나타내는 그래프를 그려보았다. points_per_games, created_at_hh가 비교적 큰 영향을 미치는 것을 확인할 수 있다.

plt.rcParams["figure.figsize"] = (20,20)
plot_importance(model)

feature importance
feature importance


마무리

이 대회의 결과는 총 11번의 test를 했음에도 불구하고, 결과가 좋지 않았다. 기억하기로는 첫 kaggle 대회였는데, 어디서부터 뭘 시작해야 하는지 헤매기도 하고 혼란스러워서 과정에서 실수도 많았고 잘 진행되지 못했다. 변수를 선택하는 과정에서도 논리적인 검증도 추가적으로 필요하고, 모델 튜닝 부분에서도 개선해야 할 점이 많다. 앞으로 EDA 분석을 심도 있게 해서 분석에 개연성을 더하는 것을 목표로 할 것이다.


 

반응형