본문 바로가기

Euron/정리

[Week8] 05. 회귀 - 캐글 실습

09. 회귀 실습 - 자전거 대여 수요 예측

 

캐글에서 받은 데이터 세트에는 2011년 1월부터 2012년 12월까지 날짜/시간, 기온, 습도 등의 정보를 기반으로 1시간 간격으로 자전거 대여 횟수 정보가 있다.

결정값은 count(대여 횟수)이다.

 

데이터 클렌징 및 가공과 데이터 시각화

 

bike_df.info()로 데이터 칼럼의 타입을 살펴본다.

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10886 entries, 0 to 10885
Data columns (total 12 columns):
 #   Column      Non-Null Count  Dtype  
---  ------      --------------  -----  
 0   datetime    10886 non-null  object 
 1   season      10886 non-null  int64  
 2   holiday     10886 non-null  int64  
 3   workingday  10886 non-null  int64  
 4   weather     10886 non-null  int64  
 5   temp        10886 non-null  float64
 6   atemp       10886 non-null  float64
 7   humidity    10886 non-null  int64  
 8   windspeed   10886 non-null  float64
 9   casual      10886 non-null  int64  
 10  registered  10886 non-null  int64  
 11  count       10886 non-null  int64  
dtypes: float64(3), int64(8), object(1)
memory usage: 1020.7+ KB

datetime 칼럼만 object형이다. 년-월-일 시:분:초 문자형식으로 돼 있으므로, 이를 년, 월, 일 그리고 시간 4개의 속성으로 분리한다.

판다스에서는 datetime 타입을 제공하므로 문자열을 datetime 타입으로 변환하는 applut(pd.to_datetime) 메서드를 이용한다.

# 문자열을 datetime 타입으로 변경. 
bike_df['datetime'] = bike_df.datetime.apply(pd.to_datetime)

# datetime 타입에서 년, 월, 일, 시간 추출
bike_df['year'] = bike_df.datetime.apply(lambda x : x.year)
bike_df['month'] = bike_df.datetime.apply(lambda x : x.month)
bike_df['day'] = bike_df.datetime.apply(lambda x : x.day)
bike_df['hour'] = bike_df.datetime.apply(lambda x: x.hour)
bike_df.head(3)

datetime 변환

새롭게 칼럼이 추가된 것을 확인할 수 있다.

 

casaul은 사전에 등록하지 않은 사용자, registered는 사전에 등록한 사용자이며 causal+registered=count이다.

이 둘은 상관도가 높아 예측을 저해할 수 있으므로 이 칼럼들과 datetime 칼럼을 삭제한다.

drop_columns = ['datetime','casual','registered']
bike_df.drop(drop_columns, axis=1,inplace=True)
# datetime 칼럼 삭제, causal+register 삭제(상관도가 높음)

 

이제 주요 칼럼별로 count가 어떻게 분포되어있는지 시각화한다. 시본의 barplot을 적용한다.

fig, axs = plt.subplots(figsize=(16, 8), ncols=4, nrows=2)
cat_features = ['year', 'month','season','weather','day', 'hour', 'holiday','workingday']
# cat_features에 있는 모든 칼럼별로 개별 칼럼값에 따른 count의 합을 barplot으로 시각화
for i, feature in enumerate(cat_features):
    row = int(i/4)
    col = i%4
    # 시본의 barplot을 이용해 칼럼값에 따른 count의 합을 표현
    sns.barplot(x=feature, y='count', data=bike_df, ax=axs[row][col])

 

다음으로 다양한 회귀 모델을 데이터 세트에 적용해 예측 성능을 측정한다.

캐글에서는 RMSLE(Root Mean Square Log Error)를 이용한 성능 평가 방법을 요구한다.

RMSLE는 사이킷런에서 제공하지 않으므로 MSE, RMSE와 함께 한꺼번에 평가하는 함수를 만든다.

from sklearn.metrics import mean_squared_error, mean_absolute_error

# log 값 변환 시 NaN등의 이슈로 log() 가 아닌 log1p() 를 이용하여 RMSLE 계산
# RMSE는 오류 값의 로그에 대한 것
def rmsle(y, pred):
    log_y = np.log1p(y)
    log_pred = np.log1p(pred)
    squared_error = (log_y - log_pred) ** 2
    rmsle = np.sqrt(np.mean(squared_error))
    return rmsle

# 사이킷런의 mean_square_error() 를 이용하여 RMSE 계산
def rmse(y,pred):
    return np.sqrt(mean_squared_error(y,pred))

