첫 번째로 시작한 kaggle 프로젝트는 바로 'Bike Sharing Demand Project' 이다.
아주 유명한 데이터셋이라 많이 들어본 분들도 있으리라 생각한다. (무려 7년 전 데이터..!)
2022년 5월 25일부터 2022년 6월 9일까지 2주 동안 분석해보았다.
프로젝트 목표
[최종 목표] test 데이터의 대여량(count) 값을 예측하라!
1. train data를 바탕으로 사용자들의 사용 패턴을 파악한다. → (1) EDA 편
2. 앞서 알아본 패턴을 바탕으로, 적절한 모델을 설정하고 모델링을 통해 test data에 대해 예측한다. → (2) 머신러닝 편
이 포스팅은 데이터를 핸들링하는 EDA 편이며, 코드보단 설명 위주(왜 이렇게 분석했는지, 무엇을 얻었는지)의 포스팅이 될 예정이다. 분석에 쓰인 모든 코드가 궁금하신 분들은 이 깃허브에서 다운받을 수 있다. 또한 캐글 데이터셋은 이 페이지에서 다운받을 수 있다.
✔Table of Contents
1. Load & Check the data
주어진 데이터는 train data와 test data이다. 어떻게 구성되어 있는지 살펴보자.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
train = pd.read_csv('data/train.csv')
test = pd.read_csv('data/test.csv')
주어진 데이터는 2011년~2012년 동안의 자전거 이용 데이터이다.
이 데이터는 1시간(hour) 단위로 구성되어 있으며, 각 월별 1일부터 19일까지는 train 데이터, 20일부터 말일까지는 test 데이터에 들어있다.
train 데이터는 약 1만개, test 데이터는 약 6500개의 데이터로 구성되어 있다.
우리가 예측해야 할 것은 바로 대여량(count 열) 값이다.
캐글 데이터 설명란(description)을 참고하면 컬럼들의 설명이 나와 있다.
- casual: number of non-registered user rentals initiated
- registered: number of registered user rentals initiated
- count: number of total rentals
즉, 대여량 값은 casual과 registered 값의 합계이며, casual은 등록되지 않은 사용자(즉, 비회원) 수, registered는 등록된 사용자(즉, 회원) 수를 의미한다.
나머지 데이터셋은 temp(온도), atemp(체감온도), humidity(습도), windspeed(풍향), season(계절, 1~4 값), holiday(공휴일), workingday(평일), weather(날씨, 1~4 값), datetime(날짜 및 시간)을 의미하고 있다.
본격적으로 EDA에 들어가기 전에, 데이터를 점검하였다.
데이터 점검은 대표적으로 데이터가 몇 개나 있는가?(행, 열 수), 결측치가 얼마나 있는가?, 중복 데이터가 있는가?, 데이터의 형태가 잘 되어 있나?(예를 들어, 시간 데이터인데 문자로 되어있지는 않은지) 등을 살펴본다. 어떤 데이터를 살펴보느냐에 따라 더 봐야 하는 것이 추가될 수도 있다. (점검 방법은 나의 블로그 카테고리 'Skillset'의 'Python, Git' 부분에 정리해 두었다!)
여기서는 중복데이터가 있는지, 그리고 결측치가 있는지를 체크하였다.
sum(train.duplicated())
train.isna().sum()
다행히, 중복도, 결측치도 없었다.
train 데이터에 한것과 마찬가지로 test 데이터에 대해서도 중복/결측치가 없는 것을 확인하였다. 바로 EDA로 넘어가자.
2. EDA train data
EDA를 하는 이유는 여러 가지가 있겠지만, 나의 경우엔 현황 분석을 할 때 EDA를 자주 사용한다.
그리고 나는 무턱대고 분석을 시작하기보다는 궁금한 질문들을 마구 쏟아내보고, 이에 대해 답하는 방식으로 분석을 한다.
실제로 내가 EDA를 하기 전, 궁금했던 질문들이다.
- 날씨와 대여량 간에 상관관계가 크지 않을까? 어떤 컬럼이 가장 상관성이 큰지 궁금! 날씨, 온도, 습도, 풍향 등
- 덧붙여서 상관성이 총대여량(count) 외에도 casual, registered랑 관련있는지도 궁금하다
- 보통은 날씨가 좋아야 대여를 할 거 같은데.. 이게 1~4 당 count 수가 몇일까? 1, 2, 3, 4 어떤 상태일 때 count가 가장 클까?(개인적으론 화창할 때에만 타고 싶을거 같다.. 비오고 이러면 불편하니까)
- 그리고 어떤 시간(월, 일)에 가장 날씨가 좋았는지 / 아님 시간당 대여량은?
- 쉬는날과 일하는날에 대여량이 큰 차이가 날까?
- 그리고 쉬는날과 일하는 날엔 빌린 시간 (hour)이 좀 차이가 날거같아..!! 어차피 지역은 하나니까. 쉬는날엔 오후에, 평일엔 아침이나 저녁에 높지않을까?
- 시간대별로 온도와 체감온도가 많이 차이가 날까?
그래서 이런 질문들을 모아 답변을 해보기로 했다.
먼저 train 데이터를 살펴보면, 데이터의 형식이 좀 달랐다. 데이터의 종류가 수치형인지, 범주형인지에 따라 그릴 수 있는 그래프가 달라지기 때문에 나는 이를 나눠서 그려보기로 하였다. (참고: 파이썬 EDA에 필요한 시각화 그래프 정리 포스팅)
그래서 목차를 나눠보자면 다음과 같다.
1) 수치형 변수와 대여량 간 관계
2) 범주형 변수와 대여량 간 관계
3) 3개 변수 간 관계 파악(month, hour와 대여량 간 관계를 중심으로)
하나씩 살펴보기 전에, train 데이터에서 datetime 값을 날짜와 시간으로 쪼개보기로 했다.
형태가 datetime값이 아니라서 이를 변형해주었다. (이렇게 datetime 형식으로 바꾸면 dt 메서드를 사용할 수 있다.)
train['datetime2'] = pd.to_datetime(train['datetime'])
train['year'] = train['datetime2'].dt.year
train['month'] = train['datetime2'].dt.month
train['day'] = train['datetime2'].dt.day
train['hour'] = train['datetime2'].dt.hour
train.head()
이렇게 위와 같이 datetime2 열을 연도 / 월 / 일 / 시간으로 나누었다.
사실 시간뿐 아니라 분, 초로도 나눌 수 있는데, 데이터가 hour별로 집계가 되어있기 때문에 분, 초 값은 전부 0이라 할 필요가 없어 이렇게만 쪼갰다.
그리고 앞에서 count(대여량) 열이 casual(비회원 대여량)과 registered(회원 대여량)의 합이라고 했었는데, 둘 중 어떤 것의 비율이 클지가 궁금해져서 이것도 알아보았다.
print('Casual의 비율: ', round((train['casual'].sum() / train['count'].sum())*100, 2))
print('Registered의 비율: ', round((train['registered'].sum() / train['count'].sum())*100, 2))
그랬더니 casual이 2, registered가 8로 대부분이 회원 대여량이었다.
또한, 이건 사실 EDA를 하다가 나중에 발견한 것인데.. 캐글 데이터 상으로는 season의 값이 계절로 1, 2, 3, 4로 되어 있고, 1이 spring이라고 되어 있었다.
그런데 이상하게도 봄 이용량이 너무 적게 나오는 것이다. 그래서 season과 month가 어떻게 나뉘어 있는지를 알아보았는데
train.groupby(['season', 'month'])[['month']].count()
이런 결과가... 즉 season은 계절이 아니라, 분기였던 것이다. 그냥 1-3월은 봄, 4-6월은 여름 이런 식으로 나눠 놓았던 것..!
그래서 나는 season을 계절이 아닌, 1분기, 2분기, 3분기, 4분기로 나눠서 부르려고 한다.
1) 수치형 변수와 대여량 간 관계
이 데이터에서의 수치형 변수(numerical data)는 온도, 체감온도, 습도, 풍속 이렇게 4가지이다.
이 4가지 데이터는 모두 연속형 숫자값을 가지므로 연속형 변수(continuous variable)로 볼 수 있다.
아무튼 숫자값을 가지므로, 이 4개와 count 간 관계를 살펴보기 위해 상관계수 히트맵 그래프를 그려보았다.
colormap = plt.cm.PuBu
plt.figure(figsize=(10, 8))
plt.title("Person Correlation of Features", y = 1.05, size = 15)
sns.heatmap(train[['temp', 'atemp', 'humidity', 'windspeed', 'casual', 'registered', 'count']].corr(), linewidths = 0.1, vmax = 1.0 ,
square = True, cmap = colormap, linecolor = "white", annot = True, annot_kws = {"size" : 16})
plt.show()
count(대여량)과 가장 큰 상관계수를 갖는 것은 온도(temp) 였다.
대여량과 가장 큰 상관성을 갖는 값부터 나열해보자면,
temp 및 atemp(0.39) > humidity(-0.32) > windspeed(0.1) 이었다. (즉, 온도/체감온도는 대여량과 정비례, 습도는 반비례, 풍속은 미미한 정비례 관계로 해석하였다.)
참고로 count뿐만 아니라, 4개의 연속형 변수들과 casual, registered 간의 상관성도 위 그래프에서 확인해볼 수 있는데,
registered보다는 casual이 4개 변수들과의 상관성이 더 높은 것으로 나타났다.
→ 아무래도 비회원의 경우엔 날씨가 좋을 때 즉석에서 빌리는 경우가 많으므로, 좀 더 온도나 습도에 상관성이 높은 것이 아닐까 라고 추측해보았다.
.
그리고 상관계수처럼 숫자값뿐만 아니라, 시각적으로도 4개 변수와 count 간 관계를 알아보고자 산점도를 그렸다.
산점도와 회귀선을 같이 그려주는 regplot을 활용했다.
plt.rc('font', size = 7)
fig, axs = plt.subplots(2,2, figsize=(8, 8))
ax1, ax2, ax3, ax4 = axs.flatten()
axs1 = sns.regplot('temp', 'count', data = train, ax = ax1, scatter_kws = {'alpha':0.2}, line_kws={'color':'blue'})
axs2 = sns.regplot('atemp', 'count', data = train, ax = ax2, scatter_kws = {'alpha':0.2}, line_kws={'color':'blue'})
axs3 = sns.regplot('humidity', 'count', data = train, ax = ax3, scatter_kws = {'alpha':0.2}, line_kws={'color':'blue'})
axs4 = sns.regplot('windspeed', 'count', data = train, ax = ax4, scatter_kws = {'alpha':0.2}, line_kws={'color':'blue'})
axs1.set_title('Temp')
axs2.set_title('Atemp')
axs3.set_title('Humidity')
axs4.set_title('Windspeed')
plt.show()
- temp, atemp는 앞에서 상관계수가 약 0.4로 약간의 정비례 관계가 보인다.
- humidity는 -0.3의 상관계수로 약간의 반비례,
- windspeed는 상관계수가 0.1 정도였으므로 경향성이 잘 안보인다.
그리고 windspeed와 count 간 산점도를 보면, 다수의 값이 0에 몰려있다. 아예 바람이 안 분 날이 많은 것일까?
아니면, 앞에서 결측치 검사를 했을 땐 아무런 결측치도 없었는데, 결측치를 NaN이 아니라 0으로 넣은 것일지도 모른다.
windspeed는 0의 값을 제거해주든, 아님 이 변수를 아예 제거해버리든, 좀 처리를 해주는 게 좋아보인다.
.
마지막으로는 우리의 target 값인, count(대여량)의 분포가 어떤지를 살펴보았다.
casual, registered, count 의 boxplot을 그려보았다.
figure, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize = (15, 7))
sns.boxplot(y = 'casual', data = train, ax = ax1)
sns.boxplot(y = 'registered', data = train, ax = ax2)
sns.boxplot(y = 'count', data = train, ax = ax3)
ax1.set_title('Casual')
ax2.set_title('Registered')
ax3.set_title('Count')
plt.show()
casual의 분포를 보면 아웃라이어(까만색 점들)가 엄청 많다.
그리고 count의 분포를 봤을 때 시간별 대여량은 150~200 정도에 몰려있는 듯.
2) 범주형 변수와 대여량 간 관계
다음으로는 카테고리형 변수와 대여량 간의 관계를 파악해보았다.
이 데이터에서 카테고리형 변수는 이렇다.
- 시간(year, month, day, hour)
- 날씨(weather)
- 분기(season)
- 평일,주말(workingday), 휴일(holiday)
범주형 변수는 앞에서 본 수치형 변수로 그래프를 그릴 때와 다른 그래프로 그려야 한다.
아무튼 하나씩 그래프로 알아보자!
2-1) year, month, day, hour와 평균 대여량 간 관계
fig, axs = plt.subplots(2,2, figsize=(12, 10))
ax1, ax2, ax3, ax4 = axs.flatten()
#y축은 평균 대여량
axs1 = sns.barplot('year', 'count', data=train, ax = ax1)
axs2 = sns.barplot('month', 'count', data=train, ax = ax2)
axs3 = sns.barplot('day', 'count', data=train, ax = ax3)
axs4 = sns.barplot('hour', 'count', data=train, ax = ax4)
## 꾸미기
axs1.set_title('Year')
axs2.set_title('Month')
axs3.set_title('Day')
axs4.set_title('Hour')
#x축 라벨 회전
axs2.tick_params(axis = 'x', labelrotation = 90)
axs4.tick_params(axis = 'x', labelrotation = -45)
plt.tight_layout()
plt.show()
[해석]
- year: 2011년에 비해 2012년 평균 대여량 많아짐
- month: 6월에 가장 평균 대여량이 높다. 5~10월 모두 높은 편.
- day: 큰 차이를 보이지 않음(게다가 train 데이터는 1~19일, test 데이터는 20일~말일이므로 머신러닝 피처로 사용하기 부적절하다) → 제거
- hour: 자주 이용하는 시간대가 뚜렷하다. 특히 오전 8시와 오후 5-6시인 출퇴근 시간에 대여량이 높다.
2-2) weather와 평균 대여량 간 관계
weather는 카테고리 변수로, 1, 2, 3, 4 의 값을 가진다. 1일수록 날씨가 좋고, 4일수록 날씨가 안좋다.
먼저 데이터에서 카테고리별 데이터 비율을 알아보았다.
#주어진 데이터에서 날씨별로 데이터가 얼마나 있나?
(train.groupby('weather')[['datetime']].count() / len(train))*100
이 데이터가 좀 편파적이긴 하다. 1이 66%, 2가 26%로 전반적으로 날씨가 좋은 것으로 기록되었다.
가장 안좋은 날씨인 4는 매우 적다.
fig, axs = plt.subplots(1,3, figsize=(18,5))
ax1, ax2, ax3 = axs.flatten()
#y축은 "평균" 대여량
axs1 = sns.barplot('weather', 'casual', data=train, ax = ax1)
axs2 = sns.barplot('weather', 'registered', data=train, ax = ax2)
axs3 = sns.barplot('weather', 'count', data=train, ax = ax3)
axs1.set_title('Causal')
axs2.set_title('Registered')
axs3.set_title('Count (ALL)')
plt.show()
3개의 그래프는 weather와 대여량(평균) 간 그래프이며, 왼쪽부터 casual, registered, count에 대한 그래프이다.
날씨가 안 좋은 4일 때에도 registered 대여건이 있다. 심지어 다른 때보다도 더 많은 편인데, 이는 y축이 평균값이기 때문이다.
날씨가 4인 데이터는 매우 적기 때문에 평균값으로 계산 시 대여량이 많아 보이는 것이다.
그래서 y축을 평균이 아닌 합계로 계산하면 그래프는 이렇게 달라진다.
w_train = train.groupby('weather')[['count']].sum().reset_index()
wc_train = train.groupby('weather')[['casual']].sum().reset_index()
wr_train = train.groupby('weather')[['registered']].sum().reset_index()
fig, axs = plt.subplots(1,3, figsize=(18,5))
ax1, ax2, ax3 = axs.flatten()
axs1 = sns.barplot('weather', 'casual', data=wc_train, ax = ax1)
axs2 = sns.barplot('weather', 'registered', data=wr_train, ax = ax2)
axs3 = sns.barplot('weather', 'count', data=w_train, ax = ax3)
axs1.set_title('Causal')
axs2.set_title('Registered')
axs3.set_title('Count (ALL)')
plt.show()
보다시피 날씨가 4일 때의 대여량 합계는 너무 적어서 거의 안보인다.
실제로, 날씨가 4인 것만 train 데이터에서 뽑아 보면 단 1개의 데이터만 나온다. 이러한 값은 아웃라이어일 수 있으므로, 나중에 제거를 할 필요가 있어 보인다.
포스팅이 길어지는 관계로, 나머지 부분은 다음 포스팅에서 이어집니다 :)
'Data Science > Kaggle' 카테고리의 다른 글
[kaggle] Bike Sharing Demand: ML 성능 개선 1편 (Ridge, Random Forest, LGBM) (0) | 2022.07.01 |
---|---|
[kaggle] Bike Sharing Demand: Baseline Model 2편 (pipeline, k-fold, scaling) (0) | 2022.06.30 |
[kaggle] Bike Sharing Demand: Baseline Model 1편 (데이터 전처리) (2) | 2022.06.30 |
[kaggle] Bike Sharing Demand: EDA 2편 (0) | 2022.06.25 |
Kaggle 프로젝트를 시작하며 (feat. 깃허브) (0) | 2022.06.24 |