자전거 수요 예측

 

문제

  • 자전거 대여 수요 예측
    • 자전거를 대여하는 Count 값을 예측하는 Regression 문제
  • 데이터

Bike Sharing Demand

답안 코드 보기 전 내가 먼저 접근해보기

  • 접근 목표
    • 선형 회귀, 트리 기반 회귀 성능 비교
      • pipeline을 통해 표준정규분포화, linearRegression 학습까지 동시에 진행
      • 점수 산출은 rmse로! 주의해야 할 점은 rmse로 할 때 cross_val_score는 neg를 꼭 붙여줘야함
    • 접근하면서 의문점
      • Null값 없음을 확인한 후 모든 컬럼을 standartscaler를 활용해 표준정규분포화 하려고 하는데, datetime의 데이터타입이 object라서 제대로 fit 함수가 안돌아감. 그렇다고 datetime을 버리자니 자전거 수요예측과 기간의 관련성이 있을것 같아서 버리기 아쉬움
        • DataFrame.sort_values()로 해결함. Series타입만 sort 가능한 줄 알았는데 그냥 데이터프레임에도 sort_values를 적용할 수 있었네

답안 코드 공부 후 피드백

  • datetime, timedate처럼 시간 관련 데이터는 굉장히 많이 나옴
    • 이를 판다스의 datetime 데이터 타입으로 변경해서 접근하면 좋을때가 많음
  • 카테고리형 데이터는 따로 전처리 해줘야함
    • 카테고리형 데이터는 값이 커지거나 했을 때 값이 커졌으니 주목해야한다 같은 개념이 아님(ex. 2012년, 2013년)
      • 판다스의 get_dummies()를 이용해서 원핫인코딩 처리를 해주면 선형 회귀 모델 성능 향상에 도움을 줌
import numpy as np
import pandas as pd
raw = pd.read_csv('./bike_train.csv')

print(raw.shape)
raw.head()
(10886, 12)
datetime season holiday workingday weather temp atemp humidity windspeed casual registered count
0 2011-01-01 00:00:00 1 0 0 1 9.84 14.395 81 0.0 3 13 16
1 2011-01-01 01:00:00 1 0 0 1 9.02 13.635 80 0.0 8 32 40
2 2011-01-01 02:00:00 1 0 0 1 9.02 13.635 80 0.0 5 27 32
3 2011-01-01 03:00:00 1 0 0 1 9.84 14.395 75 0.0 3 10 13
4 2011-01-01 04:00:00 1 0 0 1 9.84 14.395 75 0.0 0 1 1
raw.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
raw.describe()
season holiday workingday weather temp atemp humidity windspeed casual registered count
count 10886.000000 10886.000000 10886.000000 10886.000000 10886.00000 10886.000000 10886.000000 10886.000000 10886.000000 10886.000000 10886.000000
mean 2.506614 0.028569 0.680875 1.418427 20.23086 23.655084 61.886460 12.799395 36.021955 155.552177 191.574132
std 1.116174 0.166599 0.466159 0.633839 7.79159 8.474601 19.245033 8.164537 49.960477 151.039033 181.144454
min 1.000000 0.000000 0.000000 1.000000 0.82000 0.760000 0.000000 0.000000 0.000000 0.000000 1.000000
25% 2.000000 0.000000 0.000000 1.000000 13.94000 16.665000 47.000000 7.001500 4.000000 36.000000 42.000000
50% 3.000000 0.000000 1.000000 1.000000 20.50000 24.240000 62.000000 12.998000 17.000000 118.000000 145.000000
75% 4.000000 0.000000 1.000000 2.000000 26.24000 31.060000 77.000000 16.997900 49.000000 222.000000 284.000000
max 4.000000 1.000000 1.000000 4.000000 41.00000 45.455000 100.000000 56.996900 367.000000 886.000000 977.000000
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV, cross_val_score