# MSE, RMSE, RMSLE 를 모두 계산 
def evaluate_regr(y,pred):
    rmsle_val = rmsle(y,pred)
    rmse_val = rmse(y,pred)
    # MAE 는 scikit learn의 mean_absolute_error() 로 계산
    mae_val = mean_absolute_error(y,pred)
    print('RMSLE: {0:.3f}, RMSE: {1:.3F}, MAE: {2:.3F}'.format(rmsle_val, rmse_val, mae_val))

rmsle()에서 넘파이의 log() 혹은 사이킷런의 mean_squared_log_error()를 이용할 수 있으나 언더플로/오버플로 오류가 발생할 수 있다.

따라서 log()보다는 log1p()를 이용한다.

log1p()는 1+log() 값으로 문제를 해결한다. log1p()로 변환된 값은 다시 넘파이의 expm1() 함수로 복원가능하다.

 

로그 변환, 피처 인코딩과 모델 학습/예측/평가

 

회귀 모델 적용을 위해서는 중요하게 처리해야 하는 상황이 있다.

  • 결과값이 정규 분포로 돼 있는지 확인 
  • 카테고리형 회귀 모델의 경우 원-핫 인코딩으로 피처를 인코딩한다.
    • 원-핫 인코딩: 문자를 숫자로 바꾸는 방법 중 하나. 0과 1로만 이루어진 열을 생성하며, 1인 경우에 해당하는 문자 칼럼이다. get_dummies()를 활용하며 당연하게도 객체수가 늘어난다.

LinearRegression 모델을 이용해 회귀 예측 후 두 가지 상황을 확인한다.

from sklearn.model_selection import train_test_split , GridSearchCV
from sklearn.linear_model import LinearRegression , Ridge , Lasso

y_target = bike_df['count']
X_features = bike_df.drop(['count'],axis=1,inplace=False)

X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size=0.3, random_state=0)

lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)
pred = lr_reg.predict(X_test)

evaluate_regr(y_test ,pred)
RMSLE: 1.165, RMSE: 140.900, MAE: 105.924

모두 예측 오류로서는 큰 값이다.

이렇게 큰 예측 오류가 발생할 경우 먼저 Target값의 분포가 왜곡된 형태를 이루고 있는지 확인한다.

판다스의 hist()를 이용해 count칼럼이 정규 분포를 이루는지 확인한다.

y_target.hist()

정규 분포 x

칼럼 값이 0~200 사이에 왜곡돼 있다.

일반적으로 이렇게 왜곡된 값은 로그를 적용해 변환한다.

넘파이의 log1p() 함수를 적용해 Target값을 변경 후 이를 기반으로 학습하고 예측한다. 테스트를 위해서 변경된 값은 다시 expm1() 함수를 적용해 원해 scale 값으로 원상 복구한다.

y_log_transform = np.log1p(y_target)
y_log_transform.hist()

정규분포는 아니지만 왜곡 완화됨

로그변환된 Target값을 이용해 학습 데이터 분리,학습 후 원래 데이터로 테스트한 결과는 아래와 같다.

RMSLE: 1.017, RMSE: 162.594, MAE: 109.286

RMSLE의 오류는 줄었지만, RMSE는 늘어났다.

 

이번에는 개별 피처들의 인코딩을 적용한다.

먼저, 각 피처의 회귀 계숫값을 시각화한다.

coef = pd.Series(lr_reg.coef_, index=X_features.columns)
coef_sort = coef.sort_values(ascending=False)
sns.barplot(x=coef_sort.values, y=coef_sort.index)
plt.savefig('log_transform.tif', format='tif', dpi=300, bbox_inches='tight')

상대적으로 회귀 계수 영향도가 높은 피처들이 있다.

특히, year와 month는 2011/2012, 1/2/3/.../12과 같이 숫잣값 형태로 의미를 담고 있으나 이들은 모두 카테고리형 피처이다.

선형 회귀는 숫자형 카테고리 값에 큰 영향을 받아, 숫자에 의미를 부여할 수도 있다.

따라서 선형 회귀에서는 이러한 피처 인코딩에 원-핫 인코딩을 적용해 변환해야 한다.

# 'year', month', 'day', hour'등의 피처들을 One Hot Encoding
X_features_ohe = pd.get_dummies(X_features, columns=['year', 'month','day', 'hour', 'holiday',
                                              'workingday','season','weather'])

사이킷런의 선형 회귀 모델인 LinearRegression, Ridge, Lasso를 모두 학습해 예측 성능을 확인한다.

이를 위한 get_model_predict 함수를 만들고 적용한다.

# 원-핫 인코딩이 적용된 feature 데이터 세트 기반으로 학습/예측 데이터 분할. 
X_train, X_test, y_train, y_test = train_test_split(X_features_ohe, y_target_log,
                                                    test_size=0.3, random_state=0)

