스파르타 코딩클럽 데이터분석 4-4 자전거 수요 예측하기 - 데이터 준비, 시각화, 상과분석&정규화

44346 단어 2022.032022.03

04. 자전거 수요 예측하기 - 데이터 준비, 시각화, 상관분석 & 정규화

  • 데이터가 어떻게 이루어 져있는지?
  • 결측치는 없는지?
  • 정보는 어떻게 되어있는지?

6) 데이터 소개 및 데이터 로드

데이터 분석 경진대회 캐글(Kaggle)에서 자전거 대여 수요(Bike Shareing Demand) 예측 경연에서 사용된 데이터를 사용하여 선형 회귀 문제를 풀어봅시다.

언제 몇번에 자전거 대여가 발생했는지 담겨있는 데이터
(헷깔리지 않도록 봐두기)

  • datetime - 년도-월-일 시간:분:초
  • season - 1 = 봄, 2 = 여름, 3 = 가을, 4 = 겨울
  • holiday - 1 = 토, 일요일의 주말을 제외한 국경일 등의 휴일, 0 = 휴일이 아닌 날
  • workingday - 1 = 토, 일요일의 주말 및 휴일이 아닌 주중, 0 = 주말 및 휴일
  • weather
    • 1: 맑음, 약간 구름 낀 흐림
    • 2: 안개, 안개 + 흐림
    • 3: 가벼운 눈, 가벼운 비 + 천둥
    • 4: 심한 눈/비, 천둥/번개
  • temp - 온도(섭씨)
  • atemp - 체감온도(섭씨)
  • humidity - 상대 습도
  • windspeed - 풍속
  • casual - 사전에 등록되지 않은 사용자가 대여한 횟수
  • registered - 사전에 등록된 사용자가 대여한 횟수
  • count - 대여 횟수

패키지 임포트

import pandas as pd # 데이터 프레임으로 뭔가 할때 
import numpy as np # 데이터 프레임으로 뭔가 할때
import matplotlib.pyplot as plt #시각화 할때 쓰는 것 
import seaborn as sns # 시각화 할때 쓰는 것 

->

bike_df = pd.read_csv('https://raw.githubusercontent.com/jesford/bike-sharing/master/train.csv') #
print(bike_df.shape)
bike_df.head(3)
#이해하고 있어야, 데이터 파일이 저 주소에 업로드 되어서 
#`주소`를 명시하고, `read_csv()` 함수를 이용해서 `bike_df`라는 변수에다가 데이터 프레임을 저장해놓음. 
#총 몇행 몇열로 이루어져있는지 출력 
#그리고 상위 3개 행을 출력함.
(10886, 12) #행과 열

💡 이 12개의 열들을 이용해서 count라는 열을 예측하는 것

bike_df.isnull().sum() 
#결측값이 있는지 확인.insull은 T or F
#() 비어있어? 라고 물어보는 거였고, 
#뒤에 sum() 붙이면 비어있는 그 행의 개수를 출력해주는 것.

->

datetime      0
season        0
holiday       0
workingday    0
weather       0
temp          0
atemp         0
humidity      0
windspeed     0
casual        0
registered    0
count         0
dtype: int64

데이터의 각 열의 타입을 봅시다

bike_df.info() #info() 함수를 써서 전체적인 정보가 어떻게 되는지 봄

->

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10886 entries, 0 to 10885
Data columns (total 12 columns):
 #   Column      Non-Null Count  Dtype  
---  ------      --------------  -----  
 0   datetime    10886 non-null  object #자료형이 문자열
 1   season      10886 non-null  int64  
 2   holiday     10886 non-null  int64  
 3   workingday  10886 non-null  int64  
 4   weather     10886 non-null  int64  
 5   temp        10886 non-null  float64
 6   atemp       10886 non-null  float64
 7   humidity    10886 non-null  int64  
 8   windspeed   10886 non-null  float64
 9   casual      10886 non-null  int64  
 10  registered  10886 non-null  int64  
 11  count       10886 non-null  int64  
dtypes: float64(3), int64(8), object(1)
memory usage: 1020.7+ KB

datetime이 문자인거 보다 숫자인 것이 더 분석하기 쉽다.
ex)연도별 자전거 대여량 : 연도라는 열이 하나 있으면 좋을 듯, 문자형으로 되있어서 세부적인 분석이 힘들어.

  • datetime의 열을 보면 문자열 데이터이고, 나머지는 정수형 또는 실수형 데이터로 구성되어져 있습니다.
  • 사실 이 시점에서 파이썬 지식이 어느 정도 있는 분이라면, datetime의 열의 타입이 문자열인 것에 의문을 느낄 수 있습니다.

