Go together

If you want to go fast, go alone. If you want to go far, go together.

파이썬 머신 러닝 완벽 가이드

회귀 실습 - 캐글 주택 가격: 고급 회귀 기법

NowChan 2021. 11. 24. 20:46

배운 내용

  1. DataFrame.isnull() & DataFrame.sum()
  2. sns.distplot(Series)
  3. pd.get_dummies(DataFrame)
  4. Figure.tight_layout() 
  5. pd.concat() 
  6. Axes.tick_params
  7. Axes.get_xticklabels()
  8. sns.barplot()
  9. cross_val_score()
  10. GridSearchCV().fit()
  11. GridSearchCV().best_params_
  12. scipy.stats.skew()
  13. 회귀 모델의 예측 결과 혼합을 통한 최종 예측
  14. 스태킹 앙상블 모델을 통한 회귀 예측

House Prices: Advanced Regression Techniques

데이터 세트는 https://www.kaggle.com/c/house-prices-advanced-regression-techniques/data에서 다운로드 받을 수 있습니다. 이 데이터 세트를 이용해 회귀 분석을 더 심층적으로 학습해보겠습니다. 이 데이터 세트는 79개의 변수(피처)로 이루어져 있고, RMSLE를 기반으로 오류값을 측정합니다. 이는 가격이 비싼 주택일 수록 전체 예측 결과 오류에 미치는 비중이 높을 것이기 때문에 이를 상쇄하기 위함입니다.

 

데이터 사전 처리(Preprocessing)

필요한 모듈과 데이터를 로딩하고 개략적으로 데이터를 확인해보겠습니다. 이 예제에서는 데이터 가고을 많이 수행할 예정이므로 원본 csv 파일 기반 DataFrame은 보관하고 복사해서 데이터를 가공하겠습니다.

import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import seaborn as np
import matplotlib.pyplot as plt
%matplotlib inline

house_df_org = pd.read_csv('/content/drive/MyDrive/military/HousePrice/train.csv')
house_df = house_df_org.copy()
house_df.head(3)
'''
결과1
'''

결과1

Target 값은 맨 마지막 칼럼인 SalePrice입니다. 데이터 세트의 크기와 칼럼 타입 그리고 Null이 있는 칼럼과 그 건수를 내림차순으로 출력해보겠습니다.

 

DataFrame.isnull() & DataFrame.sum()

print('데이터 세트의 Shape:', house_df.shape)
print('\n전체 피처의 type \n', house_df.dtypes.value_counts())
isnull_series = house_df.isnull().sum()
print('\nNull 칼럼과 그 건수:\n', isnull_series[isnull_series>0].sort_values(ascending=False))
'''
데이터 세트의 Shape: (1460, 81)

전체 피처의 type 
object     43
int64      35
float64     3
dtype: int64

Null 칼럼과 그 건수:
PoolQC          1453
MiscFeature     1406
Alley           1369
Fence           1179
FireplaceQu      690
LotFrontage      259
GarageYrBlt       81
GarageType        81
GarageFinish      81
GarageQual        81
GarageCond        81
BsmtFinType2      38
BsmtExposure      38
BsmtFinType1      37
BsmtCond          37
BsmtQual          37
MasVnrArea         8
MasVnrType         8
Electrical         1
dtype: int64
'''

 

PoolQC, MiscFeature, Alley, Fence는 1000개 넘는 값이 null입니다. 이 칼럼은 곧 drop하겠습니다. 80개 피처 중 43개가 문자형입니다.

 

sns.distplot(Series)

회귀 모델을 적용하기 전에 타깃값의 분포도가 정규 분포인지 확인하겠습니다.

plt.title('Origin Sale Price Histogram')
sns.distplot(house_df['SalePrice'])
'''
결과2
'''

결과2

Target 값이 왼쪽으로 치우쳐져 있습니다. 먼저 log transformatiom을 적용하겠습니다. log1p()를 이용해 Target을 로그 변환한 값을 기반으로 모델을 학습한 뒤, 모델이 예측한 결과값을 expm1()으로 바꾸면 됩니다.

plt.title('Log Transformed Sale Price Histogram')
log_SalePrice = np.log1p(house_df['SalePrice'])
sns.distplot(log_SalePrice)
'''
결과3
'''

결과3

Target 데이터가 정규 분포 형태로 분포함을 알 수 있습니다. Null 값이 많은 피처인 PoolQC, MiscFeature, Alley, Fence, FireplaceQu는 삭제하고 Id도 단순 식별자이므로 삭제하겠습니다. LotFrontafe는 Null이 259개로 비교적 많으나 평균값으로 대체하고, 나머지 숫자형 피처도 Null값이 얼마 안되므로 평균값으로 대체하겠습니다.

 

로그 변환 및 Null 피처 전처리를 수행하겠습니다.

# SalePrice 로그 변환
original_SalePrice = house_df['SalePrice']
house_df['SalePrice'] = np.log1p(house_df['SalePrice'])

# Null이 너무 많은 칼럼과 불필요한 갈럼 삭제
house_df.drop(['Id', 'PoolQC', 'MiscFeature', 'Alley', 'Fence',\
               'FireplaceQu'], axis=1, inplace=True)

# 드롭하지 않는 숫자형 Null 칼럼은 평균값으로 대체
house_df.fillna(house_df.mean(), inplace=True)

숫자형 피처에 Null값이 있는지 확인하겠습니다.

