#!/usr/bin/env python3
Use of expert knowledge
여행사를 위해 항공료를 예측해야 한다고 가정하면 휴가 성수기나 공휴일 근처에서는 항공료 가격이 올라감.
크리스마스같은 날짜가 고정되어 있는 기념일은 학습이 가능하지만 음력 날짜 같은 경우에는 그레고리안 달력으로 표현하기 힘듬. 그러나 이런 공휴일의 비행스케쥴이 기록된 특성을 추가하는 일은 어렵지 않음
자전거 대여소의 예제 데이터를 이용하여 집 앞에 있는 자전거를 사람들이 얼마나 대여할 것인지를 에측하기
뉴욕에서 시티바이크는 회원 가입 시스템과 함께 자전거 대여소를 운영,
자전거 대여 데이터는 익명으로 공개되어 있고 여러방식으로 분석되어 옴
우리가 풀려는 문제는 특정 날짜와 시간에 집 앞에 있는 자전거를 사람들이 얼마나 대여할 것인지를 예측하여
빌릴 자전거가 남아 있는지 알려고 함
1. citibike 데이터의 형태와 시각화
### library import
from mglearn.datasets import load_citibike
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
### matplotlib 설정
matplotlib.rc('font', family='AppleGothic')
plt.rcParams['axes.unicode_minus'] = False
### datasets
citibike = load_citibike()
print('citibike.head() \n{}'.format(citibike.head()))
citibike.head()
>> index는 datetime형태로 이루어져 있으며 형식은 %Y-%m-%d 00:00:00이며
Freq: 3H ==> 3시간 주기로 데이터가 기록되었으며
data의 열이름은 'starttime', 'one' 이며 one에는 대여된 자전거의 횟수가 기록됨을 알 수 있음
2. 시각화를 위한 전처리
print('citibike.index.min() \n{}'.format(citibike.index.min()))
print('citibike.index.max() \n{}'.format(citibike.index.max()))
xticks = pd.date_range(start=citibike.index.min(), end=citibike.index.max(), freq='D')
print('xticks \n{}'.format(xticks))
# 그래프의 축을 만들기 위해 freq='D'로 설정하여 일별데이터로 수정
citibike의 index전처리
# strftime('%w'): datatime에서 일요일 ~ 토요일을 인덱스 0~6까지 부여
# strftime(' %m-%d'): datatime에서 '월', '일' 뽑아내기
print('''xticks.strftime(%w) \n{}'''.format(xticks.strftime('%w')))
print('''xticks.strftime( '%m-%d') \n{}'''.format(xticks.strftime(' %m-%d')))
x축 데이터를 위한 전처리
week = ['sun', 'mon', 'tue', 'wed', 'thr', 'fri', 'sat']
# xticks.strftime('%w')의 결과는 str형태이기 때문에 int()를 이용하여 숫자로 변환
xticks_name = [week[int(w)] + d for w, d in zip(xticks.strftime('%w'), xticks.strftime(' %m-%d'))] # list 축약
print('xticks_name \n{}'.format(xticks_name))
x축 데이터를 위한 xticks_name
3. citibike의 시각화
plt.xticks(xticks, xticks_name,rotation=90, ha='left') # ha: horizon align; 수평정렬
plt.plot(citibike, lw=1, c='darkblue') # lw = linewidth, c = colors
plt.xlabel('date')
plt.ylabel('count')
plt.show()
한 시티바이크 대여소에서 한달간 자전거가 대여된 횟수
>> 데이터를 보면 24시간 간격으로 낮과 밤을 확실히 구분 가능, 주중과 주말의 패턴도 구분이 가능
시계열 데이터를 이용한 예측 작업은 과거 데이터에서 학습하여 미래를 예측하는 방식을 사용
즉 데이터를 train set와 test set로 나눌 때 어떤 날짜까지의 모든 데이터를 train data로 하고, 날짜 이후의 모든 데이터를 test set로 사용
23일과 나머지 8일을 train set 와 data set로 사용
4. citibike 데이터의 기계학습과 시각화
pandas의 DatetimeIndex는 나노초이기 때문에 10**9으로 나눠줘야함
이 작업에서 우리가 사용할 특성은 일정 횟수의 대여가 일어난 날짜와 시간뿐
즉 입력 특성은 '2015-08-01 00:00:00'같은 날짜이고 출력은 대여횟수
y = citibike.values # target
x = citibike.index.astype('int64').values.reshape(-1, 1) // 10**9 # datetime ==> int64 ==> 값만 가지고오기 ==> 1열의 데이터로 ==> scale조정
### library import
from sklearn.ensemble import RandomForestRegressor
### train 갯수
n_train = 8 * 23
### data 분할
x_train, x_test, y_train, y_test = \
x[:n_train], x[n_train:], y[:n_train], y[n_train:]
### 모델 적용
rfr = RandomForestRegressor(n_estimators=100, random_state=0)
rfr.fit(x_train, y_train)
y_pred = rfr.predict(x_test)
y_pred_train = rfr.predict(x_train)
### 유효성 평가
print('R-Square \n{:.3f}'.format(rfr.score(x_test, y_test))) # -0.035
### visualization
plt.xticks(range(0, len(x), 8), xticks_name, rotation=90, ha='left') # ha = holizon align
plt.plot(range(n_train), y_train, label='train')
plt.plot(range(n_train, len(y_test) + n_train), y_test, ls=':', label='test') # plt.plot(x, y, linestyles, label)
plt.plot(range(n_train), y_pred_train, ls='--', label='train test') # ls = linestyles
plt.plot(range(n_train, len(y_test) + n_train), y_pred, ls='--', label='test pred')
plt.legend(loc=2)
plt.xlabel('date')
plt.ylabel('count')
plt.show()
POSIX 시간만 사용하여 만든 랜덤 포레스트의 예측
>> 테스트 세트에 대해선 한 가지 값으로만 예측
R-Square 값은 -0.035로 아무것도 학습되지 않음
테스트 세트에 있는 POSIX시간 특성의 값은 훈련 세트에 있는 모든 데이터보다 뒤의 시간
트리 모델인 랜덤포레스트는 훈련 세트에 있는 특성의 범위 밖으로 extrapolation외삽할 수 있는 능력이 없음
데이터 그래프를 보면 시간과 요일이라는 두 요소가 중요한 것으로 판단 <== 데이터 분석가의 판단
POSIX 시간으로 아무것도 학습되지 않으므로 이 특성은 제외
이 로직을 계속 사용할 것이기 때문에 함수로 만듬
# n_train = 8 * 23
# model = RandomForestRegressor(n_estimators=100, random_state=0)
def eval_feature(features, target, model):
# data 분할
x_train, x_test, y_train, y_test = \
features[:n_train], features[n_train:], target[:n_train], target[n_train:]
# model 적용
model.fit(x_train, y_train)
y_pred = model.predict(x_test)
y_pred_train = model.predict(x_train)
# 유효성 평가
print('R-Square \n{:.3f}'.format(model.score(x_test, y_test)))
# visualization
plt.xticks(range(0, len(x), 8), xticks_name, rotation=90, ha='left')
plt.plot(range(n_train), y_train, label='train')
plt.plot(range(n_train, len(y_test) + n_train), y_test, ls=':', label='test')
plt.plot(range(n_train), y_pred_train, ls='--', label='train test')
plt.plot(range(n_train, len(y_test) + n_train), y_pred, ls='--', label='test pred')
plt.legend(loc=2)
plt.xlabel('date')
plt.ylabel('count')
plt.show()
5. 데이터의 x축 중 시간 요소만 출력하여 분석
# x = citibike.index.astype('int64').values.reshape(-1, 1) // 10**9
x_hours = citibike.index.hour.values.reshape(-1, 1) # 시간만 출력
eval_feature(x_hours, y, model) # 사용자 정의 함수
시간만 사용하여 만든 랜덤 포레스트의 예측// R-square = 0.600
>> R-square가 나아졋지만 일별 패턴은 예측하지 못함
6. 시간요소에 요일정보를 더해서 분석
# print(citibike.index.dayofweek) : 일요일부터 토요일까지를 0~6까지로 인덱싱
### 시간데이터에 요일 정보를 추가
x_hours_week = np.hstack([citibike.index.dayofweek.values.reshape(-1, 1),
citibike.index.hour.values.reshape(-1 ,1)])
eval_feature(x_hours_week, y, model)
시간과 요일 특성을 사용해 만든 랜덤 포레스트의 예측//R-square = 0.842
>> 이제 모델은 하루의 시간과 요일에 따른 주기적인 패턴을 따르고 있음. R-square는 0.842로 좋은 예측을 나타냄
이 모델이 학습한 것은 8월 23일까지 요일별, 시간별 평균 대여 횟수
랜덤 포레스트 회귀로 만든 예측은 여러 트리가 예측한 값들의 평균이므로
그래서 8월 24일 월요일의 예측값과 8월 31일의 예측값은 동일
이런 작업에서 랜덤 포레스트 같이 복잡한 모델이 필요한 것은 아니므로 더 간단한 모델인 LinearRegression을 적용해보면
7. 시간요소에 요일정보를 더해서 LinearRegression을 적용
LinearRegression은 이전 포스팅 참조: [1]Linear Regression -- intro, [2]LinearRegression ,[3]Ridge
# library import
from sklearn.linear_model import LinearRegression, Ridge
eval_feature(x_hours_week, y, LinearRegression())
시간과 요일 특성을 사용하여 만든 선형 회귀의 예측, R-square=0.132
>> LinearRegression성능은 훨씬 나쁘고 주기적인 패턴도 다르게 나타남
그 이유는 요일과 시간이 정수로 인코딩 되어있어서 연속형 변수로 해석되기 때문임. 선형모델은 시간을 선형 함수로 함수로만 학습할 수 있어서, 시간이 흐를수록 대여 수가 늘어나게 학습됨
이 패턴을 잡아내기 위해 연속형 변수를 범주화하기 위해 OneHotEncoder를 사용
8. OneHotEncoder를 적용한 Ridge분석
# library import
from sklearn.preprocessing import OneHotEncoder
# encoder load
encoder = OneHotEncoder()
encoder.fit(x_hours_week)
x_hours_week_encoder = encoder.transform(x_hours_week).toarray() # ndarray로 전환
eval_feature(x_hours_week_encoder, y, Ridge())
시간과 요일의 원-핫 인코딩을 사용해 만든 선형 회귀의 예측, R-Square = 0.619
>> 연속형 특성일 때보다 훨씬 좋아짐
이 선형 모델은 요일에 대해 하나의 계수를 학습하고 시간에 대해서도 하나의 계수를 학습
이 말은 시간 패턴이 모든 모든 날에 걸쳐 공유된다는 뜻임
9. 상호작용 특성을 사용하여 Ridge분석
상호작용 특성을 사용하면 시간과 요일의 조합별 계수를 학습할 수 있음
상호작용 특성은 이전 포스트 참조: [1]polynomial analysis with boston house price
# library import
from sklearn.preprocessing import PolynomialFeatures
poly_transformer = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False) # include_bias=왼쪽에 절편항 추가
poly_transformer.fit(x_hours_week_encoder) # 인코더 학습
x_hours_week_encoder_poly = poly_transformer.transform(x_hours_week_encoder) # 인코더를 데이터에 적용
ridge = Ridge()
eval_feature(x_hours_week_encoder_poly, y, ridge)
시간과 요일의 곱을 특성으로 사용해서 만든 선형 회귀의 예측, R-Square = 0.845
>> 이런 특성변환을 통해 모델의 성능이 랜덤 포레스트와 거의 비슷해짐
이 모델의 가장 큰 장점은 무엇이 학습되었는지가 명확하다는 것
각 날짜와 시간에 대해 하나의 계수를 학습
랜덤 포레스트와는 달리 선형 모델은 모델이 학습한 계수를 그래프로 나타낼 수 있음
hour = ['{:02d}:00'.format(i) for i in range(0, 24, 3)]
day = ['mon', 'tue', 'wed', 'thr', 'fri','sat', 'sun']
features = day + hour
### get_featrues_names 메소드를 통해 PolynomialFeatures로 추출한 상호작용 특성에 이름을 달아주고 계수가 0이 아닌 특성만 선형
features_poly = poly_transformer.get_feature_names(features)
features_nonzero = np.array(features_poly)[ridge.coef_ != 0]
coef_nonzero = ridge.coef_[ridge.coef_ != 0]
x_axis_data = np.arange(len(coef_nonzero))
plt.scatter(x_axis_data, coef_nonzero)
plt.xticks(x_axis_data, features_nonzero, rotation=90)
plt.xlabel('feature name')
plt.ylabel('coefficient size')
plt.show()
시간과 요일의 곱을 사용한 선형 모델의 계수
선형 모델은 구간 분할이나 다항식과 상호작용 특성을 새로 추가해 큰 이득을 볼 수 있음
반면에 랜덤 포레스트나 SVM같은 비선형 모델은 특성을 늘리지 않고서도 복잡한 문제를 학습가능