AI/머신러닝 - 예제

[머신러닝 - 예제] Bike 데이터셋 - 의사 결정 나무

caramel-bottle 2023. 12. 26.

1. bike 데이터셋

어떤 지역의 날씨 정보와 자전거 대여 현황 데이터셋이다.

bike.csv
2.42MB


2. 데이터 전처리

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

bike_df = pd.read_csv('/content/drive/MyDrive/KDT/머신러닝과 딥러닝/data/bike.csv')

2-1. df.info()

# info()
bike_df.info()

output>>

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 33379 entries, 0 to 33378
Data columns (total 16 columns):
 #   Column        Non-Null Count  Dtype  
---  ------        --------------  -----  
 0   datetime      33379 non-null  object 
 1   count         33379 non-null  int64  
 2   holiday       33379 non-null  int64  
 3   workingday    33379 non-null  int64  
 4   temp          33379 non-null  float64
 5   feels_like    33379 non-null  float64
 6   temp_min      33379 non-null  float64
 7   temp_max      33379 non-null  float64
 8   pressure      33379 non-null  int64  
 9   humidity      33379 non-null  int64  
 10  wind_speed    33379 non-null  float64
 11  wind_deg      33379 non-null  int64  
 12  rain_1h       6771 non-null   float64
 13  snow_1h       326 non-null    float64
 14  clouds_all    33379 non-null  int64  
 15  weather_main  33379 non-null  object 
dtypes: float64(7), int64(7), object(2)
memory usage: 4.1+ MB

 

데이터의 내용은 이러하다.

  • datetime: 날짜
  • count: 대여 개수
  • holiday: 휴일
  • workingday: 근무일
  • temp: 기온
  • feels_like: 체감온도
  • temp_min: 최저온도
  • temp_max: 최고온도
  • pressure: 기압
  • humidity: 습도
  • wind_speed: 풍속
  • wind_deg: 풍향
  • rain_1h: 1시간당 내리는 비의 양
  • snow_1h: 1시간당 내리는 눈의 양
  • clouds_all: 구름의 양
  • weather_main: 날씨

2-2. df.describe()

# describe()
bike_df.describe()

output>>


2-3. sns.displot()

# displot()
sns.displot(bike_df['count'])

output>>


2-4. sns.boxplot()

# boxplot()
sns.boxplot(y=bike_df['count'])

output>>


2-5. sns.scatterplot()

# scatterplot()
sns.scatterplot(x='feels_like', y='count', data=bike_df, alpha=0.3)

output>>

# scatterplot()
sns.scatterplot(x='pressure', y='count', data=bike_df, alpha=0.3)

output>>

# scatterplot()
sns.scatterplot(x='wind_speed', y='count', data=bike_df, alpha=0.3)

output>>

# scatterplot()
sns.scatterplot(x='wind_deg', y='count', data=bike_df, alpha=0.3)

output>>


2-6. 결측치

# isna()
bike_df.isna().sum()

output>>

datetime            0
count               0
holiday             0
workingday          0
temp                0
feels_like          0
temp_min            0
temp_max            0
pressure            0
humidity            0
wind_speed          0
wind_deg            0
rain_1h         26608
snow_1h         33053
clouds_all          0
weather_main        0
dtype: int64

 

위의 na는 비나 눈이 오지 않은 경우로 보이기 때문에 값을 0으로 대체해도 될 것 같다.

# fillna()
bike_df = bike_df.fillna(0)

 

# info()
bike_df.info()

output>>

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 33379 entries, 0 to 33378
Data columns (total 16 columns):
 #   Column        Non-Null Count  Dtype  
---  ------        --------------  -----  
 0   datetime      33379 non-null  object 
 1   count         33379 non-null  int64  
 2   holiday       33379 non-null  int64  
 3   workingday    33379 non-null  int64  
 4   temp          33379 non-null  float64
 5   feels_like    33379 non-null  float64
 6   temp_min      33379 non-null  float64
 7   temp_max      33379 non-null  float64
 8   pressure      33379 non-null  int64  
 9   humidity      33379 non-null  int64  
 10  wind_speed    33379 non-null  float64
 11  wind_deg      33379 non-null  int64  
 12  rain_1h       33379 non-null  float64
 13  snow_1h       33379 non-null  float64
 14  clouds_all    33379 non-null  int64  
 15  weather_main  33379 non-null  object 
dtypes: float64(7), int64(7), object(2)
memory usage: 4.1+ MB

2-7. datetime

datetime은 시점의 모든 정보를 담고 있다.

시점의 모든 정보는 불필요하고 머신러닝 과정에서도 사용이 힘들기 때문에 적절하게 나눌 필요가 있다.

# column 
bike_df['datetime']

output>>