# Null 값이 있는 피처명과 타입을 추출
null_column_count = house_df.isnull().sum()[house_df.isnull().sum()>0]
print('## Null 피처의 Type :\n', house_df.dtypes[null_column_count.index])
'''
## Null 피처의 Type :
MasVnrType      object
BsmtQual        object
BsmtCond        object
BsmtExposure    object
BsmtFinType1    object
BsmtFinType2    object
Electrical      object
GarageType      object
GarageFinish    object
GarageQual      object
GarageCond      object
dtype: object
'''

문자형 피처만 Null 값을 가지고 있는데, 문자형 피처는 모두 원-핫 인코딩으로 변환하겠습니다. 원-핫 인코딩은 문자열 피처를 인코딩 변환하면서 Null 값은 'None' 칼럼으로 대체해주기 때문에 별도의 Null 값을 대체하는 로직이 필요 없습니다. 원-핫 인코딩을 적용하면 당연히 칼럼이 증가합니다.

 

 

pd.get_dummies(DataFrame)

print('get_dummies() 수행 전 데이터 Shape:', house_df.shape)
house_df_ohe = pd.get_dummies(house_df)
print('get_dummies() 수행 후 데이터 Shape:', house_df_ohe.shape)

null_column_count = house_df_ohe.isnull().sum()[house_df_ohe.isnull().sum()>0]
print('## Null 피처의 Type:\n', house_df_ohe.dtypes[null_column_count.index])
'''
get_dummies() 수행 전 데이터 Shape: (1460, 75)
get_dummies() 수행 후 데이터 Shape: (1460, 271)
## Null 피처의 Type:
Series([], dtype: object)
'''

데이터 세트의 기본적인 가공은 마치고 이제 회귀 모델을 생성해 예측 및 성능 결과를 평가해보겠습니다.

 

선형 회귀 모델 학습/예측/평가

앞서 예측 평가는 RMSLE를 이용한다고 말했습니다. 그런데 SalePrice에 log 변환한 값을 SalePrice에 넣었기 때문에, 모델이 학습할 때 쓴 target 값과, 결과로 반환하는 예측값도 log를 씌운 값이 반환되고, 오류값( 실제값 - 예측값의 절댓값)의 RMSE만 구하면 자동으로 log 변환 전 RMSLE 값을 얻을 수 있습니다.

 

여러 모델의 로그 변환된 RMSE를 측정하는 함수를 생성하겠습니다.

from sklearn.metrics import mean_squared_error

def get_rmse(model):
  pred = model.predict(X_test)
  mse = mean_squared_error(y_test, pred)
  rmse = np.sqrt(mse)
  print(model.__class__.__name__, ' 로그 변환된 RMSE:', np.round(rmse, 3))
  return rmse

def get_rmses(models):
  rmses=[]
  for model in models:
    rmse = get_rmse(model)
    rmses.append(rmse)
  return rmses

 

이제 여러 선형 회귀 모델을 학습하고 예측, 평가해보겠습니다.

from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

y_target = house_df_ohe['SalePrice']
X_features = house_df_ohe.drop(['SalePrice'], axis=1, inplace=False)
X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size=0.2,\
                                                    random_state=156)

# LinearRegression, Ridge, Lasso 학습, 예측, 평가
lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)
ridge_reg = Ridge()
ridge_reg.fit(X_train, y_train)
lasso_reg = Lasso()
lasso_reg.fit(X_train, y_train)

models = [lr_reg, ridge_reg, lasso_reg]
get_rmses(models)
'''
LinearRegression  로그 변환된 RMSE: 0.132
Ridge  로그 변환된 RMSE: 0.128
Lasso  로그 변환된 RMSE: 0.176
'''

Lasso 회귀의 경우 성능이 타 회귀 방식보다 떨어지는 결과가 나와서 alpha 하이퍼 파라미터 최적화를 수행하겠습니다. Ridge도 alpha 하이퍼 파라미터를 최적화하겠습니다. 그보다 먼저 모델별로 피처 별 회귀 계수를 시각화하고, 상위 10개, 하위 10개의 경우 회귀값들을 Series 객체로 반환하는 함수를 만들겠습니다.

 

피처별 회귀 계수 중 상위 10개, 하위 10개의 회귀 계수를 담은 Series 객체를 반환하는 함수입니다.

def get_top_botton_coef(model, n=10):
  # coef_ 속성을 기반으로 Series 객체를 생성. index는 칼럼명.
  coef = pd.Series(model.coef_, index=X_features.columns)

  # + 상위 10개, - 하위 10개의 회귀 계수를 추출해 반환.
  coef_high = coef.sort_values(ascending=False).head(n)
  coef_low = coef.sort_values(ascending=False).tail(n)
  return coef_high, coef_low

 

Figure.tight_layout() & pd.concat() & Axes.tick_params & Axes.get_xticklabels() & sns.barplot()

 

get_top_bottom_coef를 이용해 모델별 회귀 계수를 시각화하는 함수를 만듭니다.

