728x90

 

 

www.kaggle.com/c/house-prices-advanced-regression-techniques

 

 

 

이번에는 고급 회귀 기법을 이용한 주택 가격 예측을 해보고자 한다.

 

우선 대이터에 대해서 살펴보자

 

파일들은

 

훈련데이터, 테스트 데이터, 제출 샘플 csv와 데이터 설명을 위한 텍스트 파일이 있다.

 

데이터 속성들로는

 

SalePrice 주택 가격으로 타갯 변수

 

나머지는 우리들이 회귀 모델을 학습을 해야하는데 필요한 독립 변수들이 되겠다.

 

이제 우선 필요한 라이브러리들과 데이터부터 로드하자.

 

 

 

 

데이터는 1460개, 컬럼은 81개 정도가 있고

 

보면 Id 부터 시작한다.

 

 

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings

warnings.filterwarnings("ignore")
%load_ext autotime

train = pd.read_csv("./res/house-prices-advanced-regression-techniques/train.csv")
train.info()
train.describe()

 

 

결측치 체크해본 결과

 

 

몇개의 열에서는 결측치가 상당히 많이 존재한다.

 

이런 열들은 나중에 제거해주고

 

 

 

종속 변수를 한번

 

히스토그램으로 플로팅 시켜본 결과 좌측으로 치우쳐진 형태로 되어있다.

 

로그 변환을 한번 시켜보자

 

 

왜도가 0에 가까운 정규 분포의 형태가 되었다.

 

이 데이터를 반영해주고

 

널 처리를 해주자

 

 

 

 

타겟 값 로그 변환 형으로 변경

 

널 많은 열 삭제

 

정수형 널데이터 평균값 대치

 

이외 널 데이터 타입 확인

train["SalePrice"] = log_sale_price

# 널 많은 컬럼 삭제
drop_cols = ["Id", "PoolQC", "MiscFeature", "Alley", "Fence", "FireplaceQu"]
train.drop(columns=drop_cols, axis=1, inplace=True)

#정수형 데이터 평균 값 대치
train.fillna(train.mean(), inplace=True)

#정수형 이외 널이 존재하는 칼람 추출
#train.isnull().sum() 결과에서 0보다 큰 항들의 인덱스 추출
null_column = train.isnull().sum()[train.isnull().sum() > 0]

print(train.dtypes[null_column.index])

 

 

 

문자열 변수들을 원핫 인코딩으로 가변수화

 

이제 선형 회귀 모델 별 성능을 측정해보자

 

#문자열 속성 가변수로 카테고리화
#get_dummies는 컬럼 미지정시 문자열 속성들을 변환 수행
print("before get dummies train.shape : {}".format(train.shape))
train_oh = pd.get_dummies(train)
print("after get dummies train.shape : {}".format(train.shape))
null_column = train_oh.isnull().sum()[train.isnull().sum() > 0]

print(train.dtypes[null_column.index])

 

 

 

 

RMSE 결과를 살펴보면

 

라쏘 모델이 유독 RMSE가 커보인다

 

하이퍼파라미터를 튜닝해보자

 

 

 

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

def get_rmse(model, X_test, y_test):
    y_pred = model.predict(X_test)
    mse = mean_squared_error(y_test, y_pred)
    rmse = np.sqrt(mse)
    print("#### model : {0} ####\n MSE : {1:.3f}\n RMSE : {2:.3f}\n".format(model.__class__.__name__, mse, rmse))
    return rmse

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

y = train_oh["SalePrice"]
X = train_oh.drop(columns=["SalePrice"], axis=1, inplace=False)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

lr = LinearRegression()
ridge = Ridge()
lasso = Lasso()

lr.fit(X_train, y_train)
ridge.fit(X_train, y_train)
lasso.fit(X_train, y_train)

models = [lr, ridge, lasso]
get_rmses(models, X_test, y_test)

 

 

각 모델 별 가장큰 회귀계수와 작은 회귀계수들을 살펴보면

 

 

선형괴 릿지 모델은 비슷하나

 

라쏘 모델의 경우 YearBulit가 너무 크고 나머지는 작음.

 

릿지 모델은 회귀 벡터 W의 절대값 제곱에 패널티를 주는 L2 규제 

 

L2 규제의 경우 회귀 계수의 크기를 감소시켰었음

 

throwexception.tistory.com/1074

 

 

라쏘 모델은 회귀 벡터 W의 절대값에 패널티를 부여하는 L1 규제 적용

 

L1 규제는 중요하지 않은 회귀 계수를 크게 줄여 0으로 만들어 제거했었음

 

그래서인지 릿지 모델은 선형 모델보다 회귀 계수가 작아졌으며

 

라쏘 모델의 경우 중요한 변수 이외에는 크게 축소되거나 제거

def get_top_bottom_coef(model, X, n=10):
    coef = pd.Series(data=model.coef_, index=X.columns)
    coef_high = coef.sort_values(ascending=False).head(n)
    coef_low = coef.sort_values(ascending=False).tail(n)
    return coef_high, coef_low

