2023. 11. 21. 18:11ㆍ개발/💠 Alchemist
< 캐글 주택 가격: 고급 회귀 기법 >
https://www.kaggle.com/competitions/house-prices-advanced-regression-techniques/overview
House Prices - Advanced Regression Techniques | Kaggle
www.kaggle.com
1. 데이터 세트 로딩 및 데이터 전처리, 모델 import
데이터세트 로딩 및 모델 import
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
house_df_org = pd.read_csv('house_price.csv')
# 다양한 데이터 전처리 후 예측 성능을 비교할 것이기 때문에 원본 데이터세트 살려두기
house_df = house_df_org.copy()
데이터세트 정보 확인
house_df.head(3)
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))
❗데이터세트 정보 보면서 생각해야 할 점❗
숫자형 데이터와 문자형 데이터가 존재하는구나 -> 문자형 데이터는 원-핫 인코딩해줘야지~~
Null값이 많이 포함되어 있는 피처가 있구나 -> Null 값이 많은 피처는 드롭해줘야지~~
-> Null 값이 적당히 있는 피처의 Null 값은 평균값으로 대체해줘야지~~
2. 데이터 전처리
데이터세트 타깃 값의 분포 상태 시각화
# 분포 상태 시각화
plt.title('Original Sale Price Histogram')
plt.xticks(rotation=45)
sns.histplot(house_df['SalePrice'], kde=True)
plt.show()
❗분포도 보면서 생각해야 할 점❗
데이터 값의 분포가 중심보다 왼쪽으로 치우친 형태구나 (정규 분포에서 벗어나 있구나) -> 로그 변환해줘야지~~
# 로그 변환 후 분포 상태 시각화
plt.title('Log Transformed Sale Price Histogram')
log_SalePrice = np.log1p(house_df['SalePrice'])
sns.histplot(log_SalePrice, kde = True)
plt.show()
데이터세트의 분포가 이전보다 정규 분포에 가까워졌음을 확인할 수 있다
로그 변환, 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_column_count = house_df.isnull().sum()[house_df.isnull().sum() > 0]
print('## Null 피처의 Type:\n', house_df.dtypes[null_column_count.index])
문자형 피처를 제외하고는 Null 값이 존재하지 않음을 알 수 있다
문자형 피처 get_dummies() 함수로 원-핫 인코딩 진행 (문자형 데이터 전처리)
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])
원-핫 인코딩 수행 결과 칼럼값이 증가했다
더이상 Null 값이 존재하지 않음을 확인할 수 있다
원-핫 인코딩의 경우, 문자형 데이터 중 Null값을 0으로 대체해주기 때문에 Null 값을 따로 처리해주지 않아도 된다
3. 선형 회귀 모델 학습 / 예측 / 평가 ( + 평가 결과 기반으로 데이터 전처리 )
성능 예측 평가 함수 생성 - RMSE
# 예측 평가 함수 - RMSE (이미 앞에서 로그 변환했으므로 결론적으로는 RMSLE와 동일함)
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
앞서 이미 타깃값에 로그 변환을 수행했기 때문에 예측값 또한 로그 변환된 값이라고 할 수 있다
따라서 RMSE를 구하는 함수를 작성했지만 결론적으로는 RMSLE와 동일한 결과를 얻을 수 있다
LinearRegression, Ridge, Lasso 학습/예측/평가
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)
라쏘 회귀의 경우 회귀 성능이 타 회귀 방식보다 많이 떨어진다는 것을 확인할 수 있다
모델별 회귀 계수 시각화
# 상위 10개, 하위 10개의 회귀 계수를 추출하는 함수
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
# 추출된 상위, 하위 10개의 회귀 계수값을 시각화하는 함수
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)
coef_concat = pd.concat([coef_high, coef_low])
# ax subplot에 barchar로 표현. 한 화면에 표현하기 위해 tick label 위치와 font 크기 조정.
axs[i_num].set_title(model.__class__.__name__+'Coefficients', 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)
LinearRegression과 Ridge의 경우 회귀 계수가 유사한 형태를 띄지만
Lasso의 경우, 전체적으로 회귀 계수값이 매우 작고, YearBuilt 계수만 눈에 띄게 큰 값을 가진다는 것을 확인할 수 있다
학습/테스트 데이터 분할하지 않은 상태로 교차 검증
Lasso에서 나타난 문제가 학습/테스트 데이터 분할 과정에서 초래한 것인지 확인하기 위해
학습/테스트 데이터를 분할하지 않은 상태로 교차 검증을 진행한다
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)))
# 앞 예제에서 학습한 ridge_reg, lasso_reg 모델의 CV RMSE값 출력
models = [ridge_reg, lasso_reg]
get_avg_rmse_cv(models)
Lasso의 RMSE 값이 여전히 떨어진다
따라서 학습/테스트 데이터 분할 과정에서 나타난 문제가 아님을 확인할 수 있다
하이퍼 파라미터 튜닝
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)
하이퍼 파라미터 튜닝 후 모델 학습 / 예측 / 평가
# 앞의 최적화 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)
하이퍼 파라미터 튜닝 결과 Lasso의 RMSE값이 개선되었으며 회귀 계수 값도 전보다 나아졌다
하지만 여전히 다른 모델에 비해 회귀 계수 값이 작다
이 문제를 추가적으로 개선하기 위해
1 ) 피처 데이터 세트의 분포도 정규화
2 ) 이상치 처리
를 진행한다
피처 데이터 세트의 분포도 개선 - 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))
skew() 함수는 칼럼의 데이터 세트의 왜곡된 정도를 쉽게 추출할 수 있도록 돕는 함수다
대부분의 경우 skew() 함수 수행 결과값이 1 이상이면 왜곡된 정도가 높다고 판단한다
이때 skew() 함수는 숫자형 데이터에만 적용한다 (원-핫 인코딩이 수행된 카테고리형 데이터도 제외)
따라서 house_df_ohe 대신 원-핫 인코딩이 수행되기 전 데이터 세트인 house_df에 skew() 함수를 수행한다
왜곡된 정도가 높은 칼럼에 로그 변환 수행
house_df[skew_features_top.index] = np.log1p(house_df[skew_features_top.index])
원-핫 인코딩 / 피처, 타겟 데이터 분리 / 로그 변환
# 왜곡 정도가 높은 피처를 로그 변환했으므로 다시 원-핫 인코딩을 적용하고 피처/타깃 데이터 세트 생성
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)
이상치 데이터 존재 여부 확인
plt.scatter(x = house_df_org['GrLivArea'], y = house_df_org['SalePrice'])
plt.ylabel('SalePrice', fontsize=15)
plt.xlabel('GrLivArea', fontsize=15)
plt.show()
GrLivArea와 SalePrice간의 관계를 시각화한 결과 두 개의 이상치 데이터를 확인할 수 있다
이상치 데이터 삭제
# GrLivArea와 SalePrice 모두 로그 변환됐으므로 이를 반영한 조건 생성.
cond1 = house_df_ohe['GrLivArea'] > np.log1p(4000)
cond2 = house_df_ohe['SalePrice'] < np.log1p(500000)
outlier_index = house_df_ohe[cond1 & cond2].index
print('이상치 레코드 index:', outlier_index.values)
print('이상치 삭제 전 house_df_ohe shape: ', house_df_ohe.shape)
# DataFrame의 인덱스를 이용해 이상치 레코드 삭제.
house_df_ohe.drop(outlier_index, axis=0, inplace=True)
print('이상치 삭제 후 house_df_ohe shape:', house_df_ohe.shape)
이상치 데이터 삭제 후 칼럼에 변화가 일어난 것을 확인할 수 있다
하이퍼 파라미터 튜닝
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.003, 0.1, 0.5, 1, 5, 10]}
print_best_params(ridge_reg, ridge_params)
print_best_params(lasso_reg, lasso_params)
회귀 트리 모델 학습 / 예측 / 평가 - XGBRegressor, LGBMRegressor
from xgboost import XGBRegressor
xgb_params={'n_estimators':[1000]}
xgb_reg=XGBRegressor(n_estimators=1000, learning_rate=0.05, colsample_bytree=0.5, subsample=0.8)
print_best_params(xgb_reg, xgb_params)
from lightgbm import LGBMRegressor
lgbm_params = {'n_estimators':[1000]}
lgbm_reg = LGBMRegressor(n_estimators=1000, learning_rae=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)
최종 혼합 모델 학습 / 예측 / 평가
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))
# 개별 모델의 학습
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)
스태킹 앙상블 모델을 통한 회귀 예측
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
# 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)
스태킹 회귀 모델의 예측 성능 평가 - 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)
'개발 > 💠 Alchemist' 카테고리의 다른 글
💠 군집화 💠 (0) | 2023.11.28 |
---|---|
💠 AIchemist 7주차 💠 (0) | 2023.11.22 |
💠 회귀 - 캐글 자전거 대여 수요 예측 실습 💠 (1) | 2023.11.18 |
💠 AIchemist 6주차 💠 (0) | 2023.11.08 |
💠 머신러닝 회귀 모델 💠 (4) | 2023.11.07 |