# datetime object형 데이터 타입을 int형으로 변경하자
raw.sort_values(by='datetime', ascending=True)  # 시간순으로 안되어 있을 수 있으니 시간순으로 정렬
raw.reset_index(inplace=True)
raw.rename(columns={'index': 'time'}, inplace=True)
raw.drop(columns='datetime', inplace=True)

# make pipeline
pipes = Pipeline([('standard', StandardScaler()), ('linear', LinearRegression())])


# # make train, test data

features = raw.iloc[:, :-1]
target = raw.iloc[:, -1]

X_train, X_test, y_train, y_test = train_test_split(features, target)       # 처음에 stratify = target으로 설정했었는데 회귀니까 stratify 하면 안될듯. 이건 분류에 쓰이는 것임

# pipes.fit(X_train, y_train)
def get_rmse_score(estimator, train_features, test_features, train_targets, test_targets):
    scores = cross_val_score(estimator, train_features, train_targets, scoring='neg_mean_squared_error', cv=5)
    scores = np.sqrt(-1 * scores)
    print(f'cv=5를 통해 나온 rmse 값: {scores}')

    estimator.fit(train_features, train_targets)        # 위의 cross_val_score만 해주면 estimator가 fit이 완료된것으로 생각했는데 아니였음. 코드로 직접 진행해줘야함
    pred = estimator.predict(test_features)
    scores = mean_squared_error(test_targets, pred)
    scores = np.sqrt(scores)
    print(f'test 데이터를 통해 나온 rmse 값: {scores}')

    print(type(test_targets))
    df = pd.DataFrame(data = test_targets.values, columns=['real_count'])       # 그냥 Series 타입인 test_target을 data로 넣었더니 데이터가 안들어갔음. numpy-array 형태로 넣어줬더니 됨. Series에는 index 정보까지 있어서 그런건가..
    df['pred'] = pred
    df['diff'] = df['pred'] - df['real_count']
    print(f'실제값, 예측값, 차이{df}')
get_rmse_score(pipes, X_train, X_test, y_train, y_test)
cv=5를 통해 나온 rmse 값: [142.43089727 139.14456407 144.49860883 144.60685959 140.06405343]
test 데이터를 통해 나온 rmse 값: 140.89958867165467
<class 'pandas.core.series.Series'>
      real_count
0            244
1            239
2            229
3            467
4            335
...          ...
3261           5
3262          12
3263          74
3264          62
3265         172

[3266 rows x 1 columns]
실제값, 예측값, 차이      real_count        pred        diff
0            244  320.786375   76.786375
1            239  251.187369   12.187369
2            229  241.709713   12.709713
3            467  388.818040  -78.181960
4            335  291.194551  -43.805449
...          ...         ...         ...
3261           5   49.036665   44.036665
3262          12  120.713426  108.713426
3263          74  315.231513  241.231513
3264          62  181.927374  119.927374
3265         172  166.290213   -5.709787

[3266 rows x 3 columns]

데이터 클렌징 및 가공

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(3)
(10886, 12)
datetime season holiday workingday weather temp atemp humidity windspeed casual registered count
0 2011-01-01 00:00:00 1 0 0 1 9.84 14.395 81 0.0 3 13 16
1 2011-01-01 01:00:00 1 0 0 1 9.02 13.635 80 0.0 8 32 40
2 2011-01-01 02:00:00 1 0 0 1 9.02 13.635 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
# 문자열을 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 humidity windspeed casual registered count year month day hour
0 2011-01-01 00:00:00 1 0 0 1 9.84 14.395 81 0.0 3 13 16 2011 1 1 0
1 2011-01-01 01:00:00 1 0 0 1 9.02 13.635 80 0.0 8 32 40 2011 1 1 1
2 2011-01-01 02:00:00 1 0 0 1 9.02 13.635 80 0.0 5 27 32 2011 1 1 2
drop_columns = ['datetime','casual','registered']
bike_df.drop(drop_columns, axis=1,inplace=True)
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))

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

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
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
y_target.hist()
<AxesSubplot:>

