본문 바로가기

Python Programming/Notes

CRISP-DM 공공자전거 데이터 분석

CRISP-DM

Cross-industry standard process for data mining

  • CRISP-DM이란, 데이터로부터 의미를 도출해내는 일반적인 접근법이자 표준 절차다.
  • 분야/산업군을 막론하고 가장 널리 사용되고 있는 분석 모델이라고 볼 수 있다.
  • 해당 모델에서는, 데이터 마이닝 프로세스를 총 6가지 단계로 나누어 분석한다.
    1) Business Understanding:대상분야 이해단계 (비즈니스 로직 이해, 도메인 이해)
    2) Data Understanding: 데이터 이해단계 (가용 데이터 품질, 형태, 정보 등 확인)
    3) Data Exploration: 데이터 탐색단계 (그래프 그리는 단계)
    4) Data Preparation: 데이터 준비단계, 학습(데이터에 들어 있는 패턴을 파악하는 것)
    5) Model Planning: 분석 모형 기획 (알보리즘 후보군 선정)
    6) Model Building: 분석 모형 수립, 평가지표 적용(채점)
    7) Evaluation (평가): Communicate Results (결과 토의)
    8) Deployment (모델 전개), Operationalize (사내 시스템 적용)
  • 각 절차를 살펴보기 위해 미국 공공자전거 이용량 데이터를 CRISP-DM 모델에 적용하여 해석해 볼 것이다.

Data Understanding

해당 데이터는 Maryland 의 Washington D.C 공유자전거 스타트업에서 수집한 것이다.

  • 2011 ~ 2012년도까지 2년치, 시간대별(1시간 간격) - granularity
  • 날짜정보(datetime, season, holiday, workingday)
  • 기상정보(weather, temp, atemp, humidity, windspeed)
  • 고객 이용정보(casual, registered, count)
  • 날짜
    • datetime : 년-월-일 시:분:초
    • season : 계절정보
    • holiday : 휴일정보
    • workingday : 근로정보
  • 기상
    • weather : 날씨가 험악한 정도
    • temp : 온도
    • atemp : 체감온도
    • humidity : (상대)습도
    • windspeed : 풍속(Km/h)
  • 고객
    • casual : 비등록회원 이용객 수
    • registered : 등록회원 이용객 수
    • count : casual + registered 해당시간 총 이용객 수
import pandas as pd # 도구 불러오기
df = pd.read_csv('C:\\Users\\esthe\\train.csv') # 데이터셋 불러오기
print(df.shape) # 규모 파악
df.head().T # 무엇이 들어 있는 지 파악
(10886, 12)
  0 1 2 3 4
datetime 2011-01-01 00:00:00 2011-01-01 01:00:00 2011-01-01 02:00:00 2011-01-01 03:00:00 2011-01-01 04:00:00
season 1 1 1 1 1
holiday 0 0 0 0 0
workingday 0 0 0 0 0
weather 1 1 1 1 1
temp 9.84 9.02 9.02 9.84 9.84
atemp 14.395 13.635 13.635 14.395 14.395
humidity 81 80 80 75 75
windspeed 0 0 0 0 0
casual 3 8 5 3 0
registered 13 32 27 10 1
count 16 40 32 13 1

계절 별 자전거 이용량은?

import numpy as np
sc = df.groupby('season').agg({'count' : np.mean})
sc
  count
season  
1 116.343261
2 215.251372
3 234.417124
4 198.988296

연월일, 요일 column 생성하기

df['year'] = df['datetime'].apply(lambda x : x.split(' ')[0].split('-')[0])
df['month'] = df['datetime'].apply(lambda x : x.split(' ')[0].split('-')[1])
df['day'] = df['datetime'].apply(lambda x : x.split(' ')[0].split('-')[2])
df['hour'] = df['datetime'].apply(lambda x : x.split(' ')[1].split(':')[0])
df.head()
  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 01 01 00