def visualize_coefficient(models):
  # 3개 회귀 모델의 시각화를 위해 3개의 칼럼을 가지는 subplot 생성
  fig, axs = plt.subplots(figsize=(24, 10), nrows=1, ncols=3)
  fig.tight_layout()
  # 입력 인자로 받은 list 객체인 models에서 차례로 model을 추출해 회귀 계수 시각화
  for i_num, model in enumerate(models):
    # 상위 10개, 하위 10개 회귀 계수를 구하고, 이를 판다스 concat으로 결합
    coef_high, coef_low = get_top_botton_coef(model, n=10)
    coef_concat = pd.concat([coef_high, coef_low])
    # ax subplot에 barchar로 표현. 한 화면에 표현하기 위해 tick label 위치와 font 크기 조정
    axs[i_num].set_title(model.__class__.__name__+'Coefficeints', size=25)
    axs[i_num].tick_params(axis='y', direction="in", pad=-120)
    for label in (axs[i_num].get_xticklabels() + axs[i_num].get_yticklabels()):
      label.set_fontsize(22)
    sns.barplot(x=coef_concat.values, y=coef_concat.index, ax=axs[i_num])

# 앞 예제에서 학습한 lr_reg, ridge_reg, lasso_reg 모델의 회귀 계수 시각화.
models = [lr_reg, ridge_reg, lasso_reg]
visualize_coefficient(models)
'''
결과3
'''

결과3

회귀 모델 별로 회귀 계수를 보면 OLS 기반의 LinearRegression과 Ridge의 경우 회귀 계수가 유사한 형태를 분포돼 있으나, Lasso는 전체적으로 회귀 계수 값이 작고, 특히 YearBuilt 값이 너무 큽니다. 혹시 학습 데이터의 분할에 문제가 있어서 그런 것인지, 이번에는 train_test_split()으로 분할하지 않고, 전체 데이터 세트인 X_features와 y_target을 cross_val_score()를 이용해 교차 검증 폴드 세트로 분할해 평균 RMSE를 측정해보겠습니다.

 

 

cross_val_score()

from sklearn.model_selection import cross_val_score

def get_avg_rmse_cv(models):

  for model in models:
    # 분할하지 않고 전체 데이터로 cross_val_score() 수행, 모델 별 CV RMSE값과 평균 RMSE 출력
    rmse_list = np.sqrt(-cross_val_score(model, X_features, y_target,\
                                        scoring="neg_mean_squared_error", cv = 5))
    rmse_avg = np.mean(rmse_list)
    print('\n{0} CV RMSE 값 리스트: {1}'.format(model.__class__.__name__, np.round(rmse_list, 3)))
    print('{0} CV 평균 RMSE 값: {1}'.format(model.__class__.__name__, np.round(rmse_avg, 3)))

# 앞 예제에서 학습한 lr_reg, ridge_reg, lasso_reg 모델의 CV RMSE 값 출력
models = [lr_reg, ridge_reg, lasso_reg]
get_avg_rmse_cv(models)
'''
LinearRegression CV RMSE 값 리스트: [0.135 0.165 0.168 0.111 0.198]
LinearRegression CV 평균 RMSE 값: 0.155

Ridge CV RMSE 값 리스트: [0.117 0.154 0.142 0.117 0.189]
Ridge CV 평균 RMSE 값: 0.144

Lasso CV RMSE 값 리스트: [0.161 0.204 0.177 0.181 0.265]
Lasso CV 평균 RMSE 값: 0.198
'''

5개의 폴드 세트로 학습 후 평가해도 여전히 Lasso는 OLS 모델이나 릿지 모델보다 성능이 떨어집니다. 학습 데이터에 문제가 있던 것은 아닌 것 같습니다.

 

GridSearchCV().fit() & GridSearchCV().best_params_

Ridge와 Lasso 모델에 대해서 alpha 하이퍼 파라미터를 변화시키면서 최적값을 도출하는 함수를 만들겠습니다.

from sklearn.model_selection import GridSearchCV

def print_best_params(model, params):
  grid_model = GridSearchCV(model, param_grid=params,\
                            scoring="neg_mean_squared_error", cv=5)
  grid_model.fit(X_features, y_target)
  rmse = np.sqrt(-1*grid_model.best_score_)
  print('{0} 5 CV 시 최적 평균 RMSE 값:{1}, 최적 alpha:{2}'.format(model.__class__.__name__,\
                                                           np.round(rmse, 4), grid_model.best_params_))

ridge_params = { 'alpha':[0.05, 0.1, 1, 5, 8, 10, 12, 15, 20]}
lasso_params = { 'alpha':[0.001, 0.005, 0.008, 0.05, 0.03, 0.1, 0.5, 1, 5, 10]}
print_best_params(ridge_reg, ridge_params)
print_best_params(lasso_reg, lasso_params)
'''
Ridge 5 CV 시 최적 평균 RMSE 값:0.1418, 최적 alpha:{'alpha': 12}
Lasso 5 CV 시 최적 평균 RMSE 값:0.142, 최적 alpha:{'alpha': 0.001}
'''

Lasso 모델의 경우 alpha 값 최적화 이후 예측 성능이 많이 좋아졌습니다. 선형 모델에 최적 alpha 값을 설정한 뒤, train_test_split()으로 분할된 학습/테스트 데이터 세트로 모델을 학습/예측/평가하고 모델 별 회귀 계수를 시각화하겠습니다.

# 앞의 최적화 alpha 값으로 학습 데이터로 학습, 테스트 데이터로 예측 및 평가 수행.
lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)

ridge_reg = Ridge(alpha=12)
ridge_reg.fit(X_train, y_train)

lasso_reg = Lasso(alpha=0.001)
lasso_reg.fit(X_train, y_train)

# 모든 모델의 RMSE 출력
models = [lr_reg, ridge_reg, lasso_reg]
get_rmses(models)