y_log_transform = np.log1p(y_target)
y_log_transform.hist()
<AxesSubplot:>

# 타겟 컬럼인 count 값을 log1p 로 Log 변환
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)

RMSLE: 1.017, RMSE: 162.594, MAE: 109.286
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)
<AxesSubplot:>

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


X_features
season holiday workingday weather temp atemp humidity windspeed year month day hour
0 1 0 0 1 9.84 14.395 81 0.0000 2011 1 1 0
1 1 0 0 1 9.02 13.635 80 0.0000 2011 1 1 1
2 1 0 0 1 9.02 13.635 80 0.0000 2011 1 1 2
3 1 0 0 1 9.84 14.395 75 0.0000 2011 1 1 3
4 1 0 0 1 9.84 14.395 75 0.0000 2011 1 1 4
... ... ... ... ... ... ... ... ... ... ... ... ...
10881 4 0 1 1 15.58 19.695 50 26.0027 2012 12 19 19
10882 4 0 1 1 14.76 17.425 57 15.0013 2012 12 19 20
10883 4 0 1 1 13.94 15.910 61 15.0013 2012 12 19 21
10884 4 0 1 1 13.94 17.425 61 6.0032 2012 12 19 22
10885 4 0 1 1 13.12 16.665 66 8.9981 2012 12 19 23

10886 rows × 12 columns

X_features_ohe
temp atemp humidity windspeed year_2011 year_2012 month_1 month_2 month_3 month_4 ... workingday_0 workingday_1 season_1 season_2 season_3 season_4 weather_1 weather_2 weather_3 weather_4
0 9.84 14.395 81 0.0000 1 0 1 0 0 0 ... 1 0 1 0 0 0 1 0 0 0
1 9.02 13.635 80 0.0000 1 0 1 0 0 0 ... 1 0 1 0 0 0 1 0 0 0
2 9.02 13.635 80 0.0000 1 0 1 0 0 0 ... 1 0 1 0 0 0 1 0 0 0
3 9.84 14.395 75 0.0000 1 0 1 0 0 0 ... 1 0 1 0 0 0 1 0 0 0
4 9.84 14.395 75 0.0000 1 0 1 0 0 0 ... 1 0 1 0 0 0 1 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
10881 15.58 19.695 50 26.0027 0 1 0 0 0 0 ... 0 1 0 0 0 1 1 0 0 0
10882 14.76 17.425 57 15.0013 0 1 0 0 0 0 ... 0 1 0 0 0 1 1 0 0 0
10883 13.94 15.910 61 15.0013 0 1 0 0 0 0 ... 0 1 0 0 0 1 1 0 0 0
10884 13.94 17.425 61 6.0032 0 1 0 0 0 0 ... 0 1 0 0 0 1 1 0 0 0
10885 13.12 16.665 66 8.9981 0 1 0 0 0 0 ... 0 1 0 0 0 1 1 0 0 0

10886 rows × 73 columns

# 원-핫 인코딩이 적용된 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)

print(X_train.shape)
# 모델과 학습/테스트 데이터 셋을 입력하면 성능 평가 수치를 반환
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)

(7620, 73)
### LinearRegression ###
RMSLE: 0.590, RMSE: 97.687, MAE: 63.381
### 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)

<AxesSubplot:>

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.183, MAE: 31.053
### GradientBoostingRegressor ###
RMSLE: 0.330, RMSE: 53.329, MAE: 32.740
### XGBRegressor ###
RMSLE: 0.342, RMSE: 51.732, MAE: 31.251
### LGBMRegressor ###
RMSLE: 0.319, RMSE: 47.215, MAE: 29.029

의문점

  • feature 데이터들에 대해 정규화 작업을 해주지 않았는데 회귀 계수값을 시각화해서 어떤 feature가 중요한가를 가리는게 의미가 있는건가?