def visualize_coeff(models, X, n=10):
    fig, axs = plt.subplots(figsize=(24, 10), ncols=3, nrows=1)
    fig.tight_layout()
    
    for idx, model in enumerate(models):
        coef_high, coef_low = get_top_bottom_coef(model, X, n=10)
        coef_concat = pd.concat([coef_high, coef_low])
        axs[idx].set_title(model.__class__.__name__+" Coefficient", size=24)
        axs[idx].tick_params(axis="y", direction="in", pad=-120)
        for label in (axs[idx].get_xticklabels() + axs[idx].get_yticklabels()):
            label.set_fontsize(22)
        sns.barplot(x=coef_concat.values, y=coef_concat.index, ax=axs[idx])

visualize_coeff(models, X, n=10)

 

 

 

릿지와 라쏘 모델의 하이퍼 파라미터를 조정시켜보자

 

 

from sklearn.model_selection import GridSearchCV

def print_best_params(model, params, X, y):
    
    gs = GridSearchCV(model, param_grid=params, n_jobs=-1, cv=5, scoring="neg_mean_squared_error")
    gs.fit(X, y)
    rmse = np.sqrt(-1 * gs.best_score_)
    print("### {0} ###\n mean RMSE : {1:.3f}\n best alpha : {2}\n".format(model.__class__.__name__, np.mean(rmse), gs.best_params_))
    

params = {
    "alpha" : [0.001, 0.05, 0.1, 1, 5, 8, 10, 15, 20]
}

print_best_params(ridge,params, X, y)
print_best_params(lasso,params, X, y)

 

 

최적 하이퍼 파라미터를 찾았으니 성능 지표와 시각화를 시켜보면

 

라쏘 모델의 RMSE가 타 모델들보다 가장 작아졌으며 

 

 

더 데이터 튜닝을 한다면 아직 이상치를 처리하지 않았으니

 

데이터 분포도를 살펴보자

 

이전에 잠깐 한 과정을 되살펴보면

 

 

ridge = Ridge(alpha=10)
lasso = Lasso(alpha=0.001)

ridge.fit(X_train, y_train)
lasso.fit(X_train, y_train)

models = [lr, ridge, lasso]
get_rmses(models, X_test, y_test)
visualize_coeff(models, X, n=10)

 

 

 

나는 이전에 지금까지 머신러닝 문제를 푸는 과정을

 

라이브러리 임포트

 

데이터 확인, 탐색

 

전처리

 

모델 설계

 

학습

 

성능평가

 

이 단계를 다 거치고 나서

 

해야된다고 생각은 했는데

 

 

 

내가 기억하고 있는 모든 과정을 다 하고나서 조금씩 수정하는 방식으로 갔었다

 

 

그러다 보니 임의로 필요하다고 생각하는데로 전처리를 한후 모델 조정하고, 다시 전처리하고 모델 조정하고

 

이 과정을 반복했는데

 

내 방식대로만 가면 여기서 몇가지 놓친 부분이 있었던것 같았다.

 

 

 

 

 

 

다시 생각해보자

 

데이터 로드하고,

 

간단하게 살펴본다음에

 

최소한의 데이터 처리

- 결측치 열 제거, 대치, 카테고리형 변환

 

학습 전에 타겟 분포를 살펴보고 왜곡된 형태였는지 살펴봤었다.

 

왜곡이 심하면 로그 변환을 수행해주고

 

 

그 다음 기본 모델들을 사용하여 성능 평가와

 

중요한 회귀 계수들 시각화해서 살펴보았었다.

 

거기서 라쏘 모델 L1 규제를 적용하는(중요한 피처만 남기고 나머지 크게 축소, 제거)으로 인해 문제점을 확인하였고

 

 

하이퍼 파라미터를 조정해서 타 모델보다 개선된 결과를 구한게 여기까지 한일

 

 

 

 

타깃 값과 마찬가지로 독립 변수들의 왜곡도들을 살펴보자

 

 

from scipy.stats import skew

ftr_idx = train.dtypes[train.dtypes != "object"].index
skew_ftrs = train[ftr_idx].apply(lambda x : skew(x))
skew_ftrs_top = skew_ftrs[skew_ftrs > 1]
print(skew_ftrs_top.sort_values(ascending=False))

 

 

왜곡도가 심한 변수들을 로그 변환하고

 

 

다시 최적 하이퍼파라미터를 탐색한 결과

 

왜도가 큰 데이터를 로그 변환하기 전

 

0.142였던 RMSE가 

 

 

 

0.128, 0.125로 줄어들었다.

train[skew_ftrs_top.index] = np.log1p(train[skew_ftrs_top.index])
train_oh = pd.get_dummies(train)
y = train_oh["SalePrice"]
X = train_oh.drop("SalePrice", axis=1, inplace=False)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

print_best_params(ridge,params, X, y)
print_best_params(lasso,params, X, y)

 

 

 

 

 

 

이렇게 구한 파라미터로 학습 결과를 살펴보면

 

성능이 크게 변화하지는 않았다.

 

내가 처음부터 random_state를 고정시켜서 했어야됬는데 ;;

 

 