# 모든 모델의 회귀 계수 시각화
models = [lr_reg, ridge_reg, lasso_reg]
visualize_coefficient(models)
'''
LinearRegression  로그 변환된 RMSE: 0.132
Ridge  로그 변환된 RMSE: 0.124
Lasso  로그 변환된 RMSE: 0.12
'''
'''
결과1
'''

 

결과1

alpha 값 최적화 이후 Ridge와 Lasso 모델에서 비슷한 피처의 회귀 계수가 높습니다. 다만, Lasso 모델의 경우 Ridge에 비해 동일한 피처라도 회귀 계수 값이 상당히 작습니다.

 

데이터를 추가적으로 가공해서 모델 튜닝을 더 진행해보겠습니다. 두 가지를 살펴 볼 것입니다.

  1. 피처 데이터 세트의 데이터 분포도
  2. 이상치(Outlier) 데이터 처리입니다.

 

 

scipy.stats.skew()

Target 데이터 세트의 경우 데이터 분포도에 왜곡이 있음을 확인했습니다. 피처 데이터 세트의 경우도 지나치게 왜곡된 피처가 존재하면 회귀 예측 성능을 저하시킬 수 있습니다. 모든 숫자형 피처의 데이터 분포도를 확인해 어느 정도로 왜곡됐는지 알아봅시다.

 

scipy의 stats 모듈의 skew() 함수를 이용해 칼럼 데이터 세트의 왜곡된 정도를 쉽게 추출할 수 있습니다. 일반적으로 skew() 함수의 반환 값이 1 이상인 경우 왜곡 정도가 높다고 판단합니다. 물론, 상황에 따라 편차는 있습니다. 이 예제에서는 반환값이 1 이상인 피처에 log 변환을 취하겠습니다. 여기서 주의해야할 점은, 원-핫 인코딩 된 피처는 코드성 피처이므로 인코딩 시 당연히 왜곡될 가능성이 높습니다.(예를 들어, '화장실 여부'가 1로 1000건 , 0으로 10건이 될 수 있지만, 이는 왜곡과 무관합니다.) 따라서 원-핫 인코딩이 적용되지 않은 DataFrame에 skew()함수를 적용해 숫자형 피처 칼럼들을 받아와야합니다.

from scipy.stats import skew

# object가 아닌 숫자형 피처의 칼럼 index 객체 추출
features_index = house_df.dtypes[house_df.dtypes != 'object'].index

# house_df에 칼럼 index를 입력하면 해달하는 칼럼 데이터 세트 반환
# 반환된 칼럼에 apply lambda로 skew() 호출
skew_features = house_df[features_index].apply(lambda x : skew(x))

# skew(왜곡) 정도가 1 이상인 칼럼만 추출.
skew_features_top = skew_features[skew_features > 1]
print(skew_features_top.sort_values(ascending=False))
'''
MiscVal          24.451640
PoolArea         14.813135
LotArea          12.195142
3SsnPorch        10.293752
LowQualFinSF      9.002080
KitchenAbvGr      4.483784
BsmtFinSF2        4.250888
ScreenPorch       4.117977
BsmtHalfBath      4.099186
EnclosedPorch     3.086696
MasVnrArea        2.673661
LotFrontage       2.382499
OpenPorchSF       2.361912
BsmtFinSF1        1.683771
WoodDeckSF        1.539792
TotalBsmtSF       1.522688
MSSubClass        1.406210
1stFlrSF          1.375342
GrLivArea         1.365156
dtype: float64
'''

이제 추출된 왜곡 정도가 높은 피처를 로그 변환합니다.

house_df[skew_features_top.index] = np.log1p(house_df[skew_features_top.index])

house_df_ohe가 아닌 house_df를 이용했으므로, 다시 원-핫 인코딩하고, 학습/테스트 데이터 세트 분리 후 하이퍼 파라미터를 튜닝하겠습니다.

# 왜곡 정도가 높은 피처를 로그 변환했으므로 다시 원-핫 인코딩을 적용하고 피처/타깃 데이터 세트 생성
house_df_ohe = pd.get_dummies(house_df)
y_target = house_df_ohe['SalePrice']
X_features = house_df_ohe.drop(['SalePrice'], axis=1, inplace=False)
X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size=0.2,\
                                                    random_state=156)

# 피처를 로그 변환한 후 다시 최적 하이퍼 파라미터와 RMSE 출력
ridge_params = { 'alpha':[0.05, 0.1, 1, 5, 8, 10, 12, 15, 20]}
lasso_params = { 'alpha':[0.001, 0.005, 0.008, 0.05, 0.03, 0.1, 0.5, 1, 5, 10]}
print_best_params(ridge_reg, ridge_params)
print_best_params(lasso_reg, lasso_params)
'''
Ridge 5 CV 시 최적 평균 RMSE 값:0.1275, 최적 alpha:{'alpha': 10}
Lasso 5 CV 시 최적 평균 RMSE 값:0.1252, 최적 alpha:{'alpha': 0.001}
'''

Ridge의 경우 alpha가 12로 변경됐고, 두 모델 모두 5 폴드 교차 검증의 평균 RMSE 값이 향상됐습니다. 다시 학습/테스트 데이터를 이용해 피처의 회귀 계수를 시각화해보겠습니다.

lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)

ridge_reg = Ridge(alpha=10)
ridge_reg.fit(X_train, y_train)

lasso_reg = Lasso(alpha=0.001)
lasso_reg.fit(X_train, y_train)

