[ECC DS 10주차] 회귀 1_자전거 대여 수요 예측
0. Introduction
- 해당 데이터 세트에는 2011-01부터 2012-12까지 날짜/시간, 기온, 습도, 풍속 등의 정보를 기반으로 1시간 간격 동안의
자전거 대여 횟수
가 기재되어 있음
📌 Data Description
-
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
: 대여 횟수 –> target 데이터
1. 데이터 클렌징 및 가공
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('/content/drive/MyDrive/Colab Notebooks/ECC 48기 데과B/10주차/data/bike_train.csv')
print(bike_df.shape)
bike_df.head(3)
(10886, 12)
datetime season holiday workingday weather temp atemp \ 0 2011-01-01 00:00:00 1 0 0 1 9.84 14.395 1 2011-01-01 01:00:00 1 0 0 1 9.02 13.635 2 2011-01-01 02:00:00 1 0 0 1 9.02 13.635 humidity windspeed casual registered count 0 81 0.0 3 13 16 1 80 0.0 8 32 40 2 80 0.0 5 27 32
### 데이터 정보 확인
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
-
Null 데이터는 없음
-
대부분의 칼럼이 int 또는 float 숫자형임
-
datetime
칼럼만 object형임-
datetime
칼럼의 경우 년-월-일-시:분:초 문자 형식으로 돼 있으므로 이에 대한 가공 필요 -
문자열을 datetime 형으로 변경
-
### 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 season holiday workingday weather temp atemp \ 0 2011-01-01 00:00:00 1 0 0 1 9.84 14.395 1 2011-01-01 01:00:00 1 0 0 1 9.02 13.635 2 2011-01-01 02:00:00 1 0 0 1 9.02 13.635 humidity windspeed casual registered count year month day hour 0 81 0.0 3 13 16 2011 1 1 0 1 80 0.0 8 32 40 2011 1 1 1 2 80 0.0 5 27 32 2011 1 1 2
-
새롭게
year
,month
,day
,hour
칼럼이 추가됨 -
기존의
datetime
칼럼 삭제 -
casual
+registered
=count
이므로casual
과registered
가 따로 필요하지는 x- 오히려 상관도가 높아 예측을 저해할 우려가 있으므로 해당 컬럼들을 삭제
drop_columns = ['datetime','casual','registered']
bike_df.drop(drop_columns, axis = 1,inplace = True)
2. 모델링
### 모델 성능 평가를 위한 함수 정의
from sklearn.metrics import mean_squared_error, mean_absolute_error
def rmsle(y, pred):
# log 값 변환 시 NaN등의 이슈로 log() 가 아닌 log1p() 를 이용하여 RMSLE 계산
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))
-
log1p()
의 경우는 1 + log() 값으로 log 변환값에 1을 더하므로 오버플로우/언더플로우 발생 문제를 해결해 줌 -
log1p()
로 변환된 값은 다시 넘파이의expm1()
함수로 쉽게 원래 스케일로 복원 가능
📌 회귀 모델 적용 전 확인 사항
-
데이터 세트가
정규 분포
를 따르는지 -
카테고리형 회귀 모델의 경우
원-핫 인코딩
으로 피처를 인코딩해야 함
2-1. 선형 회귀
a) 로그 변환
### 원본 데이터로 회귀 예측 수행
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 데이터 값인 대여 횟수(Count)를 감안하면 예측 오류로서는 비교적 큰 값임
### 상위 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)
real_count predicted_count diff 1618 890 322.0 568.0 3151 798 241.0 557.0 966 884 327.0 557.0 412 745 194.0 551.0 2817 856 310.0 546.0
-
큰 순서대로 상위 5위 오류 값은 546 ~ 568로 실제 값을 감안하면 예측 오류가 꽤 큼
-
회귀에서 예측 오류가 큰 경우 target 값의 분포가
왜곡된
형태를 이루고 있는지 확인-
target 값의 분포는
정규 분포
형태가 가장 좋음 -
왜곡된 경우에는 회귀 예측 성능이 저하되는 경우가 발생하기 쉬움
-
### target 값의 분포 확인
y_target.hist()
<Axes: >
<Figure size 640x480 with 1 Axes>
-
count 칼럼 값이 정규 분포가 아닌 0 ~ 200 사이에
왜곡
돼 있는 것을 알 수 있음 -
일반적으로
로그 변환
을 통해 왜곡된 값을 정규 분포 형태로 바꿈- 넘파이의
np.log1p()
이용
- 넘파이의
-
변경된 target 값을 기반으로 학습 후 예측한 값은 다시
expm1()
함수를 적용해 원래 scale 값으로 원상 복구하면 됨
### 로그 변환 후 target 변수의 분포 확인
y_log_transform = np.log1p(y_target)
y_log_transform.hist()
<Axes: >
<Figure size 640x480 with 1 Axes>
- 정규 분포 형태는 아니지만 변환 전보다는 왜곡 정도가 많이 향상됨
# 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 값은 로그 변환되었으므로 다시 expm1를 이용하여 원래 scale로 변환
y_test_exp = np.expm1(y_test)
# 예측값 역시 로그 변환된 target 기반으로 학습되어 예측되었으므로 다시 exmpl으로 scale변환
pred_exp = np.expm1(pred)
evaluate_regr(y_test_exp ,pred_exp)
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)
<Axes: >
<Figure size 640x480 with 1 Axes>
-
Year
피처의 회귀 계수 값이 독보적으로 큰 값을 가지고 있음 -
year 변수의 경우 2011, 2012로 되어 있음
- 연도에 대한 정보
b) 원-핫 인코딩(One-hot Encoding)
-
숫자형 카테고리 값을 선형 회귀에 사용할 경우 회귀 계수 연산 시 숫자형 값의
크기
에 영향을 크게 받는 경우가 발생할 수 있음- 선형 회귀에서는 피처 인코딩에
원-핫 인코딩
을 적용해 변환해야 함
- 선형 회귀에서는 피처 인코딩에
# 'year', month', 'day', hour'등의 피처들을 One Hot Encoding
X_features_ohe = pd.get_dummies(X_features, columns=['year', 'month','day', 'hour', 'holiday',
'workingday','season','weather'])
# 원-핫 인코딩이 적용된 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)
# 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.688, MAE: 63.382 ### Ridge ### RMSLE: 0.590, RMSE: 98.529, MAE: 63.893 ### Lasso ### RMSLE: 0.635, RMSE: 113.219, MAE: 72.803
원-핫 인코딩
적용 후 선형 회귀의 예측 성능이 많이 향상됨
### 피처 중요도를 다시 시각화
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)
<Axes: >
<Figure size 640x480 with 1 Axes>
-
month_9
,month_8
,month_7
등의 월 관련 피처들과workingday
관련 피처들, 그리고hour
관련 피처들의 회귀 계수가 높음을 확인할 수 있음- 상식선에서 자전거를 타는 데 필요한 피처의 회귀 계수가 높아짐
-
선형 회귀 수행 시에는 피처를 어떻게 인코딩하는가가 성능에 큰 영향을 미칠 수 있음
2-2. 회귀 트리 활용
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from xgboost import XGBRegressor
from lightgbm import LGBMRegressor
# 랜덤 포레스트, GBM, XGBoost, LightGBM model 별로 평가 수행
rf_reg = RandomForestRegressor(n_estimators = 500)
gbm_reg = GradientBoostingRegressor(n_estimators = 500)
xgb_reg = XGBRegressor(n_estimators = 500)
lgbm_reg = LGBMRegressor(n_estimators = 500)
for model in [rf_reg, gbm_reg, xgb_reg, lgbm_reg]:
# XGBoost의 경우 DataFrame이 입력될 경우 버전에 따라 오류 발생 가능
# 따라서 ndarray로 변환
get_model_predict(model, X_train.values, X_test.values,
y_train.values, y_test.values,is_expm1 = True)
### RandomForestRegressor ### RMSLE: 0.354, RMSE: 50.155, MAE: 31.126 ### GradientBoostingRegressor ### RMSLE: 0.330, RMSE: 53.324, MAE: 32.733 ### XGBRegressor ### RMSLE: 0.342, RMSE: 51.732, MAE: 31.251 ### LGBMRegressor ### RMSLE: 0.319, RMSE: 47.215, MAE: 29.029
- 앞의 선형 회귀 모델에 비해 회귀 예측 성능이 개선됨