💡 어떻게 datetime을 년,월,일,시 별로 나누어 저장할 수 있는가?

7) datetime 타입

참고 : https://datascienceschool.net/01 python/02.15 파이썬에서 날짜와 시간 다루기.html
https://datascienceschool.net/01%20python/02.15%20%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%97%90%EC%84%9C%20%EB%82%A0%EC%A7%9C%EC%99%80%20%EC%8B%9C%EA%B0%84%20%EB%8B%A4%EB%A3%A8%EA%B8%B0.html

문자형 -> 자료형 (날짜 형태의)

  • 2018-09-15 00:01:14

날짜와 시간을 저장하기 위한 타입
datetime 자료형의 형식과 똑같아. 그런데 문자열로 되있는.
형식을 날짜형이라고 알려줄수 있도록 datetime 이라는 자료형으로 변경

# 문자열을 datetime 타입으로 변경. 
bike_df['datetime'] = bike_df.datetime.apply(pd.to_datetime)
bike_df['datetime']
#apply함수는 각행마다 괄호안에 들어있는 어떤 함수를 각 행마다 적용시킴 

->

0       2011-01-01 00:00:00
1       2011-01-01 01:00:00
2       2011-01-01 02:00:00
3       2011-01-01 03:00:00
4       2011-01-01 04:00:00
                ...        
10881   2012-12-19 19:00:00
10882   2012-12-19 20:00:00
10883   2012-12-19 21:00:00
10884   2012-12-19 22:00:00
10885   2012-12-19 23:00:00
Name: datetime, Length: 10886, dtype: datetime64[ns]

좀 더 세분화 된 분석을 위해서 datetime 형태의 데이터로부터 연, 월, 일, 시간과 같은 정보를 전부 분리해서 다뤄봅시다. 이는 판다스의 시리즈에서 datetime 타입의 데이터를 위해 제공하는 함수인 dt.year(), dt.month, dt.day, dt.hour를 통해 바로 추출이 가능합니다.

bike_df["year"] = bike_df["datetime"].dt.year # 연만 추출
bike_df["month"] = bike_df["datetime"].dt.month # 월만 추출
bike_df["day"] = bike_df["datetime"].dt.day # 일만 추출
bike_df["hour"] = bike_df["datetime"].dt.hour # 시간만 추출
bike_df.shape

->

(10886, 16)

열의 개수가 12개에서 16개로 늘었습니다. 한 번 출력해봅시다.

bike_df.head()

year, month, day, hour라는 열이 전부 새로 생긴 것을 알 수 있습니다.

8) subplot을 이용한 한 번에 여러 차트 그리기

시각화 작업.

  • plt.subplot(행의 수, 열의 수) : 행의 수와 열의 수만큼의 차트 공간을 만들어냅니다. 이를 이용하여 여러 차트를 배치시킬 수 있습니다.
figure, (ax1, ax2, ax3, ax4) = plt.subplots(nrows=1, ncols=4)

->


아래의 실습을 통해 좀 더 정확하게 이해할 수 있습니다.

# ax1은 첫번째 위치에 배정
# ax2는 두번째 위치에 배정
# ax3는 세번째 위치에 배정
# ax4는 네번째 위치에 배정
figure, (ax1, ax2, ax3, ax4) = plt.subplots(nrows=1, ncols=4)

# 차트의 크기를 조절
figure.set_size_inches(16,4)

# sns(seaborn)을 이용한 bar 차트를 4개 생성.
# 이때, ax에 각각 ax1, ax2, ax3, ax4를 배정하므로서 차트의 위치가 결정.
sns.barplot(data=bike_df, x="year", y="count", ax=ax1)
sns.barplot(data=bike_df, x="month", y="count", ax=ax2)
sns.barplot(data=bike_df, x="day", y="count", ax=ax3)
sns.barplot(data=bike_df, x="hour", y="count", ax=ax4)

# ax의 각 위치에 y축의 레이블과 차트의 제목을 정해줄 수 있다.
ax1.set(ylabel='Count',title="Year")
ax2.set(xlabel='month',title="Month")
ax3.set(xlabel='day', title="Day")
ax4.set(xlabel='hour', title="Hour")

->