1 2011-01-01 01:00:00 1 0 0 1 9.02 13.635 80 0.0 8 32 40 2011 01 01 01
2 2011-01-01 02:00:00 1 0 0 1 9.02 13.635 80 0.0 5 27 32 2011 01 01 02
3 2011-01-01 03:00:00 1 0 0 1 9.84 14.395 75 0.0 3 10 13 2011 01 01 03
4 2011-01-01 04:00:00 1 0 0 1 9.84 14.395 75 0.0 0 1 1 2011 01 01 04
df['dayofweek'] = df['datetime'].apply(lambda x : pd.to_datetime(x).dayofweek)
df.head()
  datetime season holiday workingday weather temp atemp humidity windspeed casual registered count year month day hour dayofweek
0 2011-01-01 00:00:00 1 0 0 1 9.84 14.395 81 0.0 3 13 16 2011 01 01 00 5
1 2011-01-01 01:00:00 1 0 0 1 9.02 13.635 80 0.0 8 32 40 2011 01 01 01 5
2 2011-01-01 02:00:00 1 0 0 1 9.02 13.635 80 0.0 5 27 32 2011 01 01 02 5
3 2011-01-01 03:00:00 1 0 0 1 9.84 14.395 75 0.0 3 10 13 2011 01 01 03 5
4 2011-01-01 04:00:00 1 0 0 1 9.84 14.395 75 0.0 0 1 1 2011 01 01 04 5
df['year_month'] = df['year'].apply(lambda x : x[2:]) + '-' + df['month']
df.head()
  datetime season holiday workingday weather temp atemp humidity windspeed casual registered count year month day hour dayofweek year_month
0 2011-01-01 00:00:00 1 0 0 1 9.84 14.395 81 0.0 3 13 16 2011 01 01 00 5 11-01
1 2011-01-01 01:00:00 1 0 0 1 9.02 13.635 80 0.0 8 32 40 2011 01 01 01 5 11-01
2 2011-01-01 02:00:00 1 0 0 1 9.02 13.635 80 0.0 5 27 32 2011 01 01 02 5 11-01
3 2011-01-01 03:00:00 1 0 0 1 9.84 14.395 75 0.0 3 10 13 2011 01 01 03 5 11-01
4 2011-01-01 04:00:00 1 0 0 1 9.84 14.395 75 0.0 0 1 1 2011 01 01 04 5 11-01

Data Exploration

  • EDA (Explore + atory Data Analysis, 탐색적 데이터분석)
  • Data Exploration은 그래프를 통해 데이터 안의 유효한 패턴을 눈으로 확인하는 단계이다.
  • 파이썬에서 그래프를 그릴 수 있는 여러 가지 방법이 있는데, 그 중 가장 미적인 효과가 풍부한 seaborn을 활용할 것이다.
import seaborn as sns
sns.scatterplot(data = df, x = 'registered', y = 'casual', hue = 'dayofweek')
<matplotlib.axes._subplots.AxesSubplot at 0x254c0c28>

sns.scatterplot(data = df, x = 'registered', y = 'casual', hue = 'workingday')
<matplotlib.axes._subplots.AxesSubplot at 0x265485e0>

  • 위 두 그래프를 통해 공공자전거 등록회원과 비등록회원의 차이가 유의미하게 나타났음을 확인할 수 있다.
  • 등록회원(registered)의 경우, 일하는 날에 자전거를 많이 이용하며, 비등록회원(casual)은 주말에 자전거를 많이 이용하는 경향성을 보인다.

등록회원과 비등록회원 비교

sns.lineplot(data = df, x = 'hour', y = 'registered', hue = 'dayofweek')
<matplotlib.axes._subplots.AxesSubplot at 0x26c6d220>