# 모델과 학습/테스트 데이터 셋을 입력하면 성능 평가 수치를 반환
def get_model_predict(model, X_train, X_test, y_train, y_test, is_expm1=False):
    model.fit(X_train, y_train)
    pred = model.predict(X_test)
    if is_expm1 :
        y_test = np.expm1(y_test)
        pred = np.expm1(pred)
    print('###',model.__class__.__name__,'###')
    evaluate_regr(y_test, pred)
# end of function get_model_predict    

# model 별로 평가 수행
lr_reg = LinearRegression()
ridge_reg = Ridge(alpha=10)
lasso_reg = Lasso(alpha=0.01)

for model in [lr_reg, ridge_reg, lasso_reg]:
    get_model_predict(model,X_train, X_test, y_train, y_test,is_expm1=True)
### LinearRegression ###
RMSLE: 0.590, RMSE: 97.687, MAE: 63.382
### Ridge ###
RMSLE: 0.590, RMSE: 98.529, MAE: 63.893
### Lasso ###
RMSLE: 0.635, RMSE: 113.219, MAE: 72.803

원-핫 인코딩 적용 이후 예측 성능이 많이 향상되었다.

 

이번엔는 회귀 트리를 이용해 회귀 예측을 수행한다. 앞에서 수행한 데이터 전처리를 그대로 사용하여 랜덤 포레스트, GBM, XGBoost, LightGBM을 순차적으로 성능 평가한다.

### RandomForestRegressor ###
RMSLE: 0.355, RMSE: 50.219, MAE: 31.119
### GradientBoostingRegressor ###
RMSLE: 0.330, RMSE: 53.344, MAE: 32.751
### XGBRegressor ###
RMSLE: 0.342, RMSE: 51.732, MAE: 31.251
### LGBMRegressor ###
RMSLE: 0.319, RMSE: 47.215, MAE: 29.029

선형 회귀 모델보다 회귀 예측 성능이 개선되었다.

 


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

 

이번 데이터는 가격이 비쌀수록 예측 결과 오류가 전체 오류에 미치는 비중이 높으므로 이것을 상쇄하기 위해 오류 값을 로그 변환한 RMSLE를 이용한다.

 

데이터 사전 처리(Preprocessing)

 

이번 예제는 데이터 가공을 많이 수행할 예정이므로 copy()메서드를 이용해 데이터 세트를 복사 후 가공한다.

Target값은 SalePrice이며, 데이터 세트 크리와 칼럼 타입, 그리고 Null이 있는 칼럼과 그 건수를 내림차순으로 출력한다.

print('데이터 세트의 Shape:', house_df.shape)
print('\n전체 feature 들의 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)

전체 feature 들의 type 
 object     43
int64      35
float64     3
dtype: int64

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

Null 값이 너무 많은 피처는 드롭하도록 한다.

 

다음은 SalePrice 컬럼의 분포도가 정규 분포인디 확인한다.

plt.title('Original Sale Price Histogram')
plt.xticks(rotation=15)
sns.histplot(house_df['SalePrice'], kde=True)
plt.show()

정규 분포가 아니므로 log1p()를 이용해 변환 후 학습하고, 예측 시에는 결괏값을 expm1()로 환원 후 수행한다.

plt.title('Log Transformed Sale Price Histogram')
log_SalePrice = np.log1p(house_df['SalePrice'])
sns.histplot(log_SalePrice, kde=True)
plt.show()

결과는 생략한다. 정규 분포로 변환되었음.

 

앞서 Null값이 많은 피처와 ID 컬럼은 드롭하면서, 숫자형 피처의 경우 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)
# Drop 하지 않는 숫자형 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()는 자동으로 문자열 피처를 원-핫 인코딩으로 변환하면서 Null값은 0으로 대체된다.

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)

get_dummies 수행으로 피처가 증가하고, Null값을 가진 피처는 이제 없는 것을 알 수 있다.

 

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

 

여러 보델의 로그 변환된 RMSE를 측정할 것이므로 이를 계산하는 함수를 먼저 만든다.

SalePrice가 로그 변환 되었으므로 예측값 역시 로그 변환된 값이다. 따라서 예측 결과 오류에 RMSE만 적용하면 RMSLE가 자동으로 특정된다.