0         2018-01-01 0:00
1         2018-01-01 1:00
2         2018-01-01 2:00
3         2018-01-01 3:00
4         2018-01-01 4:00
               ...       
33374    2021-08-31 19:00
33375    2021-08-31 20:00
33376    2021-08-31 21:00
33377    2021-08-31 22:00
33378    2021-08-31 23:00
Name: datetime, Length: 33379, dtype: object

 

문자열 형식의 날짜를 datetime 형식으로 변환하여 데이터 처리하기 좋게 만든다.

# to_datetime()
bike_df['datetime'] = pd.to_datetime(bike_df['datetime'])

output>>

0       2018-01-01 00:00:00
1       2018-01-01 01:00:00
2       2018-01-01 02:00:00
3       2018-01-01 03:00:00
4       2018-01-01 04:00:00
                ...        
33374   2021-08-31 19:00:00
33375   2021-08-31 20:00:00
33376   2021-08-31 21:00:00
33377   2021-08-31 22:00:00
33378   2021-08-31 23:00:00
Name: datetime, Length: 33379, dtype: datetime64[ns]
 

 

자전거 대여 수의 동향은 정확한 날짜보단 계절이 중요하고, 정확한 시간보단 아침, 점심, 저녁 등으로 나누는 것이 더 좋다.

위 datetime을 년도, 월, 시간으로 나눠보자.

# 년도, 월, 시간
bike_df['date'] = bike_df['datetime'].dt.date
bike_df['year'] = bike_df['datetime'].dt.year
bike_df['month'] = bike_df['datetime'].dt.month
bike_df['hour'] = bike_df['datetime'].dt.hour

bike_df.head()

output>>

 

날짜에 따른 대여수 동향을 lineplot()을 통해 확인.

 

plt.figure(figsize=(14, 4))
sns.lineplot(x='date', y='count', data=bike_df)
plt.xticks(rotation=45)
plt.show()

output>>

그래프의 반투명한 부분은 ci(Confidence Interval)으로 추정신뢰구간을 의미한다. 

errobar의 옵션

"ci": 신뢰 구간
"pi": 예측 구간
"se": 표준 오차
"sd": 표준 편차

 

ci=None 혹은 errorbar=None을 통해 지울 수도 있다.

 

2020-01 ~ 2020-07

특이한 선이 존재한다.

해당 구간에 데이터가 존재하지 않기 때문에 생긴 것으로, 해당 구간은 코로나의 영향으로 자전거 대여 정보가 없다고 생각된다.

 

정확한 정보를 위해 월별 평균 대여수를 확인해보자.


2-8. 월별 대여수

2019년도 월별 대여수:

# groupby()
bike_df[bike_df['year'] == 2019].groupby('month')['count'].mean()

output>>

month
1     193.368862
2     221.857718
3     326.564456
4     482.931694
5     438.027848
6     478.480053
7     472.745785
8     481.267366
9     500.862069
10    446.279070
11    307.295393
12    213.148886
Name: count, dtype: float64

 

2020년 월별 대여수:

# groupby()
bike_df[bike_df['year'] == 2020].groupby('month')['count'].mean()

output>>

month
1     260.445997
2     255.894320
3     217.135241
5     196.581064
6     290.900937
7     299.811688
8     331.528809
9     338.876478
10    293.640777
11    240.507324
12    138.993540
Name: count, dtype: float64

2019년도와 다르게 2020년도 4월에는 데이터가 아예 없고 근방의 데이터 수도 현저히 적은 것을 알 수 있다.


2-9. covid

테이터의 특성을 알았으니 구간을 나눠서 라벨링을 해보자.

2020년 4월 1일부터 2021년 4월 1일을 covid라고 하고 그 이전을 precovid, 이후를 postcovid로 하여 column을 추가해보자.

 

# COVID
# 2020-04-01 이전: precovid
# 2020-04-01 이후 ~ 2021-04-01 이전: covid
# 2021-04-01 이후: postcovid
def covid(x):
    if x['date'] <= dt.date(2020, 4, 1):
        return 'precovid'
    elif x['date'] < dt.date(2021, 4, 1):
        return 'covid'
    else:
        return 'postcovid'

bike_df['covid'] = bike_df.apply(covid, axis=1)
print(bike_df['covid'].unique())

output>>

['precovid' 'covid' 'postcovid']

 

위 covid() 함수의 경우 일회성인 경우가 많기 때문에 lambda를 사용해도 좋다.

bike_df['covid'] = bike_df['date'].apply(lambda date: 'precovid' if str(date) < '2020-04-01' else 'covid' if str(date) < '2021-04-01' else 'postcovid')
print(bike_df['covid'].unique())

output>>

['precovid' 'covid' 'postcovid']

 


2-10. season

month를 season으로 라벨링해보자.

자전거 대여는 월별보다는 계절별로 대여량이 다를 것이기 때문이다.