위 바 차트로부터 얻을 수 있는 정보들은 다음과 같습니다.

  • 연도별 대여량은 2011년 보다 2012년이 더 많다. 아마 입소문이 늘어나서 그렇지 않을까 생각해볼 수 있다.
  • 월별 대여량은 6월에 가장 많고 그 다음으로는 7월, 8월, 9월, 그리고 10월, 다음에는 5월 순이다. 아무래도 날씨가 따뜻할 때 많이 이용한다고 보여진다.
  • 일별대여량은 1일부터 19일까지만 존재한다.
  • 시간별 대여량을 보면 오전 7~8시와 저녁 18시 이후가 가장 많은데, 이는 직장인들의 출, 퇴근 시간과 연관이 있을 것으로 보인다.

위 subplot의 경우에는 행은 1, 열은 4개의 값을 가지도록 했습니다. 그런데 만약에 행이 2개 이상. 즉, 두 줄 이상으로 차트를 배치할 수도 있지 않을까요? 다음은 행이 2개, 열이 2개인 경우에 subplot을 사용하는 예시입니다. 같은 행끼리 묶어주는 것이 앞 예시와 다릅니다.

fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(nrows=2, ncols=2)

->


다음은 boxplot이라고 불리는 차트를 보여줍니다.

-100, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 100

이런 식으로 총 20개의 데이터가 있을 때, 상위 25% 지점이 Q3 (11이라는 값), 하위 25% 지점이 Q1 (0이라는 값) 가 되는 것이고, 정확하지는 않지만 최대값은 14, 최소값은 -3 이라고 이해하시면 됩니다.

그리고 100, -100은 다른 데이터에 비해서 너무 크거나, 작기 때문에 **이상치** 라고 표현을 합니다.

2행 2열의 위치에 seaborn에서 제공하는 boxplot을 시각화해봅시다.

fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(nrows=2,ncols=2)

fig.set_size_inches(12, 10)
sns.boxplot(data=bike_df, y="count",orient="v",ax=ax1)
sns.boxplot(data=bike_df, y="count",x="season",orient="v",ax=ax2)
sns.boxplot(data=bike_df, y="count",x="hour",orient="v",ax=ax3)
sns.boxplot(data=bike_df, y="count",x="workingday",orient="v",ax=ax4)

ax1.set(ylabel='Count',title="Count")
ax2.set(xlabel='Season', ylabel='Count',title="Season Count")
ax3.set(xlabel='Hour Of The Day', ylabel='Count',title="Hour Of The Day Count")
ax4.set(xlabel='Working Day', ylabel='Count',title="Working Day Count")

->


Working day(평일, 주말 구분)을 보니 이런 생각이 듭니다. 혹시, 평일, 주말로만 비교할 것이 아니라 요일별로 대여량도 한 번 시각화해보면 어떨까? 앞서 datetime으로부터 연, 월, 일, 시간을 추출했었습니다. 그런데 요일 또한 비슷한 방법으로 추출이 가능합니다.

시리즈의 datetime 타입을 위한 함수인 dt.dayofweek를 사용하면 요일을 추출합니다.

bike_df["dayofweek"] = bike_df["datetime"].dt.dayofweek
bike_df.shape

->

(10886, 17)

열이 1개 더 증가한 것을 볼 수 있습니다. 어떤 값들이 생겼는지 확인해봅시다.

bike_df["dayofweek"].value_counts()

->

5    1584
6    1579
3    1553
2    1551
0    1551
1    1539
4    1529
Name: dayofweek, dtype: int64

0부터 5까지의 값이 생겼으며 각각의 값은 약 1,500개씩 존재합니다.

참고로 0은 월요일이고 그 후 차례대로 요일이 배정되어 6이 일요일에 해당됩니다.

이번에는 요일도 포함하여 시각화를 해봅시다.

# 이번에는 행만 5개입니다.
fig, (ax1, ax2, ax3, ax4, ax5)= plt.subplots(nrows=5)
fig.set_size_inches(20,25)

sns.pointplot(data=bike_df, x="hour", y="count", ax=ax1)
sns.pointplot(data=bike_df, x="hour", y="count", hue="workingday", ax=ax2)
sns.pointplot(data=bike_df, x="hour", y="count", hue="dayofweek", ax=ax3)
sns.pointplot(data=bike_df, x="hour", y="count", hue="weather", ax=ax4)
sns.pointplot(data=bike_df, x="hour", y="count", hue="season", ax=ax5)

->

첫번째 그래프인 시간별 대여량을 보면, 주로 출퇴근 시간에 많이 대여합니다.

두번째 그래프인 평일과 주말을 비교해서보면 평일에는 주로 출, 퇴근 시간에 대여량이 많지만, 주말에는 이보다는 오후 시간대에 대여량이 더 많습니다.