models=[lr_reg, ridge_reg, lasso_reg]
visualize_coefficient(models)
'''
결과2
'''

결과2

확실히 세 모델 피처 별 회귀 계수와 그 크기가 비슷해졌습니다.

 

다음으로 분석할 요소는 이상치 데이터입니다. 특히, 회귀 계수가 높은 피처, 즉 예측에 많은 영향을 미치는 피처가 중요 피처이기 때문에 이상치 데이터 처리가 중요합니다. 먼저 세 모델 모두 가장 큰 회귀 계수를 가지는 GrLivArea 피처의 데이터 분포를 살펴보겠습니다.

 

주택 가격 데이터의 원본 데이터 세트 house_df_org에서 GrLivArea와 타깃값인 SalePrice의 관계를 시각화해보겠습니다. 

plt.scatter(x=house_df_org['GrLivArea'], y=house_df_org['SalePrice'])
plt.ylabel('Saleprice', fontsize=15)
plt.xlabel('GrLivArea', fontsize=15)
plt.show()
'''
결과3
'''

결과3

 

의미적인 측면에서, 주거 공간이 큰 집일 수록 가격이 비싸기 때문에 GrLivArea와 SalePrice가 양의 상관도가 매우 높음을 알 수 있습니다. 결과 3을 볼 때 그래프 우측에 두 점은 다른 점들과 달리 양의 상관도와 거리가 멀어 보입니다. 4000 평방 피트 이상임에도 가격이 500,000 달러 이하인 데이터는 모두 이상치로 간주하고 삭제하겠습니다.

 

데이터 변환이 완료된 house_df_ohe 데이터에서 log값을 씌운 조건들을 통해 이상치 데이터를 drop하겠습니다.

# GrLivArea와 SalePrice 모두 로그 변환됐으므로 이를 반영한 조건 생성
cond1 = house_df_ohe['GrLivArea'] > np.log1p(4000)
cond2 = house_df_ohe['SalePrice'] < np.log1p(500000)
outlier_index = house_df[cond1 & cond2].index

print('이상치 레코드 index:', outlier_index.values)
print('이상치 삭제 전 house_df_ohe shape:', house_df_ohe.shape)

# DatFrame의 인덱스를 이용해 이상치 레코드 삭제.
house_df_ohe.drop(outlier_index, axis=0, inplace=True)
print('이상치 삭제 후 house_df_ohe shape:', house_df_ohe.shape)
'''
이상치 레코드 index: [523 1298]
이상치 삭제 전 house_df_ohe shape: (1460, 271)
이상치 삭제 후 house_df_ohe shape: (1458, 271)
'''

이상치가 삭제됐음을 확인할 수 있었습니다. 이제 이상치 데이터를 삭제했으므로, 다시 house_df_ohe를 기반으로 피처 데이터, target 데이터를 생성하고 릿지와 라쏘 모델의 최적화를 수행하고 결과를 출력해보겠습니다.

y_target = house_df_ohe['SalePrice']
X_features = house_df_ohe.drop('SalePrice', axis=1, inplace=False)
X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size=0.2,\
                                                    random_state=156)
ridge_params = {'alpha':[0.05, 0.1, 1, 5, 8, 10, 12, 15, 20]}
lasso_params = {'alpha':[0.001, 0.005, 0.008, 0.05, 0.03, 0.1, 0.5, 1, 5, 10]}
print_best_params(ridge_reg, ridge_params)
print_best_params(lasso_reg, lasso_params)
'''
Ridge 5 CV 시 최적 평균 RMSE 값:0.1125, 최적 alpha:{'alpha': 8}
Lasso 5 CV 시 최적 평균 RMSE 값:0.1122, 최적 alpha:{'alpha': 0.001}
'''

단 두 개의 이상치 데이터만 제거했는데, 예측 수치가 매우 크게 향상됐습니다. 이번에도 Ridge 모델의 경우 alpha가 8로 최적화됐습니다. 웬만큼 하이퍼 파라미터를 튜닝해도 평균 RMSE 값이 0.1275 > 0.1125, 0.1252 > 0.1122로 개선되기 쉽지 않습니다. 그만큼 GrLivArea 피처가 회귀 모델에서 차지하는 영향도가 크기에 이상치를 개선하는 것이 성능 개선에 큰 의미를 가졌습니다.

 

회귀에서 중요 피처의 이상치 데이터를 찾는 것은 이렇게 중요한 것입니다. 바람직한 머신러닝 모델 생성 과정은 대략적으로 데이터를 가공하고 모델 최적화를 수행한 뒤, 다시 이에 기반한 여러 가지 기법의 데이터 가공과 하이퍼 파라미터 기반의 모델 최적화를 반복적으로 수행하는 것입니다.

 

이상치가 제거된 데이터 세트를 기반으로 학습/테스트 데이터의 RMSE 수치 및 회귀 계수를 시각화해보겠습니다.

lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)

ridge_reg = Ridge(alpha=8)
ridge_reg.fit(X_train, y_train)

lasso_reg = Lasso(alpha=0.001)
lasso_reg.fit(X_train, y_train)

models = [lr_reg, ridge_reg, lasso_reg]

get_rmses(models)
visualize_coefficient(models)
'''
LinearRegression  로그 변환된 RMSE: 0.129
Ridge  로그 변환된 RMSE: 0.103
Lasso  로그 변환된 RMSE: 0.1
'''
'''
결과4
'''

