Scrabble Player Rating 예측 - XGBoost 알고리즘
OUTLINE
- Scrabble Player Rating 대회 개요
- scrabble 게임이란?
- 분석 데이터
- 분석 방법
Scrabble Player Rating 대회 개요
이 대회는 2023년 1월쯤 개최된 대회이다. Woogles.io에서 데이터를 받아서 Scrabble 게임에서 플레이어들의 등급을 예측하는 것이 대회의 목표다. 평가 방법은 RMSE를 사용한다.
Scarbble 게임이란?
알파벳 철자를 활용하여 단어를 만드는 게임. 각 알파벳 별로 점수가 다르고, 단어 별로 합산된 점수에 따라 승패가 결정된다.
분석 데이터
이 대회에 사용되는 데이터는 총 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.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()
테스트 데이터셋도 train.csv와 동일한 컬럼으로 이루어져 있고, 총 44,726 rows이다.
games.csv
games = pd.read_csv('games.csv')
print('len(games):',len(games))
games.head()
게임에 대한 메타데이터이다. 예를 들어, 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()
각 게임별로 모든 순서에 대하여 몇 점을 얻었고, 어떤 알파벳이 남아 있는지에 대한 데이터이다. 총 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
두 번째, 닉네임별로 게임의 길이가 영향을 미칠 수 있다는 가설을 세웠다. 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)
세 번째, 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
네 번째, nickname별 평균 point를 계산하여 avg_points 컬럼을 생성했다.
imsi = turns.groupby(['nickname'])['points'].mean().reset_index()
imsi.rename(columns={'points':'avg_points'},inplace=True)
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()
일곱 번째, 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()
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)
마무리
이 대회의 결과는 총 11번의 test를 했음에도 불구하고, 결과가 좋지 않았다. 기억하기로는 첫 kaggle 대회였는데, 어디서부터 뭘 시작해야 하는지 헤매기도 하고 혼란스러워서 과정에서 실수도 많았고 잘 진행되지 못했다. 변수를 선택하는 과정에서도 논리적인 검증도 추가적으로 필요하고, 모델 튜닝 부분에서도 개선해야 할 점이 많다. 앞으로 EDA 분석을 심도 있게 해서 분석에 개연성을 더하는 것을 목표로 할 것이다.
'kaggle' 카테고리의 다른 글
[kaggle] Binary Classification of Machine Failures - XGBoost 알고리즘 (2) | 2024.08.06 |
---|---|
[kaggle] GoDaddy - Microbusiness Density Forecasting 예측 - XGBoost 알고리즘 (5) | 2024.07.30 |