sns.lineplot(data = df, x = 'hour', y = 'casual', hue = 'workingday')
<matplotlib.axes._subplots.AxesSubplot at 0x42aa610>

  • 등록회원과 비등록회원에 대한 시간별 line graph를 각각 살펴보았다.
  • 등록회원과 비등록회원 둘 다 평일과 주말에 각각 다른 그래프 형태를 보였다.
    • 등록회원 / 평일: 출근 시간과 퇴근 시간에 주로 자전거를 이용한다.
    • 등록회원 / 주말: 주로 점심시간과 저녁시간 사이에 자전거를 이용한다.
    • 비등록회원 / 평일: 이용량이 거의 없으며, 오후 시간대에 미미하게 높은 이용률을 보인다.
    • 비등록회원 / 주말: 주로 오후에 이용량이 급격히 증가하는 형태를 보인다.
for_plot = df.loc[df['dayofweek'] >= 5]
sns.pointplot(data = for_plot, x = 'hour', y = 'registered', hue = 'dayofweek')
<matplotlib.axes._subplots.AxesSubplot at 0xf3e8748>

  • 등록회원들의 주말 이용률만 따로 추출하여 살펴보자.
  • 토요일 저녁보다 일요일 저녁의 사용량이 더 적게 나타난다.
  • 일요일에는 다음 날 출근을 해야 한다는 부담감이 있기 때문에 이와 같은 결과가 나타난다고 볼 수 있다.

Data Preparation

  • 데이터를 분석 모형에 넣기 위해 형태를 맞추는 작업을 진행한다.
  • train / test 분리 -> row 분리
  • 문제와 정답 분리 -> feature / label 분리 -> column 분리
    • [연도, 월, 일, 시, 요일, 날씨]이(가) 이용객 수에 영향을 줄까?
    • Generalization: A가 B에 영향을 줄까? -> A = feature, B = label
#column 분리 : 문제와 정답 분리
features = ['season', 'holiday', 'workingday', 'weather', 'temp', 
            'atemp', 'humidity', 'windspeed', 'year', 'month', 
            'day', 'hour', 'dayofweek']

label = 'count'#지도학습에서 맞히고자 하는 대상

#row 분리
train, test = df[0::2], df[1::2] #홀/짝 기준으로 공부용/시험용 분류
#reset_index
train, test = train.reset_index(), test.reset_index()

#공부용/시험용 문제정답 분리
X_train, y_train = train[features], train[label]
X_test, y_test = test[features], test[label]

Model Planning

  • 지도학습은 2가지 시험 유형으로 나눌 수 있다.
    1. 숫자 맞히기
      • Regress ~ 라는 수식어 사용 (Regression, Regressor ...)
      • 회귀문제라고 부름
    2. 경우의 수 맞히기
      • Classify ~ 라는 수식어 사용 (Classification, classifier ...)
      • 분류문제라고 부름
  • 이 데이터셋의 경우, 숫자(수요량)를 맞히는 것이므로, 회귀 알고리즘을 사용한다.

Model Building

from sklearn.ensemble import RandomForestRegressor as rf #알고리즘 불러오기
model = rf() 
model.fit(X_train, y_train) #model fitting (학습)
model.score(X_train, y_train)
0.9920201431437241
model.score(X_test, y_test)
0.9206981367962218

R-squared 이외에 회귀분석에서 자주 사용되는 평가지표 구성요소

  • R : Root, 제곱근
  • M : Mean, 평균
  • S : Squared, 제곱
  • L : Log, 로그
  • P : Percentage, 퍼센트
  • A : Absolute, 절대값
  • E : Error, 오차

예시

  • MAPE : 오차의 퍼센트에 절대값을 씌우고 평균
  • MAE : 오차의 절대값의 평균
  • MSE : 오차 제곱의 평균
  • RMSE : 오차 제곱 평균의 제곱근
  • RMSLE : 로그오차의 제곱을 평균에 대한 제곱근
import pandas as pd
for_plot = pd.DataFrame() 
for_plot['tr_predict'] = model.predict(X_train)
for_plot['te_predict'] = model.predict(X_test) 
for_plot['tr_actual'] = y_train
for_plot['te_actual'] = y_test
for_plot.head()
  tr_predict te_predict tr_actual te_actual
