Hyun's Wonderwall

[AIchemist] 파머완 - CH05. 회귀 | 9. 회귀 실습 - 자전거 대여 수요 예측 본문

Study/Python-Machine-Learning

[AIchemist] 파머완 - CH05. 회귀 | 9. 회귀 실습 - 자전거 대여 수요 예측

Hyun_! 2023. 11. 7. 19:55

[Chapter 05] 회귀

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

캐글 자전거 대여 수요 예측 경연

2011.1~2012.12 동안 날짜/시간, 기온, 습도, 풍속 등의 정보를 기반으로 1시간 간격 동안의 자전거 대여 횟수가 기재되어 있다. 데이터 세트의 주요 칼럼은 아래와 같고 이 중 결정 값은 맨 마지막 칼럼인 count(대여 횟수를 의미)이다.

  • datetime: hourly date + timestamp
  • season: 1=봄, 2=여름, 3=가을, 4=겨울
  • holiday: 1=토, 일요일의 주말을 제외한 국경일 등의 휴일, 0=휴일이 아닌 날
  • workingday: 1=토, 일요일의 주말 및 휴일이 아닌 주중, 0=주말 및 휴일
  • weather:
    1=맑음
    2=안개, 안개+흐림
    3=가벼운 눈, 가벼운 비+천둥
    4=심한 눈/비, 천둥/번개
  • temp: 온도(섭씨)
  • atemp: 체감온도(섭씨)
  • humidity: 상대습도
  • windspeed: 풍속
  • casual: 사전에 등록되지 않은 사용자가 대여한 횟수
  • registered: 사전에 등록된 사용자가 대여한 횟수
  • count: 대여 횟수

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

데이터 세트로 모델 학습 후 대여 횟수(count)를 예측해 보자.

# DateFrame으로 로드해 데이터 확인

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

%matplotlib inline

import warnings
warnings.filterwarnings("ignore", category=RuntimeWarning)
bike_df = pd.read_csv('./bike_train.csv')
print(bike_df.shape)
bike_df.head()

-> 10886개의 레코드, 12개의 칼럼으로 구성되어 있다.

-> 10886개의 row 데이터 중 Null 데이터는 없음. 칼럼 자료형은 대부분 int 또는 float이었고 datetime 칼럼만 object형임.

Datetime 칼럼의 경우 년-월-일 시:분:초 문자 형식으로 되어 있으므로 이에 대한 가공이 필요.

-> 년, 월, 일, 시간 4개 속성으로(칼럼으로) 분리.

판다스는 문자열을 datetime 타입으로 변환하는 apply(pd.to_datetime) 메서드를 제공. datetime.apply(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)

-> year, month, day, hour 칼럼이 추가됨.

이제 datetime, casual, registered 칼럼 삭제 (casual+registered=count 이므로 불필요)

drop_columns = ['datetime', 'casual', 'registered']
bike_df.drop(drop_columns, axis=1, inplace=True)

 

주요 칼럼별로 Target 값인 count(대여 횟수)가 어떻게 분포되어 있는지 시각화해보자.

- 총 8개의 칼럼 'year', 'month', 'seaseon', 'weather', 'day', 'hour', 'holiday', 'workingday'

- 시본의 barplot을 적용. 8개의 칼럼들을 한번에 시각화하기 위해 matplotlib의 subplots()를 기반으로 barplot을 표현해 볼 것.

- 총 8개의 barplot을 그리기 위해 plt.subplots()의 인자로 ncols=4, nrows=2를 입력해 2개의 행과 4개의 열을 가진 그래프로 표현함

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])

  • year 별 count : 2011 < 2012 (시간이 지날수록 자전거 대여 횟수가 지속적으로 증가한 경과)
  • month 별 count : 1, 2, 3월이 낮고 6, 7, 8, 9월이 높음.
  • season 별 count : 봄(1), 겨울(4)이 낮고 여름(2), 가을(3)이 높음.
  • weather 별 count : 눈 또는 비가 있는 경우 (3, 4)가 낮고 맑거나(1) 약간 안개가 있는 경우(2)가 높음.
  • hour을 보면 오전 출근 시간(8시)와 퇴근 시간 (17, 18)이 상대적으로 높음. day간의 차이는 크지 않음.
  • holiday(0)또는 workingday(1) 여부는 주중일 경우가 상대적으로 약간 높음.

 

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