def get_rmse(model):
    pred = model.predict(X_test)
    mse = mean_squared_error(y_test , pred)
    rmse = np.sqrt(mse)
    print('{0} 로그 변환된 RMSE: {1}'.format(model.__class__.__name__,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

라쏘 회귀의 경우 타 방식보다 많이 떨어지는 결과가 나왔다.

따라서 라쏘는 최적 하이퍼 파라미터 튜닝을 수행한다.

 

먼저 피처별 회귀 계수를 시각화한다. 상위/하위 별 10개의 피처명과 회귀 계수 값을 가지는 판다스 Series 객체를 반환하는 함수를 만든 후 모델별로 회귀 계수를 시각화한다.

def get_top_bottom_coef(model):
    # coef_ 속성을 기반으로 Series 객체를 생성. index는 컬럼명. 
    coef = pd.Series(model.coef_, index=X_features.columns)
    
    # + 상위 10개 , - 하위 10개 coefficient 추출하여 반환.
    coef_high = coef.sort_values(ascending=False).head(10)
    coef_low = coef.sort_values(ascending=False).tail(10)
    return coef_high, coef_low
    
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_bottom_coef(model)
        coef_concat = pd.concat( [coef_high , coef_low] )
        # 순차적으로 ax subplot에 barchar로 표현. 한 화면에 표현하기 위해 tick label 위치와 font 크기 조정. 
        axs[i_num].set_title(model.__class__.__name__+' Coeffiecents', 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)

라쏘 주목

라쏘는 전체적으로 회귀 계수 값이 매우 작고, 그중 YearBuilt가 가장 크다.

학습 데이터의 데이터 분할에 문제가 있는지 확인하기 위해 train_test_split() 대신 교차 검증 폴드 세트로 데이터 세트를 분할 후 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

여전히 라쏘는 성능이 떨어지므로 데이터 세트 분할의 문제는 아니다.

 

다음은 릿지와 라쏘 모델에 대해서 alpha하이퍼 파라미터를 변화시키면서 최적 값을 도출한다.

  • 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_))
    return grid_model.best_estimator_

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] }
best_rige = print_best_params(ridge_reg, ridge_params)
best_lasso = 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}

라쏘 모델은 alpha 최적화 이후 예측 성능이 많이 좋아졌다.

또한 최적화 이후 모델별 회귀 계수도 달라졌다.

 

이제 데이터 세트를 추가적으로 가공한다.

피처 데이터 세트의 분포도와 이상치(Outlier)데이터를 처리한다.

 

사이파이 stats 모듈의 skew() 함수를 이용해 숫자형 피처의 왜곡 정도를 쉽게 확인할 수 있다.

일반적으로 skew() 함수의 반환 값이 1 이상인 경우를 왜곡 정도가 높다고 판단한다.

이때, 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])

로그 변환 후 여전히 높은 왜곡 정도를 가진 피처가 있으나, 더이상의 개선은 어려우므로 그대로 유지한다.

 

로그 변환을 수행한 데이터에 다시 원-핫 인코딩을 적용후 이에 기반한 학습/데이터 세트를 다시 만든다.

여시에 앞서 생성한 print_best_params() 함수를 이용해 최적 alpha값과 RMSE를 출력한다.

# Skew가 높은 피처들을 로그 변환 했으므로 다시 원-핫 인코딩 적용 및 피처/타겟 데이터 셋 생성,
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] }
best_ridge = print_best_params(ridge_reg, ridge_params)
best_lasso = 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}

 

분할된 데이터 세트의 시각화 결과는 다음과 같다.

모두 GrLivArea, 즉 주거 공간 크기가 회귀 계수가 가장 높은 피처가 됐다.

 

다음으로는 이상치 데이터를 분석한다.

회귀 계수가 높은 피처, 즉 예측에 많은 영향을 미치는 GrLivArea 피처의 데이터 분포를 살펴본다.

먼저 주택 가격 데이터가 변환되기 전 GrLivArea 피처와 타겟값의 관계를 시각화한다.

# 이상치 제거
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 모두 로그 변환되었으므로 이를 반영한 조건 생성. 
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의 index를 이용하여 아웃라이어 레코드 삭제. 
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)

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.03, 0.1, 0.5, 1,5, 10] }
best_ridge = print_best_params(ridge_reg, ridge_params)
best_lasso = 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}

 예측 성능이 모두 크게 좋아졌다.

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

 

이번에는 회귀 트리를 이용해 회귀 모델을 만든다.

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)
best_xgb = print_best_params(xgb_reg, xgb_params)

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)
best_lgbm = print_best_params(lgbm_reg, lgbm_params)
XGBRegressor 5 CV 시 최적 평균 RMSE 값: 0.1171, 최적 alpha:{'n_estimators': 1000}
LGBMRegressor 5 CV 시 최적 평균 RMSE 값: 0.1163, 최적 alpha:{'n_estimators': 1000}

 

