굴러가는 분석가의 일상

[Kaggle] Bike Share Demand 본문

Data Science/구현

[Kaggle] Bike Share Demand

G3LU 2023. 10. 19. 11:49

*본 커널은 아래의 URL을 통해 참조하였습니다. 참고 부탁드립니다. 

https://www.kaggle.com/code/viveksrinivasan/eda-ensemble-model-top-10-percentile

 

1. 프로젝트 이해하기 

 

캐글에서 소개한 Bike Share Demand는 Washington DC의 자전거 대여 수요에 대해 Regression으로 예측하는 것 입니다. 

 

2. 데이터 이해하기 

 

datetime - hourly date + timestamp  
season -  1 = spring, 2 = summer, 3 = fall, 4 = winter 
holiday - whether the day is considered a holiday
workingday - whether the day is neither a weekend nor holiday
weather - 1: Clear, Few clouds, Partly cloudy, Partly cloudy
                 2: Mist + Cloudy, Mist + Broken clouds, Mist + Few clouds, Mist
                 3: Light Snow, Light Rain + Thunderstorm + Scattered clouds, Light Rain + Scattered clouds
                 4: Heavy Rain + Ice Pallets + Thunderstorm + Mist, Snow + Fog 
temp - temperature in Celsius
atemp - "feels like" temperature in Celsius
humidity - relative humidity
windspeed - wind speed
casual - number of non-registered user rentals initiated
registered - number of registered user rentals initiated
count - number of total rentals

 

3.  프로젝트 프로세스 

 

i) 데이터 형태 파악 

ii) EDA / Feature Engineering

iii) 모델링

 

 

4. 데이터 형태 파악

  • 데이터 및 Library Import 함수를 통해 불러오기 
  • 데이터의 전반적인 모양 및 형태 파악 

4.1 데이터 및 Library Import 함수를 통해 불러오기 

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

pd.set_option('display.max_rows',100)
pd.set_option('display.max_columns',100)
train = pd.read_csv('train.csv')
test  = pd.read_csv('test.csv')

train.head(20)

 

4.2 데이터의 전반적인 모양 및 형태 파악

"TRAIN 데이터의 형태"
"TEST 데이터의 형태"

  • 여기에서 주목해야할 부분은 Train 및 Test 데이터의 Column에 차이가 있습니다. Train 데이터에는 "Casual"," Registered", "Count" 라는 변수가 있지만 Test 데이터는 없으므로, 예측해야하는 변수는 "Count"임을 알 수 있습니다. 
  • 또한, 모든 변수 또는 특성이 숫자형으로 구성 되어있므로,  전형적인 회귀분석의 Example 입니다. 
# Train dataset의 크기 => (10886, 15)
train.shape

 

5. 데이터 EDA 진행 

  • 현재 datetime의 형태는 yyyy-mm-dd 00:00:00 입니다. 이를 날짜 및 시간별로 나눠서 자전거 수요량이 어떻게 변화하는지 파악
  • 여기서 날짜의 Month 및 Weekday 추출할 것이고, 시간은 Hour만 추출
    • calendar.day_name[datetime.datetime.strptime()].weekday() 사용하게 된다면; 1 → Saturday 같은 형식으로 변환! 
    • 또한, map() 함수를 통해 숫자형을 범주형으로 변환
    • 필요가 없어진 Datetime 피쳐를 삭제
import calendar, datetime

train['date'] = train['datetime'].apply(lambda x: x.split()[0])
train['hour'] = train['datetime'].apply(lambda x: x.split()[1].split(":")[0])
train['weekday'] = train['date'].apply(lambda x: calendar.day_name[datetime.datetime.strptime(x,'%Y-%m-%d').weekday()])
train["month"] = train['date'].apply(lambda x : calendar.month_name[datetime.datetime.strptime(x,"%Y-%m-%d").month])
train['season'] = train['season'].map({1: "Spring",
                                       2: "Summer",
                                       3: "Fall",
                                       4: "Winter"})
train['weather'] = train['weather'].map({1: " Clear + Few clouds + Partly cloudy + Partly cloudy",
                                         2 : " Mist + Cloudy, Mist + Broken clouds, Mist + Few clouds, Mist ",
                                         3 : " Light Snow, Light Rain + Thunderstorm + Scattered clouds, Light Rain + Scattered clouds",
                                         4 :" Heavy Rain + Ice Pallets + Thunderstorm + Mist, Snow + Fog " })
#필요가 없어진 "datetime" 을 삭제해줍니다!
train.drop('datetime',axis = 1, inplace = True)
#Category 변수로 변환
var_to_categorical = ['hour','weekday','month','season','holiday','workingday','weather']
for var in var_to_categorical:
    train[var] = train[var].astype('category')