0 21.85 22.20 16 40
1 25.95 15.11 32 13
2 2.73 3.82 1 1
3 3.01 22.60 2 3
4 24.10 51.64 8 14

Training set의 회귀분석 결과

  • R-squared가 거의 1에 수렴한다
import seaborn as sns
sns.scatterplot(data = for_plot, x = 'tr_predict', y = 'tr_actual')
<matplotlib.axes._subplots.AxesSubplot at 0x288daa78>

Test set의 회귀분석 결과

sns.scatterplot(data = for_plot, x = 'te_predict', y = 'te_actual')
<matplotlib.axes._subplots.AxesSubplot at 0x288ad490>

Feature Selection

  • 유의미한 분석을 위해서는 여러 가지 column 중에서 알고리즘에 넣을 변수를 잘 선정하는 것이 중요하다.
  • Feature Select 시에 활용하는 대표적인 방법들을 적용해 보며 중요한 변수들이 무엇인 지 확인해 볼 것이다.

Wrapper 방식

  • feature의 조합에 따라 평가지표의 수치가 달라질 것이다.
  • 따라서, 조합 별로 평가지표를 확인하고, 가장 좋은 평가지표를 선택한다.
def tt_split(features):
    label = 'count' 
    train, test = df[0::2], df[1::2] 
    train, test = train.reset_index(), test.reset_index()
    X_train, y_train = train[features], train[label] 
    X_test, y_test = test[features], test[label] 
    return X_train, y_train, X_test, y_test
sample_features = ['holiday', 'workingday', 'weather', 'temp',
       'atemp', 'humidity', 'windspeed', 'year', 'month', 'hour', 'dayofweek']

X_train, y_train, X_test, y_test = tt_split(sample_features)
model = rf()
model.fit(X_train, y_train)
model.score(X_test, y_test)
0.9247400117866152
from sklearn.feature_selection import RFE
#Recursive Feature Elimination
rfe = RFE(estimator = model) #어떤 알고리즘을 이용하여 평가할까?
rfe.fit(X_train, y_train) #RFE 방식의 학습 진행 (어떤 feature가 중요한 지?)
RFE(estimator=RandomForestRegressor())
for_rfe = pd.DataFrame()
for_rfe['ranking'] = rfe.ranking_ #변수의 중요도 순서(랭킹, 순위)
for_rfe['features'] = sample_features
for_rfe = for_rfe.sort_values(by = 'ranking')
for_rfe #ranking이 1인 것들은 알고리즘에서 뺐을 때 성능이 크게 떨어진다고 해석 가능함
#ranking이 낮은 순위들은 알고리즘에서 뺐을 때 성능변화가 없거나 성능이 좋아진다고 판단
  ranking features
3 1 temp
4 1 atemp
7 1 year
9 1 hour
10 1 dayofweek
8 2 month
1 3 workingday
5 4 humidity
2 5 weather
6 6 windspeed
0 7 holiday

Embed 방식

  • model 변수에 RandomForest를 사용한다.
  • 이 방법을 통해 각 feature 별로 중요도를 파악할 수 있다.
fi = pd.DataFrame()
fi['importance'] = model.feature_importances_ 
fi['feature'] = sample_features
fi = fi.sort_values(by = 'importance', ascending = False)
fi
  importance feature
9 0.608508 hour
7 0.069298 year
4 0.068666 atemp
8 0.055296 month
1 0.050644 workingday
10 0.047750 dayofweek
3 0.043840 temp
5 0.027434 humidity
2 0.016500 weather
6 0.010107 windspeed
0 0.001957 holiday
import matplotlib.pyplot as plt
sns.barplot(data = fi, x = 'importance', y = 'feature')
plt.title("Importance of Features")
Text(0.5, 1.0, 'Importance of Features')