캐글에서 요구한 성능 평가 방법은 RMSLE 성능 평가 방법 (오류 값의 로그에 대한 RMSE)

사이킷런은 RMSLE를 제공하지 않는다. RMSLE를 수행하는 성능 평가 함수 직접 만들어보자. MSE, RMSE 평가 함수도 만들자.

from sklearn.metrics import mean_squared_error, mean_absolute_error

# log 값 변환 시 NaN등의 이슈로 log() 가 아닌 log1p() 를 이용하여 RMSLE 계산
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()를 이용하거나 사이킷런의 man_squared_log_error()를 이용할 수도 있지만 데이터 값의 크기에 따라 오버플로 혹은 언더플로 오류 발생할 수 있음.

ex. def rmsle(y, pred):

  msle = mean_squared_log_error(y, pred)

  rmsle=np.sqrt(mse)

  return rmsle

따라서 log1p를 이용한다. log1p는 1+log()값으로 log 변환값에 1을 더하여 이 문제를 해결한다. 그리고 log1p로 변환된 값은 다시 넘파이의 expm1() 함수로 쉽게 원래의 스케일로 복원될 수 있다.

 

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

회귀 모델 적용 전 데이터 세트에 대해 먼저 처리해야 할 사항

1. 결과값이 정규 분포로 되어 있는지 확인하기

2. 카테고리형 회귀 모델의 경우 원-핫 인코딩으로 피처를 인코딩하기

 

먼저 사이킷런의 LinearRegession 객체를 이용해 회귀 예측을 하자

# 로그 변환, 피처 인코딩, 모델 학습/예측/평가
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)

-> [Output] RMSLE: 1.165, RMSE: 140.900, MAE: 105.924

실제 Target 데이터 값인 대여 횟수(Count)를 감안하면 예측 오류로서는 비교적 큰 값..

 

실제 값과 예측값 차이를 DataFrame 칼럼으로 만들어서 오류값이 가장 큰 순으로 5개만 확인해 보자.

def get_top_error_data(y_test, pred, n_tops = 5):
    # DataFrame에 컬럼들로 실제 대여횟수(count)와 예측 값을 서로 비교 할 수 있도록 생성. 
    result_df = pd.DataFrame(y_test.values, columns=['real_count'])
    result_df['predicted_count']= np.round(pred)
    result_df['diff'] = np.abs(result_df['real_count'] - result_df['predicted_count'])
    # 예측값과 실제값이 가장 큰 데이터 순으로 출력. 
    print(result_df.sort_values('diff', ascending=False)[:n_tops])
    
get_top_error_data(y_test,pred,n_tops=5)

가장 큰 상위 5위 오류 값은 546~586로 실제 값을 감안하면 예측 오류가 꽤 크다.

회귀에서 이렇게 큰 예측 오류가 발생할 경우 가장 먼저 살펴볼 것은: Target 값의 분포가 왜곡된 형태를 이루고 있는지 확인하는 것! Target 값의 분포는 정규 분포가 가장 좋고, 그렇지 않고 왜곡된 경우엔 회귀 예측 성능이 저하되는 경우가 발생하기 쉽다.

 

1. 판다스 DataFrame의 hist()를 사용해 자전거 대여 모델의 Target값인 count칼럼이 정규 분포를 이루는지 확인해 보겠다.

-> count 칼럼 값이 정규 분포가 아닌 0~200사이에 왜곡되어 있는것을 알 수 있다!