#Train 데이터의 타입 파악
dataTypeDf = pd.DataFrame(train.dtypes.astype(str).value_counts()).reset_index().rename(columns={"index":"variableType",0:"count"})
fig,ax = plt.subplots()
fig.set_size_inches(12,5)
sns.barplot(data=dataTypeDf,x="variableType",y="count",ax=ax)
ax.set(xlabel='variableTypeadriable Type', ylabel='Count',title="Variables DataType Count")

  •  

 

5.1 피처별 데 자전거 수요량 확인 (시각화) 

 

- Categorical Feature 

# Category 변수 시각화
fig, axes = plt.subplots(2,2, figsize = (12,10))
sns.boxplot(data = train, y = 'count', orient = 'v',ax = axes[0][0])
sns.boxplot(data = train, x = 'season', y = 'count', ax = axes[0][1])
sns.boxplot(data = train, x = 'hour', y = 'count', ax = axes[1][0])
sns.boxplot(data = train, x = 'workingday', y = 'count', ax = axes[1][1])

- Numerical Feature 

  • atemp 및 temp 같은 경우 서로 Strong Correlation 이기에 모델링 시 삭제 필요 → Multicollinearity 문제 야기 
  • Windspeed는 Count 변수에게 큰 영향을 주지 않음 
  • Humidity와 atemp 영향 줄 가능성 있음 
  • "humidity","windspeed" 및 "temp" 변수에 Regplot 적용 → Windspeed "0"의 값이 무수히 많음 (Feature Engineering 작업 필요) 
corrMatt = train[["temp","atemp","casual","registered","humidity","windspeed","count"]].corr()
mask = np.array(corrMatt)
mask[np.tril_indices_from(mask)] = False
fig,ax= plt.subplots()
fig.set_size_inches(20,10)
sns.heatmap(corrMatt, mask=mask,vmax=.8, square=True,annot=True)

- Dependent Variable (count 분포 확인) 

  • Dependent Variable의 분포는 Skew to the Right 아래와 같이 확인할 수 있습니다. 하지만 머신러닝에 적용하기 위해서는 dependent variable 이상적인 분포는 Normal Distribution 입니다. 이에 log변환을 통해 정규분포와 비스무리하게 변환시켜줍니다.
fig, axes = plt.subplots(2,2,figsize = (12,10))
sns.distplot(train['count'],ax = axes[0][0])
stats.probplot(train['count'], dist = 'norm', fit = True, plot = axes[0][1])
sns.distplot(np.log(train['count']), ax = axes[1][0])
stats.probplot(np.log(train['count']), dist = 'norm', fit = True, plot = axes[1][1])

각 Hour 평균과 Month, Season, Weekday 변수가 Count에 미치는 영향

  • June, July, August (여름)에 자전거를 사용하는 수요가 가장 많은걸 파악 가능 
  • 평일에는 출/퇴근 시간 (7:00AM ~ 09:00AM / 17:00PM ~ 19:00PM) 수요가 가장 높고, 주말에는 오후 (13:00PM~17:00PM)에 가장 많음.
  • 사계절내내 17:00PM~18:00PM 시간대에 수요가 가장 높음 
fig, axes = plt.subplots(3,1,figsize=(15,10))
monthOrder = ["January","February","March","April","May","June","July","August","September","October","November","December"]
DayOrder   = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]

#month
agg_month = pd.DataFrame(train.groupby("month")["count"].mean()).reset_index()
sort_month = agg_month.sort_values(by = 'count', ascending= False)
sns.barplot(data = sort_month, x = 'month', y = 'count', ax = axes[0], order =monthOrder)
axes[0].set(xlabel = ' ',title = 'Average Count By Month')

#season
agg_hour = pd.DataFrame(train.groupby(['hour','season'], sort = True)['count'].mean()).reset_index()
sns.pointplot(x = agg_hour['hour'], y = agg_hour['count'], hue = agg_hour['season'], ax = axes[1])
axes[1].set(xlabel = ' ',title = 'Average Users Count by Hour & Season')

#weekday
agg_weekday = pd.DataFrame(train.groupby(['hour','weekday'], sort = True)['count'].mean()).reset_index()
sns.pointplot(x=agg_weekday["hour"], y=agg_weekday["count"],hue=agg_weekday["weekday"],hue_order=DayOrder, data=agg_weekday, join=True,ax=axes[2])
axes[2].set(xlabel = ' ', title = 'Average Users Count by Hour & weekday')

6. Feature Engineering

  • Windspeed의 regplot 통해 "0" 이라는 값이 무수히 많은걸 파악했습니다. 개인적인 생각으로는 데이터가 수집되어 있지 않아 임의 값 "0"으로 Replaced 된거 같습니다. 이에 RandomForestRegressor 대처하고자 합니다. 
  • correlation에서 큰 변화를 기대했던 저는.... 0.01 밖에 안오른거 보고.... 놀랐습니다... 굳이 안해도 되는 작업이었네요...
