머신러닝·딥러닝 문제해결 전략 책을 읽으면서
Kaggle 경진대회 코드와 문제해결 전략을 정리한 글
import numpy as np
import pandas as pd
# 데이터 경로
data_path = '/kaggle/input/bike-sharing-demand/'
train = pd.read_csv(data_path + 'train.csv') # 훈련 데이터
test = pd.read_csv(data_path + 'test.csv') # 테스트 데이터
submission = pd.read_csv(data_path + 'sampleSubmission.csv') # 제출 샘플 데이터
train.shape, test.shape
((10886, 12), (6493, 9))
# 훈련 데이터 확인
train.head()
datetime | season | holiday | workingday | weather | temp | atemp | humidity | windspeed | casual | registered | count | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2011-01-01 00:00:00 | 1 | 0 | 0 | 1 | 9.84 | 14.395 | 81 | 0.0 | 3 | 13 | 16 |
1 | 2011-01-01 01:00:00 | 1 | 0 | 0 | 1 | 9.02 | 13.635 | 80 | 0.0 | 8 | 32 | 40 |
2 | 2011-01-01 02:00:00 | 1 | 0 | 0 | 1 | 9.02 | 13.635 | 80 | 0.0 | 5 | 27 | 32 |
3 | 2011-01-01 03:00:00 | 1 | 0 | 0 | 1 | 9.84 | 14.395 | 75 | 0.0 | 3 | 10 | 13 |
4 | 2011-01-01 04:00:00 | 1 | 0 | 0 | 1 | 9.84 | 14.395 | 75 | 0.0 | 0 | 1 | 1 |
# 테스트 데이터 확인
test.head()
datetime | season | holiday | workingday | weather | temp | atemp | humidity | windspeed | |
---|---|---|---|---|---|---|---|---|---|
0 | 2011-01-20 00:00:00 | 1 | 0 | 1 | 1 | 10.66 | 11.365 | 56 | 26.0027 |
1 | 2011-01-20 01:00:00 | 1 | 0 | 1 | 1 | 10.66 | 13.635 | 56 | 0.0000 |
2 | 2011-01-20 02:00:00 | 1 | 0 | 1 | 1 | 10.66 | 13.635 | 56 | 0.0000 |
3 | 2011-01-20 03:00:00 | 1 | 0 | 1 | 1 | 10.66 | 12.880 | 56 | 11.0014 |
4 | 2011-01-20 04:00:00 | 1 | 0 | 1 | 1 | 10.66 | 12.880 | 56 | 11.0014 |
훈련 데이터를 활용해 모델을 훈련한 뒤, 테스트 데이터를 활용해 대여 수량(count)을 예측해야 하는데
테스트 데이터에 casual과 registered 피처가 없으므로 모델을 훈련할 때도 훈련 데이터의 casual과 registered 피처를 빼야함
# 제출 샘플 파일 확인
submission.head()
datetime | count | |
---|---|---|
0 | 2011-01-20 00:00:00 | 0 |
1 | 2011-01-20 01:00:00 | 0 |
2 | 2011-01-20 02:00:00 | 0 |
3 | 2011-01-20 03:00:00 | 0 |
4 | 2011-01-20 04:00:00 | 0 |
제출 파일은 데이터를 구분하는 ID 값(datetime)과 타깃값으로 구성되어 있음
여기서 ID 값(datetime)은 데이터를 구분하는 역할만 하므라 타깃값을 예측하는 데에는 아무런 도움을 주지 않음
따라서 추후 모델 훈련 시 훈련 데이터에 있는 datetime 피처는 제거할 계획
(atetime 피처는 연도, 월, 시간 등의 정보를 포함하기 때문에 이들 정보를 추출한 뒤 제거)
# info() 함수를 사용하여 DataFrame 각 열의 결측값이 몇 개인지, 데이터 타입은 무엇인지 파악
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 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
test.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6493 entries, 0 to 6492
Data columns (total 9 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 datetime 6493 non-null object
1 season 6493 non-null int64
2 holiday 6493 non-null int64
3 workingday 6493 non-null int64
4 weather 6493 non-null int64
5 temp 6493 non-null float64
6 atemp 6493 non-null float64
7 humidity 6493 non-null int64
8 windspeed 6493 non-null float64
dtypes: float64(3), int64(5), object(1)
memory usage: 456.7+ KB
datetime 피처는 연도, 월, 일, 시간, 분, 초로 구성되어 있음
따라서 세부적으로 분석해보기 위해 구성요소별로 나누어 피처 추가
datetime 피처는 object 타입이기 때문에 문자열처럼 다룰 수 있어 split() 함수를 사용해 공백 기준으로 문자를 나눔
print(train['datetime'][100]) # datetime 100번째 원소
print(train['datetime'][100].split()) # 공백 기준으로 문자열 나누기
print(train['datetime'][100].split()[0]) # 날짜
print(train['datetime'][100].split()[1]) # 시간
2011-01-05 09:00:00
['2011-01-05', '09:00:00']
2011-01-05
09:00:00
# 날짜 문자열을 연도, 월, 일로 나누기
print(train['datetime'][100].split()[0]) # 날짜
print(train['datetime'][100].split()[0].split("-")) # "-" 기준으로 문자열 나누기
print(train['datetime'][100].split()[0].split("-")[0]) # 연도
print(train['datetime'][100].split()[0].split("-")[1]) # 월
print(train['datetime'][100].split()[0].split("-")[2]) # 일
2011-01-05
['2011', '01', '05']
2011
01
05
# 시간 문자열을 시, 분, 초로 나누기
print(train['datetime'][100].split()[1]) # 시간
print(train['datetime'][100].split()[1].split(":")) # ":" 기준으로 문자열 나누기
print(train['datetime'][100].split()[1].split(":")[0]) # 시간
print(train['datetime'][100].split()[1].split(":")[1]) # 분
print(train['datetime'][100].split()[1].split(":")[2]) # 초
09:00:00
['09', '00', '00']
09
00
00
판다스 apply() 함수로 앞의 로직을 datetime에 적용해
날짜(date), 연도(year), 월(month), 일(day), 시(hour), 분(minute), 초(second) 피처 생성
train['date'] = train['datetime'].apply(lambda x: x.split()[0]) # 날짜 피처 생성
# 연도, 월, 일, 시, 분, 초 피처를 차례로 생성
train['year'] = train['datetime'].apply(lambda x: x.split()[0].split('-')[0])
train['month'] = train['datetime'].apply(lambda x: x.split()[0].split('-')[1])
train['day'] = train['datetime'].apply(lambda x: x.split()[0].split('-')[2])
train['hour'] = train['datetime'].apply(lambda x: x.split()[1].split(':')[0])
train['minute'] = train['datetime'].apply(lambda x: x.split()[1].split(':')[1])
train['second'] = train['datetime'].apply(lambda x: x.split()[1].split(':')[2])
calendar와 datetime 라이브러리를 활용해 요일 피처 생성
0은 월요일, 1은 화요일, 2는 수요일 순으로 매핑
(머신러닝 모델은 숫자만 인식하므로 모델을 훈련할 때는 피처 값을 문자로 바꾸면 안됨)
from datetime import datetime # datetime 라이브러리 임포트
import calendar
print(train['date'][100]) # 날짜
# datetime 타입으로 변경
print(datetime.strptime(train['date'][100], '%Y-%m-%d'))
# 정수로 요일 반환
print(datetime.strptime(train['date'][100], '%Y-%m-%d').weekday())
# 문자열로 요일 반환
print(calendar.day_name[datetime.strptime(train['date'][100], '%Y-%m-%d').weekday()])
2011-01-05
2011-01-05 00:00:00
2
Wednesday
앞의 로직을 apply() 함수로 적용해 요일(weekday) 피처 추가
train['weekday'] = train['date'].apply(
lambda dateString:
calendar.day_name[datetime.strptime(dateString, '%Y-%m-%d').weekday()])
season과 weather 피처는 범주형 데이터인데 현재 1, 2, 3, 4라는 숫자로 표현되어 있어서 정확히 어떤 의미인지 파악하기 어려움
시각화 시 의미가 잘드러나도록 map() 함수를 사용하여 문자열로 변화
train['season'] = train['season'].map({1: 'Spring',
2: 'Summer',
3: 'Fall',
4: 'Winter'})
train['weather'] = train['weather'].map({1: 'Clear',
2: 'Mist, Few clouds',
3: 'Light Snow, Rain, Thunderstorm',
4: 'Heavy Rain, Thunderstorm, Snow, Fog'})
train.head()
datetime | season | holiday | workingday | weather | temp | atemp | humidity | windspeed | casual | registered | count | date | year | month | day | hour | minute | second | weekday | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2011-01-01 00:00:00 | Spring | 0 | 0 | Clear | 9.84 | 14.395 | 81 | 0.0 | 3 | 13 | 16 | 2011-01-01 | 2011 | 01 | 01 | 00 | 00 | 00 | Saturday |
1 | 2011-01-01 01:00:00 | Spring | 0 | 0 | Clear | 9.02 | 13.635 | 80 | 0.0 | 8 | 32 | 40 | 2011-01-01 | 2011 | 01 | 01 | 01 | 00 | 00 | Saturday |
2 | 2011-01-01 02:00:00 | Spring | 0 | 0 | Clear | 9.02 | 13.635 | 80 | 0.0 | 5 | 27 | 32 | 2011-01-01 | 2011 | 01 | 01 | 02 | 00 | 00 | Saturday |
3 | 2011-01-01 03:00:00 | Spring | 0 | 0 | Clear | 9.84 | 14.395 | 75 | 0.0 | 3 | 10 | 13 | 2011-01-01 | 2011 | 01 | 01 | 03 | 00 | 00 | Saturday |
4 | 2011-01-01 04:00:00 | Spring | 0 | 0 | Clear | 9.84 | 14.395 | 75 | 0.0 | 0 | 1 | 1 | 2011-01-01 | 2011 | 01 | 01 | 04 | 00 | 00 | Saturday |
date 피처가 제공하는 정보는 모두 year, month, day 피처에도 있으므로 추후 date 피처는 제거
또한 세분화된 month 피처를 세 달씩 묶으면 season 피처와 의미가 같아짐
지나치게 세분화된 피처를 더 큰 분류로 묶으면 성능이 좋아지는 경우가 있어 여기서는 season 피처만 남기고 month 피처는 제거
import seaborn as sns
import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline
# 타깃값인 count의 분포도 그리기
# 이번 장에서는 타깃값의 분포를 알면 훈련 시 타깃값을 그대로 사용할지 변환해 사용할지 파악할 수 있기 때문
mpl.rc('font', size=15) # 폰트 크기를 15로 설정
sns.displot(train['count']) # 분포도 출력
<seaborn.axisgrid.FacetGrid at 0x7f11587a4650>
x축은 타깃값인 count를 나타내고, y축은 총 개수를 나타냄
분포도를 보면 타깃값인 count가 0 근처에 몰려 있음. 즉, 분포가 왼쪽으로 많이 편향되어 있음
회귀 모델이 좋은 성능을 내려면 데이터가 정규분포를 따라야 하는데,
현재 타깃값 count는 정규분포를 따르지 않음
따라서 현재 타깃값을 그대로 사용해 모델링한다면 좋은 성능을 기대하기 어려움
데이터 분포를 정규분포에 가깝게 만들기 위해 가장 많이 사용하는 방법은 로그변환
로그변환은 count 분포와 같이 데이터가 왼쪽으로 편향되어 있을 때 사용
# count를 로그변환한 값의 분포
sns.displot(np.log(train['count']))
<seaborn.axisgrid.FacetGrid at 0x7f1151678c10>
타깃값 분포가 정규분포에 가까울수록 회귀 모델 성능이 좋음
피처를 바로 활용해 count를 예측하는 것보다 log(count)를 예측하는 편이 더 정확
따라서 여기서도 타깃값을 log(count)로 변환해 사용
다만, 마지막에 지수변환을 하여 실제 타깃값인 count로 복원해야 함
# 스텝 1: m행 n열 Figure 준비하기
mpl.rc('font', size=14) # 폰트 크기 설정
mpl.rc('axes', titlesize=15) # 각 축의 제목 크기 설정
figure, axes = plt.subplots(nrows=3, ncols=2) # 3행 2열 Figure 생성 (서브플롯 전체가 figure 변수에 할당되며, 각각의 서브플롯 축 6개는 axes 변수에 할당)
plt.tight_layout() # 그래프 사이에 여백 확보
figure.set_size_inches(10, 9) # 전체 Figure 크기를 10x9인치로 설정 (너비 10인치, 높이 9인치)
# 스텝 2 : 각 축에 서브플롯 할당
# 각 축에 연도, 월, 일, 시간, 분, 초별 평균 대여 수량 막대 그래프 할당
sns.barplot(x='year', y='count', data=train, ax=axes[0, 0])
sns.barplot(x='month', y='count', data=train, ax=axes[0, 1])
sns.barplot(x='day', y='count', data=train, ax=axes[1, 0])
sns.barplot(x='hour', y='count', data=train, ax=axes[1, 1])
sns.barplot(x='minute', y='count', data=train, ax=axes[2, 0])
sns.barplot(x='second', y='count', data=train, ax=axes[2, 1])
# 스텝 3 : 세부 설정 (각 서브플롯에 제목을 추가하고, x축 라벨이 겹치지 않도록 개선)
# 3-1 : 서브플롯에 제목 달기
axes[0, 0].set(title='Rental amounts by year')
axes[0, 1].set(title='Rental amounts by month')
axes[1, 0].set(title='Rental amounts by day')
axes[1, 1].set(title='Rental amounts by hour')
axes[2, 0].set(title='Rental amounts by minute')
axes[2, 1].set(title='Rental amounts by second')
# 3-2 : 1행에 위치한 서브플롯들의 x축 라벨 90도 회전
axes[1, 0].tick_params(axis='x', labelrotation=90)
axes[1, 1].tick_params(axis='x', labelrotation=90)
# axis 파라미터에 원하는 축을 명시하고 labelrotation 파라미터에 회전 각도를 입력
# 스텝 1 : m행 n열 Figure 준비
figure, axes = plt.subplots(nrows=2, ncols=2) # 2행 2열
plt.tight_layout()
figure.set_size_inches(10, 10)
# 스텝 2 : 서브플롯 할당
# 계절, 날씨, 공휴일, 근무일별 대여 수량 박스플롯
sns.boxplot(x='season', y='count', data=train, ax=axes[0, 0])
sns.boxplot(x='weather', y='count', data=train, ax=axes[0, 1])
sns.boxplot(x='holiday', y='count', data=train, ax=axes[1, 0])
sns.boxplot(x='workingday', y='count', data=train, ax=axes[1, 1])
# 스텝 3 : 세부 설정
# 3-1: 서브플롯에 제목 달기
axes[0, 0].set(title='Box Plot On Count Across Season')
axes[0, 1].set(title='Box Plot On Count Across Weather')
axes[1, 0].set(title='Box Plot On Count Across Holiday')
axes[1, 1].set(title='Box Plot On Count Across Working Day')
# 3-2 : x축 라벨 겹침 해결
axes[0, 1].tick_params(axis='x', labelrotation=10) # 10도 회전
# 스텝 1 : m행 n열 Figure 준비
mpl.rc('font', size=11)
figure, axes = plt.subplots(nrows=5) # 5행 1열
figure.set_size_inches(12, 18)
# 스텝 2 : 서브플롯 할당
# 근무일, 공휴일, 요일, 계절, 날씨에 따른 시간대별 평균 대여 수량 포인트플롯
sns.pointplot(x='hour', y='count', data=train, hue='workingday', ax=axes[0])
sns.pointplot(x='hour', y='count', data=train, hue='holiday', ax=axes[1])
sns.pointplot(x='hour', y='count', data=train, hue='weekday', ax=axes[2])
sns.pointplot(x='hour', y='count', data=train, hue='season', ax=axes[3])
sns.pointplot(x='hour', y='count', data=train, hue='weather', ax=axes[4])
<AxesSubplot:xlabel='hour', ylabel='count'>
# 스텝 1 : m행 n열 Figure 준비
mpl.rc('font', size=15)
figure, axes = plt.subplots(nrows=2, ncols=2) # 2행 2열
plt.tight_layout()
figure.set_size_inches(7, 6)
# 스텝 2 : 서브플롯 할당
# 온도, 체감 온도, 풍속, 습도 별 대여 수량 산점도 그래프
sns.regplot(x='temp', y='count', data=train, ax=axes[0, 0],
scatter_kws={'alpha': 0.2}, line_kws={'color': 'blue'})
sns.regplot(x='atemp', y='count', data=train, ax=axes[0, 1],
scatter_kws={'alpha': 0.2}, line_kws={'color': 'blue'})
sns.regplot(x='windspeed', y='count', data=train, ax=axes[1, 0],
scatter_kws={'alpha': 0.2}, line_kws={'color': 'blue'})
sns.regplot(x='humidity', y='count', data=train, ax=axes[1, 1],
scatter_kws={'alpha': 0.2}, line_kws={'color': 'blue'})
# regplot() 함수의 파라미터 중 scatter_kws={'alpha': 0.2}는 산점도 그래프에 찍히는 점의 투명도를 조절.
# alpha를 0.2로 설정하면 평소에 비해 20% 수준으로 투명해짐 (alpha가 1이면 완전 불투명하고, 0이면 완전 투명해서 안보임)
# line_kws={'color': 'blue'}는 회귀선의 색상을 선택하는 파라미터 (회귀선이 잘 보이도록 그래프에 찍히는 점보다 짙은 색으로 설정)
<AxesSubplot:xlabel='humidity', ylabel='count'>
train[['temp', 'atemp', 'humidity', 'windspeed', 'count']].corr()
temp | atemp | humidity | windspeed | count | |
---|---|---|---|---|---|
temp | 1.000000 | 0.984948 | -0.064949 | -0.017852 | 0.394454 |
atemp | 0.984948 | 1.000000 | -0.043536 | -0.057473 | 0.389784 |
humidity | -0.064949 | -0.043536 | 1.000000 | -0.318607 | -0.317371 |
windspeed | -0.017852 | -0.057473 | -0.318607 | 1.000000 | 0.101369 |
count | 0.394454 | 0.389784 | -0.317371 | 0.101369 | 1.000000 |
# corr() 함수로 구한 상관관계 매트릭스를 heatmap() 함수에 인수로 넣어주기
corrMat = train[['temp', 'atemp', 'humidity', 'windspeed', 'count']].corr() # 피처 간 상관관계 매트릭스
fig, ax = plt.subplots()
fig.set_size_inches(10, 10)
sns.heatmap(corrMat, annot=True) # 상관관계 히트맵 그리기 (annot=True : 상관계수가 숫자로 표시됨)
ax.set(title='Heatmap of Numerical Data')
[Text(0.5, 1.0, 'Heatmap of Numerical Data')]
타깃값인 count와의 상관관계가 중요