European Soccer
- Country: 국가 정보
- League: 리그 정보
- Match: 경기 정보
- Player: 플레이어 정보
- Player_Attributes: 플레이어의 특성
- Team: 팀 정보
- Team_Attributes: 팀의 특성
데이터셋 준비
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import sqlite3
# sqlite3.connect()와 pd.read_sql_query()로 csv파일 읽어들이기
conn = sqlite3.connect('../input/soccer/database.sqlite')
df_country = pd.read_sql_query('SELECT * from Country', conn)
df_league = pd.read_sql_query('SELECT * from League', conn)
df_match = pd.read_sql_query('SELECT * from Match', conn)
df_player = pd.read_sql_query('SELECT * from Player', conn)
df_player_att = pd.read_sql_query('SELECT * from Player_Attributes', conn)
df_team = pd.read_sql_query('SELECT * from Team', conn)
df_team_att = pd.read_sql_query('SELECT * from Team_Attributes', conn)
EDA 및 데이터 기초 통계 분석
df_country.head()

df_country.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 11 entries, 0 to 10
Data columns (total 2 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 11 non-null int64
1 name 11 non-null object
dtypes: int64(1), object(1)
memory usage: 304.0+ bytes
df_league.head()

각 11개 국가의 리그 이름
df_league.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 11 entries, 0 to 10
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 11 non-null int64
1 country_id 11 non-null int64
2 name 11 non-null object
dtypes: int64(2), object(1)
memory usage: 392.0+ bytes
print(df_league['id'].unique()) # [1 1729 4769 7809 10257 13274 15722 17642 19694 21518 24558]
print(df_league['country_id'].unique()) # [1 1729 4769 7809 10257 13274 15722 17642 19694 21518 24558]
id 컬럼과 country_id 컬럼은 같은 값을 가진다.
df_match.head()

for c in df_match.columns:
print(c)
id
country_id
league_id
season
stage
...
GBD
GBA
BSH
BSD
BSA
for c, num in zip(df_match.columns, df_match.isna().sum()):
print(c, num)
id 0
country_id 0
league_id 0
season 0
stage 0
...
GBD 11817
GBA 11817
BSH 11818
BSD 11818
BSA 11818
# 도박 관련 컬럼 삭제
df_match.drop(df_match.columns[-38:], axis=1, inplace=True)
df_match.head()
df_match['season'].unique()
array(['2008/2009', '2009/2010', '2010/2011', '2011/2012', '2012/2013',
'2013/2014', '2014/2015', '2015/2016'], dtype=object)
season 컬럼에는 2008년부터 2016년까지 2년씩 묶어서 데이터가 들어있다.
df_player.head()

df_player_att.head()

df_team.head()

df_team_att.head()

# 데이터프레임 간 중복되는 Column이 있는지 확인하고 유용한 Column 식별
# match 테이블의 away_player_1과 player_att 테이블의 player_api_id가 서로 매칭되는지 확인
df_player_att['player_api_id'].value_counts()
41269 56
210278 56
42116 55
26472 54
179795 53
..
33688 2
470720 2
37366 2
9144 2
240561 2
Name: player_api_id, Length: 11060, dtype: int64
df_match['away_player_1']
0 NaN
1 NaN
2 NaN
3 NaN
4 NaN
...
25974 462944.0
25975 42276.0
25976 10637.0
25977 274776.0
25978 156175.0
Name: away_player_1, Length: 25979, dtype: float64
# null 데이터를 삭제하고 int로 형변환
df_match['away_player_1'].dropna().apply(int)
144 34480
145 37937
146 38252
147 36835
148 104378
...
25974 462944
25975 42276
25976 10637
25977 274776
25978 156175
Name: away_player_1, Length: 24745, dtype: int64
# player_att 테이블에서 date별로 player_api_id의 데이터가 존재하기 때문에 그룹화하여 평균값 사용
df_match['away_player_1'].dropna().apply(int).map(df_player_att.groupby('player_api_id').mean()['overall_rating'])
144 67.888889
145 64.952381
146 68.000000
147 65.050000
148 69.428571
...
25974 55.833333
25975 63.638889
25976 69.333333
25977 60.100000
25978 73.133333
Name: away_player_1, Length: 24745, dtype: float64
# away_player_1와 player_api_id가 모두 매칭되면 null값이 존재하지 않음
df_match['away_player_1'].dropna().apply(int).map(df_player_att.groupby('player_api_id').mean()['overall_rating']).isna().sum() # 0
df_match['home_player_1'].dropna().apply(int).map(df_player_att.groupby('player_api_id').mean()['overall_rating']).isna().sum() # 0
match 테이블의 home_player_1과 player_att 테이블의 player_api_id도 서로 매칭된다.
df_match['away_team_api_id'].map(df_team_att.groupby('team_api_id').mean()['buildUpPlaySpeed']).isna().sum() # 178
match 테이블의 away_team_api_id과 team_att 테이블의 team_api_id은 서로 매칭된다.
# seaborn의 heatmap()로 히트맵 그리기
fig = plt.figure(figsize=(5, 15))
sns.heatmap(df_player_att.drop(['id', 'player_fifa_api_id', 'player_api_id'], axis=1).corr()[['overall_rating']], annot=True)

종합적인 평가는 잠재력과 반사신경과 연관성이 높고, 골키퍼 능력과는 거의 연관성이 없다.
# 매치 데이터프레임에 팀 특성 데이터프레임 통합
df_team_att.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1458 entries, 0 to 1457
Data columns (total 25 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 1458 non-null int64
1 team_fifa_api_id 1458 non-null int64
2 team_api_id 1458 non-null int64
3 date 1458 non-null object
4 buildUpPlaySpeed 1458 non-null int64
5 buildUpPlaySpeedClass 1458 non-null object
6 buildUpPlayDribbling 489 non-null float64
7 buildUpPlayDribblingClass 1458 non-null object
8 buildUpPlayPassing 1458 non-null int64
9 buildUpPlayPassingClass 1458 non-null object
10 buildUpPlayPositioningClass 1458 non-null object
11 chanceCreationPassing 1458 non-null int64
12 chanceCreationPassingClass 1458 non-null object
13 chanceCreationCrossing 1458 non-null int64
14 chanceCreationCrossingClass 1458 non-null object
15 chanceCreationShooting 1458 non-null int64
16 chanceCreationShootingClass 1458 non-null object
17 chanceCreationPositioningClass 1458 non-null object
18 defencePressure 1458 non-null int64
19 defencePressureClass 1458 non-null object
20 defenceAggression 1458 non-null int64
21 defenceAggressionClass 1458 non-null object
22 defenceTeamWidth 1458 non-null int64
23 defenceTeamWidthClass 1458 non-null object
24 defenceDefenderLineClass 1458 non-null object
dtypes: float64(1), int64(11), object(13)
memory usage: 284.9+ KB
buildUpPlayDribbling 컬럼에 null 값이 많이 들어있다. → drop
df_team_att.drop('buildUpPlayDribbling', axis=1, inplace=True)
# 범주형 데이터는 가장 많이 나온 값을 사용
def most(x):
return x.value_counts().index[0]
# aggregate(), agg() : 여러 함수를 동시에 입력하여 결과를 출력
# dict을 이용하여 열이나 행마다 적용되는 함수를 다르게 설정 가능
team_map = df_team_att.groupby('team_api_id').aggregate(
{
'buildUpPlaySpeed' : 'mean',
'buildUpPlaySpeedClass' : most,
'buildUpPlayDribblingClass' : most,
'buildUpPlayPassing' : 'mean',
'buildUpPlayPassingClass' : most,
'buildUpPlayPositioningClass' : most,
'chanceCreationPassing' : 'mean',
'chanceCreationPassingClass' : most,
'chanceCreationCrossing' : 'mean',
'chanceCreationCrossingClass' : most,
'chanceCreationShooting' : 'mean',
'chanceCreationShootingClass' : most,
'chanceCreationPositioningClass' : most,
'defencePressure' : 'mean',
'defencePressureClass' : most,
'defenceAggression' : 'mean',
'defenceAggressionClass' : most,
'defenceTeamWidth' : 'mean',
'defenceTeamWidthClass' : most,
'defenceDefenderLineClass' : most
}
)
team_map

df = df_match[['home_team_goal', 'away_team_goal']].copy()
for team in ['home_', 'away_']:
team_map.index.name = team + 'team_api_id'
for col in team_map.columns:
df[team + col] = df_match[team_map.index.name].map(team_map[col])
team_api_id에 따른 팀 특성 데이터를 home과 away로 나눠서 생성
df.dropna(inplace=True)
df.head()

# 홈과 어웨이의 골 수를 범주형 데이터로 변환 (0: 홈팀 승, 1: 무승부, 2: 어웨이팀 승)
df['matchResult'] = df[['home_team_goal', 'away_team_goal']].agg(lambda x: 0 if x[0] > x[1] else 1 if x[0] == x[1] else 2, axis=1)
df.drop(['home_team_goal', 'away_team_goal'], axis=1, inplace=True)
df

데이터 전처리
# get_dummies()를 이용하여 범주형 데이터 전처리
# Class로 끝나는 컬럼 = 범주형 데이터
col_cats = list(filter(lambda s: s.find('Class') >= 0, df.columns))
df_cats = pd.get_dummies(df[col_cats], drop_first=True)
df_cats

from sklearn.preprocessing import StandardScaler
# StandardScaler을 이용하여 수치형 데이터 표준화 진행
X_num = df.drop(['matchResult'] + col_cats, axis=1)
scaler = StandardScaler()
scaler.fit(X_num)
X_scaled = scaler.transform(X_num)
X_scaled = pd.DataFrame(X_scaled, index=X_num.index, columns=X_num.columns)
X_cat = df_cats
X = pd.concat([X_scaled, X_cat], axis=1)
y = df['matchResult']
X

학습 데이터와 테스트 데이터 분리
from sklearn.model_selection import train_test_split
# train_test_split을 이용하여 학습 데이터와 테스트 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1)
Classification 모델 학습
1. Logistic Regression 모델
from sklearn.linear_model import LogisticRegression
# LogisticRegression 모델 생성/학습
model_lr = LogisticRegression(max_iter=10000)
model_lr.fit(X_train, y_train)
from sklearn.metrics import classification_report
pred = model_lr.predict(X_test)
print(classification_report(y_test, pred))
precision recall f1-score support
0 0.49 0.87 0.63 3484
1 0.22 0.00 0.00 1972
2 0.44 0.31 0.36 2233
accuracy 0.48 7689
macro avg 0.39 0.39 0.33 7689
weighted avg 0.41 0.48 0.39 7689
정확도(accuracy) = 0.48 (48%)
print(sum((y_test == 0) / len(y_test))) # 0.45311483938094915
print(sum((y_test == 1) / len(y_test))) # 0.2564702822213462
print(sum((y_test == 2) / len(y_test))) # 0.29041487839770624
2. XGBoost 모델
from xgboost import XGBClassifier
# XGBClassifier 모델 생성/학습
model_xgb = XGBClassifier(use_label_encoder=False, eval_metric='mlogloss')
model_xgb.fit(X_train, y_train)
pred = model_xgb.predict(X_test)
print(classification_report(y_test, pred))
precision recall f1-score support
0 0.54 0.72 0.61 3484
1 0.29 0.15 0.20 1972
2 0.45 0.40 0.42 2233
accuracy 0.48 7689
macro avg 0.42 0.42 0.41 7689
weighted avg 0.45 0.48 0.45 7689
정확도(accuracy) = 0.48 (48%)
모델 학습 결과 심화 분석
1. Logistic Regression 모델 계수로 상관성 파악
# Logistic Regression 모델의 coef_ 속성으로 plot 그리기
fig = plt.figure(figsize=(15, 5))
plt.plot(X.columns, model_lr.coef_[0])
plt.xticks(rotation=90)
plt.title('What makes Home Team Win?')
plt.grid()
plt.show()

어웨이팀의 수비진 압박이 높고, 홈팀의 수비진 공격성이 press 일수록 홈팀이 승리할 확률이 높다.
fig = plt.figure(figsize=(15, 5))
plt.plot(X.columns, model_lr.coef_[2])
plt.xticks(rotation=90)
plt.title('What makes Away Team Win?')
plt.grid()
plt.show()

홈팀의 수비진 압박이 높고, 홈팀의 측면공격수 포지션이 체계화되어 있을수록 어웨이팀이 승리할 확률이 높다.
2. XGBoost 모델로 특징의 중요도 확인
# XGBoost 모델의 feature_importances_ 속성으로 plot 그리기
fig = plt.figure(figsize=(15, 6))
plt.bar(X.columns, model_xgb.feature_importances_)
plt.xticks(rotation=90)
plt.show()

home_chanceCreationPositioningClass_Organized 피쳐 중요도가 가장 높다.
모델 성능 개선
df = df_match[['home_team_goal', 'away_team_goal']].copy()
for team in ['home_', 'away_']:
team_map.index.name = team + 'team_api_id'
for col in team_map.columns:
df[team + col] = df_match[team_map.index.name].map(team_map[col])
df.dropna(inplace=True)
df.head()

# feature가 너무 많으면 차원의 저주로 인해 학습률이 떨어질 수 있음
# 선수 특성 중 유의미한 정보를 매치 데이터프레임에 통합
player_map = df_player_att.groupby('player_api_id').mean()['overall_rating']
player_map
player_api_id
2625 60.142857
2752 69.380952
2768 69.285714
2770 71.133333
2790 70.200000
...
744907 51.909091
746419 59.000000
748432 58.000000
750435 56.444444
750584 58.000000
Name: overall_rating, Length: 11060, dtype: float64
for col in (s + str(idx) for s in ['home_player_', 'away_player_'] for idx in range(1, 12)):
df[col + '_rating'] = df_match[col].map(player_map)
df.dropna(inplace=True)
df.head()
# 홈과 어웨이의 골 수를 범주형 데이터로 변환 (0: 홈팀 승, 1: 무승부, 2: 어웨이팀 승)
df['matchResult'] = df[['home_team_goal', 'away_team_goal']].agg(lambda x: 0 if x[0] > x[1] else 1 if x[0] == x[1] else 2, axis=1)
df.drop(['home_team_goal', 'away_team_goal'], axis=1, inplace=True)
데이터 전처리 (성능 개선 후)
# StandardScaler을 이용하여 수치형 데이터 표준화 진행
X_num = df.drop(['matchResult'] + col_cats, axis=1)
scaler = StandardScaler()
scaler.fit(X_num)
X_scaled = scaler.transform(X_num)
X_scaled = pd.DataFrame(X_scaled, index=X_num.index, columns=X_num.columns)
X_cat = pd.get_dummies(df[col_cats], drop_first=True)
X = pd.concat([X_scaled, X_cat], axis=1)
y = df['matchResult']
학습 데이터와 테스트 데이터 분리 (성능 개선 후)
# train_test_split을 이용하여 학습 데이터와 테스트 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1)
Classification 모델 학습 (성능 개선 후)
1. Logistic Regression 모델
# LogisticRegression 모델 생성/학습
model_lr = LogisticRegression(max_iter=10000)
model_lr.fit(X_train, y_train)
pred = model_lr.predict(X_test)
print(classification_report(y_test, pred))
precision recall f1-score support
0 0.56 0.83 0.67 2945
1 0.32 0.01 0.02 1587
2 0.48 0.50 0.49 1842
accuracy 0.53 6374
macro avg 0.45 0.45 0.39 6374
weighted avg 0.48 0.53 0.45 6374
정확도(accuracy) = 0.53 (53%)
print(sum((y_test == 0) / len(y_test))) # 0.46203326011920937
print(sum((y_test == 1) / len(y_test))) # 0.24898023219328813
print(sum((y_test == 2) / len(y_test))) # 0.2889865076874782
2. XGBoost 모델
# XGBClassifier 모델 생성/학습
model_xgb = XGBClassifier(use_label_encoder=False, eval_metric='mlogloss')
model_xgb.fit(X_train, y_train)
pred = model_xgb.predict(X_test)
print(classification_report(y_test, pred))
precision recall f1-score support
0 0.56 0.75 0.64 2945
1 0.26 0.12 0.17 1587
2 0.48 0.44 0.45 1842
accuracy 0.50 6374
macro avg 0.43 0.44 0.42 6374
weighted avg 0.46 0.50 0.47 6374
정확도(accuracy) = 0.50 (50%)
모델 학습 결과 심화 분석 (성능 개선 후)
1. Logistic Regression 모델 계수로 상관성 파악
# Logistic Regression 모델의 coef_ 속성으로 plot 그리기
fig = plt.figure(figsize=(15, 5))
plt.plot(X.columns, model_lr.coef_[0])
plt.xticks(rotation=90)
plt.title('What makes Home Team Win?')
plt.grid()
plt.show()

홈팀의 수비진 공격성이 press 이고, 어웨이팀의 수비진 공격성이 double 일수록 홈팀이 승리할 확률이 높다.
fig = plt.figure(figsize=(15, 5))
plt.plot(X.columns, model_lr.coef_[2])
plt.xticks(rotation=90)
plt.title('What makes Away Team Win?')
plt.grid()
plt.show()

홈팀의 드리블 플레이를 단련하는 것이 보통이고, 어웨이팀의 수비진 넓이가 보통일수록 어웨이팀이 승리할 확률이 높다.
2. XGBoost 모델로 특징의 중요도 확인
# XGBoost 모델의 feature_importances_ 속성으로 plot 그리기
fig = plt.figure(figsize=(15, 6))
plt.bar(X.columns, model_xgb.feature_importances_)
plt.xticks(rotation=90)
plt.show()

away_defenceDefenderLineClass_Offside Trap 피쳐 중요도가 가장 높다.