covid와 동일한 방법으로 범위를 지정하여 라벨링한다.

 

# month -> 계절
# season
# 12월 ~ 2월: winter
# 3월 ~ 5월: spring
# 6월 ~ 8월: summer
# 9월 ~ 11월: fall

bike_df['season'] = bike_df['month'].apply(lambda x: 'winter' if x == 12 else 'fall' if x >= 9 else 'summer' if x >= 6 else 'spring' if x >= 3 else 'winter')
bike_df[['month', 'season']]

output>>


2-11. time

시간대도 month와 마찬가지로 시간마다의 정보보다는 아침시간대, 저녁시간대 등으로 나누는 것이 더 효과적이다.

# day_night
# 21 이후 ~ : night
# 19 이후 ~ : late evening
# 17 이후 ~ : early evening

# 16 이후 ~ : late afternoon
# 13 이후 ~ : early afternoon

# 11 이후 ~ : late morning
# 5 이후 ~ : early morning


def day_night(time):
    if time >= 21:
        return 'night'
    elif time >= 19:
        return 'late evening'
    elif time >= 17:
        return 'early evening'
    elif time >= 16:
        return 'late afternoon'
    elif time >= 13:
        return 'early afternoon'
    elif time >= 11:
        return 'late morning'
    elif time >= 5:
        return 'early morning'
    else:
        return 'night'

bike_df['day_night'] = bike_df['hour'].apply(day_night)
print(bike_df['day_night'].unique())

output>>

['night' 'early morning' 'late morning' 'early afternoon' 'late afternoon'
 'early evening' 'late evening']

 

day_night()의 경우도 일회성 함수이기 때문에 lambda를 사용할 수 있다. 하지만 코드가 오히려 복잡하게 보일 수 있다.

bike_df['day_night'] = bike_df['hour'].apply(lambda x: 'night' if x >= 21
                                             else 'late evening' if x >= 19
                                             else 'early evening' if x>= 17
                                             else 'late afternoon' if x>= 16
                                             else 'early afternoon' if x>= 13
                                             else 'late morning' if x>= 11
                                             else 'early morning' if x>= 5
                                             else 'night')

lambda를 사용하면 메모리를 아낄 수 이지만 성능에는 큰 효과를 주지 못한다.

lambda는 보통 일회성 함수 대신 사용한다.

코드 길이를 줄이고 가독성을 높일 수 있는 상황에 사용하면 좋다.

 

이제 새로운 column인 season, day_night, covid가 생겼으니 불필요한 column은 제거하도록 한다.

# drop()
bike_df.drop(['datetime', 'month', 'date', 'hour'], axis=1, inplace=True)

 

# info()
bike_df.info()

output>>

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 33379 entries, 0 to 33378
Data columns (total 19 columns):
 #   Column        Non-Null Count  Dtype  
---  ------        --------------  -----  
 0   count         33379 non-null  int64  
 1   holiday       33379 non-null  int64  
 2   workingday    33379 non-null  int64  
 3   temp          33379 non-null  float64
 4   feels_like    33379 non-null  float64
 5   temp_min      33379 non-null  float64
 6   temp_max      33379 non-null  float64
 7   pressure      33379 non-null  int64  
 8   humidity      33379 non-null  int64  
 9   wind_speed    33379 non-null  float64
 10  wind_deg      33379 non-null  int64  
 11  rain_1h       33379 non-null  float64
 12  snow_1h       33379 non-null  float64
 13  clouds_all    33379 non-null  int64  
 14  weather_main  33379 non-null  object 
 15  year          33379 non-null  int64  
 16  covid         33379 non-null  object 
 17  season        33379 non-null  object 
 18  day_night     33379 non-null  object 
dtypes: float64(7), int64(8), object(4)
memory usage: 4.8+ MB

2-12. One Hot Encoding

열심히 만든 covid, season, day_night와 weather_main은 문자열 데이터이다.

머신러닝을 위해 숫자로 바꿔주는 작업이 필요하다.

for i in ['weather_main', 'covid', 'season', 'day_night']:
    print(i, bike_df[i].nunique())

output>>

weather_main 11
covid 3
season 4
day_night 7
 

이대로 One Hot Encoding을 하게 되면 새로운 column이 엄청 많아질 것이다.

차원이 매우 높아지는 경우가 아니면 크게 문제되진 않는다.

하지만 차원이 높아질수록 모델의 학습과 예측 속도에 영향을 주고, 차원의 저주 문제를 일으킬 수도 있으니 조심해야한다.

 

# get_dummies()
bike_df = pd.get_dummies(bike_df, columns=['weather_main', 'covid', 'season', 'day_night'])

2-13. train_test_split()

count를 제외한 나머지를 독립변수로 하고 count를 종속변수로 한다.

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(bike_df.drop('count', axis=1), bike_df['count'], test_size=0.2, random_state=2023)