결과4

이상치 데이터를 제거하니 GrLivArea 속성의 회귀 계수가 많이 작아졌음을 알 수 있습니다.

 

회귀 트리 모델 학습/예측/평가

회귀 트리 XGBoost, LightGBM를 이용해 회귀 모델을 만들어보겠습니다. XGBoost, LightGBM 모두 수행 시간이 오래 걸릴 수 있어 아래와 같은 하이퍼 파라미터 설정을 적용한 상태로 5 폴드 세트에 대한 평균 RMSE 값을 구하겠습니다.

 

XGBoost

from xgboost import XGBRegressor

xgb_params = {'n_estimators':[1000]}
xgb_reg = XGBRegressor(n_estimators=1000, learning_rate=0.05, \
                       colsample_bytree=0.5, subsamle=0.8)
print_best_params(xgb_reg, xgb_params)
'''
XGBRegressor 5 CV 시 최적 평균 RMSE 값:0.0093, 최적 alpha:{'n_estimators': 1000}
'''

각 피처의 중요도를 상위 20개만 시각화하겠습니다.

import seaborn as sns
xgb_reg.fit(X_features, y_target)
plt.title('XGBRegressor Feature Importances')
feature_series = pd.Series(data=xgb_reg.feature_importances_, index=X_features.columns)
feature_series = feature_series.sort_values(ascending=False)[:20]
sns.barplot(x=feature_series, y=feature_series.index
'''
결과5
'''

결과5

 

 

LightGBM

from lightgbm import LGBMRegressor
lgbm_params={'n_estimators':[1000]}
lgbm_reg = LGBMRegressor(n_estimators=1000, learning_rate=0.05, num_leaves=4,\
                         subsample=0.6, colsample_bytree=0.4, reg_lambda=10, n_jobs=-1)
print_best_params(lgbm_reg, lgbm_params)
'''
LGBMRegressor 5 CV 시 최적 평균 RMSE 값:0.0096, 최적 alpha:{'n_estimators': 1000}
'''

각 피처의 중요도를 상위 20개만 시각화하겠습니다.

plt.title('LGBMRegressor Feature Importances')
feature_series = pd.Series(data=lgbm_reg.feature_importances_, index=X_features.columns)
feature_series = feature_series.sort_values(ascending=False)[:20]
sns.barplot(x=feature_series, y=feature_series.index)
'''
결과6
'''

결과6

 

 

회귀 모델의 예측 결과 혼합을 통한 최종 예측

개별 회귀 모델의 예측 결과값을 혼합해 이를 기반으로 최종 회귀 값을 예측하겠습니다. 기본적으로 예측 결과 혼합은 매우 간단합니다. 가령 A 모델과 B 모델이 있다고 가정하고, A 모델의 예측값의 40%, B 모델의 예측값의 60%를 더해 최종 회귀값으로 예측하는 것입니다. 

 

Ridge, Lasso 모델을 서로 혼합해보겠습니다. 먼저, 최종 혼합 모델 , 개별 모델의 RMSE 값을 출력하는 get_rmse_pred()함수를 생성하겠습니다.

def get_rmse_pred(preds):
  for key in preds.keys():
    pred_value = preds[key]
    mse = mean_squared_error(y_test, pred_value)
    rmse = np.sqrt(mse)
    print('{0} 모델의 RMSE: {1}'.format(key, rmse))

혼합 모델의 RMSE, 개별 모델의 RMSE 값을 출력해보겠습니다.

# 개별 모델의 학습
ridge_reg = Ridge(alpha=8)
ridge_reg.fit(X_train, y_train)

lasso_reg = Lasso(alpha=0.001)
lasso_reg.fit(X_train, y_train)

# 개별 모델 예측
ridge_pred = ridge_reg.predict(X_test)
lasso_pred = lasso_reg.predict(X_test)

# 개별 모델 예측값 혼합으로 최종 예측값 도출
pred = 0.4*ridge_pred + 0.6*lasso_pred
preds = {
    '최종 혼합': pred,
    'Ridge': ridge_pred,
    'Lasso': lasso_pred
}

# 최종 혼합 모델, 개별 모델의 RMSE 값 출력
get_rmse_pred(preds)
'''
최종 혼합 모델의 RMSE: 0.010124967713293133
Ridge 모델의 RMSE: 0.009688003269068832
Lasso 모델의 RMSE: 0.011420106121003798
'''

혼합 모델의 경우 조금 더 성능이 좋은 모델에 가중치를 더 두면 됩니다. 특별한 기준은 없습니다. 이번엔 XGBoost, LightGBM을 혼합해 결과를 살펴보겠습니다.

xgb_reg = XGBRegressor(n_estimators=1000, learning_rate=0.05,\
                       colsample_bytree=0.5, subsample=0.8)
lgbm_reg = LGBMRegressor(n_estimators=1000, learning_rate=0.05, num_leaves=4,\
                         subsample=0.6, colsample_bytree=0.4, reg_lambda=10, n_jobs=-1)
xgb_reg.fit(X_train, y_train)
lgbm_reg.fit(X_train, y_train)
xgb_pred = xgb_reg.predict(X_test)
lgbm_pred = lgbm_reg.predict(X_test)

pred = 0.5*xgb_pred + 0.5*lgbm_pred
preds={
    '최종 혼합': pred,
    'XGB': xgb_pred,
    'LGBM': lgbm_pred
}
get_rmse_pred(preds)
'''
최종 혼합 모델의 RMSE: 0.009628490166003907
XGB 모델의 RMSE: 0.010130099960029714
LGBM 모델의 RMSE: 0.00953789880298775
'''

이 예제에서는 혼합 모델이 성능 향상이 되지 않았지만, 다른 상황에서는 성능이 향상될 수 있다.

 

스태킹 앙상블 모델을 통한 회귀 예측

4장 분류(Classification)에서 소개한 스태킹 모델도 회귀에 적용할 수 있습니다. 스태킹 모델의 구현 방법을 다시 정리하자면, 스태킹 모델은 두 종류의 모델이 필요합니다. 첫 번째는 개별적인 기반 모델, 두 번째는 이 기반 모델의 예측 데이터를 학습 데이터로 만들어서 학습하는 최종 메타 모델입니다.

 

스태킹 모델의 핵심은 여러 개별 모델의 예측 데이터를 각각 스태킹 형태로 결합해 최종 메타 모델의 학습용 피처 데이터 세트테스트용 피처 데이터 세트를 만드는 것입니다.

 

최종 메타 모델이 학습할 피처 데이터 세트는 원본 학습 피처 세트로 학습한 개별 모델의 예측값을 스태킹 형태로 결합한 것입니다. 4장에서 이미 소개한 개별 모델을 스태킹 모델로 제공하기 위해 데이터 세트를 생성하기 위한 get_stacking_base_datasets() 함수를 만들겠습니다.

from sklearn.model_selection import KFold
from sklearn.metrics import mean_absolute_error

# 개별 기반 모델에서 최종 메타 모델이 사용할 학습 및 테스트용 데이터를 생성하기 위한 함수.
def get_stacking_base_datasets(model, X_train_n, y_train_n, X_test_n, n_folds):
  # 지정된 n_folds 값으로 KFold 생성.
  kf = KFold(n_splits=n_folds, shuffle=False)
  # 추후에 메타 모델이 사용할 학습 데이터 반환을 위해 넘파이 배열 초기화
  train_fold_pred = np.zeros((X_train_n.shape[0], 1))
  test_pred = np.zeros((X_test_n.shape[0], n_folds))
  print(model.__class__.__name__, 'model 시작')
  
  for folder_counter, (train_index, valid_index) in enumerate(kf.split(X_train_n)):
    # 입력된 학습 데이터에서 기반 모델이 학습/예측할 폴드 데이터 세트 추출
    print('\t 폴드 세트: ', folder_counter, ' 시작 ')
    X_tr = X_train_n[train_index]
    y_tr = y_train_n[train_index]
    X_te = X_train_n[valid_index]

    # 폴드 세트 내부에서 다시 만들어진 학습 데이터로 기반 모델의 학습 수향.
    model.fit(X_tr, y_tr)
    # 폴드 세트 내부에서 다시 만들어진 검증 데이터로 기반 모델 예측 후 데이터 저장
    train_fold_pred[valid_index, :] = model.predict(X_te).reshape(-1, 1)
    # 입력된 원본 테스트 데이터를 폴드 세트 내 학습된 기반 모델에서 예측 후 데이터 저장.
    test_pred[:, folder_counter] = model.predict(X_test_n)

  # 폴드 세트 내에서 원본 테스트 데이터를 예측한 데이터를 평균하여 테스트 데이터로 생성
  test_pred_mean = np.mean(test_pred, axis=1).reshape(-1, 1)

  # train_fold_pred는 최종 메타 모델이 사용하는 학습 데이터, test_pred_mean은 테스트 데이터
  return train_fold_pred, test_pred_mean

함수 내에서는 K-폴드 세트로 설정된 폴드 세트 내부에서 원본 학습 데이터 세트를 학습/검증 데이터 세트로 나누고 모델을 학습시킵니다. 검증 데이터를 학습된 모델을 통해 예측해 매 개별 폴드마다 예측 결과값을 저장합니다. 이 예측 결과값을 최종 메타 모델의 학습용 피처 데이터로 사용합니다. 또한 매 개별 폴드마다 테스트 데이터를 학습된 모델에 넣고 예측값을 저장합니다. 모든 폴드 세트에서 예측을 완료하면 저장된 테스트 데이터 예측값을 평균 내서 최종 메타 모델의 테스트용 피처 데이터로 사용합니다.

 

이제 모델에 get_stacking_base_datasets()를 적용해서 최종 메타 모델이 사용할 학습 피처 데이터 세트와 테스트 피처 데이터 세트를 추출하겠습니다. 적용할 개별 모델은 Ridge, Lasso, XGBoost, LightGBM 총 4개 입니다.

# get stacking_base_datasets()는 넘파이 ndarray를 인자로 사용하므로 DataFrame을 넘파이로 변환.
X_train_n = X_train.values
X_test_n = X_test.values
y_train_n = y_train.values

# 각 개별 기반(Base) 모델이 생성한 학습용/테스트용 데이터 반환.
ridge_train, ridge_test = get_stacking_base_datasets(ridge_reg, X_train_n ,y_train_n, X_test_n, 5)
lasso_train, lasso_test = get_stacking_base_datasets(lasso_reg, X_train_n, y_train_n, X_test_n, 5)
xgb_train, xgb_test = get_stacking_base_datasets(xgb_reg, X_train_n, y_train_n, X_test_n, 5)
lgbm_train, lgbm_test = get_stacking_base_datasets(lgbm_reg, X_train_n, y_train_n, X_test_n, 5)

각 개별 모델이 반환하는 학습용 피처 데이터와 테스트용 피처 데이터 세트를 결합해 최종 메타 모델에 적용해보겠습니다. 최종 메타 모델은 Lasso 모델을 이용하며 최종적으로 예측 및 RMSE를 측정합니다.

# 개별 모델이 반환하는 학습 및 테스트용 데이터 세트를 스태킹 형태로 결합.
Stack_final_X_train = np.concatenate((ridge_train, lasso_train, xgb_train, lgbm_train), axis=1)
Stack_final_X_test = np.concatenate((ridge_test, lasso_test, xgb_test, lgbm_test), axis=1)

# 최종 메타 모델은 라쏘 모델을 적용.
meta_model_lasso = Lasso(alpha=0.0005)

# 개별 모델 예측값을 기반으로 새롭게 만들어진 학습/테스트 데이터로 메타 모델 예측 및 RMSE 측정.
meta_model_lasso.fit(Stack_final_X_train, y_train)
final = meta_model_lasso.predict(Stack_final_X_test)
mse = mean_squared_error(y_test, final)
rmse = np.sqrt(mse)
print('스태킹 회귀 모델의 최종 RMSE 값은:', rmse)
'''
스태킹 회귀 모델의 최종 RMSE 값은: 0.020432522162305712
'''

스태킹 모델은 분류 뿐만 아니라 회귀에서 특히 효과적으로 사용될 수 있는 모델입니다.

 


궁금한 점

  1. DataFrame.isnull() & DataFrame.sum()
    1. DataFrame.isnull()을 하면 그림1과 같이 되고, 여기에 sum()을 하면 그림2와 같이 출력 된다. 즉, sum()을 하게 되면 column이 index로 된다. 

그림1

 

  • sns.distplot(Series) 문법
    • Series의 index를 파악해, 빈(막대 그래프 갯수)을 자동으로 계산해서 matplotlib의 histogram을 그려준다. 
      • histogram: 표로 되어 있는 일정 범위의 값들의 분포를 그림으로 나타낸 것이다

 

  •  DataFrame.fiilna(DataFrame.mean())
    • DataFrame.mean()은 자동으로 숫자형 칼럼만 추출해 칼럼별 평균값을 Series 객체로 반환해줍니다.
    • DataFrame.fillna()에 value 파라미터는 scalar, dict, Series, or DataFrame를 인자로 받아서 null 값을 채워준다.

 

  • pd.get_dummies()는 자동으로 문자열 칼럼의 Null값을 None 칼럼으로 바꾼다는 것이 칼럼을 안만든다는 말일까?
    • Yes, DataFrame.get_dummies(dummy_na=True)를 사용하면 Null 컬럼값을 만들 수 있다.

 

  • fig, axs = plt.subplots(figsize=(24, 10), nrows=1, ncols=3)
    • axs[0], axs[1], axs[2]을 이용해 각 칸마다 그래프를 그릴 수 있다.

 

  • Figure.tight_layout()
    • axs[0], axs[1], axs[2]의 그래프들이 안겹치게 그려지게 조정해주는 함수

 

  • Axes.tick_params()
    • tick은 숫자를 기준으로 그리는 선 같은 것이다. 파라미터는 아래의 것들이 있다.
      • direction: 안쪽에 선을 그을 것인가?
      • pad: 선 간격
      • length: tick 길이
      • labelsize: 틱 크기

tick 설명

  • Axes.get_xticklabels()
    • x축의 tick들의 리스트를 반환 ,위 그림에서는 [1, 2, 3, 4]를 반환 한다.

 

  • sns.barplot()
    • 쉽게 matplot의 막대 그래프(bar plot)를 그려준다.

 

  • Boolean Indexing
    •  보통 house_df.dtypes[house_df.dtypes != 'object'] 처럼 자기 자신을 조건 처럼 쓸 때가 많다, 혹은 특정 칼럼이라도 쓴다. 

 

  • condition = DataFrame['column'] > value의 결과는 각 인덱스에 불린값이 들어간 Series가 반환된다. 

 

  • print_best_params()함수에서 train 데이터를 이용해 교차학습을 하지 않고 피처 데이터를 통으로 학습하는 이유
    • 내 생각엔 데이터가 부족해서 전부 쓰는 것 같다. 에초에 폴드 교차 검증도 데이터가 부족할 때 자주 쓰는 방법임

 

  • 스태킹 모델의 train_fold_pred[valid_index, :] = model.predict(X_te).reshape(-1, 1) 부분에서 이렇게 쓰는 이유는 검증 데이터에 의한 결과를 저장하기 위함이고, valid_index는 검증 폴드마다 전체 index를 n_folds 부분 만큼 쓰기 때문에 결국 모든 인덱스가 검증 데이터에 의한 결과값으로 차서 메타 모델의 학습용 피처 데이터 세트로 변한다.

 

 

출처: 파이썬 머신러닝 완벽 가이드(권철민)

사진 출처:

https://codetorial.net/matplotlib/set_grid_and_tick.html

'파이썬 머신 러닝 완벽 가이드' 카테고리의 다른 글

군집화  (0) 2021.11.28
차원 축소  (0) 2021.11.26
회귀 실습 - 자전거 대여 수요 예측  (0) 2021.11.23
회귀  (0) 2021.11.19
분류 - 스태킹 앙상블  (0) 2021.11.16