다중공선성과 VIF
다중 공선성(Multicollnearity)
- 회귀분석은 독립변수들로 변수들을 선정해 주어야 하는데 변수들 간의 상관관계가 높아지면 회귀계수 추정치의 신뢰성과 안전성에 문제를 발생시킵니다. 이 때 다중회귀모형에서 일부 독립변수(예측변수, 회귀자; regressor)가 다른 독립변수와 높은 상관관계를 가지는 것을 다중 공선성이라고 합니다.
- 이 때 주의할 점은 상관관계분석은 변수들 간의 관련성을 나타내는 것이므로, 인과관계로 파악해서는 안됩니다.(쉽게 생각하면 상관관계는 방향성이 없고, 인과관계는 원인과 결과의 방향성을 가집니다). 다중공선성이 있다면 상관관계가 높지만 상관관계가 높다고 다중공선성이 반드시 있는 것은 아닙니다.
다중공선성 확인 : VIF(Variance Inflation Factor)
- VIF란 분산 팽창 인수라고 하며, 이 값은 다중회귀분석에서 독립변수가 다중 공선성(Multicollnearity)의 문제를 갖고 있는지 판단하는 기준이 됩니다.
- 판단 기준은 주로 10보다 크면 그 독립변수는 다중공산성일 가능성이 있다고 말하며 , 5로 두는 경우도 있습니다.
다중공선성 해결 방법
- 다중공선성을 가진 두 변수 중 하나를 제거
- 제거시 $R^{2}$이 유지되는 변수를 제거
- 주의할 점은 다중공선성이 높은 컬럼을 무조건 제거하는게 아닙니다. 자세한 설명은 아래 소스코드를 보며 설명하겠습니다.
VIF 함수
statsmodels.stats.outliers_influence
의variance_inflation_factor
함수를 이용합니다.
프로야구 선수 연봉 데이터셋으로 VIF확인
- 데이터는 아래 링크에서 받을 수 있습니다.
- http://www.statiz.co.kr
import pandas as pd
picher_file_path = '/content/drive/MyDrive/Colab Notebooks/data/picher_stats_2017.csv'
batter_file_path = '/content/drive/MyDrive/Colab Notebooks/data/batter_stats_2017.csv'
picher = pd.read_csv(picher_file_path)
batter = pd.read_csv(batter_file_path)
데이터 확인
print(picher.head())
print(picher.columns)
print(picher.shape)
선수명 팀명 승 패 세 홀드 ... RA9-WAR FIP kFIP WAR 연봉(2018) 연봉(2017)
0 켈리 SK 16 7 0 0 ... 6.91 3.69 3.44 6.62 140000 85000
1 소사 LG 11 11 1 0 ... 6.80 3.52 3.41 6.08 120000 50000
2 양현종 KIA 20 6 0 0 ... 6.54 3.94 3.82 5.64 230000 150000
3 차우찬 LG 10 7 0 0 ... 6.11 4.20 4.03 4.63 100000 100000
4 레일리 롯데 13 7 0 0 ... 6.13 4.36 4.31 4.38 111000 85000
[5 rows x 22 columns]
Index(['선수명', '팀명', '승', '패', '세', '홀드', '블론', '경기', '선발', '이닝', '삼진/9',
'볼넷/9', '홈런/9', 'BABIP', 'LOB%', 'ERA', 'RA9-WAR', 'FIP', 'kFIP', 'WAR',
'연봉(2018)', '연봉(2017)'],
dtype='object')
(152, 22)
2018년 연봉 분포를 출력합니다.
picher['연봉(2018)'].hist(bins=100)
<matplotlib.axes._subplots.AxesSubplot at 0x7f594f75e1d0>
standard scaling을 수행합니다.
picher_features_df = picher[['승', '패', '세', '홀드', '블론', '경기', '선발', '이닝', '삼진/9',
'볼넷/9', '홈런/9', 'BABIP', 'LOB%', 'ERA', 'RA9-WAR', 'FIP', 'kFIP', 'WAR',
'연봉(2018)', '연봉(2017)']]
# pandas 형태로 정의된 데이터를 출력할 때, scientific-notation이 아닌 float 모양으로 출력되게 해줍니다.
pd.options.mode.chained_assignment = None
# 피처 각각에 대한 standard_scaling을 수행하는 함수
def standard_scaling(df, scale_columns):
for col in scale_columns:
series_mean = df[col].mean()
series_std = df[col].std()
df[col] = df[col].apply(lambda x: (x-series_mean)/series_std)
return df
# 피처 각각에 대한 scaling을 수행합니다.
scale_columns = ['승', '패', '세', '홀드', '블론', '경기', '선발', '이닝', '삼진/9',
'볼넷/9', '홈런/9', 'BABIP', 'LOB%', 'ERA', 'RA9-WAR', 'FIP', 'kFIP', 'WAR', '연봉(2017)']
picher_df = standard_scaling(picher, scale_columns)
picher_df = picher_df.rename(columns={'연봉(2018)': 'y'})
picher_df.head(5)
선수명 | 팀명 | 승 | 패 | 세 | 홀드 | 블론 | 경기 | 선발 | 이닝 | 삼진/9 | 볼넷/9 | 홈런/9 | BABIP | LOB% | ERA | RA9-WAR | FIP | kFIP | WAR | y | 연봉(2017) | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 켈리 | SK | 3.313623 | 1.227145 | -0.306452 | -0.585705 | -0.543592 | 0.059433 | 2.452068 | 2.645175 | 0.672099 | -0.869000 | -0.442382 | 0.016783 | 0.446615 | -0.587056 | 3.174630 | -0.971030 | -1.058125 | 4.503142 | 140000 | 2.734705 |
1 | 소사 | LG | 2.019505 | 2.504721 | -0.098502 | -0.585705 | -0.543592 | 0.059433 | 2.349505 | 2.547755 | 0.134531 | -0.987502 | -0.668521 | -0.241686 | -0.122764 | -0.519855 | 3.114968 | -1.061888 | -1.073265 | 4.094734 | 120000 | 1.337303 |
2 | 양현종 | KIA | 4.348918 | 0.907751 | -0.306452 | -0.585705 | -0.543592 | 0.111056 | 2.554632 | 2.706808 | 0.109775 | -0.885929 | -0.412886 | -0.095595 | 0.308584 | -0.625456 | 2.973948 | -0.837415 | -0.866361 | 3.761956 | 230000 | 5.329881 |
3 | 차우찬 | LG | 1.760682 | 1.227145 | -0.306452 | -0.585705 | -0.543592 | -0.043811 | 2.246942 | 2.350927 | 0.350266 | -0.945180 | -0.186746 | -0.477680 | 0.558765 | -0.627856 | 2.740722 | -0.698455 | -0.760385 | 2.998081 | 100000 | 3.333592 |
4 | 레일리 | 롯데 | 2.537153 | 1.227145 | -0.306452 | -0.585705 | -0.543592 | 0.059433 | 2.452068 | 2.587518 | 0.155751 | -0.877464 | -0.294900 | -0.196735 | 0.481122 | -0.539055 | 2.751570 | -0.612941 | -0.619085 | 2.809003 | 111000 | 2.734705 |
팀명 피처를 one-hot encoding으로 변환합니다.
team_encoding = pd.get_dummies(picher_df['팀명'])
picher_df = picher_df.drop('팀명', axis=1)
picher_df = picher_df.join(team_encoding)
picher_df.head()
선수명 | 승 | 패 | 세 | 홀드 | 블론 | 경기 | 선발 | 이닝 | 삼진/9 | 볼넷/9 | 홈런/9 | BABIP | LOB% | ERA | RA9-WAR | FIP | kFIP | WAR | y | 연봉(2017) | KIA | KT | LG | NC | SK | 두산 | 롯데 | 삼성 | 한화 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 켈리 | 3.313623 | 1.227145 | -0.306452 | -0.585705 | -0.543592 | 0.059433 | 2.452068 | 2.645175 | 0.672099 | -0.869000 | -0.442382 | 0.016783 | 0.446615 | -0.587056 | 3.174630 | -0.971030 | -1.058125 | 4.503142 | 140000 | 2.734705 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
1 | 소사 | 2.019505 | 2.504721 | -0.098502 | -0.585705 | -0.543592 | 0.059433 | 2.349505 | 2.547755 | 0.134531 | -0.987502 | -0.668521 | -0.241686 | -0.122764 | -0.519855 | 3.114968 | -1.061888 | -1.073265 | 4.094734 | 120000 | 1.337303 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | 양현종 | 4.348918 | 0.907751 | -0.306452 | -0.585705 | -0.543592 | 0.111056 | 2.554632 | 2.706808 | 0.109775 | -0.885929 | -0.412886 | -0.095595 | 0.308584 | -0.625456 | 2.973948 | -0.837415 | -0.866361 | 3.761956 | 230000 | 5.329881 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
3 | 차우찬 | 1.760682 | 1.227145 | -0.306452 | -0.585705 | -0.543592 | -0.043811 | 2.246942 | 2.350927 | 0.350266 | -0.945180 | -0.186746 | -0.477680 | 0.558765 | -0.627856 | 2.740722 | -0.698455 | -0.760385 | 2.998081 | 100000 | 3.333592 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
4 | 레일리 | 2.537153 | 1.227145 | -0.306452 | -0.585705 | -0.543592 | 0.059433 | 2.452068 | 2.587518 | 0.155751 | -0.877464 | -0.294900 | -0.196735 | 0.481122 | -0.539055 | 2.751570 | -0.612941 | -0.619085 | 2.809003 | 111000 | 2.734705 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
학습데이터와 테스트 데이터 분리
from sklearn import linear_model
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from math import sqrt
# 학습 데이터와 테스트 데이터로 분리합니다.
X = picher_df[picher_df.columns.difference(['선수명', 'y'])]
y = picher_df['y']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 회귀 분석 계수를 학습합니다 (회귀 모델 학습)
lr = linear_model.LinearRegression()
model = lr.fit(X_train, y_train)
# 학습된 계수를 출력합니다.
print(lr.coef_)
[ -1863.27167152 1147.15608757 -52147.32574794 5915.51391759
2299.44885884 -1744.6150334 397.17996335 -249.60365919
-1024.27838506 399.1396206 12274.79760529 44088.31585257
-3602.91866901 -5319.02202278 617.34035282 4644.19380296
879.30541662 -3936.74747195 1521.68382584 -10999.04385918
-700.8303505 4526.7078132 21785.5776696 6965.59101874
154.91380911 2018.54543747 -1217.59759673 9090.86143072]
분산 팽창 요인 계산
- 각 컬럼별로 VIF(분산팽창요인)을 계산해 보았습니다. 10이상일 경우 다중공선성일 가능성이 있다고 했는데요.VIF Factor가 10이상인 컬럼은 다음과 같습니다.
- FIP
- kFIP
- 홈런/9
- 삼진/9
- 이닝
- 볼넷/9
- 선발
- 경기
- RA9-WAR
- ERA
- WAR
from statsmodels.stats.outliers_influence import variance_inflation_factor
import numpy as np
#데이터 프레임 생성
vif = pd.DataFrame()
#X 가 가지고 있는 데이터들을 이용해서 분산 팽창 요인을 계산
vif["VIF Factor"] = [variance_inflation_factor(X.values, i)
for i in range(X.shape[1])]
#컬럼 이름을 복사해서 생성
vif["features"] = X.columns
#소수 첫번째 짜리까지 반올림
vif.round(1)
vif.sort_values(by='VIF Factor', ascending=False)
/usr/local/lib/python3.7/dist-packages/statsmodels/tools/_testing.py:19: FutureWarning: pandas.util.testing is deprecated. Use the functions in the public API at pandas.testing instead.
import pandas.util.testing as tm
VIF Factor | features | |
---|---|---|
2 | 14238.289281 | FIP |
11 | 10264.067472 | kFIP |
27 | 425.628625 | 홈런/9 |
18 | 89.484808 | 삼진/9 |
23 | 63.817732 | 이닝 |
15 | 57.840351 | 볼넷/9 |
19 | 39.587016 | 선발 |
12 | 14.620874 | 경기 |
8 | 13.551449 | RA9-WAR |
1 | 10.633696 | ERA |
10 | 10.359736 | WAR |
21 | 7.969674 | 승 |
24 | 5.883327 | 패 |
6 | 4.300985 | LOB% |
26 | 3.765144 | 홀드 |
0 | 3.207869 | BABIP |
20 | 3.120906 | 세 |
16 | 2.979844 | 블론 |
22 | 2.493525 | 연봉(2017) |
13 | 1.227296 | 두산 |
17 | 1.226774 | 삼성 |
7 | 1.146670 | NC |
9 | 1.144145 | SK |
4 | 1.132708 | KT |
25 | 1.124340 | 한화 |
14 | 1.120740 | 롯데 |
5 | 1.113030 | LG |
3 | 1.109791 | KIA |
- 다중공선성은 다른 컬럼과 원인관계에 있는 것이므로, 하나의 컬럼만 제거해도 다중공선성의 문제는 해결이 됩니다. 따라서 다중공선성이 높다고 판단된 컬럼들 중 하나(FIP)와 다중공선성이 낮은 컬럼들을 가지고 다시 분산 팽창 요인을 계산하여 VIF Factor가 낮게 나오는지 확인해보고, 낮게 나오면, 그 컬럼들을 가지고 회귀모델로 분석을 수행하여 정확성이 높아지는지 확인해봐야 합니다.
인과관계에 있는 컬럼들에 대해 분산팽창요인을 계산할 경우
- VIF Factor가 높은 두 컬럼 ‘FIP’와 ‘kFIP’ 을 같이 넣어 분산팽창요인을 계산해보면 급격히 VIF Factor가 높아집니다.
X = picher_df[['FIP','kFIP', 'WAR', '볼넷/9', '삼진/9', '연봉(2017)']]
#데이터 프레임 생성
vif = pd.DataFrame()
#X 가 가지고 있는 데이터들을 이용해서 분산 팽창 요인을 계산
vif["VIF Factor"] = [variance_inflation_factor(X.values, i)
for i in range(X.shape[1])]
#컬럼 이름을 복사해서 생성
vif["features"] = X.columns
#소수 첫번째 짜리까지 반올림
vif.round(1)
VIF Factor | features | |
---|---|---|
0 | 669.0 | FIP |
1 | 762.8 | kFIP |
2 | 2.1 | WAR |
3 | 2.8 | 볼넷/9 |
4 | 24.8 | 삼진/9 |
5 | 1.9 | 연봉(2017) |
인과관계에 있는 컬럼들 중 하나를 제거하여 분산팽창요인을 계산할 경우
- 반면, ‘FIP’와 ‘kFIP’ 컬럼 중에 하나를 제거하여 분산팽창요인을 계산하면, VIF Factor가 매우 안정적인 수준으로 낮아지는 것을 볼 수 있습니다. 따라서 ‘FIP’와 ‘kFIP’ 컬럼 둘 중에 하나만 삭제하면 됩니다.
#피처의 개수를 줄여서 다중 공선성 문제를 해결
X = picher_df[['FIP', 'WAR', '볼넷/9', '삼진/9', '연봉(2017)']]
#데이터 프레임 생성
vif = pd.DataFrame()
#X 가 가지고 있는 데이터들을 이용해서 분산 팽창 요인을 계산
vif["VIF Factor"] = [variance_inflation_factor(X.values, i)
for i in range(X.shape[1])]
#컬럼 이름을 복사해서 생성
vif["features"] = X.columns
#소수 첫번째 짜리까지 반올림
vif.round(1)
VIF Factor | features | |
---|---|---|
0 | 1.2 | 홈런/9 |
1 | 2.1 | WAR |
2 | 1.3 | 볼넷/9 |
3 | 1.1 | 삼진/9 |
4 | 1.9 | 연봉(2017) |
회귀분석모델로 학습 및 검증수행
인과관계에 있는 컬럼들에 대해 학습 및 검증
# 피처를 재선정합니다.
X = picher_df[['FIP', 'WAR', '볼넷/9', '삼진/9', '연봉(2017)']]
y = picher_df['y']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=19)
# 모델을 학습합니다.
lr = linear_model.LinearRegression()
model = lr.fit(X_train, y_train)
# 결과를 출력합니다.
print(model.score(X_train, y_train)) # train R2 score를 출력합니다.
print(model.score(X_test, y_test)) # test R2 score를 출력합니다.
0.9150591192570362
0.9038759653889862
y_predictions = lr.predict(X_train)
print(sqrt(mean_squared_error(y_train, y_predictions))) # train RMSE score를 출력합니다.
y_predictions = lr.predict(X_test)
print(sqrt(mean_squared_error(y_test, y_predictions))) # test RMSE score를 출력합니다.
7893.462873347693
13141.866063591096
인과관계에 있는 컬럼들에 대해 학습 및 검증
# 피처를 재선정합니다.
X = picher_df[['FIP','kFIP', 'WAR', '볼넷/9', '삼진/9', '연봉(2017)']]
y = picher_df['y']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=19)
# 모델을 학습합니다.
lr = linear_model.LinearRegression()
model = lr.fit(X_train, y_train)
# 결과를 출력합니다.
print(model.score(X_train, y_train)) # train R2 score를 출력합니다.
print(model.score(X_test, y_test)) # test R2 score를 출력합니다.
0.9152778787072587
0.9046627899565158
y_predictions = lr.predict(X_train)
print(sqrt(mean_squared_error(y_train, y_predictions))) # train RMSE score를 출력합니다.
y_predictions = lr.predict(X_test)
print(sqrt(mean_squared_error(y_test, y_predictions))) # test RMSE score를 출력합니다.
7883.291782515135
13087.969083364505
요약
- VIF(분산팽창요인)으로 다중 공선성 확인
- VIF Factor가 10 이상인 컬럼과 아닌 컬럼들을 추출하여 다시 다중 공선성 확인
- VIF Factor가 높다면 해당 컬럼 제거하고 다시 분석팽창요인 계산하고, VIF Factor가 낮으면 4로.
- 회귀모델로 정확도 확인하여 성능확인
Leave a comment