3. 의사 결정 나무(Decision Tree)

2023.12.28 - [AI/머신러닝] - [머신러닝] 의사 결정 나무 (Decision Tree)

 

의사 결정 나무(Decision Tree)는 분류와 회귀에 사용되는 지도학습 방법이다.

 

데이터 특성에 따라 의사 결정 규칙을 학습하고 예측하는 모델을 만든다.

 

의사결정나무의 회귀모델은 연속형 타켓 변수를 예측하는 데 사용하고, 평균을 활용하여 예측값을 계산한다.

 

의사결정나무의 분류모델은 범주형 클래스를 예측하는 데 사용하고, 주로 다수결 투표를 통해 예측값을 결정한다.

3-1. DecisionTreeRegressor

from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error

dtr = DecisionTreeRegressor(random_state=2023) # Hyper Parameter: 모델을 조절해주는 값

dtr.fit(X_train, y_train)

pred1 = dtr.predict(X_test)

sns.scatterplot(x=y_test, y=pred1)

# 회귀 모델 성능 지표 RMSE
mean_squared_error(y_test, pred1, squared=False)

output>>

222.90547303762153

 

위의 산점도 그래프에서 점들의 분포가 대각선을 기준으로 대칭적이다. 이는 예측이 어느정도 일정하다는 의미이다.

 

모든 점이 대각선 위에 존재한다면 오차가 0인 예측이라고 할 수 있다. 하지만 위 그래프의 점들은 많이 퍼져있다.

 

의사 결정 나무는 하이퍼 파라미터에 따라 결과가 많이 달라질 수 있으니 적절한 하이퍼 파라미터를 찾아야한다.


4. 선형 회귀 VS 의사 결정 나무

의사 결정 나무와 선형 회귀의 성능을 비교해본다.

from sklearn.linear_model import LinearRegression

lr = LinearRegression()

lr.fit(X_train, y_train)

pred2 = lr.predict(X_test)

sns.scatterplot(x=y_test, y=pred2)

mean_squared_error(y_test, pred2, squared=False)

output>>

224.5257704711731
# 의사결정나무: 222.90547303762153  win
# 선형회귀: 224.5257704711731
222.90547303762153 - 224.5257704711731 = -1.6202974335515705

 

의사 결정 나무의 성능이 더 좋게 측정되었다. 하지만 모든 상황에서 이러한 결과가 나오지는 않는다.

선형 회귀 방식은 시계열 데이터인 경우에 유리하다.

결론: 때에 따라 적절한 방식을 사용해야 한다.


4-1. 하이퍼 파라미터(Hyperparameter)

하이퍼 파라미터는 모델을 훈련시키기 전에 설정되는 매개변수이다. 어떻게 설정하냐에 따라 알고리즘에 큰 영향을 줄 수 있다.

하이퍼 파라미터는 수동으로 설정되기 때문에 적절한 파라미터값을 제어하기 위해서는 반복적인 실험과 경험이 필요하다.

 

의사 결정 나무의 경우 깊이, 리프 샘플 수, 특성 수 등을 설정할 수 있다.

dtr = DecisionTreeRegressor(random_state=2023, max_depth=50, min_samples_leaf=30) # Hyper Parameter: 모델을 조절해주는 값

dtr.fit(X_train, y_train)

pred3 = dtr.predict(X_test)

sns.scatterplot(x=y_test, y=pred3)

mean_squared_error(y_test, pred3, squared=False)

output>>

186.56448037541028

 

하이퍼 파라미터를 적용하지 않은 것과 비교를 하면

# 산점도 그리기
sns.scatterplot(x=y_test, y=pred1, label='origin')
sns.scatterplot(x=y_test, y=pred3, label='hyperparameter')

# 라벨과 제목 추가
plt.xlabel('count')
plt.ylabel('Predicted Values')
plt.title('Comparison of Model Predictions')

# 범례 추가
plt.legend()

# 그래프 표시
plt.show()

output>>

# 의사결정나무: 222.90547303762153
# 의사결정나무(하이퍼 파라미터 튜닝): 186.56448037541028 win
186.56448037541028 - 222.90547303762153 = -36.34099266221125

5. plot_tree

plot_tree를 통해 의사 결정 나무 모델의 트리 구조를 볼 수 있다.

 

from sklearn.tree import plot_tree

plt.figure(figsize=(24, 12))
plot_tree(dtr, max_depth=5, fontsize=12, feature_names=X_train.columns)
plt.show()

output>>


끝내며

의사 결정 나무는 회귀, 분류에 사용된다.

선형, 시계열 데이터에는 선형 회귀가 더 유리하다.

그 외에는 의사 결정 나무가 더 유리한 것 같다.

 

댓글