아무튼 세 모델다 GrLiveArea가 판매 가격에 상당히 큰 영향을 미치는걸 그래프로 볼수 있다.

 

이 변수와 판매 가격 사이 관계를 살펴보자

 

 

lr = LinearRegression()
ridge = Ridge(alpha=0.10)
lasso = Lasso(alpha=0.001)
lr.fit(X_train, y_train)
ridge.fit(X_train, y_train)
lasso.fit(X_train, y_train)

models = [lr, ridge, lasso]
get_rmses(models, X_test, y_test)
visualize_coeff(models, X, n=10)

 

 

 

 

편하게 살펴보려 했는데 지금 train은 로그 변환을 해서인지 정규 분포 형태를 띄고 있다.

원본 데이터로 다시보면

 

GrLivArea의 4000너머로 크게 동떨어진 이상치가 보인다

train_org = pd.read_csv("./res/house-prices-advanced-regression-techniques/train.csv")
sns.regplot(x=train_org["GrLivArea"], y=train_org["SalePrice"])

 

이 아웃라이어 들의 인덱스를 다음과 같이 조건문으로 구하고 제거하자

* GrLivArea가 4000보다 크고, Saleprice가 500000이하인 데이터

 

그리고 다시 하이퍼 파라미터 재탐색

 

cond1 = train_oh["GrLivArea"] > np.log1p(4000)
cond2 = train_oh["SalePrice"] < np.log1p(500000)
outlier = train_oh[cond1 & cond2].index
train_oh.drop(outlier, axis=0, inplace=True)

y = train_oh["SalePrice"]
X = train_oh.drop("SalePrice", axis=1, inplace=False)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

print_best_params(ridge,params, X, y)
print_best_params(lasso,params, X, y)

 

 

 

 

다시 구한 하이퍼파라미터로 재 평가, 시각화 한 결과

 

릿지 모델과 라쏘 모델로 더 좋은 결과를 구하였다.

 

lr = LinearRegression()
ridge = Ridge(alpha=8)
lasso = Lasso(alpha=0.001)
lr.fit(X_train, y_train)
ridge.fit(X_train, y_train)
lasso.fit(X_train, y_train)

models = [lr, ridge, lasso]
get_rmses(models, X_test, y_test)
visualize_coeff(models, X, n=10)

 

 

스태킹 기법으로 풀어보자

 

 

스태킹 기법은

 

최종 모델과 최종 모델 학습 데이터용 모델들로 나눠지는데

 

학습 데이터용 모델은 기존의 데이터로 학습하여(학습 데이터 -> 학습/검증 용으로 분할) 추정 결과들을

 

최종 모델의 입력(학습 데이터)으로 사용하여 개선된 결과를 구하는 기법이었다.

 

 

 

다음 함수를 통해 최종 모델에 사용할 학습/ 테스트 데이터를 만들었다.

 

폴드를 나누고, 해당 폴드 데이터로 학습, 테스트하여 예측 결과를 저장. 테스트 값은 폴드별 열방향 추가 후 평균

 

 

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

def get_stacking_db(model, X_train, y_train, X_test, n_folds):
    kf = KFold(n_splits=n_folds, shuffle=False, random_state=0)
    # (X rows, 1)
    train_fold_pred = np.zeros((X_train.shape[0], 1))
    test_pred = np.zeros((X_test.shape[0], 1))
    print("## model : {} ##".format(model.__class__.__name__))
    for fold_cnt, (train_idx, valid_idx) in enumerate(kf.split(X_train)):
        print("fond set : {}".format(fold_cnt))
        X_fd = X_train[train_idx]
        y_fd = y_train[valid]
        model.fit(X_fd, y_fd)
        train_fold_pred[valid_idx, :] = model.predict(X_fd).reshape(-1, 1)
        test_pred[:, fold_cnt] = model.predict(X_test)
    test_pred_mean = np.mean(test_pred, axis = 1).reshape(-1, 1)
    return train_fold_pred, test_pred_mean

 

 

최종 모델을 라쏘 모델로 사용한 스태킹 모델의 성능은 0.960165로 기존의 방법보다 최적의 결과를 구할 수 있었슴.

from lightgbm import LGBMRegressor


ridge = Ridge(alpha=8)
lasso = Lasso(alpha=0.001)
lgbm = LGBMRegressor(n_estimators=1000)

ridge_train, ridge_test = get_stacking_db(ridge, X_train.values, y_train.values, X_test.values, 5)
lasso_train, lasso_test = get_stacking_db(lasso, X_train.values, y_train.values, X_test.values, 5)
lgbm_train, lgbm_test = get_stacking_db(lgbm, X_train.values, y_train.values, X_test.values, 5)


X_stack_train = np.concatenate((ridge_train, lasso_train, lgbm_train), axis=1)
X_stack_test = np.concatenate((ridge_test, lasso_test, lgbm_test), axis=1)

final_lasso = Lasso(alpha=0.001)
final_lasso.fit(X_stack_train, y_train)
final_pred = final_lasso.predict(X_stack_test)

get_rmse(final_lasso,X_stack_test, y_test)

 

 

 

 

300x250

+ Recent posts