데이터 분석/실습

European Soccer

eunki 2021. 12. 27. 11:39
728x90
  • 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 피쳐 중요도가 가장 높다.

728x90