빅데이터 분석 기사, ADP 자격증 취득(가능할지는 모르겠지만)을 목표로 공부 중이지만, 애석하게도 애송이 실력이라 혼자 힘만으로는 코드를 짜는 게 쉽지 않다.
글을 잘 쓰고 싶은 사람들이 필사를 해보는 것처럼, Kaggle에 다른 분들이 남긴 코드(작성 방향)을 필사하는 느낌으로
데이터 분석 flow를 정리해보면 내 실력도 조금은 나아지지 않을까?
첫 시작으로 Kaggle 입문용으로 쓰는 bike demand 데이터로 연습해봤던 내용들을 정리해보려고 한다.
분석 flow를 참고하되, code는 R을 사용해서 내 식대로(정돈이 안 된...) 작성했으며,
단계마다 궁금한 부분을 찾아보고, 채워넣는 식으로 정리할 예정이다.
데이터 소개
www.kaggle.com/c/bike-sharing-demand
- 1시간 간격으로 기온, 습도, 풍속 등의 정보와 함께 자전거 대여 횟수를 기록한 데이터이다.
- 2011년 1월 ~ 2012년 12월까지, 2년 간의 정보가 기록되어 있다.
- (개인적으로) Test 데이터 셋에는 종속변수에 대한 내용이 빠져 있어 train 데이터에 대해서만 EDA & 예측모델을 생성하였다.
필요한 라이브러리 호출
## 라이브러리 호출
library(tidyverse)
library(lubridate) ## 시간 데이터를 다룰 때 사용하는 패키지
library(gridExtra)
library(reshape2)
library(naniar) ## 결측치 여부 판단할 때 사용하면 유리!
library(corrplot)
library(caret)
library(randomForest) ## 랜덤포레스트
library(gbm) ## 부스팅
library(MLmetrics) ## 모델 평가에 사용하는 패키지
datetime 컬럼을 분해 & 데이터 타입 변환
## datetime 컬럼을 연, 월, 일, 시간, 요일로 분해.
bike_demand$datetime <- as_datetime(bike_demand$datetime)
bike_demand <- bike_demand %>% mutate(year = as.factor(year(datetime)), month = as.factor(month(datetime)),
day = as.factor(day(datetime)),
wday = wday(datetime, abbr = T, label = T),
hour = as.factor(hour(datetime)))
# 나머지 항목도 데이터 명세서(?)에 맞게 형변환
bike_demand$season <- as.factor(bike_demand$season)
bike_demand$holiday <- as.factor(bike_demand$holiday)
bike_demand$workingday <- as.factor(bike_demand$workingday)
bike_demand$weather <- as.factor(bike_demand$weather)
str(bike_demand)
datetime을 연, 월, 일, 시간, 요일 등으로 분해하여,
예측모델에 사용할 파생변수를 만드는 것들을 볼 수 있었다.
어떻게 생각하면 당연한 게, datetime 변수 그대로 사용하면, 시계열 회귀분석이 될텐데 골치아픈(?) 길을 가는 것보다는 각각을 변수로 분해해서 feature를 통해 종속변수를 예측하는 방향으로 가는 것이 더 안전(?)할 것 같다.
처음 봤을 때는 'datetime을 어떻게 처리하지?' 싶었는데, 다 파생변수로 만들어서 처리하는 걸 보고 또 한 번 배웠다.
그리고 범주형 변수임에도 불구하고, int로 지정된 일부 변수에 대해서 다시 범주형 변수로 바꾸어주는 과정을 거쳤다.
이 단계들을 거쳤을 때 데이터 타입은 다음과 같이 얻을 수 있다.
결측치 및 이상치 확인
## EDA를 하기 위해 1차적으로 결측치 확인
naniar::gg_miss_var(bike_demand)
## 결측치는 없음을 확인할 수 있다!
gg_miss_var이라는 함수를 사용하면, 결측치가 있는지에 대해 깔끔하게 시각화하여 보여준다.
단, 여기서 간과한 게 있는데 '명시적 결측치'에 대해서는 해당 그래프로 확인할 수 있지만,
묵시적 결측치에 대해서는 확인이 불가능하다는 것이다.
가령 왼쪽과 같이 '3일'에 해당하는 일자에 값이 빠져 있으면 '아 값이 빠져있구나'를 알 수 있지만,
'3일' 자체가 아예 기록에서 누락된 경우 해당 패키지를 사용하면 결측치를 확인할 수 없다.
bike_demand %>% group_by(month, day, hour) %>%
summarise(min_year = min(as.numeric(as.character(year))), max_year = max(as.numeric(as.character(year)))) %>%
filter(min_year == max_year)
월, 일, 시간대 기준으로 최소 연도, 최대 연도를 집계해보았다.
2년 간의 데이터가 집계되어 있으므로 결측치가 없다면, 각 월, 일, 시간대별로 최소연도 = 2011, 최대연도 = 2012가 잡혀야 한다.
그런데..
일부 데이터에 대해 month, day, hour 별로 min_year & max_year가 같은 경우가 존재한다..
이를 통해 해당 월, 일, 시간대에 대해 기록조차 되지 않은 결측치가 있음을 파악할 수 있다.
결국, 결측치가 있기는 있다는 뜻인데..
전체 데이터에서 극히 일부이기도 하고,
시계열 데이터로 다루는 게 아니라 target ~ feature 간의 모델을 만드는 것이므로 일단 무시하고 넘어갔다(..)
## 이상치 확인
bike_demand$is_count_outlier <- as.factor(ifelse(bike_demand$count > (1.5*IQR(bike_demand$count)), 1, 0))
bike_demand %>% filter(is_count_outlier == 1) %>%
group_by(season, holiday, workingday, weather, is_count_outlier) %>% summarise(n = n()) %>% arrange(desc(n))
# IRQ 기준으로 이상치가 있음을 확인 가능. 일단 따로 이상치를 처리하지 않고 있다는 것만 확인.
이상치에 대해서도 확인을 해 보았다.
1.5*IQR을 벗어난 값을 이상치로 가정했을 때, 계절, 휴일 여부 등에 따라 특징이 나타나는 게 있지 않을까 싶어 표를 그려봤지만... 봐도 와닿지가 않았다.
결과적으로 이상치도 별도의 처리 없이 넘어가기는 했지만,
사실 이상치와 결측치 처리는 데이터 전처리에 있어 중요한 요소라고 생각한다.
다만, '근의 공식'처럼 딱 떨어지는 그런 개념이 아니다보니, 상황에 따라 처리 방법이 달라지는 게 멘붕일 뿐.
이상치 처리 방법에 대해 'R을 활용한 데이터 과학' 책에서 관련 내용을 일부 발췌하면,
- 이상치를 제외하거나 포함하여 분석을 반복. 이상값이 결과에 최소한의 영향을 미치고 왜 이상값이 발생했는지 그 이유를 알 수 없다면 결측값으로 대체한 후 계속 진행하는 것이 합리적
- 이상값이 결과에 상당히 영향을 미치는 경우 타당한 이유없이 제외해서는 안 되고, 문제의 원인(데이터 입력 오류 등)을 파악하고, 이상값을 제거한 사실을 밝혀야 한다고 한다.
종합하자면, 이상치를 일단 안고가되, 모형을 만드는 과정에서 이상치를 어떻게 처리할지 결정하는 느낌이다.
당장 EDA 과정에서 이상치가 보인다고, '이걸 빼야지, 대치해야지' 판단할 수 없는 것 같다.
결측치는 일반적으로
- 결측치가 들어간 행을 완전 제거
- 평균(중위수) 등으로 대체
- 결측치가 포함된 변수를 사용하지 않고 진행
하는 등의 방법을 쓰는 것으로 알고 있는데, 이 쪽은 아직 정리가 덜 되어서 추후 결측치에 대해서 따로 정리를 해봐야 할 것 같다.
시각화
사실상 boxplot 원툴이다
## 계절별 수요 - 여름, 가을에 수요량 증가
ggplot(bike_demand, aes(x = as.factor(season), y = count)) + geom_boxplot()
## 계절별 사젼예약 여부별 수요 - count, registered 간 차이가 조금 있는 것처럼 보임.
bike_demand %>% gather(`casual`, `registered`, key = "use_type", value = "rental_count") %>%
ggplot(., aes( x= as.factor(season), y = rental_count)) + geom_boxplot() + facet_wrap(.~use_type)
## 계절 + 시간대별 수요 - 8시, 17시 쯤 수요량 증가하는 특성. 계절에 따라 특별히 패턴이 바뀌지는 않음.
ggplot(bike_demand, aes(x = hour, y = count, group = hour)) + geom_boxplot() + facet_wrap(.~season)
## 시간대별 + 사젼예약 여부별 수요 - 미리 등록했는지, 아닌지에 따라 시간대가 달리 설정된다.
## casual & registered 는 시간대에 달리 영향을 받는다 -> casual & registered를 나눠서 봐야 할 근거 1
bike_demand %>% gather(`casual`, `registered`, key = "use_type", value = "rental_count") %>%
ggplot(., aes( x= as.factor(hour), y = rental_count)) + geom_boxplot() + facet_wrap(.~use_type)
count = registered(사전등록) + casual(사전등록X)으로 이루어지는데,
kaggle에서 EDA를 진행하신 분들의 결과물들을 보면 registered와 casual을 나눠서 각 변수 간의 관계성을 확인하는 것을 심심치 않게 찾아볼 수 있었다.
사실상 결말을 알고 보는 추리소설 느낌으로, casual, registered 을 나누고 나니 눈에 들어오는 것들이 왕왕 있었다.
계절(1 = 봄, 2 = 여름, 3 = 가을, 4 = 겨울)과 count 간 관계를 보면, 봄을 제외한 나머지 계절에 수요량이 높다.
이걸 사전등록 여부(registered, casual)로 나눠서 보면 패턴이 조금 달라지는 것을 확인할 수 있다.
- casual은 여름, 가을에 좀 더 집중되고,
- registered은 count와 비슷하게 봄을 제외한 나머지 계절에 수요가 좀 더 집중된다.
계절과 시간대를 조합할 경우에는, 계절과 시간대에 따라 count의 패턴이 달라지는 것은 확인할 수 없다.
사전등록 여부, 시간대에 따라 수요량을 확인해보면 패턴이 달라지는 것을 확인할 수 있는데,
- registered(사전등록)인 경우에는 8시, 17시에 수요가 집중되는 반면
- casual은 낮 시간대에 수요가 집중되는 것을 확인할 수 있다.
## 요일별 수요 - 단순 count로만 보면 요일 별로 큰 차이가 없는 것 같아 보인다.
ggplot(bike_demand, aes(x = as.factor(wday), y = count)) + geom_boxplot()
## casual & registered 는 요일에 달리 영향을 받는다
bike_demand %>% gather(`casual`, `registered`, key = "use_type", value = "rental_count") %>%
ggplot(., aes( x= wday, y = rental_count)) + geom_boxplot() + facet_wrap(.~use_type)
다음으로는 요일별로 데이터를 살펴보았다.
요일별로 count의 분포는 큰 차이가 없어 보인다.
그러나 사전등록 여부에 따라 요일별 수요 분포는 차이를 보이는데,
- registered(사전등록)인 경우에는 평일에 수요가 집중되는 반면,
- casual(사전등록X)인 경우에는 주말에 수요가 집중되는 것을 확인할 수 있다.
## 변수 간 상관관계 분석 - temp, atemp 가 높은 상관성을 보이는 것을 알 수 있음.
bike_cor <- cor(select_if(bike_demand, is.numeric))
corrplot.mixed(bike_cor)
pairs(select_if(bike_demand, is.numeric))
## 상관도(?)로 그려봐도 temp & atemp 간 상관성이 높게 나타남.
## count 자체와 상관관계가 높은 변수는 registered인데 결국 count = registered + casual 이기 때문에 당연한 결과.
다음으로는 연속형 변수(수치형 변수)에 대해서 상관관계를 확인해보았다.
온도 ~ 체감온도 간에 굉장히 상관관계가 높음을 확인할 수 있다.
(선형회귀분석을 실시할 경우, 다중공선성 문제를 일으킬 가능성이 있다.)
또, registered & count 간에 높은 관계성을 보이는 것을 확인할 수 있다.
## 종속변수(casual, registered)에 대한 히스토그램 - 정규성 만족X -> 회귀분석 돌릴 때 종속변수에 log 씌워서 봐야할 근거.
a<- ggplot(bike_demand, aes(y = casual))+ geom_histogram()
b<- ggplot(bike_demand, aes(y = registered))+ geom_histogram()
grid.arrange(a,b)
그림에서의 count는 빈도의 count다..! 그래프를 그릴 때 헷갈리지 않도록 설명을 잘 다는 게 좋겠다.
전반적으로 registered, casual 모두 시간대별 빈도는 0에 가까운 편으로 보인다.
히스토그램만 봤을 때는 데이터가 skewed 되어 있으므로, 선형회귀분석을 이용할 경우에는 log를 씌워 작업을 하는 게 좋을 것 같다.
시각화 결과를 종합하자면
- 주말에는 casual 수요가, 평일에는 registered 수요가 상대적으로 높다.
- registered의 경우 오전 8시, 오후 5시에 수요가 상대적으로 높아지는데 이는 출퇴근 시간에 자전거를 이용하는 것으로 해석할 수 있다.
(외국은 9 to 6가 아니라 8 to 5인가?) - 어째서인지 봄에 수요량이 상대적으로 낮게 나타난다.
외국의 봄은 내가 아닌 그 봄이 아닌걸까. - 수요량은 skewed한 형태를 보인다.
묻지도 따지지도 않고, 알고 있는 함수에 변수를 다 때려넣는 것보다
EDA를 통해 어느 정도 데이터에 대한 이해를 하는 것이 중요하다는 걸, 코드를 필사하는 과정에서 다시금 깨닫게 되었다.
EDA 과정에서
범주형 변수는 종속변수와의 관계성을 파악하기 위해 각각을 boxplot으로 그렸고,
연속형 변수는 상관행렬, 산점도로 나눠서 그렸다.
사실, 범주형 변수도 어떻게 상관계수로 퉁칠수 없을까 생각했지만, 당시에는 방법을 찾지 못하여 boxplot으로 나눠 그린 것인데 검색해보니 범주형 - 범주형, 범주형 - 연속형 끼리도 상관계수를 구할 수 있는 듯 하다.
- 연속형 - 연속형 : 피어슨 상관계수, 스피어만 상관계수
- 범주형 - 연속형 : point biserial correlation
- 범주형 - 범주형 : phi corelation
다만, 이런 개념이 있다는 것만 새로 알게 된 것이지
실제로 어떻게 구현되는지는 잘 모르고 있는 상태라, 이 역시도 추가로 더 공부를 해봐야 할 것 같다.
참고 링크
* 이상치 처리
* 결측치 처리
statkclee.github.io/data-science/ds-missing.html
m.blog.naver.com/tjdudwo93/220976082118
* 상관분석
www.statisticssolutions.com/point-biserial-correlation/
'Tools > R' 카테고리의 다른 글
맨날 헷갈리는 ggplot 시각화 정리(1) (0) | 2021.11.15 |
---|