로그를 적용해 변환하면 정규분포 형태로 바꿀 수 있다. 넘파이의 log1p()를 이용해 변경하자.

변경된 Target값을 기반으로 학습하고, 예측한 값은 다시 expm1()함수를 이용해 원래 scale값으로 원상 복구하면 된다. log1p()를 적용한 'count'값의 분포를 확인하자.

로그로 Target 값을 변환하니 변환하기 전보다는 왜곡 정도가 많이 향상됐다. 이를 이용해 다시 학습한 후 평가를 수행해본다.

# 타깃 칼럼인 count 값을 log1p로 로그 변환
y_target_log = np.log1p(y_target)

# 로그 변환된 y_target_log를 반영하여 학습/테스트 데이터 셋 분할
X_train, X_test, y_train, y_test = train_test_split(X_features, y_target_log, test_size=0.3, random_state=0)
lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)
pred = lr_reg.predict(X_test)

# 테스트 데이터 셋의 Target 값은 Log 변환되었으므로 다시 expm1를 이용하여 원래 scale로 변환
y_test_exp = np.expm1(y_test)

# 예측 값 역시 Log 변환된 타깃 기반으로 학습되어 예측되었으므로 다시 exmpl으로 scale변환
pred_exp = np.expm1(pred)

evaluate_regr(y_test_exp ,pred_exp)

-> [Output] RMSLE: 1.017, RMSE: 162.594, MAE: 109.286

// RMSLE 오류는 감소했는데 RMSE 는 오히려 증가함...

 

2. 이번엔 개별 피처들의 인코딩을 적용해보자.

먼저 각 피처의 회귀 계숫값을 시각화해보기

year, hour, month, season, holiday, workingday 피처들의 회귀 계수 영향도가 상대적으로 높아졌다.

이들 피처들을 살펴보면 year는 2011 2012 값으로, month는 1, 2, ...12와 같이 숫자값 형태로 의미를 담고 있다.

year, hour, month 등은 숫자 값으로 표현되었지만 모두 category형 피처이다. (이런 숫자형 카테고리 값을 선형 회귀에 사용하면 회귀 계수 연산시 잘못된 영향 줄 수 있...)

 

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

판다스의 get_dummies() 이용해 카테고리형 칼럼들 모두 원-핫 인코딩하자

# '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

 

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

원-핫 인코딩된 데이터 세트에서 회귀 계수가 높은 피처를 다시 시각화하자. (현재 원-핫 인코딩으로 피처 늘어남) 회귀 계수 상위 20개 피처 추출해보자.

coef = pd.Series(lr_reg.coef_ , index=X_features_ohe.columns)
coef_sort = coef.sort_values(ascending=False)[:20]
sns.barplot(x=coef_sort.values , y=coef_sort.index)

-> 원-핫 인코딩 후 - 여전히 year 관련 피처들의 회귀계수 값이 가장 높지만 season관련, weather 관련 속성들의 회귀 계수값도 상대적으로 커졌다.

 

원-핫 인코딩 통해 피처들의 영향도가 달라지고, 모델의 성능도 향상되었다.

이렇게 선형 회귀의 경우 중요 카테고리성 피처들을 원-핫 인코딩으로 변환하는 것이성능에 중요한 영향을 미칠 수 있다.

 

 

이번엔 회귀 트리로 회귀 예측을 수행하겠다.

1. Target 값의 로그 변환된 값과 2. 원-핫 인코딩된 피처 데이터 세트 그대로 이용해

랜덤 포레스트, GBM, XGBoost, LightGBM을 순차적으로 성능 평가해본다. //XGBoost경우 DF의 values속성 이용해 ndarray로 변환

이 경우는 앞의 선형 회귀 모델보다 회귀 예측 성능이 개선되었다 (데이터 세트의 유형 따라 뭐가 더 나은 성능인지는 다름)