세번째 그래프인 요일별 대여량을 보면 5, 6이 토요일, 일요일인만큼 역시나 오후 대여량이 더 많으며, 월, 화, 수, 목, 금의 경우에는 출, 퇴근 시간에 대여량이 많습니다.

네번째 그래프인 날씨별 대여량을 보면 눈, 비가 올 때는 대여량이 굉장히 적습니다.

다섯번째 그래프인 계절별 대여량을 보면 봄에 대여량이 가장 적은 편입니다.


9) 상관분석

수치형 데이터들의 상관분석을 진행하고, heatmap을 통해 시각화해봅시다.

cols = ["temp", "atemp", "humidity", "windspeed", "count"]
corr = bike_df[cols].corr(method='pearson')
plt.figure(figsize=(8, 6))
sns.set(font_scale=1.5)
hm = sns.heatmap(corr.values,
            cbar=True,
            annot=True, 
            square=True,
            fmt='.2f',
            annot_kws={'size': 15},
            yticklabels=cols,
            xticklabels=cols)

plt.tight_layout()
plt.show()

->

위 상관분석 결과를 보면 풍속은 거의 연관관계가 없습니다.

atemp와 temp 서로는 0.98로 상관관계가 높지만 이 둘은 의미하는 바가 온도와 체감온도로 사실상 같은 데이터라고 봐도 무방하여 예측을 위해 사용하는 열, 즉, 특성(feature)으로 사용할 때는 둘 다 사용하기 보다는 둘 중 하나만 사용하는 게 낫겠다 라는 생각을 해볼 수 있습니다.


10) 정규화 (정규분포화)

우리가 예측해야하는 대상인 종속 변수인 count에 대해서 데이터의 분포를 확인해봅시다.

bike_df['count'].plot(kind='hist', bins=50)

다음과 같이 분포가 0~200 사이의으로 몰려있는 것을 확인할 수 있습니다. 이렇게 데이터가 한 쪽으로 몰려있는 경우를 우리는 '데이터의 분포가 정규 분포가 아니다' 라고 말합니다.


정규 분포란, 평균에 가장 많은 값이 몰려있고, 평균값을 기준으로 데이터가 좌우 대칭의 종 모양의 분포를 가질 때를 말합니다. 정규 분포의 예시 그림은 다음과 같습니다.

위 데이터는 25~29세의 남성의 평균키의 분포를 표현한 데이터인데요. 평균값 172.5를 기준으로 좌, 우 대칭의 분포를 보이고 있어 25~29세의 남성의 평균키 데이터는 정규 분포를 따른다라고 말할 수 있습니다. 일반적으로 사람의 키와 몸무게는 평균인 사람이 많고, 심하게 마르거나 심하게 살이 찐 사람은 적기 때문에 정규 분포를 따르는 편입니다.

부의 분배로 예시로 들면, 평균 중산층이 가장 많고, 부자와 가난한 자의 수는 상대적으로 적은 사회구조에 비유할 수 있겠습니다. 빈부격차가 심한 나라는 위처럼 종모양이 아니라, 양 극단이 높고 평균층은 오히려 적은 형태로 나타날 수 있습니다.

자, 다시 본론으로 돌아가서 선형 회귀를 수행할 때, 레이블이 정규 분포를 따르지 않으면 모델의 성능에 악영향을 줄 수도 있습니다. 현재 count 값은 정규 분포를 따르지 않고 있는 상태구요. 그런데, 데이터를 바꿀 수도 없는 노릇인데 이를 어떻게 하면 좋을까요?


데이터가 왜곡된 분포를 보일 때, 정규 분포 형태로 바꾸는 가장 일반적인 방법은 데이터 전체에 로그를 적용하는 것입니다. 주로 Numpy의 log1p를 사용합니다.

numpy의 log1p는 입력값에 +1을 더해준 뒤에 log를 씌워주는 역할을 합니다.

가령, 입력 x에 np.log1p()를 사용하면 x에 1을 더한 뒤에 log를 씌워주는 것이죠.

  • np.log1p(x) : log(x + 1)

Numpy의 log1p()를 사용한 후의 분포를 봅시다.

y_log_transform = np.log1p(bike_df['count'])
y_log_transform.plot(kind='hist', bins=50)

로그로 Target 값을 변환한 후에 원하는 정규 분포 형태는 아니지만

변환하기 전보다는 정규 분포에 가깝도록 많이 개선된 것을 확인할 수 있습니다.

좋은 웹페이지 즐겨찾기