그리고 이 모델들의 피처 중요도를 시각화한다.

# 모델의 중요도 상위 20개의 피처명과 그때의 중요도값을 Series로 반환.
def get_top_features(model):
    ftr_importances_values = model.feature_importances_
    ftr_importances = pd.Series(ftr_importances_values, index=X_features.columns  )
    ftr_top20 = ftr_importances.sort_values(ascending=False)[:20]
    return ftr_top20

def visualize_ftr_importances(models):
    # 2개 회귀 모델의 시각화를 위해 2개의 컬럼을 가지는 subplot 생성
    fig, axs = plt.subplots(figsize=(24,10),nrows=1, ncols=2)
    fig.tight_layout() 
    # 입력인자로 받은 list객체인 models에서 차례로 model을 추출하여 피처 중요도 시각화. 
    for i_num, model in enumerate(models):
        # 중요도 상위 20개의 피처명과 그때의 중요도값 추출 
        ftr_top20 = get_top_features(model)
        axs[i_num].set_title(model.__class__.__name__+' Feature Importances', size=25)
        #font 크기 조정.
        for label in (axs[i_num].get_xticklabels() + axs[i_num].get_yticklabels()):
            label.set_fontsize(22)
        sns.barplot(x=ftr_top20.values, y=ftr_top20.index , ax=axs[i_num])

# 앞 예제에서 print_best_params( )가 반환한 GridSearchCV로 최적화된 모델의 피처 중요도 시각화    
models = [best_xgb, best_lgbm]
visualize_ftr_importances(models)

 

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

 

예측 결과 혼합이란 A, B 두 모델의 예측값이 있을때 A 모델 예측값의 40%, B 모델 예측값의 60%를 더해 최종 회귀 값으로 예측하는 것이다.

최종 혼합 모델, 개별 모델의 RMSE 값을 출력하는 get_rmse_pred() 함수를 생성하고 각 모델의 예측값을 계산한 뒤 개별 모델과 최종 혼합 모델의 RMSE를 구한다.

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)
최종 혼합 모델의 RMSE: 0.10007930884470519
Ridge 모델의 RMSE: 0.10345177546603272
Lasso 모델의 RMSE: 0.10024170460890039

최종 혼합 모델이 개별 모델보다 약간 성능이 개선되었다.

성능이 조금 좋은 쪽(RMSE가 낮은 쪽)에 06, 릿지 모델 예측값에 0.4를 곱한 뒤 더했다.

 

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

 

스태킹 모델은 두 종류의 모델이 필요하다.

첫번째는 개별적인 기반 모델이고, 두번째는 이 개별 기반 모델의 예측 데이터를 학습 데이터로 만들어서 학습하는 최종 메타 모델이다.

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

 

이를 위한 get_stacking_base_datasets()를 생성한다.

인자로 개별 기반 모델, 그리고 원래 사용되는 학습 데이터 세트와 테스트용 피처 데이터를 입력받는다

개별 모델이 K-폴드 세트로 설정된 폴드 세트 내부에서 원본의 학습 데이터를 다시 추출해 학습과 예측을 수행한 뒤 그 결과를 저장한다.

학습된 개별 모델이 인자로 입력된 원본 테스트 데이터를 예측한 뒤, 예측 결과를 평균해 테스트 데이터로 생성한다.

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

메타 모델은 별도의 라쏘 모델, 개별 모델은 릿지, 라쏘, XGBoost, LightGBM을 이용한다.

# 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)
# 개별 모델이 반환한 학습 및 테스트용 데이터 세트를 Stacking 형태로 결합.  
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.0979915296518969

최종적으로 스태킹 회귀 모델을 적용한 결과, RMSE가 현재까지 가장 좋은 성능 평가를 보여준다.

스태킹 모델이 회귀에서도 매우 효과적으로 사용될 수 있는 모델임을 보여준다.

 


# 회고

 

회귀 끝!!

역시 단순한 분류 보다는 훨씬 어렵다...데이터 전처리든 모델이든!

지금은 동아리 휴동 기간이지만 미리 하고 복습과제도 미리 하고...회귀 모델도 한 번 정리하는 시간을 가져야겠다.

'Euron > 정리' 카테고리의 다른 글

[Week 8] 🥑💰 PyCaret 및 EDA를 사용한 아보카도 가격 회귀 💹  (0) 2023.11.06
[Week9] 06. 차원 축소  (1) 2023.11.02
[Week5] 05. 회귀(2)  (1) 2023.10.08
[Week5] 05. 회귀(1)  (1) 2023.10.07
[Week4] 04. 분류 - 캐글 실습  (0) 2023.09.29