Kaggle 를 통해 처음 공부해볼 것은 "Bike Sharing Demand" 이다.
train.csv 데이터를 이용해 학습하고 test.csv 를 활용하여 count 를 예측하는데에 목적을 둔다..
Data Fields
- datetime : 날짜
- season : 1 = spring | 2 = summer | 3 = fall | 4 = winter
- holiday : 휴일인지 여부
- workingday : 근무일인지 여부
- weather : 1, 2, 3, 4 종류별로 다름
- temp : 온도
- atemp : 체감온도
- humidity : 습기
- windspeed : 풍속
- casual : 등록되지 않은 사용자 기록
- registered : 등록된 사용자 기록
- count : 대여 횟수
test.csv 파일에는 세 가지 항목(casual, registered, count)이 없다.
import pandas as pd
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
# 노트북 안에 그래프를 그리기 위해
%matplotlib inline
# 그래프에서 마이너스 폰트 깨지는 문제에 대한 대처
plt.style.use('ggplot')
# 그래프에서 마이너스 폰트 깨지는 문제에 대한 대처
mpl.rcParams['axes.unicode_minus'] = False
# seaborn 그래프 한글 깨짐 방지
plt.rcParams['font.family'] = 'NanumGothic'
코드 진행에 필요한 라이브러리를 모두 선언한다.
train = pd.read_csv("/SBA/bike/train.csv", parse_dates = ["datetime"])
train.shape
------------------------------------------
(10886, 12)
pandas 를 통해 kaggle에서 주어진 train.csv 파일을 불러들인다.
* parse_dates : 파일(데이터) 안에 날짜/시간이 있을 때 데이터를 파싱하는 것이다.
Null 개수를 파악하는 3가지 방법(물론 더 있을 것이다).
1. info()
train.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 datetime64[ns]
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: datetime64[ns](1), float64(3), int64(8)
memory usage: 1020.7 KB
2. isnull().sum()
train.isnull().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
sum() 을 붙이는 이유는 보기 쉬운 숫자로 나오기 때문이다.
3. import missingno
import missingno as msno
msno.matrix(train, figsize = (12,5))
--------------------------------------------
<matplotlib.axes._subplots.AxesSubplot at 0x25cb280aa00>
모두 null 값이 없는 모습이다.
train["year"] = train["datetime"].dt.year
train["month"] = train["datetime"].dt.month
train["day"] = train["datetime"].dt.day
train["hour"] = train["datetime"].dt.hour
train["minute"] = train["datetime"].dt.minute
train["second"] = train["datetime"].dt.second
train.shape
---------------------------------------------------
(10886, 18)
날짜/시간을 세분화하여 조사하기 위해 분리하는 것이다.
column 이 12에서 18로 증가한 모습을 볼 수 있다.
# barplot
figure, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(nrows = 2, ncols = 3)
figure.set_size_inches(18, 8)
sns.barplot(data = train, x = "year", y = "count", ax = ax1)
sns.barplot(data = train, x = "month", y = "count", ax = ax2)
sns.barplot(data = train, x = "day", y = "count", ax = ax3)
sns.barplot(data = train, x = "hour", y = "count", ax = ax4)
sns.barplot(data = train, x = "minute", y = "count", ax = ax5)
sns.barplot(data = train, x = "second", y = "count", ax = ax6)
ax1.set(ylabel = 'Count', title = '연도별 대여량')
ax2.set(ylabel = 'Month', title = '월별 대여량')
ax3.set(ylabel = 'Day', title = '일별 대여량')
ax4.set(ylabel = 'Hour', title = '시간별 대여량')
ax5.set(ylabel = 'Minute', title = '분별 대여량')
ax6.set(ylabel = 'Second', title = '초별 대여량')
--------------------------------------------------------------------------
- 연도별 대여량 : 2011년 < 2012년
- 월별 대여량 : 6월 > 7~9월 > 10월 > 11월 > 12월 > ... // 겨울에 대여량이 적어진다.
- 일별 대여량 : 1~19일 데이터만 있으므로 사용불가. (test.csv에 나머지 날짜가 있다.)
- 시간별 대여량 : 출퇴근 시간에 급격히 상승하는 모습을 볼 수 있다.
- 분, 초별 대여량 : 의미없음.
# boxplot
fig, axes = plt.subplots(nrows = 2, ncols = 2)
fig.set_size_inches(12, 10)
sns.boxplot(data = train, y = "count", orient = 'v', ax = axes[0][0])
sns.boxplot(data = train, y = "count", x = "season", orient = 'v', ax = axes[0][1])
sns.boxplot(data = train, y = "count", x = "hour", orient = 'v', ax = axes[1][0])
sns.boxplot(data = train, y = "count", x= "workingday", orient = 'v', ax = axes[1][1])
axes[0][0].set(ylabel = "Count", title = "대여량")
axes[0][1].set(ylabel = "Season", title = "계절별 대여량")
axes[1][0].set(ylabel = "Hour Of the day", title = "시간별 대여량")
axes[1][1].set(ylabel = "Working Day", title = "근무일 여부에 따른 대여량")
- 계절별 대여량 : 가을에 가장 많은 것을 볼 수 있고 겨울에 가장 봄에 가장 적은 것을 볼 수 있다.
- 시간별 대여량 : barplot과 마찬가지로 출퇴근 시간에 높은 것을 볼 수 있다.
- 주중 / 주말별 대여량 : 근무날일 때는 출퇴근시간의 큰 격차가 보여지고 있다.
# pointplot
train["dayofweek"] = train["datetime"].dt.dayofweek
train.shape
--------------------------------------------------
(10886, 19)
# 시간대별 대여량
fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots(nrows = 5)
fig.set_size_inches(18, 25)
sns.pointplot(data = train, x = "hour", y = "count", ax = ax1)
sns.pointplot(data = train, x = "hour", y = "count", hue = "workingday", ax = ax2)
sns.pointplot(data = train, x = "hour", y = "count", hue = "dayofweek", ax = ax3)
sns.pointplot(data = train, x = "hour", y = "count", hue = "weather", ax = ax4)
sns.pointplot(data = train, x = "hour", y = "count", hue = "season", ax = ax5)
2번째 그래프 : 근무일에는 출퇴근 시간에 대여량이 많지만 주말에는 완만한 곡선이 그려지는 것을 알 수 있다.
3번째 그래프 : 요일별로 봤을 때도 토,일요일을 제외하고는 평일은 완만하다.
4번째 그래프 : 날씨로 봤을 때 날씨가 안좋을수록 대여량이 낮아지는 것을 볼 수 있다.
5번째 그래프 : 계절별 그래프로 봤을 때 가을 > 여름 > 겨울 > 봄 순으로 대여량이 적어지는 것을 볼 수 있다.
# heatmap
corrMatt = train[["temp", "atemp", "casual", "registered", "humidity", "windspeed", "count"]]
corrMatt = corrMatt.corr()
print(corrMatt)
mask = np.array(corrMatt)
mask[np.tril_indices_from(mask)] = False
--------------------------------------------------------------------------
temp atemp casual registered humidity windspeed \
temp 1.000000 0.984948 0.467097 0.318571 -0.064949 -0.017852
atemp 0.984948 1.000000 0.462067 0.314635 -0.043536 -0.057473
casual 0.467097 0.462067 1.000000 0.497250 -0.348187 0.092276
registered 0.318571 0.314635 0.497250 1.000000 -0.265458 0.091052
humidity -0.064949 -0.043536 -0.348187 -0.265458 1.000000 -0.318607
windspeed -0.017852 -0.057473 0.092276 0.091052 -0.318607 1.000000
count 0.394454 0.389784 0.690414 0.970948 -0.317371 0.101369
count
temp 0.394454
atemp 0.389784
casual 0.690414
registered 0.970948
humidity -0.317371
windspeed 0.101369
count 1.000000
fig, ax = plt.subplots()
fig.set_size_inches(20,10)
sns.heatmap(corrMatt, mask = mask, vmax = .8, square = True, annot = True)
- 온도, 습도, 풍속은 거의 연관관계가 없다.
- 대여량과 가장 연관이 높은 건 registered 이지만 test 데이터에는 이 값이 없다.
# regplot
fig, (ax1, ax2, ax3) = plt.subplots(ncols = 3)
fig.set_size_inches(12, 5)
sns.regplot(x = "temp", y = "count", color = 'red', data = train, ax = ax1)
sns.regplot(x = "windspeed", y = "count", color = 'blue', data = train, ax = ax2)
sns.regplot(x = "humidity", y = "count", color = 'green', data = train, ax = ax3)
regplot 은 산점도 그래프이다.
산점도 그래프를 사용하면 어느 부분에 데이터들이 뭉쳐있는지 쉽게 알아볼 수 있다.
풍속이 없을 때에는 0으로 몰려있는 모습을 볼 수 있다. 0 인 데이터들은 없애는 게 좋은 것 같다.
# 년도와 월을 붙여서 보자.
def concatenate_year_month(datetime):
return "{0}-{1}".format(datetime.year, datetime.month)
train["year_month"] = train["datetime"].apply(concatenate_year_month)
print(train.shape)
train[["datetime", "year_month"]].head()
------------------------------------------------------
(10886, 20)
datetime year_month
0 2011-01-01 00:00:00 2011-1
1 2011-01-01 01:00:00 2011-1
2 2011-01-01 02:00:00 2011-1
3 2011-01-01 03:00:00 2011-1
4 2011-01-01 04:00:00 2011-1
* Pandas - apply(func, axis)
잘 알지 못하는 기능은 짚고 넘어가자.
apply 에는 함수가 들어간다. 그 함수에서 반환되는 값이 데이터프레임을 업데이트 시킨다.
이 경우에서는
datetime.year : "2011"
datetime.month : "1"
이 두 데이터를 "2011-1" 로 이어서 반환한 것이다. 그것을 year_month 라는 새로운 컬럼을 만들어 넣었다.
fig, (ax1, ax2) = plt.subplots(nrows = 1, ncols = 2)
fig.set_size_inches(18, 4)
sns.barplot(data = train, x = "year", y = "count", ax = ax1)
sns.barplot(data = train, x = "month", y = "count", ax = ax2)
fig, ax3 = plt.subplots(nrows = 1, ncols = 1)
fig.set_size_inches(18, 4)
sns.barplot(data = train, x = "year_month", y = "count", ax = ax3)
- 시간이 지날수록 대여량이 증가하는 추세인 것을 알 수 있다.
# trainWithoutOutliers : 0에 몰려있는 데이터
trainWithoutOutliers = train[np.abs(train["count"] - train["count"].mean()) <= (3 * train["count"].std())]
print(train.shape)
print(trainWithoutOutliers.shape)
-------------------------------------------------------
(10886, 20)
(10739, 20)
이제 코드 해석을 해봐야겠다. 이번 코드는 한 눈에 안들어온다.
# train[np.abs(train["count"] - train["count"].mean()) <= (3 * train["count"].std())]
각각의 count 값에서 count의 평균을 빼고 그 값을 절대값을 씌운다.
그래서 그 값이 count의 표준편차 곱하기 3한 값보다 작거나 같은 값을 trainWithoutOutliers 에 넣는다.
이상치를 제거하는 부분이다.
표준편차에 3을 곱하는 것은 3시그마 법칙을 사용하여 모든 데이터의 99.7% 를 사용하고 아닌 것은 걸러내는 것 같다.
# distplot, probplot
# count값의 데이터 분포도 파악
figure, axes = plt.subplots(ncols = 2, nrows = 2)
figure.set_size_inches(12, 10)
sns.distplot(train["count"], ax = axes[0][0])
stats.probplot(train["count"], dist = "norm", fit = True, plot = axes[0][1])
sns.distplot(np.log(trainWithoutOutliers["count"]), ax = axes[1][0])
stats.probplot(np.log1p(trainWithoutOutliers["count"]), dist = 'norm', fit = True, plot = axes[1][1])
-------------------------------------------------------------------------------------------
((array([-3.82819677, -3.60401975, -3.48099008, ..., 3.48099008,
3.60401975, 3.82819677]),
array([0.69314718, 0.69314718, 0.69314718, ..., 6.5971457 , 6.59850903,
6.5998705 ])),
(1.3486990121229776, 4.562423868087808, 0.9581176780909612))
이상치를 제거한 후 봤더니 count변수가 오른쪽에 치우쳐져 있다.
대부분의 기계학습은 종속변수가 normal 이어야 하기에 정규분포를 갖는 것이 바람직하다.
대안으로 outlier data를 제거하고 "count"변수에 로그를 씌워 변경해 봐도 정규분포를 따르지는 않지만
이전 그래프보다는 좀 더 자세히 표현하고 있다.
정규분포를 따르기 위해서는 결측치, 이상치 등등을 잘 해결해야 하는 것 같다.
이 부분에 대해서 좀 더 공부해봐야겠다.
* 공부자료 출처 : Kaggle & Youtube