#판다스의 concat 함수를 통해 train 및 test 데이터를 합쳐주겠습니다
TrainData = pd.read_csv('train.csv')
TestData  = pd.read_csv('test.csv')

CombinedData = pd.concat([TrainData,TestData], axis = 0).reset_index()
CombinedData.drop('index',axis = 1, inplace = True)
#이전에 진행하였던 것과 동일하게 합쳐진 데이터에 적용시켜줍니다. 
CombinedData['date'] = CombinedData['datetime'].apply(lambda x: x.split()[0])
CombinedData['hour'] = CombinedData['datetime'].apply(lambda x: x.split()[1].split(":")[0]).astype('int')
CombinedData['weekday'] = CombinedData['date'].apply(lambda x: datetime.datetime.strptime(x,"%Y-%m-%d").weekday())
CombinedData['month'] = CombinedData['date'].apply(lambda x: datetime.datetime.strptime(x,"%Y-%m-%d").month)
CombinedData["year"] = CombinedData.datetime.apply(lambda x : x.split()[0].split("-")[0]).astype('int')
# "0"이 포함되어 있는 Windspeed 변수에 랜덤포레스트 Regressor 적용하여, 값을 바꿔주겠습니다. 
from sklearn.ensemble import RandomForestRegressor

dataWind0 = CombinedData[CombinedData["windspeed"]==0]
dataWindNot0 = CombinedData[CombinedData["windspeed"]!=0]
rfModel_wind = RandomForestRegressor()
windColumns = ["season","weather","humidity","month","temp","year","atemp","hour"]
rfModel_wind.fit(dataWindNot0[windColumns], dataWindNot0["windspeed"])

wind0Values = rfModel_wind.predict(X= dataWind0[windColumns])
dataWind0["windspeed"] = wind0Values

data = dataWindNot0.append(dataWind0)
data.reset_index(inplace=True)
data.drop('index',inplace=True,axis=1)
corrMatt = data[["temp","atemp","casual","registered","humidity","windspeed","count"]].corr()
mask = np.array(corrMatt)
mask[np.tril_indices_from(mask)] = False
fig,ax= plt.subplots()
fig.set_size_inches(20,10)
sns.heatmap(corrMatt, mask=mask,vmax=.8, square=True,annot=True)

7. Modeling  

#범주형 데이터로 변환 시켜줍니다! 
categoricalFeature = ["season","holiday","workingday","weather","weekday","month","year","hour"]

for var in categoricalFeature:
    data[var] = data[var].astype('category')
train_df = data[pd.notnull(data['count'])].sort_values(by=['datetime'])
test_df = data[~pd.notnull(data['count'])].sort_values(by=['datetime'])
datetimecol = test_df["datetime"]
yLabels = train_df["count"]
yLablesRegistered = train_df["registered"]
yLablesCasual = train_df["casual"]
dropFeature = ['casual','count','datetime','date','registered']
train_df  = train_df.drop(dropFeature,axis=1)
test_df  = test_df.drop(dropFeature,axis=1)

RMSLE 값은 0에 가까우면 가까울수록 좋습니다!!! 

from sklearn.linear_model import LinearRegression
from sklearn.model_selection import GridSearchCV
from sklearn import metrics

lr = LinearRegression()
yLabels_log = np.log(yLabels)
lr.fit(train_df, yLabels_log)
prediction = lr.predict(train_df)
print("RMSLE Value for Linear Regression :", np.sqrt(metrics.mean_squared_log_error(np.exp(yLabels_log),np.exp(prediction))))

----- 결과 ------ 
#RMSLE Value for Linear Regression : 1.017857808335659
from sklearn.ensemble import BaggingRegressor, GradientBoostingRegressor,RandomForestRegressor
from sklearn.svm import SVR
from sklearn.neighbors import KNeighborsRegressor

models = [RandomForestRegressor(), BaggingRegressor(), GradientBoostingRegressor(),SVR(),KNeighborsRegressor()]
model_name = ['RandomForestRegressor','BaggingRegressor','GradientBoostingRegressor','SVR','KNeighborsRegressor']
rmsle = []
d = {}
for model in range(len(models)):
    clf = models[model]
    clf.fit(train_df, yLabels_log)
    test_pred = clf.predict(train_df)
    rmsle.append(np.sqrt(metrics.mean_squared_log_error(np.exp(yLabels_log),np.exp(test_pred))))
    result={'Modelling Algo':model_name,'RMSLE':rmsle}

result_df = pd.DataFrame(result)
result_df

두둥탁..... 1.017857에서 0.110024까지 줄어들었네요!!! 

 

근 길 읽어주셔서 감사드립니다.

궁금하신 사항이 있으시다면, 댓글 부탁드립니다!!!