[캐글 안내서] ❸캐글러들은 머신러닝 딥러닝 문제를 어떻게 풀까?

[Must Have] 머신러닝·딥러닝 문제해결 전략
골든래빗 출판사
신백균 지음



* 1_ 괄호 안은 라이브러리 버전입니다. 버전을 명시하지 않은 라이브러리는 파이썬 표준 라이브러리라서 파이썬 버전을 따릅니다.
학습 목표
자전거 대여 수요 예측 경진대회에 참가하여 머신러닝 모델링 프로세스와 기본적인 회귀 모델들을 배우게 됩니다. 참가하기 전에 경진대회 세부 메뉴를 알아보고, 안내 사항을 숙지합니다. 이어서 캐글 코드를 활용해 데이터가 어떻게 구성되어 있는지 살펴보고, 그래프로 데이터를 시각화해봅니다. 간단한 회귀 모델을 훈련/평가하는 방법도 알아봅니다. 마지막으로 훈련된 모델로 예측한 결과를 제출해보고 순위까지 확인합니다.
학습 순서
  • 유형 및 평가지표 : 회귀, RMSLE
  • 탐색적 데이터 분석 : 분포도, 막대 그래프, 박스플롯, 포인트플롯, 산점도, 히트맵
  • 머신러닝 모델 : 선형 회귀, 릿지 회귀, 라쏘 회귀, 랜덤 포레스트 회귀
  • 피처 엔지니어링 : 파생 피처 추가, 피처 제거
  • 하이퍼파라미터 최적화 : 그리드서치
6.1 경진대회 이해
처음으로 접해볼 경진대회는 자전거 대여 수요 예측 경진대회Bike Sharing Demand Competition입니다. 2014년 5월에서 2015년 5월까지 약 1년 동안 개최되었으며, 총 3,242팀이 참가했습니다.
워싱턴 D.C의 자전거 무인 대여 시스템 과거 기록을 기반으로 향후 자전거 대여 수요를 예측하는 대회입니다. 자전거 무인 대여 시스템은 ‘서울시 따릉이’와 비슷한 시스템이라고 보면 됩니다. 사용자는 한 장소에서 자전거를 대여해 원하는 만큼 타고 다른 장소에 반납합니다.
본 대회는 플레이그라운드 대회playground competition입니다. 플레이그라운드 대회는 난이도가 낮은 연습용 대회입니다. 입문자도 참가할 수 있는 난이도입니다. 플레이그라운드 대회는 상금과 메달이 없는 경우가 많습니다. 대신 티셔츠나 머그잔 같은 상품이 주어지기도 합니다. 상금과 메달은 없지만 입문자가 프로세스를 익히고 실력을 키우기에 좋은 대회입니다.
주어진 데이터는 2011년부터 2012년까지 2년간의 자전거 대여 데이터입니다. 캐피털 바이크셰어 회사가 공개한 운행 기록에 다양한 외부 소스에서 얻은 당시 날씨 정보를 조합하여 만들었다고 합니다.2 대여 데이터는 한 시간 간격으로 기록되어 있습니다. 그중 훈련 데이터는 매달 1일부터 19일까지의 기록이고, 테스트 데이터는 매달 20일부터 월말까지의 기록입니다. 피처는 대여 날짜, 시간, 요일, 계절, 날씨, 실제 온도, 체감 온도, 습도, 풍속, 회원 여부입니다. 이 데이터를 활용해 시간별 자전거 대여 수량을 예측하면 됩니다. 예측할 값이 범주형 데이터3가 아니므로 본 대회는 회귀4 문제에 속합니다.

* 2_ Fanaee-T, Hadi, and Gama, Joao, Event labeling combining ensemble detectors and background knowledge, Progress in Artificial Intelligence (2013): pp. 1-15, Springer Berlin Heidelberg.
* 3_ 4.1절 ‘데이터 종류’ 참고
* 4_ 5.1.2절 ‘회귀’ 참고
피처와 타깃값이란?
머신러닝에서 피처feature는 원하는 값을 예측하기 위해 활용하는 데이터를 의미합니다. 타깃값target value은 예측해야 할 값입니다.
대여 날짜, 시간, 요일, 계절, 날씨, 온도를 활용하여 대여 수량을 예측하는 문제를 생각해 봅시다. 여기서 피처는 대여 날짜, 시간, 요일, 계절, 날씨, 온도입니다. 타깃값은 대여 수량입니다. 다른 말로 피처는 독립변수이고, 타깃값은 종속변수입니다.
피처를 우리말로 특성 혹은 특징이라고도 합니다. 표 형태의 자료구조에서 열column을 피처라고 생각하면 됩니다. 타깃값은 목푯값, 목표변수, 타깃변수라고 표현하기도 합니다.
전체 데이터 크기가 1.1MB로 작고, 피처 수도 적어 몸풀기용으로 적합한 대회입니다. 본 운동 전에 준비 운동을 해야 하듯이 이번 대회로 몸을 한번 풀어보겠습니다.
6.2 경진대회 접속 방법 및 세부 메뉴
이 책에서 처음 소개드리는 경진대회이니 접속 방법과 세부 메뉴를 자세히 설명하겠습니다. 다음 장부터는 중요 내용 위주로 간략히 설명드릴 테니 이번 장을 통해 캐글에서 경진대회를 찾고 훑어보는 인터페이스를 잘 익혀두길 바랍니다.
Note
캐글 사이트의 UI는 언제든 예고없이 바뀔 수 있습니다. 캐글의 UI가 책의 설명과 맞지 않다면 다음 주소에 공개해둔 온라인 문서를 참고하세요.
https://bit.ly/3IznqWn
6.2.1 경진대회 접속 방법
자전거 대여 수요 예측 경진대회에 접속해보겠습니다. 캐글 홈페이지 상단에 검색창이 있습니다.
▼ 캐글 검색창
❶ 검색창에 “bike sharing demand”라고 입력해 검색합니다. ❷ 이어서 Competitions 영역에서 가장 위에 나타난 경진대회를 클릭합니다.
▼ 자전거 대여 수요 예측 경진대회 검색
그러면 다음과 같이 자전거 대여 수요 예측 경진대회 메인 페이지가 뜹니다.
▼ 자전거 대여 수요 예측 경진대회 검색
경진대회 제목 바로 밑에는 주최 측, 참여한 팀 수, 대회 종료 시기가 표시되어 있습니다.
▼ 자전거 대여 수요 예측 경진대회 검색
자전거 대여 수요 예측 경진대회는 캐글이 주최했습니다. 총 3,242팀이 참여했으며, 현시점을 기준으로 7년 전에 종료되었습니다.
그 바로 아래에 세부 메뉴가 보이며, 다음 절에서 하나씩 자세히 살펴보겠습니다.
6.2.2 경진대회 메뉴 설명
경진대회의 공통 세부 메뉴를 왼쪽부터 차례로 소개하겠습니다.
Overview 메뉴
가장 먼저 Overview는 경진대회 전반을 소개하는 메뉴입니다. Description 페이지에 경진대 회 소개글이 있습니다. 경진대회에 참여하기 전, 가장 먼저 읽어야 하는 페이지입니다.
▼ 경진대회 Overview 메뉴(Description 페이지)
Evaluation 페이지에서는 평가지표와 제출 형식을 설명합니다. 평가지표는 등수를 매기는 데 사용되므로 주의 깊게 봐야 합니다.
▼ 경진대회 Overview 메뉴(Description 페이지)
본 대회의 평가지표는 RMSLERoot Mean Squared Logarithmic Error5입니다. 제출 형식은 일시(datetime)와 대여 수량(count)으로 구성되어 있습니다. 일시는 1시간 간격으로 기록돼 있고, 대여 수량은 모두 0입니다. 추후 우리가 만든 모델로 대여 수량을 예측해 값을 바꿔주면 됩니다. 즉, 우리가 구해야 하는 값은 일시별 대여 수량이고, 이 형식대로 파일로 만들어 제출해야 합니다.

* 5_ 5.1.2절 ‘회귀’ 참고
Data 메뉴
Overview의 내용을 모두 숙지했다면 다음으로 Data 메뉴를 봐야 합니다. 경진대회가 제공하는 데이터에 대해 설명해놓은 메뉴입니다. 어떤 피처를 사용해 어떤 값을 예측해야 하는지 설명되어 있습니다. 유의해서 읽어보시기 바랍니다.
▼ 경진대회 Data 메뉴
참고로, Data 메뉴 하단에 Data Explorer라는 항목이 있습니다. 여기서 데이터를 미리 살펴볼 수도 있습니다.
▼ Data Explorer 항목
❶ 왼쪽에 데이터 파일이 있습니다. 파일을 클릭하면 해당 데이터에 관한 정보를 오른쪽 영역에 표시해줍니다.
❷ 오른쪽엔 세부 탭이 있습니다. [Detail] 탭에서는 피처별 분포도와 실젯값을 볼 수 있습니다. [Compact] 탭은 분포도 없이 실젯값만 테이블 형태로 제공합니다. [Column] 탭에서는 피처별 통계를 볼 수 있습니다.
[Detail]과 [Compact] 탭에서는 필터 기능도 제공합니다. ❸ 각 피처에서 오른쪽 상단의 필터 아이콘을 클릭하면 다음과 같이 메뉴가 확장됩니다. MS 엑셀의 필터 기능과 비슷합니다. 오름차순, 내림차순 정렬을 할 수 있고, 값의 범위도 설정할 수 있습니다.
▼ 피처별 필터
이번에는 [Column] 탭의 화면을 자세히 보겠습니다.
▼ [Column] 탭이 제공하는 통계 정보
❶ 왼쪽에는 값의 분포를 보여주는 막대 그래프가 보이고, ❷ 오른쪽에는 몇 가지 통계를 보여줍니다. 통계는 다시 두 영역으로 나뉩니다. ❸ 영역은 모든 피처의 공통 요소입니다. Valid와 Mismatched는 해당 피처에 사전 정의된 값이 잘 들어가 있는지를 표시합니다. Valid는 사전 정의된 값으로 기록된 값의 비율을, Mismatched는 매칭되지 않는 값의 비율을 표시합니다. 그런데 캐글에서 사전 정의를 어떻게 했는지는 그리 중요한 요소가 아니므로 이 두 값은 크게 신경 쓰지 않아도 됩니다. 다음으로 Missing은 결측값 비율입니다. ❹ 영역은 해당 피처의 데이터 타입에 따라 다른 정보로 채워집니다. 피처가 실수형이나 정수형 타입이라면 평균, 표준편차, 최솟값, 25%값, 중간값, 75%값, 최댓값도 보여줍니다. 피처가 범주형 타입이면 고윳값 개수와 최빈값을 보여줍니다. 앞의 그림은 날짜(datetime)를 나타내는 타입이라 최솟값, 중앙값, 최댓값을 표시합니다.
지금까지 Data 메뉴 아래쪽의 Data Explorer 기능을 알아봤습니다. Data Explorer를 활용하면 시각화를 직접 해보지 않고도 어느 정도 수준까지는 데이터를 훑어볼 수 있습니다. 그럼에도 처음 보는 데이터에서 원하는 정보를 자유롭게 뽑아보려면 직접 시각화해보는 연습이 필요합니다. 그래서 이 책에서는 가능한 한 코드로 하나하나 시각화해볼 것입니다.
Code 메뉴
Code 메뉴에서 다른 참가자가 공유한 코드(노트북)를 볼 수 있습니다. 추천순Most Votes, 점수순Best Score으로 정렬해 상위권 코드 위주로 참고하면 됩니다.
▼ 경진대회 Code 메뉴
코드를 Public으로 설정해 공유하면 캐글 규정에 따라 아파치 2.0 라이선스가 적용됩니다. 아파치 2.0 라이선스는 누구나 해당 소프트웨어에서 파생된 프로그램을 만들 수 있으며 저작권을 양도, 전송할 수 있는 라이선스 규정입니다.6 따라서 Code 메뉴에 공유된 다른 참가자의 코드는 자유롭게 사용할 수 있습니다.

* 6_ https://ko.wikipedia.org/wiki/아파치_라이선스
Discussion 메뉴
경진대회에서 좋은 성적을 내려면 Discussion 메뉴도 잘 활용해야 합니다. 새로 알게 된 인사이트, 주의사항, 질의응답 등 경진대회에 도움되는 내용이 많이 올라오기 때문입니다. 특히 경진대회가 끝나면 상위권 캐글러들이 자신의 문제해결 노하우를 공개합니다. 추천순으로 정렬해 상위권 글을 쭉 읽어보는 것이 좋습니다.
Leaderboard 메뉴
Leaderboard 메뉴에서 참가자의 등수와 점수를 확인할 수 있습니다. 각 행에 순위, 팀명, 팀원, 점수, 결과 제출 횟수Entries, 최종 결과 제출 시기Last, 공유한 코드순으로 표시되어 있습니다. 참고로 해당 팀이 코드를 Public으로 공개하지 않으면 ‘Code’ 열에 아무것도 표시되지 않습니다.
▼ 자전거 대여 수요 예측 경진대회 Leaderboard 메뉴
자전거 대여 수요 예측 경진대회의 리더보드는 특수한 형태를 갖습니다. 그렇기 때문에 일반적인 리더보드를 기준으로 먼저 설명하겠습니다.
▼ 일반적인 경진대회 Leaderboard 메뉴
경진대회에서는 예측할 때 테스트 데이터를 사용합니다. 그런데 테스트 데이터 ‘전체’를 사용해 점수를 매기는 시기는 대회 종료 직후입니다. 한편 대회 종료 전까지는 보통 ‘일부’ 테스트 데이터만 사용해 점수를 매깁니다. 이 점수는 ‘Public Leaderboard’에서 확인할 수 있습니다. 다시 말해 Public Leaderboard는 대회 종료 전까지 대략적인 점수와 등수를 확인하는 곳입니다. 대회 종료 후에는 테스트 데이터 전체를 사용해 매긴 점수와 등수를 ‘Private Leaderboard’에서 확인할 수 있습니다. 최종적으로 Private Leaderboard를 기준으로 메달과 상금을 수여합니다.
▼ 대회 종료 전후 평가점수 산출에 사용되는 테스트 데이터 차이
Public Leaderboard에서 등수가 높더라도 Private Leaderboard에서 떨어질 수 있고, 그 반대도 가능합니다. 두 리더보드의 등수 차이가 큰 경우가 많을 때 셰이크업shake-up이 심하다고 합니다. 셰이크업이 심한 대회도 많기 때문에 Public Leaderboard에 집착할 필요는 없습니다. 어느 정도 참고만 하면 됩니다.
하지만 자전거 대여 수요 예측 경진대회는 둘의 차이가 없습니다. Private Leaderboard와 Public Leaderboard 모두 테스트 데이터 전체를 사용해 점수를 매겼기 때문입니다. 그래서 Private Leaderboard와 Public Leaderboard 메뉴가 나누어져 있지 않습니다. 연습용 대회라서 그렇습니다. 극히 예외적인 경우입니다.
Rules 메뉴
Rules 메뉴에서는 대회 규정을 볼 수 있습니다. 대회에 참가하려면 규정에 동의해야 합니다. 규정을 읽어보고 [I Understand and Accept] 버튼을 클릭하세요. 이미 다른 메뉴에서 동의를 했다면 “You have accepted the rules for this competition. Good luck!”이라는 문구가 뜰 겁니다.
▼ 경진대회 Rules 메뉴
대회마다 규정은 다르지만 몇 가지만 주의하면 됩니다.
  • 1 프라이빗private 코드를 팀원 외 다른 참가자와 공유하면 안 됩니다. 걸리면 참가 자격이 박탈됩니다.
  • 2 외부 데이터 사용이 불가능한 대회에서는 캐글에서 제공한 데이터만 사용해야 합니다.
  • 3 사전 훈련된 외부 모델 사용을 불허하는 대회에서는 본인이 훈련한 모델만 사용해야 합니다.
하지 말라고 규정한 것은 하지 않고, 상식선에서 지킬 것만 잘 지키면 됩니다.
Team 메뉴
Team 메뉴에서 팀을 꾸릴 수 있습니다. 초대하고 싶은 팀이나 캐글러 이름을 입력한 뒤 ‘Request Merge’를 누르면 초대가 됩니다. 참고로 대회에 이미 참가한 사람만 초대할 수 있습니다. 경진대회 페이지에서 ‘Join Competition’을 클릭해야 참가할 수 있다는 건 2장에서 이미 배웠죠? 초대받은 사람이 본인의 Team 메뉴에서 ‘Join This Team’을 클릭하면 수락이 됩니다.
❶ Team Name에서 팀명을 지정하면 리더보드에서 해당 팀명으로 표시됩니다. 5명까지 꾸릴 수 있으며, 팀원 목록에서 초대된 팀원을 볼 수 있습니다.
이상으로 경진대회 세부 메뉴를 모두 살펴봤습니다.
6.3 탐색적 데이터 분석
본격적으로 코드를 짜면서 대회 문제를 풀어보겠습니다.
문제를 해결하려면 당연히 주어진 문제가 무엇인지부터 이해해야 합니다. 이번 경진대회의 과제는 자전거 무인 대여 시스템의 과거 기록을 기반으로 향후 수요를 예측하는 것이었습니다. 이 문제를 풀려면 우선 주어진 데이터를 면밀히 살펴서 어느 데이터가 예측에 도움될지, 혹은 되지 않을지를 파악해야 합니다. 이를 파악하는 단계가 바로 탐색적 데이터 분석입니다.7 이 분석 과정을 다음 순서로 진행할 것입니다.

* 7_ 캐글 경진대회 프로세스에 관한 자세한 설명은 3장을 참고하세요.
이번 절에서 사용한 코드는 본 경진대회에서 추천수가 가장 많은 다음 코드를 참고했습니다.
6.3.1 캐글 노트북 환경 설정
캐글 노트북 환경이 기본으로 제공하는 라이브러리들은 시간이 지나면 조금씩 버전업됩니다. 물론 최신 버전을 쓰면 좋겠지만, 버전이 다르면 실행 결과가 책과 달라질 수 있습니다.
그래서 (반드시 필요한 과정은 아닙니다만) 라이브러리 버전을 책과 일치시키는 방법을 소개하겠습니다. 라이브러리 버전이 고정된 노트북 양식을 복사해서 사용하시면 됩니다. 노트북 양식은 제가 공유해놨으며, 장 시작 페이지의 표 마지막 줄에 링크를 적어두었습니다.
복사 방법은 아주 간단합니다. 공유된 노트북의 오른쪽 위 [Copy & Edit] 버튼을 클릭하면 끝입니다(2.6절에서 더 자세히 설명한 바 있습니다).
이로써 라이브러리 버전이 고정된 노트북 환경을 구축했습니다. 간단하죠? 이 노트북 환경에서 작업하시면 책과 같은 결과를 얻을 수 있습니다.
전체 코드가 적힌 캐글 노트북 복사하기
책의 내용을 빠르게 나의 것으로 만들고 싶다면 예제를 직접 타이핑해가며 따라 해보시길 추천드립니다. ‘백문이 불여일타!’ 사소한 오탈자 하나로 한참을 헤매실 수도 있지만, 그것마저도 다 경험이 되고 노하우가 됩니다. 하지만 아무리 살펴보고 인터넷을 검색해봐도 원인을 찾지 못하겠다면 언제까지고 매달릴 순 없겠지요. 그럴 땐 제가 공유한 캐글 노트북을 복사해 실행하신 후 본인의 노트북과 비교해보세요. 예제 코드 캐글 노트북 링크 역시 장 시작 페이지에 표로 정리해두었습니다.
6.3.2 데이터 둘러보기
이제 주어진 데이터가 어떻게 구성되어 있는지 살펴보겠습니다. 우선 판다스로 훈련, 테스트, 제출 샘플 데이터를 DataFrame 형태로 불러오겠습니다.
참고로, 판다스pandas란 표 형태의 데이터(정형 데이터)를 효율적으로 다루기 위한 라이브러리로, 고수준의 자료구조와 함수를 제공합니다. DataFrame은 판다스가 제공하는 대표적인 표 형태(행과 열로 구성)의 자료구조입니다.
https://www.kaggle.com/werooring/ch6-eda
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') # 제출 샘플 데이터
shape() 함수로 훈련 데이터와 테스트 데이터의 크기를 확인해보겠습니다.
train.shape, test.shape
((10886, 12), (6493, 9))
훈련 데이터는 10,886행 12열로 구성되어 있고, 테스트 데이터는 6,493행 9열로 구성되어 있습니다. 열의 개수는 피처 개수를 나타냅니다. 그런데 두 데이터의 피처 개수가 서로 다릅니다. 어떤 피처 데이터를 담고 있는지 직접 살펴보겠습니다. head() 함수는 DataFrame의 첫 5행을 출력합니다.
train.head()
▼ 실행 결과
다음 표에 각 피처의 의미를 설명해놓았습니다. 경진대회 [Data] 메뉴8에서 확인할 수 있는 내용입니다.
▼ 자전거 대여 수요 예측 경진대회 데이터의 피처들

* 9_ datetimeseason 피처의 값을 확인해보면 봄, 여름, 가을, 겨울은 실제로는 1분기, 2분기, 3분기, 4분기에 해당합니다. 예를 들어 spring 이 1~3월입니다. 데이터상으로는 계절을 분기로 해석해야 정확하겠으나, 이번 장의 설명은 경진대회의 정의를 따라 진행하겠습니다.
datetime부터 registered까지는 예측에 사용할 수 있는 피처고, count는 예측해야 할 타깃값입니다. datetime은 한 시간 간격으로 기록되어 있습니다. 결국 예측해야 할 값은 시간당 총 자전거 대여 수량입니다. 테스트 데이터도 한번 보겠습니다.
train.head()
▼ 실행 결과
테스트 데이터는 피처 수가 훈련 데이터보다 적습니다. 훈련 데이터의 피처에서 casualregistered가 빠졌습니다.
훈련 데이터를 활용해 모델을 훈련한 뒤, 테스트 데이터를 활용해 대여 수량(count)을 예측해야 합니다. 예측할 때 사용하는 데이터가 테스트 데이터입니다. 그런데 테스트 데이터에 casualregistered 피처가 없으므로 모델을 훈련할 때도 훈련 데이터의 casualregistered 피처를 빼야 합니다.
분석 결과
casual, registered 피처 제거
이제 제출 샘플 파일이 어떻게 생겼는지 보겠습니다.
submission.head()
▼ 실행 결과
제출 파일은 보통 이런 형태입니다. 데이터를 구분하는 ID 값(여기서는 datetime)과 타깃값으로 구성되어 있습니다. 현재는 타깃값인 count가 모두 0입니다. 시간대별 대여 수량을 예측해 이 값을 바꾼 뒤 제출하면 됩니다. 여기서 ID 값(datetime)은 데이터를 구분하는 역할만 하므로 타깃 값을 예측하는 데에는 아무런 도움을 주지 않습니다. 따라서 추후 모델 훈련 시 훈련 데이터에 있는 datetime 피처는 제거할 계획입니다.10

* 10_ datetime 피처는 연도, 월, 시간 등의 정보를 포함하기 때문에 이들 정보를 추출한 뒤에 제거할 계획입니다.
info() 함수를 사용하면 DataFrame 각 열의 결측값이 몇 개인지, 데이터 타입은 무엇인지 파악할 수 있습니다.
train.info()

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
모든 피처의 비결측값 데이터 개수(Non-Null Count)가 전체 데이터 개수와 똑같은 10,886개 이므로 훈련 데이터에는 결측값이 없습니다. 만약 결측값이 있다면 적절히 처리해줘야 합니다.11

* 11_ 결측값을 해당 피처의 평균값, 중앙값, 최빈값으로 대체하거나 결측값을 포함하는 피처를 아예 제거하는 방법이 있습니다. 또는 결측값을 타깃값으로 간주하고, 다른 피처를 활용해 결측값을 예측할 수도 있습니다. 결측값이 없는 데이터를 훈련 데이터, 결측값이 있는 데이터를 테스트 데이터로 생각해 모델링하면 됩니다. 8장과 9장에서 몇 가지 예를 만나볼 수 있습니다.
데이터 타입은 object, int64, float64로 다양하네요.
테스트 데이터도 살펴보겠습니다.
test.info()

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
테스트 데이터에도 결측값이 없고, 데이터 타입도 훈련 데이터와 동일합니다.
이상으로 이번 경진대회에서 사용할 데이터의 모습을 간단히 둘러보았습니다.
6.3.3 더 효과적인 분석을 위한 피처 엔지니어링
기본적인 분석을 마쳤다면 다음은 데이터 시각화 차례입니다. 데이터를 다양한 관점에서 시각화해보면 날 데이터raw data 상태에서는 찾기 어려운 경향, 공통점, 차이 등이 드러날 수 있기 때문입니다. 그런데 일부 데이터는 시각화하기에 적합하지 않은 형태일 수 있습니다. 본 경진대회에서는 datetime 피처가 그렇습니다. 시각화하기 전에 이 피처를 분석하기 적합하게 변환(피처 엔지니어링)해봅시다.
datetime 피처의 데이터 타입은 object입니다. 판다스에서 object 타입은 문자열 타입이라고 보면 됩니다. datetime은 연도, 월, 일, 시간, 분, 초로 구성되어 있습니다. 따라서 세부적으로 분석해보기 위해 구성요소별로 나누어보겠습니다. 파이썬 내장 함수인 split()을 쓰면 쉽게 나눌 수 있습니다. datetime의 100번째 원소를 예로 들어 어떻게 나누는지 설명하겠습니다.
분석 결과
연도, 월, 일, 시간, 분, 초 피처 추가
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
datetime 피처는 object 타입이기 때문에 문자열처럼 다룰 수 있습니다. 앞의 예에서는 split() 함수를 사용해 공백 기준으로 앞 뒤 문자를 나누었습니다. 첫 번째 문자열 ‘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) 피처를 생성하겠습니다.
Note
이처럼 기존 피처에서 파생된 피처를 ‘파생 피처’ 혹은 ‘파생 변수’라고 합니다.
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])
apply() 함수는 DataFrame의 데이터를 일괄 가공해줍니다. 보다시피 종종 람다lambda 함수와 함께 사용됩니다. 람다 함수를 DataFrame 축(기본값은 DataFrame의 각 열(column)에 대해 수행)을 따라 적용하는 기능을 합니다. 연도 피처를 추가하는 코드를 예로 동작 방식을 자세히 살펴보겠습니다.
이제 요일 피처도 생성해보겠습니다. 요일 피처는 calendardatetime ‘라이브러리’를 활용해 만들 수 있습니다. 여기서 datetime은 날짜와 시간을 조작하는 라이브러리로 datetime 피처와는 다른 것입니다. 날짜 문자열에서 요일을 추출하는 방법을 한 단계씩 알아보겠습니다.
from datetime import datetime # datetime 라이브러리 임포트 
import calendar

print(train['date'][100]) # 날짜
print(datetime.strptime(train['date'][100], '%Y-%m-%d')) # datetime 타입으로 변경 
# 정수로 요일 반환
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
다소 복잡하지만 calendardatetime 라이브러리를 사용하면 요일 피처를 문자로 구할 수 있습니다. 0은 월요일, 1은 화요일, 2는 수요일순으로 매핑됩니다. 단, 모델을 훈련할 때는 피처값을 문자로 바꾸면 안 됩니다. 머신러닝 모델은 숫자만 인식하기 때문입니다. 문자 피처도 모두 숫자로 변환해야 합니다. 여기서는 그래프로 나타냈을 때 쉽게 알아보려고 요일 피처를 문자열로 바꾼 겁니다.12

* 12_ 문자를 숫자로 바꾸는 문자 인코딩에 관해서는 7장에서 자세히 다룹니다.
Waring
머신러닝 모델은 숫자만 인식하므로 모델을 훈련할 때는 피처 값을 문자로 바꾸면 안 됩니다.
앞의 로직을 apply() 함수로 적용해 요일(weekday) 피처를 추가하겠습니다.
분석 결과
요일 피처 추가
train['weekday'] = train['date'].apply(
lambda dateString: 
calendar.day_name[datetime.strptime(dateString,"%Y-%m-%d").weekday()])
다음은 seasonweather 피처 차례입니다. 이 두 피처는 범주형 데이터인데 현재 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'})
이제 훈련 데이터의 첫 5행을 출력해 피처가 어떻게 바뀌었는지 보겠습니다.
train.head()
date, year, month, day, hour, minute, second, weekday 피처가 추가되었고,
seasonweather 피처는 숫자에서 문자로 바뀌었습니다.
참고로 date 피처가 제공하는 정보는 모두 year, month, day 피처에도 있어서 추후 date 피처는 제거하겠습니다.
분석 결과
date 피처 제거
또한 세 달씩 ‘월’을 묶으면 ‘계절’이 됩니다. 즉, 세분화된 month 피처를 세 달씩 묶으면 season 피처와 의미가 같아집니다. 지나치게 세분화된 피처를 더 큰 분류로 묶으면 성능이 좋아지는 경우가 있어 여기서는 season 피처만 남기고 month 피처는 제거하겠습니다.
분석 결과
month 피처 제거
6.3.4 데이터 시각화
피처를 추가한 훈련 데이터를 그래프로 시각화해보죠. 시각화는 탐색적 데이터 분석에서 가장 중요한 부분입니다. 데이터 분포나 데이터 간 관계를 한눈에 파악할 수 있기 때문입니다. 모델링에 도움될 만한 정보를 얻을 수도 있죠.
시각화를 위해 matplotlib과 seaborn 라이브러리를 활용하겠습니다. matplotlib은 파이썬으로 데이터를 시각화할 때 표준처럼 사용되는 라이브러리이며, seaborn은 matplotlib에 고수준 인터페이스를 덧씌운 라이브러리입니다.
먼저 두 라이브러리를 임포트합니다.
import seaborn as sns
import matplotlib as mpl
import matplotlib.pyplot as plt 
%matplotlib inline
참고로 코드 %matplotlib inline을 추가하면 matplotlib이 그린 그래프를 주피터 노트북에서 바로 출력해줍니다. 사실 캐글 환경에서는 %matplotlib inline이 없어도 그래프를 보여줍니다만, matplotlib을 사용하는 경우 관습적으로 쓰는 경향이 있어서 추가해뒀습니다.
import seaborn as sns?
‘seaborn’을 ‘sns’라는 이름으로 임포트하는 게 좀 어색할 겁니다. 직관적으로 잘 와닿지 않는 이름이죠. sns는 TV 드라마인 〈The West Wing〉의 등장인물 ‘새뮤얼 노먼 시 본Samuel Norman Seaborn’의 약자로 알려져 있습니다. 개발자 측에서는 “이름의 유래를 알면 재미날 것”이라고만 말하고 자세한 이야기는 밝히지 않았습니다. 어쨌든 모든 공식 예제가 sns로 쓰기 때문에 다른 사람들도 거의 이 관례를 따릅니다.
이번 절은 주어진 데이터를 살펴보는 데 집중합니다. 그래프 자체에 관한 좀 더 자세한 설명은 4장을 참고해주세요. 이제부터 이 라이브러리들을 활용하여 각종 그래프를 그려보겠습니다.
분포도
분포도distribution plot13는 수치형 데이터의 집계 값을 나타내는 그래프입니다. 집계 값은 총 개수나 비율 등을 의미합니다. 타깃값인 count의 분포도를 그려보겠습니다. 타깃값의 분포를 알면 훈련 시 타깃값을 그대로 사용할지 변환해 사용할지 파악할 수 있기 때문입니다.

* 13_ 4.3.3절 ‘분포도(displot)’ 참고
mpl.rc('font', size=15) # 폰트 크기를 15로 설정 
sns.displot(train['count']); # 분포도 출력
▼ 실행 결과 - 타깃값(count)의 분포
x축은 타깃값인 count를 나타내고, y축은 총 개수를 나타냅니다. 분포도를 보면 타깃값인 count가 0 근처에 몰려 있습니다. 즉, 분포가 왼쪽으로 많이 편향되어 있습니다. 회귀 모델이 좋은 성능을 내려면 데이터가 정규분포를 따라야 하는데, 현재 타깃값 count는 정규분포를 따르지 않습니다. 따라서 현재 타깃값을 그대로 사용해 모델링한다면 좋은 성능을 기대하기 어렵습니다.
TIP
회귀 모델이 좋은 성능을 내려면 데이터가 정규분포를 따라야 합니다.
데이터 분포를 정규분포에 가깝게 만들기 위해 가장 많이 사용하는 방법은 로그변환입니다. 로그변환은 count 분포와 같이 데이터가 왼쪽으로 편향되어 있을 때 사용합니다. 로그변환을 하는 방법은 간단합니다. 원하는 값에 로그를 취해주면 됩니다. count를 로그변환한 값의 분포를 살펴보겠습니다.
sns.displot(np.log(train['count']));
▼ 실행 결과 - 타깃값(count)의 분포
변환 전보다 정규분포에 가까워졌습니다. 타깃값 분포가 정규분포에 가까울수록 회귀 모델 성능이 좋다고 했습니다. 다시 말해, 피처를 바로 활용해 count를 예측하는 것보다 log(count)를 예측하는 편이 더 정확합니다. 따라서 우리도 타깃값을 log(count)로 변환해 사용하겠습니다.
분석 결과
타깃값을 count가 아닌 log(count)로 변환해 사용
다만, 마지막에 지수변환을 하여 실제 타깃값인 count로 복원해야 합니다. 다음 수식이 나타내는 바와 같이 log(y)를 지수변환하면 y가 됩니다.
분석 결과
마지막에는 지수변환하여 count로 복원
▼ 기존 타깃값과 로그변환한 타깃값 비교
막대 그래프
다음으로 연도, 월, 일, 시, 분, 초별로 총 여섯 가지의 평균 대여 수량을 막대 그래프14로 그려보겠습니다. 이 피처들은 범주형 데이터입니다. 각 범주형 데이터에 따라 평균 대여 수량이 어떻게 다른지 파악하려고 합니다. 그래야 어떤 피처가 중요한지 알 수 있습니다. 이럴 때 막대 그래프를 이용합니다. 막대 그래프는 seaborn의 barplot() 함수로 그릴 수 있습니다.

* 14_ 4.4.1절 ‘막대 그래프(barplot)’ 참고
이번에는 그래프를 여섯 개나 그려야 해서 코드에서 설명할 게 많습니다. 그래서 결과 그래프를 먼저 본 후, 어떻게 구현했는지를 이어서 살펴보겠습니다.
▼ 연도, 월, 일, 시, 분, 초별 평균 대여 수량 barplot
보다시피 총 6개의 그래프를 3행 2열로 배치해 그렸습니다. 어떤 정보를 담고 있는지 가볍게 훑어봅시다.
먼저, ❶ ‘연도별 평균 대여 수량’ 그래프를 봅시다. 2011년보다 2012년에 대여가 많았습니다.
❷번 그래프로는 ‘월별 평균 대여 수량의 추세’를 파악할 수 있습니다. 평균 대여 수량은 6월에 가장 많고 1월에 가장 적습니다. 날씨가 따뜻할수록 대여 수량이 많다고 짐작할 수 있습니다.
❸은 ‘일별 평균 대여 수량’ 그래프입니다. 일별 대여 수량에는 뚜렷한 차이가 없습니다. 소개 페이지에서 말했다시피 훈련 데이터에는 매월 1일부터 19일까지의 데이터만 있습니다. 나머지 20일부터 월말까지의 데이터는 테스트 데이터에 있습니다. 그래서 일자(day)는 피처로 사용하지 못 합니다. day를 피처로 사용하려면 훈련 데이터와 테스트 데이터에 공통된 값이 있어야 하는데, 훈련 데이터의 day와 테스트 데이터의 day는 전혀 다른 값을 갖기 때문입니다.
분석 결과
day 피처 제거
❹번 그래프는 ‘시간별 평균 대여 수량’입니다. 그래프 모양이 쌍봉형입니다. 새벽 4시에 대여 수량이 가장 적습니다. 당연하겠죠. 새벽 4시에 자전거를 타는 사람은 거의 없을 테니까요. 반면 아침 8시와 저녁 5~6시에 대여가 가장 많습니다. 사람들이 등하교 혹은 출퇴근길에 자전거를 많이 이용한다고 짐작해볼 수 있습니다.
❺와 ❻에 있는 분별, 초별 평균 대여 수량 그래프는 아무 정보도 담고 있지 않습니다. 훈련 데이터에 분과 초는 모두 0으로 기록되어 있기 때문입니다. 따라서 나중에 모델을 훈련할 때 분과 초 피처는 사용하지 않겠습니다.
분석 결과
minute, second 피처 제거
TIP
아무 정보도 담고 있지 않은 피처는 모델 훈련에 사용하지 않습니다.
자, 이제부터 앞의 그래프를 출력해준 코드를 살펴볼 차례입니다. 다음 그림과 같이 크게 3개 스텝으로 나눠 진행하겠습니다.
▼ m행 n열 Figure 작성 절차
Waring
책에서는 편의상 코드를 나눠 설명합니다만, 스텝 1~3 코드는 한 셀에서 실행해야 정상적으로 동작합니다.
스텝 1 : m행 n열 Figure 준비하기
첫 번째로 총 6개의 그래프(서브플롯)를 품는 3행 2열짜리 Figure를 준비합니다.
mpl.rc('font', size=14) # 폰트 크기 설정
mpl.rc('axes', titlesize=15) # 각 축의 제목 크기 설정
figure, axes = plt.subplots(nrows=3, ncols=2) # 3행 2열 Figure 생성 ❶
plt.tight_layout() # 그래프 사이에 여백 확보 ❷
figure.set_size_inches(10, 9) # 전체 Figure 크기를 10x9인치로 설정 ❸
코드 ❶은 matplotlib 라이브러리의 subplots() 함수 사용 예시입니다. 지금 예처럼 한 화면에 여러 그래프를 동시에 그릴 때 사용합니다. 파라미터를 두 개 받는데 nrows는 행 개수를, ncols는 열 개수를 뜻합니다. ❶을 실행하면 3행 2열의 서브플롯subplot 전체가 figure 변수에 할당되며, 각각의 서브플롯 축 6개는 axes 변수에 할당됩니다. 다음 그림은 출력해본 모습입니다.
▼ 코드 ❶까지의 실행 결과 - Figure 객체
이어서 axes에는 어떤 객체가 할당되어 있는지 출력해보겠습니다.
axes
array([[, ],
       [, ],
       [, ]], dtype=object) 
AxesSubplot 객체 6개가 3행 2열로 구성된 배열이 출력되었습니다. 이 배열을 입력으로 axes.shape를 실행하면 (3, 2)가 출력됩니다. 출력 결과의 각 AxesSubplot 객체는 순서대로 서브플롯의 0행 0열, 0행 1열, 1행 0열, 1행 1열, 2행 0열, 2행 1열 축을 의미합니다. 예를 들어, axes[0, 0]은 서브플롯의 0행 0열 축을 의미합니다.
코드 ❷의 plt.tight_layout()은 서브플롯 사이에 여백을 줘 간격을 넓히는 기능을 합니다. 앞의 그림에서는 서브플롯 사이의 간격이 좁아 숫자가 일부 가려져 보기 좋지 않았습니다. 그렇다면 이 함수를 적용하면 모습이 어떻게 바뀔까요?
▼ 실행 결과 - plt.tight_layout() 적용 후 Figure의 모습(서브플롯 간 간격이 넓어짐)
보다시피 서브플롯 간 공간이 넉넉해서 보기 좋습니다.
마지막으로 코드 ❸의 figure.set_size_inches(10, 9)로는 Figure 크기를 지정합니다. 서브플롯 하나의 크기가 아니라 서브플롯 6개를 합친 ‘전체’ Figure 크기이며, 단위는 함수 이름에서 알 수 있듯이 인치inch입니다. 첫 번째 파라미터로는 너비, 두 번째 파라미터로는 높이를 조정합니다. 여기서는 너비 10인치, 높이 9인치로 설정했습니다.
스텝 2 : 각 축에 서브플롯 할당
이어서 연도, 월, 일, 시간, 분, 초별 평균 대여 수량 막대 그래프를 스텝 1에서 준비한 Figure의 각 축에 할당하겠습니다.
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])
막대 그래프 생성에는 seaborn의 barplot() 함수를 이용했습니다. x 파라미터에 연도, 월, 일, 시간, 분, 초를 전달하고, y 파라미터에 대여 수량을 전달했습니다. data 파라미터에는 훈련 데이터를 DataFrame 형식으로 전달하면 됩니다. ax 파라미터에는 AxesSubplot 객체를 전달하면 됩니다. 0행 0열의 축부터 2행 1열의 축까지 순서대로 전달했습니다.
그럼 서브플롯들이 잘 설정되었는지 스텝 2 코드까지 실행해보겠습니다.
▼ 실행 결과 - 서브플롯들이 들어찬 Figure의 모습(세부 설정 전)
의도한 대로 잘 할당되었습니다. 하지만 각 서브플롯이 어떤 정보를 표현하는지가 한눈에 안 들어오고, 어떤 서브플롯은 x축 라벨이 서로 겹치는 등 아쉬운 점이 조금 보입니다.
스텝 3 : (선택) 세부 설정
아쉬움이 남는다면 다양한 형태로 세부 속성을 설정할 수 있습니다. 이번 예에서는 각 서브플롯에 제목을 추가하고, x축 라벨이 겹치지 않게 개선해보겠습니다. 먼저 각 축에 그려진 서브플롯에 제목을 달아줍니다.
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')
이어서 1행의 두 서브플롯의 x축 라벨들을 90도 회전시키겠습니다.
axes[1, 0].tick_params(axis='x', labelrotation=90)
axes[1, 1].tick_params(axis='x', labelrotation=90)
보다시피 axis 파라미터에 원하는 축을 명시하고 labelrotation 파라미터에 회전 각도를 입력하면 됩니다. axis의 값으로는 ‘x’, ‘y’, ‘both’를 지정할 수 있으며, 기본값이 ‘both’이므로 생략하면 두 축을 한꺼번에 회전시킵니다.
Note
matplotlib.pyplot.tick_params 함수는 labelrotation 외에도 너비, 색상 등 다양한 미세조정용 파라미터를 지원합니다. matplotlib의 API 문서(https://bit.ly/3DBCQaQ)를 참고하세요.
이상으로 세부 설정까지 모두 마쳤습니다. 다음의 전체 코드를 실행하면 이번 절에서 처음 보여드린 막대 그래프가 나타납니다.
▼ 전체 코드
# 스텝 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 생성 
plt.tight_layout() # 그래프 사이에 여백 확보 
figure.set_size_inches(10, 9) # 전체 Figure 크기를 10x9인치로 설정

# 스텝 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 : 세부 설정
# 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)
박스플롯
박스플롯box plot15은 범주형 데이터에 따른 수치형 데이터 정보를 나타내는 그래프입니다. 막대 그래프보다 더 많은 정보를 제공하는 특징이 있습니다.

* 15_ 4.4.3절 ‘박스플롯(boxplot)’ 참고
여기서는 계절, 날씨, 공휴일, 근무일(범주형 데이터)별 대여 수량(수치형 데이터)을 박스플롯으로 그려보겠습니다. 각 범주형 데이터에 따라 타깃값인 대여 수량이 어떻게 변하는지 알 수 있습니다.
이번에는 2행 2열 Figure를 만들 것이며, 코드는 막대 그래프 때와 같은 ‘Figure 준비’ → ‘서브플롯 할당’ → ‘세부 설정’ 순서로 작성했습니다.
# 스텝 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도 회전
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도 회전
코드 ❶ 영역은 seaborn의 boxplot()이라는 점만 빼고 앞의 막대 그래프 코드와 형태가 같습니다.
▼ 실행 결과 - 계절, 날씨, 공휴일, 근무일별 대여 수량 boxplot
그래프 ❶번의 계절별 대여 수량 박스플롯을 보겠습니다. 자전거 대여 수량은 봄에 가장 적고, 가을에 가장 많습니다. ❷의 박스플롯이 보여주는 날씨별 대여 수량은 우리의 직관과 일치합니다. 날씨가 좋을 때 대여 수량이 가장 많고, 안 좋을수록 수량이 적습니다. 폭우, 폭설이 내리는 날씨(그래프의 가장 오른쪽 박스)에는 대여 수량이 거의 없습니다.
❸은 공휴일 여부에 따른 대여 수량을 나타내는 박스플롯입니다. x축 라벨 0은 공휴일이 아니라는 뜻이고, 1은 공휴일이라는 뜻입니다. 공휴일일 때와 아닐 때 자전거 대여 수량의 중앙값은 거의 비슷합니다. 다만, 공휴일이 아닐 때는 이상치outlier가 많습니다. ❹의 박스플롯도 마찬가지입니다. 근무일 여부에 따른 대여 수량을 나타내는데, 근무일일 때 이상치가 많습니다. 참고로 근무일은 공휴일과 주말을 뺀 나머지 날을 뜻합니다.
포인트플롯
다음으로 근무일, 공휴일, 요일, 계절, 날씨에 따른 시간대별 평균 대여 수량을 포인트플롯point plot16으로 그려보겠습니다. 포인트플롯은 범주형 데이터에 따른 수치형 데이터의 평균과 신뢰구간17을 점과 선으로 표시합니다. 막대 그래프와 동일한 정보를 제공하지만, 한 화면에 여러 그래프를 그려 서로 비교해보기에 더 적합합니다.

* 16_ 4.4.2절 ‘포인트플롯(pointplot)’ 참고
* 17_ ‘신뢰구간’이란 모수가 어느 구간 안에 있는지를 확률적으로 나타내는 방법입니다.
모든 포인트플롯의 hue 파라미터에 비교하고 싶은 피처를 전달했습니다. hue 파라미터에 전달한 피처를 기준으로 그래프가 나뉩니다.
▼ 실행 결과 - 근무일, 공휴일, 요일, 계절, 날씨에 따른 시간대별 평균 대여 수량 pointplot
❶번 그래프를 보면 근무일에는 출퇴근 시간에 대여 수량이 많고 쉬는 날에는 오후 12~2시에 가장 많습니다. ❷ 공휴일 여부, ❸ 요일에 따른 포인트플롯도 근무일 여부에 따른 포인트플롯(❶)과 비슷한 양상을 보입니다.
❹ 계절에 따른 시간대별 포인트플롯을 보겠습니다. 대여 수량은 가을에 가장 많고, 봄에 가장 적습니다. ❺ 마지막 그래프는 날씨에 따른 시간대별 포인트플롯입니다. 예상대로 날씨가 좋을 때 대여량이 가장 많습니다. 그런데 폭우, 폭설이 내릴 때 18시(저녁 6시)에 대여 건수가 있습니다. 정말 급한 일이 있었나 봅니다. 이런 이상치는 제거를 고려해보는 것도 괜찮은 방법입니다. 실제로 실험해보니 이 데이터를 제거한 경우 최종 모델 성능이 더 좋았습니다. 따라서 추후 이 데이터는 제거하겠습니다.
회귀선을 포함한 산점도 그래프
수치형 데이터인 온도, 체감 온도, 풍속, 습도별 대여 수량을 ‘회귀선을 포함한 산점도 그래프scatter plot graph with regression line18로 그려보겠습니다. 회귀선을 포함한 산점도 그래프는 수치형 데이터 간 상관관계를 파악하는 데 사용합니다.

* 18_ 4.5.4절 ‘회귀선을 포함한 산점도 그래프(regplot)’ 참고
이 그래프는 seaborn의 regplot() 함수로 그릴 수 있습니다.
# 스텝 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’}는 회귀선의 색상을 선택하는 파라미터입니다. 회귀선이 잘 보이도록 그래프에 찍히는 점보다 짙은 색으로 설정했습니다.
▼ 실행 결과 - 온도, 체감 온도, 풍속, 습도별 대여 수량 regplot
회귀선 기울기로 대략적인 추세를 파악할 수 있습니다. ❶과 ❷ 그래프부터 보면, 온도와 체감 온도가 높을수록 대여 수량이 많습니다. ❹ 습도는 낮을수록 대여를 많이 합니다. 다시 말해 대여 수량은 추울 때보다 따뜻할 때 많고, 습할 때보다 습하지 않을 때 많습니다. 여기까지는 우리의 직관과 일치합니다.
❸번 그래프를 보겠습니다. 회귀선을 보면 풍속이 셀수록 대여 수량이 많습니다. 바람이 약할수록 많을 것 같은데 조금 이상하군요. 이유는 windspeed 피처에 결측값이 많기 때문입니다. 자세히 보면 풍속이 0인 데이터가 꽤 많습니다. 실제 풍속이 0이 아니라 관측치가 없거나 오류로 인해 0으로 기록됐을 가능성이 높습니다. 결측값이 많아서 그래프만으로 풍속과 대여 수량의 상관관계를 파악하기는 힘듭니다. 결측값이 많은 데이터는 적절히 처리해야 합니다. 결측값을 다른 값으로 대체하거나 windspeed 피처 자체를 삭제하면 됩니다. 우리는 피처 자체를 삭제할 것입니다.
히트맵
temp, atemp, humidity, windspeed, count는 수치형 데이터입니다. 수치형 데이터끼리 어떤 상관관계19가 있는지 알아보겠습니다. corr() 함수는 DataFrame 내의 피처 간 상관계수를 계산해 반환합니다.

* 19_ 5.1.2절 중 ‘상관계수’ 참고
train[['temp', 'atemp', 'humidity', 'windspeed', 'count']].corr()
▼ 실행 결과 - 수치형 데이터 간 상관관계 매트릭스
하지만 조합이 많아 어느 피처들의 관계가 깊은지 한눈에 들어오지 않습니다. 히트맵heatmap20이 필요한 순간입니다. 히트맵은 데이터 간 관계를 색상으로 표현하여, 여러 데이터를 한눈에 비교하기에 좋습니다. 히트맵은 seaborn의 heatmap() 함수로 그릴 수 있습니다.

* 20_ 4.5.1절 ‘히트맵(heatmap)’ 참고
# 피처 간 상관 관계 매트릭스 ❶
corrMat = train[['temp', 'atemp', 'humidity', 'windspeed', 'count']].corr() fig, ax= plt.subplots()
fig.set_size_inches(10, 10)
sns.heatmap(corrMat, annot=True) # 상관관계 히트맵 그리기 ❷
ax.set(title='Heatmap of Numerical Data');
corr() 함수로 구한 상관관계 매트릭스 corrMat를 ❷ heatmap() 함수에 인수로 넣어주면 됩니다. 이때 annot 파라미터를 True로 설정하면 상관계수가 숫자로 표시됩니다.
▼ 실행 결과 - 수치형 데이터 간 상관관계 히트맵
온도(temp)와 대여 수량(count) 간 상관계수는 0.39입니다. 양의 상관관계를 보이는군요. 온도가 높을수록 대여 수량이 많다는 뜻입니다. 반면, 습도(humidity)와 대여 수량은 음수이니 습도가 ‘낮을수록’ 대여 수량이 많다는 뜻입니다. 앞서 산점도 그래프에서 분석한 내용과 동일합니다.
풍속(windspeed)과 대여 수량의 상관계수는 0.1입니다. 상관관계가 매우 약합니다. windspeed 피처는 대여 수량 예측에 별 도움을 주지 못할 것 같습니다. 성능을 높이기 위해 모델링 시 windspeed 피처는 제거하겠습니다(참고로 바로 앞의 ‘회귀선을 포함한 산점도 그래프’ 절에서는 결측값이 많다는 이유로 같은 결론에 도달했습니다).
분석 결과
windspeed 피처 제거
분석 정리 및 모델링 전략
분석 정리
지금까지 다양한 측면에서 데이터를 살펴보았습니다. 분석 과정에서 파악한 주요 내용을 정리해 보겠습니다.
  • 타깃값 변환 : 분포도 확인 결과 타깃값인 count가 0 근처로 치우쳐 있으므로 로그변환하여 정규분포에 가깝게 만들어야 합니다. 타깃값을 count가 아닌 log(count)로 변환해 사용할 것이므로 마지막에 다시 지수변환해 count로 복원해야 합니다.
  • 파생 피처 추가 : datetime 피처는 여러 가지 정보의 혼합체이므로 각각을 분리해 year, month, day, hour, minute, second 피처를 생성할 수 있습니다.
  • 파생 피처 추가 : datetime에 숨어 있는 또 다른 정보인 요일(weekday) 피처를 추가하겠습니다.
  • 피처 제거 : 테스트 데이터에 없는 피처는 훈련에 사용해도 큰 의미가 없습니다. 따라서 훈련 데이터에만 있는 casualregistered 피처는 제거하겠습니다.
  • 피처 제거 : datetime 피처는 인덱스 역할만 하므로 타깃값 예측에 아무런 도움이 되지 않습니다.
  • 피처 제거 : date 피처가 제공하는 정보는 year, month, day 피처에 담겨 있습니다.
  • 피처 제거 : monthseason 피처의 세부 분류로 볼 수 있습니다. 데이터가 지나치게 세분화 되어 있으면 분류별 데이터 수가 적어서 오히려 학습에 방해가 되기도 힙니다.
  • 피처 제거 : 막대 그래프 확인 결과 파생 피처인 day는 분별력이 없습니다.
  • 피처 제거 : 막대 그래프 확인 결과 파생 피처인 minutesecond에는 아무런 정보가 담겨 있지 않습니다.
  • 이상치 제거 : 포인트 플롯 확인 결과 weather4인 데이터는 이상치입니다.
  • 피처 제거 : 산점도 그래프와 히트맵 확인 결과 windspeed 피처에는 결측값이 많고 대여 수량과의 상관관계가 매우 약합니다.
모델링 전략
경진대회에서 우수한 성적을 거두려면 본인만의 최적화된 모델을 구상해야 합니다. 하지만 6장은 캐글과 친해지기 위한 몸풀기 목적이 강하므로 사이킷런이 제공하는 기본 모델들만 사용하기로 했습니다. 차후 자신이 만든 모델이 최소한 기본 모델들보다는 우수해야 대회에 참여한 의의가 있을 테니 이번 기회에 친해지길 바랍니다.
  • 베이스라인 모델 : 가장 기본적인 회귀 모델인 LinearRegression 채택
  • 성능 개선 : 릿지, 라쏘, 랜덤 포레스트 회귀 모델
  • 피처 엔지니어링 : 앞의 분석 수준에서 모든 모델에서 동일하게 수행
  • 하이퍼파라미터 최적화 : 그리드서치
  • 기타 : 타깃값이 count가 아닌 log(count)
베이스라인 모델과 성능 개선 절들은 본 경진대회에서 추천수가 가장 많은 다음 노트북을 리팩터링하여 작성했습니다.
6.4 베이스라인 모델
이번 절에서는 앞 절에서 추린 피처들을 활용해 베이스라인 모델을 훈련하고 결과를 제출해보겠습니다.
베이스라인 모델이란 뼈대가 되는 가장 기본적인 모델을 의미합니다. 우리는 베이스라인 모델에서 출발해 성능을 점차 향상시키는 방향으로 모델링할 것입니다. 경진대회에 참가하다 보면 다른 참가자들이 베이스라인 모델을 공유할 겁니다. 공유된 모델을 사용해도 되고, 직접 자신만의 모델을 만들어도 됩니다.
이번 장에서는 사이킷런이 제공하는 기본 선형 회귀 모델을 베이스라인으로 사용할 것입니다. 전체 프로세스는 다음과 같습니다.
▼ 베이스라인 모델 전체 프로세스
먼저 제가 설정해둔 노트북 양식을 복사한 후, 바로 이어서 판다스로 경진대회 데이터를 다시 불러옵니다.
https://www.kaggle.com/werooring/ch6-baseline
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')
6.4.1 피처 엔지니어링
피처 엔지니어링은 데이터를 변환하는 작업입니다. 보통은 이 변환을 훈련 데이터와 테스트 데이터에 공통으로 반영해야 하기 때문에, 피처 엔지니어링 전에 두 데이터를 합쳤다가 다 끝나면 도로 나눠줍니다.
▼ 피처 엔지니어링 전후의 데이터 합치기 및 나누기
그런데 데이터를 합치기 전에 훈련 데이터에서 이상치 하나만 제거하고 가겠습니다.
이상치 제거
앞서 포인트 플롯에서 확인한 결과 훈련 데이터에서 weather4인 데이터(폭우, 폭설이 내리는 날 저녁 6시에 대여)는 이상치였습니다. 제거하겠습니다(분석 정리 10).
# 훈련 데이터에서 weather가 4가 아닌 데이터만 추출
train = train[train['weather'] != 4]
데이터 합치기
훈련 데이터와 테스트 데이터에 같은 피처 엔지니어링을 적용하기 위해 두 데이터를 하나로 합치겠습니다. 판다스의 concat() 함수를 사용하면 축을 따라 DataFrame을 이어붙일 수 있습니다.
훈련 데이터는 10,886행, 테스트 데이터는 6,493행으로 구성되어 있습니다. 합치면 17,379행입니다. 앞서 weather4인 데이터를 제거했으니(1개 있음), 최종적으로 17,378행이 됩니다. 다음 코드를 실행해 제대로 합쳐지는지 보겠습니다.
all_data_temp = pd.concat([train, test])
all_data_temp
▼ 실행 결과 - 훈련 데이터와 테스트 데이터 concat 결과
총 17,378행인데 인덱스가 6,492까지밖에 안 보입니다. 그림에서는 중간이 생략되어 있는데, 실제로는 0부터 10,885까지 매기고 다시 0부터 6,492까지 매긴 결과입니다(중간에 ‘앞서 제거한’ 인덱스도 있습니다). 원래 데이터의 인덱스를 무시하고 이어붙이려면 ignore_index=True를 전달하면 됩니다.
all_data = pd.concat([train, test], ignore_index=True) 
all_data
▼ 실행 결과 - 훈련 데이터와 테스트 데이터 concat 결과(ignore_index=True)
❶ 인덱스가 0부터 17,377까지 잘 나타났습니다. 테스트 데이터에 casual, registered 피처와 count 타깃값이 없으므로 NaNNot a Number으로 표시된 것도 볼 수 있습니다.
파생 피처(변수) 추가
6.3.3절에서 다룬 피처 엔지니어링(파생 피처 추가)을 비슷한 방식으로 적용해보겠습니다(분석 정리 2, 3).
from datetime import datetime

# 날짜 피처 생성
all_data['date'] = all_data['datetime'].apply(lambda x: x.split()[0]) # 연도 피처 생성
all_data['year'] = all_data['datetime'].apply(lambda x: x.split()[0].split('-')[0])
# 월 피처 생성
all_data['month'] = all_data['datetime'].apply(lambda x: x.split()[0].split('-')[1])
# 시 피처 생성
all_data['hour'] = all_data['datetime'].apply(lambda x: x.split()[1].split(':')[0])
# 요일 피처 생성
all_data["weekday"] = all_data['date'].apply(lambda dateString : datetime.strptime(dateString,"%Y-%m-%d").weekday())
훈련 데이터는 매달 1일부터 19일까지의 기록이고, 테스트 데이터는 매달 20일부터 월말까지의 기록입니다. 그러므로 대여 수량을 예측할 때 일(day) 피처는 사용할 필요가 없습니다. minutesecond 피처도 모든 기록에서 값이 같으므로 예측에 사용할 필요가 없습니다. 그래서 day, minute, second는 피처로 생성하지 않았습니다(분석 정리 8, 9).
필요 없는 피처 제거
이제 훈련 데이터와 테스트 데이터에서 필요 없는 피처를 제거하겠습니다. casualregistered 피처는 테스트 데이터에 없는 피처이므로 제거하겠습니다(분석 정리 4). datetime 피처는 인덱스 역할이고, date 피처가 갖는 정보는 다른 피처들(year, month, day)에도 담겨 있기 때문에 datetimedate 피처도 필요 없습니다(분석 정리 5, 6). season 피처가 month의 대분류 성격이라서 month 피처도 제거하겠습니다(분석 정리 7). windspeed 피처도 타깃값과 상관관계가 약해서 제거하겠습니다(분석 정리 11).
drop_features = ['casual', 'registered', 'datetime', 'date', 'windspeed', 'month']
all_data = all_data.drop(drop_features, axis=1)
casual, registered, datetime, date, windspeed, month 피처를 제거했습니다. 필요 없는 피처를 제거함으로써 모델링할 때 사용할 피처를 모두 선별했습니다. 탐색적 데이터 분석에서 얻은 인사이트를 활용해 의미 있는 피처와 불필요한 피처를 구분한 것입니다. 이러한 과정을 피처 선택이라고 합니다.
피처 선택이란?
모델링 시 데이터의 특징을 잘 나타내는 주요 피처만 선택하는 작업을 피처 선택feature selection이라고 합니다. 피처 선택은 머신러닝 모델 성능에 큰 영향을 줍니다. 타깃값 예측과 관련 없는 피처가 많다면 오히려 예측 성능이 떨어집니다. 피처가 많다고 무조건 좋은 게 아니라는 말입니다. 예측 성능을 높이려면 타깃값과 관련 있는 피처가 필요합니다. 피처 선택 방법에 정답은 없습니다. 어떤 피처를 선택해야 성능이 가장 좋을지 바로 알 방법은 없습니다. 탐색적 데이터 분석, 피처 중요도feature importance, 상관관계 매트릭스 등을 활용해 종합적으로 판단해야 합니다.
이상으로 모든 피처 엔지니어링을 끝냈습니다.
데이터 나누기
모든 피처 엔지니어링을 적용했으므로 훈련 데이터와 테스트 데이터를 다시 나누겠습니다.
# 훈련 데이터와 테스트 데이터 나누기 ❶
X_train = all_data[~pd.isnull(all_data'count'])] 
X_test = all_data[pd.isnull(all_data['count'])]

# 타깃값 count 제거 ❷
X_train = X_train.drop(['count'], axis=1) X_test = X_test.drop(['count'], axis=1)
y = train['count'] # 타깃값 ❸
❶ 타깃값이 있으면 훈련 데이터이고, 없으면 테스트 데이터입니다. all_data[‘count’]가 타깃값입니다. 따라서 all_data[‘count’]null이 아니면 훈련 데이터입니다. 그래서 훈련 데이터를 추릴 때는 pd.isnull() 앞에 부정(not)을 의미하는 ‘~’ 기호를 붙였습니다.
❷ 이렇게 나눈 X_trainX_test에는 타깃값인 count도 포함돼 있어 제거했습니다. ❸ 그리고 타깃값인 train[‘count’]는 변수 y에 따로 할당했습니다.
피처 엔지니어링을 모두 마친 후 훈련 데이터 구성이 어떻게 바뀌었는지 살펴보겠습니다.
X_train.head()
▼ 실행 결과 - 피처 엔지니어링 후의 훈련 데이터(X_train)
6.3.2절에서 확인한 처음 훈련 데이터와 비교해보면 datetime, windspeed, casual, registered, count가 빠졌고, 대신 year, hour, weekday가 추가되었습니다.
6.4.2 평가지표 계산 함수 작성
훈련이란 어떠한 능력을 개선하기 위해 배우거나 단련하는 행위입니다. 따라서 훈련이 제대로 이루어졌는지 확인하려면 대상 능력을 평가할 수단, 즉 평가지표가 필요합니다. 그래서 본격적인 훈련에 앞서 본 경진대회 평가지표인 RMSLE를 계산하는 함수를 만들겠습니다.
import numpy as np

def rmsle(y_true, y_pred, convertExp=True): 
    # 지수변환 ❶
    if convertExp:
        y_true = np.exp(y_true) 
        y_pred = np.exp(y_pred)

    #로그 변환 후 결측값을 0으로 변환 ❷
    log_true = np.nan_to_num(np.log(y_true+1)) 
    log_pred = np.nan_to_num(np.log(y_pred+1))

    # RMSLE 계산 ❸
    output = np.sqrt(np.mean((log_true – log_pred)**2)) 
    return output
실제 타깃값 y_true와 예측값 y_pred를 인수로 전달하면 RMSLE 수치를 반환하는 함수입니다. convertExp는 입력 데이터를 지수변환할지를 정하는 파라미터입니다. 기본값인 convertExp=True를 전달하면 ❶과 같이 y_truey_pred를 지수변환합니다. 지수변환에는 넘파이 내장 함수인 exp()를 이용했습니다. 지수변환하는 이유는 타깃값으로 count가 아닌 log(count)를 사용하기 때문입니다. 예측한 log(count)에 지수변환을 하면 count를 구할 수 있습니다(분석 정리 1). 만약 타깃값이 정규분포를 따른다면 타깃값으로 count를 그대로 사 용해도 됩니다. 그럴 경우 RMSLE를 계산할 때 지수변환을 하지 않아도 됩니다. ❷는 y_truey_pred를 로그변환하고(분석 정리 1) 결측값은 0으로 변환합니다. 참고로 np.log() 함수의 밑은 e입니다. np.nan_to_num() 함수는 NaN 결측값을 모두 0으로 바꾸는 기능을 합니다. 또한 np.log(y+1)은 간단히 np.log1p(y)로 표현하기도 합니다.21

* 21_ y의 값이 굉장히 작다면 np.log1p(y)np.log(y+1)의 결과가 달라집니다. 예를 들어, np.log(1e-100 + 1)0.0이고 np.log1p(1e-100)1e-100입니다.
❸은 아래 RMSLE 공식을 넘파이로 그대로 구현한 코드입니다. RMSLE 수치를 최종적으로 계산해줍니다.
▼ 실행 결과 - 피처 엔지니어링 후의 훈련 데이터(X_train)
6.4.3 모델 훈련
데이터와 평가 함수가 준비되었으니 본격적으로 모델을 생성한 뒤 훈련시켜보겠습니다.
먼저 사이킷런이 제공하는 가장 간단한 선형 회귀 모델인 LinearRegression을 임포트하여 모델을 생성합니다.
from sklearn.linear_model import LinearRegression 
linear_reg_model = LinearRegression()
이어서 훈련 데이터로 모델을 훈련시킵니다.
log_y = np.log(y) # 타깃값 로그변환 
linear_reg_model.fit(X_train, log_y) # 모델 훈련
훈련 전에 타깃값을 로그변환했습니다(분석 정리 1). y는 타깃값인 train[‘count’]를 할당한 변수였죠.
선형 회귀 모델을 훈련한다는 것은 독립변수(피처)X_train과 종속변수(타깃값)log_y에 대응하는 최적의 선형 회귀 계수를 구한다는 의미입니다. 선형 회귀 식은 다음과 같습니다.
이 선형 회귀 식으로 다시 설명해보겠습니다. 독립변수 x1, x2, x3와 종속변수 Y를 활용하여 선형 회귀 모델을 훈련하면 독립변수와 종속변수에 대응하는 최적의 선형 회귀계수 θ1, θ2, θ3를 구할 수 있습니다. 이 과정이 ‘훈련’입니다. θ1, θ2, θ3 값을 아는 상태에서 새로운 독립변수 x1, x2, x3가 주어진다면 종속변수 Y를 구할 수 있습니다. 이 과정이 ‘예측’입니다. 훈련 단계에서 한 번도 보지 못한 독립변수가 주어지더라도 회귀계수를 알고 있기 때문에 종속변수를 예측할 수 있습니다.
▼ 선형 회귀 훈련과 예측의 의미
다시 익숙한 용어로 풀어보면 다음과 같습니다.
  • 훈련 : 피처(독립변수)와 타깃값(종속변수)이 주어졌을 때 최적의 가중치(회귀계수)를 찾는 과정
  • 예측 : 최적의 가중치를 아는 상태(훈련된 모델)에서 새로운 독립변수(데이터)가 주어졌을 때 타깃값을 추정하는 과정
이 맥락에서 탐색적 데이터 분석과 피처 엔지니어링은 다음처럼 풀어 생각할 수 있습니다.
  • 탐색적 데이터 분석 : 예측에 도움이 될 피처를 추리고, 적절한 모델링 방법을 탐색하는 과정
  • 피처 엔지니어링 : 추려진 피처들을 훈련에 적합하도록, 성능 향상에 도움되도록 가공하는 과정
6.4.4 모델 성능 검증
훈련을 마쳤으니 예측을 해본 후 RMSLE 값까지 확인하겠습니다.
다음은 모델 성능 검증을 위해 예측을 수행하는 코드입니다.
preds = linear_reg_model.predict(X_train)
코드를 실행하면 훈련된 선형 회귀 모델이 X_train 피처를 기반으로 타깃값을 예측합니다.
그런데 검증 시 훈련 데이터를 사용했습니다. 모델을 훈련하고, 결과를 예측하고, 평가지표인 RMSLE까지 한번 구해보려고 시험 삼아 짠 것입니다. 원래는 훈련 시 훈련 데이터를 사용하고, 검증 시 검증 데이터를 사용하며, 테스트 시 테스트 데이터를 사용해야 합니다. 지금처럼 훈련 시 사용한 데이터를 예측할 때도 사용하는 경우는 거의 없습니다. 시험공부할 때 이미 풀어본 문제가 실제 시험에 나오면 안 되는 이유와 같습니다. 풀어본 문제가 나오면 당연히 쉽게 맞출 수 있기 때문이죠. 그러므로 이번 장에서 수행한 모델 성능 검증은 올바른 방법은 아닙니다. 단지 훈련과 예측을 코드로 어떻게 구현하는지 간단히 보여주려는 것이니 참고만 해주세요. 바로 다음 장에서 올바른 검증 방법을 배울 것입니다.
Note
제출하기 전까지는 테스트 데이터로 RMSLE를 구할 수 없습니다. RMSLE를 구하려면 예측 타깃값과 실제 타깃 값이 있어야 하는데 테스트 데이터에는 실제 타깃값이 없기 때문이죠. 이런 경우에 보통 훈련 데이터를 훈련용과 검증용으로 나눠서 훈련용 데이터로는 모델을 훈련하고, 검증용 데이터로는 훈련된 모델의 성능을 평가합니다. 이 방법은 다음 장에서 알아보겠습니다. 이번 장에서는 모델 훈련과 예측을 코드로 어떻게 수행하는지 전체적인 흐름만 이해하시면 충분합니다.
마지막으로 예측 결과로부터 훈련이 얼마나 잘 되었는지를 평가해보겠습니다. 타깃값 log_y와 예측 결과 preds 사이의 RMSLE 값을 구하면 됩니다.
print (f'선형 회귀의 RMSLE 값 : {rmsle(log_y, preds, True):.4f}')
선형 회귀의 RMSLE 값 : 1.0205
rmsle() 함수의 세 번째 인수로 True를 전달했으므로 RMSLE를 계산하기 전에 지수변환을 해 줍니다(기본값이 True이므로 생략해도 됩니다). ‘:.4f’는 소수점 넷째 자리까지 구하라는 명령어입니다. 출력 결과에서 볼 수 있듯이 단순 선형 회귀 모델의 RMSLE 값은 1.02입니다.
6.4.5 예측 및 결과 제출
베이스라인 모델로 예측한 결과를 제출해보겠습니다. 주의할 점은 두 가지입니다.
  • 테스트 데이터로 예측한 결과를 이용해야 합니다. 앞서 모델 성능 검증 과정에서는 RMSLE 값을 구해보고자 훈련 데이터를 이용했습니다.
  • 예측한 값에 지수변환을 해줘야 합니다. 현재 예측값이 count가 아니라 log(count)이기 때문입니다.
linearreg_preds = linear_reg_model.predict(X_test) # 테스트 데이터로 예측

submission['count'] = np.exp(linearreg_preds) # 지수변환
submission.to_csv('submission.csv', index=False) # 파일로 저장
to_csv()는 DataFrame을 csv 파일로 저장하는 함수입니다. index=False로 설정해야 DataFrame 인덱스를 제외하고 저장합니다.
이제 다 끝났습니다. 커밋 후 제출해보겠습니다. 제출 방법은 2.4절의 설명을 참고하세요.
▼ 기본 선형 회귀 모델 평가점수
제출 결과 평가점수는 1.02142입니다. 이 점수면 2,773등이므로 3,242명 중 상위 85.5%입니 다. 만족스럽지 못한 결과군요. 다음 절에서는 다른 모델들과 최적화 기법을 이용하여 더 높은 등수에 도전해보겠습니다.
6.5 성능 개선 I : 릿지 회귀 모델
앞 절에서 베이스라인 모델을 만들어 제출까지 해봤습니다. 이번 절부터는 사이킷런이 제공하는 모델 중 세 가지(릿지(ridge), 라쏘(lasso), 랜덤 포레스트(random forest) 회귀)를 더 다뤄보며 가장 우수한 모델이 무엇인지 알아보겠습니다.
먼저 릿지 회귀 모델입니다.
릿지 회귀 모델은 L2 규제를 적용한 선형 회귀 모델입니다. 규제regularization란 모델이 훈련 데이터에 과대적합overfitting되지 않도록 해주는 방법입니다. 훈련 데이터에 과대적합되면 모델이 훈련 데이터에만 너무 잘 들어맞고, 테스트 데이터로는 제대로 예측하지 못합니다. 따라서 모델이 과대적합되지 않게 훈련하는 게 중요합니다. 규제는 이럴 때 사용하는 방법입니다.
릿지 회귀 모델은 성능이 좋은 편은 아닙니다. 캐글러도 잘 쓰지 않습니다. 단순 선형 회귀 모델보다 과대적합이 적은 모델 정도로 생각하면 됩니다.
성능 개선 프로세스는 베이스라인 모델 때와 비슷합니다.
▼ 베이스라인 모델과 모델 성능 개선 프로세스 비교
보다시피 피처 엔지니어링을 본격적으로 수행하며 모델 훈련 단계에서 하이퍼파라미터를 최적화합니다. 그리고 성능이 만족스럽지 못하면 피처 엔지니어링이나 하이퍼파라미터 최적화를 더 고민해봅니다.
이번 장은 튜토리얼이므로 피처 엔지니어링을 추가로 진행하진 않겠습니다. 그러니 평가지표 계산 함수 작성 단계까지는 베이스라인 모델과 똑같습니다. 베이스라인용 노트북을 복사한 뒤, ‘데이터 불러오기’ → ‘피처 엔지니어링’ → ‘평가지표 계산 함수 작성’까지 진행해주세요.
6.5.1 하이퍼파라미터 최적화(모델 훈련)
이번에는 ‘모델 훈련’ 단계에서 그리드서치 기법을 사용할 것입니다. 그리드서치grid search22는 하이퍼파라미터를 격자grid처럼 촘촘하게 순회하며 최적의 하이퍼파라미터 값을 찾는 기법입니다. 각 하이퍼파라미터를 적용한 모델마다 교차 검증cross-validation23하며 성능을 측정하여 최종적으로 성능이 가장 좋았을 때의 하이퍼파라미터 값을 찾아줍니다.

* 22_ 5.7.1절 ‘그리드서치’ 참고
* 23_ 5.5절 ‘교차 검증’ 참고
▼ 그리드서치 도식
교차 검증 평가점수는 보통 에러 값이기 때문에 낮을수록 좋습니다. 즉, 이 그림에서는 alpha=0.1일 때 평가점수가 가장 좋습니다. 따라서 최적 alpha 값은 0.1입니다(alpha는 릿지 모델의 파라미터입니다).
그리드서치를 이용하지 않으면 alpha0.1, 1, 2 등의 값을 전달하여 교차 검증으로 모델 성능을 각각 측정해야 합니다. 수작업으로 하나하나 수행한 뒤 최적 하이퍼파라미터를 찾아야 하니 무척 번거롭습니다. 특히 하이퍼파라미터의 개수가 하나 늘어날 때마다 번거로움은 기하급수적으로 커질 것입니다. 그리드서치는 이 일을 자동으로 해줍니다. 테스트하려는 하이퍼파라미터와 값의 범위만 전달하면 알아서 모든 가능한 조합을 순회하며 교차 검증합니다.
그리드서치가 추가되면서 하이퍼파라미터 최적화 절차는 다음 그림처럼 세분화됩니다.
▼ 하이퍼파라미터 최적화 절차(그리드서치)
이제 그리드서치로 최적의 릿지 회귀 모델을 찾아내는 코드를 살펴봅시다.
모델 생성
가장 먼저 릿지 모델을 생성합니다.
https://www.kaggle.com/werooring/ch6-modeling
from sklearn.linear_model import Ridge
from sklearn.model_selection import GridSearchCV from sklearn import metrics
ridge_model = Ridge()
특별한 것 없이 사이킷런의 기본 릿지 모델을 생성했습니다. 새로 추가된 임포트문들은 이어지는 단계들에서 사용합니다.
그리드서치 객체 생성
이어서 그리드서치 객체를 생성합니다. 앞서 그리드서치는 ‘하이퍼파라미터의 값’을 바꿔가며 ‘모델’의 성능을 교차 검증으로 ‘평가’해 최적의 하이퍼파라미터 값을 찾아준다고 했습니다. 이 말은 그리드서치 객체가 다음의 세 가지를 알고 있어야 한다는 뜻입니다.
  • 비교 검증해볼 하이퍼파라미터 값 목록
  • 대상 모델
  • 교차 검증용 평가 수단(평가 함수)
대상 모델은 앞서 만들었으니, 하이퍼파라미터 값 목록과 평가 함수만 더 준비하면 됩니다.
릿지 모델은 규제를 적용한 회귀 모델이라고 했습니다. 릿지 모델에서 중요한 하이퍼파라미터는 alpha로, 값이 클수록 규제 강도가 세집니다. 적절한 규제를 적용한다면, 즉 alpha를 적당한 크기로 하면 과대적합 문제를 개선할 수 있습니다.
# 하이퍼파라미터 값 목록 ❶
ridge_params = {'max_iter':[3000], 'alpha':[0.1, 1, 2, 3, 4, 10, 30, 100, 200, 300, 400, 800, 900, 1000]}

# 교차 검증용 평가 함수(RMSLE 점수 계산)
rmsle_scorer = metrics.make_scorer(rmsle, greater_is_better=False)

# 그리드서치(with 릿지) 객체 생성 ❷
    gridsearch_ridge_model = GridSearchCV(estimator=ridge_model, # 릿지 모델
            param_grid=ridge_params, # 값 목록 
            scoring=rmsle_scorer, # 평가지표
            cv=5) # 교차 검증 분할 수
❷ 그리드서치 객체를 생성하는 GridSearchCV() 함수의 주요 파라미터는 다음과 같습니다.
  • estimator : 분류 및 회귀 모델
  • param_grid : 딕셔너리 형태로 모델의 하이퍼파라미터명과 여러 하이퍼파라미터 값을 지정
  • scoring : 평가지표. 사이킷런에서 기본적인 평가지표를 문자열 형태로 제공함. 예를 들어, 정확도는 ‘accuracy’, F1 점수는 ‘f1’, ROC-AUC는 ‘roc_auc’, 재현율은 ‘recall’로 표시 함. 사이킷런에서 제공하는 평가지표를 사용하지 않고 별도로 만든 평가지표를 사용해도 됨. 앞의 코드에서는 mertics.make_scorer를 활용해 별도로 만든 평가지표를 사용. make_scorer는 평가지표 계산 함수와 평가지표 점수가 높으면 좋은지 여부 등을 인수로 받는 교차 검증용 평가 함수임
  • cv : 교차 검증 분할 개수(기본값은 5)
한편 ❶ 하이퍼파라미터 값 목록에서 max_iter3000으로 고정했고, alpha0.1에서 1000까지 다양합니다. 그리드서치 객체는 param_grid로 전달된 모든 하이퍼파라미터를 대입해 교차 검증으로 모델 성능 점수를 계산하여 어떤 값일 때 점수가 가장 좋은지 찾아줍니다. 하지만 모든 값에 대해 교차 검증 개수만큼 훈련 및 평가하므로 시간이 오래 걸립니다. 예시 코드에서는 데이터가 적고 모델이 단순해서 괜찮습니다만, 데이터가 많고 복잡한 모델을 사용하면 GridSearchCV의 수행 시간은 상당히 길어집니다.
warning
교차 검증 시에는 해당 경진대회의 평가지표를 그대로 사용해야 합니다. 평가 방식이 다르면 애써 찾은 최적의 하이퍼파라미터가 대회 성적과는 무관한 쪽으로 우수한(?) 예측 결과를 내어줄 것입니다.
그리드서치 수행
다음은 방금 만든 그리드서치 객체를 이용하여 그리드서치를 수행합니다.
log_y = np.log(y) # 타깃값 로그변환 
gridsearch_ridge_model.fit(X_train, log_y) # 훈련(그리드서치)
코드가 일관되도록 그리드서치 객체도 모델 객체와 똑같이 fit() 메서드를 제공합니다. fit()을 실행하면 객체 생성 시 param_grid에 전달된 값들을 순회하면서 교차 검증으로 평가지표 점수를 계산합니다. 이때 가장 좋은 성능을 보인 값을 best_params_ 속성에 저장하며, 이 최적값으로 훈련한 모델(최적 예측기)best_estimator_ 속성에 저장합니다.
그렇다면 최적 하이퍼파라미터로는 어떤 값이 선정되었는지 살펴봅시다.
print('최적 하이퍼파라미터 :', gridsearch_ridge_model.best_params_) 
최적 하이퍼파라미터 : {'alpha': 0.1, 'max_iter': 3000}
출력 결과를 보면 alpha0.1이고 max_iter3000일 때 가장 좋은 성능을 낸다는 사실을 알 수 있습니다.
6.5.2 성능 검증
이후 과정은 간단하니 한꺼번에 진행하겠습니다.
그리드서치를 완료하고 나면 그리드서치 객체의 best_estimator_ 속성에 최적 예측기가 저장되어 있습니다. 따라서 예측은 그리드서치 객체의 best_estimator_ 속성에 저장된 모델로 수행하면 됩니다.
# 예측
preds = gridsearch_ridge_model.best_estimator_.predict(X_train)
# 평가
print(f'릿지 회귀 RMSLE 값 : {rmsle(log_y, preds, True):.4f}')
릿지 회귀의 RMSLE 값 : 1.0205
참 값(log_y)과 예측값(preds) 사이의 RMSLE는 1.02로, 선형 회귀 모델의 결과와 다르지 않음을 알 수 있습니다. 따라서 이번 결과는 굳이 제출하지 않겠습니다.
6.6 성능 개선 II : 라쏘 회귀 모델
라쏘 회귀 모델은 L1 규제를 적용한 선형 회귀 모델입니다. 앞 절에서 다룬 릿지 회귀 모델과 마찬 가지로 성능이 좋은 편은 아니라서 캐글러들이 잘 쓰지 않습니다. 달리 말하자면, 나중에 여러분이 직접 만드는 모델은 적어도 릿지나 라쏘보다는 성능이 좋아야 할 것입니다.
이번 대회의 문제에서 라쏘 회귀 모델이 어느 정도의 성능을 보이는지 측정해보겠습니다.
Note
별도 노트북 파일을 만들어도 되지만 ‘평가지표 계산 함수 작성’ 단계까지가 계속 똑같기 때문에 6.5절의 노트북을 그대로 사용하겠습니다.
6.6.1 하이퍼파라미터 최적화(모델 훈련)
사용한 모델과 파라미터만 다를 뿐 릿지 회귀 때와 똑같은 흐름입니다(rmsle_scorer 함수는 릿지 회귀 때 정의한 것을 재활용했습니다). 릿지 회귀와 마찬가지로 alpha는 규제 강도를 조정하는 파라미터입니다.
from sklearn.linear_model import Lasso

# 모델 생성
lasso_model = Lasso()
# 하이퍼파라미터 값 목록
lasso_alpha = 1/np.array([0.1, 1, 2, 3, 4, 10, 30, 100, 200, 300, 400, 800, 900, 1000])
lasso_params = {'max_iter':[3000], 'alpha':lasso_alpha}
# 그리드서치(with 라쏘) 객체 생성
gridsearch_lasso_model = GridSearchCV(estimator=lasso_model,
                                      param_grid=lasso_params,
                                      scoring=rmsle_scorer,
                                      cv=5)
# 그리드서치 수행
log_y = np.log(y)
gridsearch_lasso_model.fit(X_train, log_y)

print('최적 하이퍼파라미터 :', gridsearch_lasso_model.best_params_)
최적 하이퍼파라미터 : {'alpha': 0.00125, 'max_iter': 3000}
6.6.2 성능 검증
그리드서치로 찾은 최적 예측기로 예측하여 RMSLE 값을 확인해보겠습니다.
# 예측
preds = gridsearch_lasso_model.best_estimator_.predict(X_train)

# 평가
print(f'라쏘 회귀 RMSLE 값 : {rmsle(log_y, preds, True):.4f}')
라쏘 회귀 RMSLE의 값 : 1.0205
결과를 보면 RMSLE 값은 1.02로, 여전히 개선되지 않았습니다. 그래서 이번에도 결과 제출은 생략하겠습니다.
6.7 성능 개선 III : 랜덤 포레스트 회귀 모델
마지막으로 랜덤 포레스트 회귀 모델을 사용해보겠습니다. 랜덤 포레스트 회귀는 간단히 생각하면 훈련 데이터를 랜덤하게 샘플링한 모델 n개를 각각 훈련하여 결과를 평균하는 방법입니다.24

* 24_ 5.6.5절 ‘랜덤 포레스트’ 참고
역시 앞서 살펴본 릿지 회귀, 라쏘 회귀와 같은 흐름입니다.
Note
‘평가지표 계산 함수 작성’ 단계까지가 계속 똑같기 때문에 앞 절의 노트북을 그대로 사용하겠습니다.
6.7.1 하이퍼파라미터 최적화(모델 훈련)
랜덤 포레스트 회귀 모델로 그리드서치를 수행하고 최적 하이퍼파라미터 값까지 출력해보겠습니다.
from sklearn.ensemble import RandomForestRegressor 

# 모델 생성
randomforest_model = RandomForestRegressor()
# 그리드서치 객체 생성
rf_params = {'random_state':[42], 'n_estimators':[100, 120, 140]} # 1 gridsearch_random_forest_model = GridSearchCV(estimator=randomforest_model,
                                              param_grid=rf_params,
                                              scoring=rmsle_scorer,
                                              cv=5)
# 그리드서치 수행
log_y = np.log(y)
gridsearch_random_forest_model.fit(X_train, log_y)
print('최적 하이퍼파라미터 :', gridsearch_random_forest_model.best_params_)
최적 하이퍼파라미터 : {'n_estimators': 140, 'random_state': 42}
❶ 그리드서치를 수행할 때 사용한 랜덤 포레스트 회귀 모델의 파라미터는 random_staten_estimators입니다. random_state는 랜덤 시드값으로, 값을 명시하면 코드를 다시 실행해도 같은 결과를 얻을 수 있습니다. n_estimators는 랜덤 포레스트를 구성하는 결정 트리 개수를 의미 합니다.
랜덤 포레스트부터는 그리드서치에 시간이 좀 걸립니다.
6.7.2 모델 성능 검증
최적 예측기의 성능을 확인해보죠.
# 예측
preds = gridsearch_random_forest_model.best_estimator_.predict(X_train)

# 평가
print(f'랜덤 포레스트 회귀 RMSLE 값 : {rmsle(log_y, preds, True):.4f}') 
랜덤 포레스트 회귀 RMSLE 값: 0.1126
랜덤 포레스트 회귀 모델을 사용하니 RMSLE 값이 큰 폭으로 개선되었네요. 선형 회귀, 릿지 회귀, 라쏘 회귀 모델의 RMSLE 값은 모두 1.02였습니다. 반면 랜덤 포레스트 회귀 모델은 0.11입니다(값이 작을수록 좋습니다). 네 모델 중 성능이 가장 좋은 모델은 랜덤 포레스트입니다.
6.7.3 예측 및 결과 제출
이제 성능이 가장 좋은 모델의 예측 결과를 제출하면 경진대회가 모두 끝이 납니다. 검증 결과 랜덤 포레스트 회귀 모델의 성능이 가장 좋았습니다. 물론 성능 측정을 훈련 데이터로 했기 때문에 테스트 데이터에서도 성능이 좋다고 보장할 수는 없습니다. 다행히 본 경진대회는 훈련 데이터와 테스트 데이터의 분포가 비슷합니다. 두 데이터 분포가 비슷하면 과대적합 문제가 상대적으로 적기 때문에 훈련 데이터에서 성능이 좋다면 테스트 데이터에서도 좋을 가능성이 큽니다.
그러면 훈련 데이터 타깃값과 테스트 데이터 타깃 예측값의 분포를 살펴보겠습니다. histplot()으로 분포도(히스토그램)를 그려보죠. histplot() 함수는 ax 파라미터를 사용해 여러 축에 그래프를 그릴 수 있습니다.
Note
일반적으로는 분포도를 그릴 때 활용 범위가 넓은 displot()을 사용하지만 여기서는 ax 파라미터를 이용하려고 histplot()을 사용했습니다.
import seaborn as sns
import matplotlib.pyplot as plt

randomforest_preds = gridsearch_random_forest_model.best_estimator_.predict(X_
test)

figure, axes = plt.subplots(ncols=2)
figure.set_size_inches(10, 4)

sns.histplot(y, bins=50, ax=axes[0])
axes[0].set_title('Train Data Distribution')
sns.histplot(np.exp(randomforest_preds), bins=50, ax=axes[1])
axes[1].set_title('Predicted Test Data Distribution');
▼ 실행 결과 – 훈련 데이터 타깃값, 테스트 데이터 타깃 예측값 분포
보다시피 두 데이터의 분포가 비슷합니다.
랜덤 포레스트로 예측한 결과를 파일로 저장하고, 커밋 후 제출해보세요.
submission['count'] = np.exp(randomforest_preds) # 지수변환
submission.to_csv('submission.csv', index=False)
▼ 랜덤 포레스트 모델 평가점수
제출 결과 평가점수는 0.39567입니다. 이 점수면 등수가 193등으로, 상위 6.0%입니다(베이스 라인 모델은 상위 85.5%였습니다). 상위 10%까지 동메달이므로 동메달을 딸 수 있는 등수군요. 단, 본 대회는 플레이그라운드 대회이기 때문에 메달이 수여되지는 않습니다. 이처럼 간단한 피처 엔지니어링과 기본 모델로도 꽤 높은 등수를 기록할 수 있습니다.
TIP
지금은 공부하는 단계라서 코드 셀 각각을 실행한 뒤 마지막에 커밋하는 식으로 설명했지만, 시간을 아끼려면 전체 코드를 한 번에 실행하면서 동시에 커밋해도 됩니다. 즉, 캐글 노트북 위쪽의 [Run All] 버튼을 클릭하고 바로 커밋을 누르면, 코드 실행이 끝나고 얼마 지나지 않아 커밋도 완료됩니다. 8장부터는 코드 실행 시간이 꽤 길어질 테니 참고해주세요.
학습 마무리
자전거 대여 수요 예측 경진대회를 통해 머신러닝 모델링 프로세스를 배웠습니다. 탐색적 데이터 분석으로 데이터 구성도 파악하고, 간단한 분석도 수행했습니다. 몇 가지 회귀 모델을 훈련하고 성능을 측정해봤습니다. 랜덤 포레스트 회귀 모델로 예측한 결과를 제출해 동메달권 점수를 기록했습니다.
또한 이 책에서 각 경진대회를 풀어가는 방식도 경험해보았습니다. 정리하면 다음 그림과 같은 흐름입니다.
핵심 요약
  • 캐글 경진대회 프로세스는 크게 ‘경진대회 이해’ → ‘탐색적 데이터 분석’ → ‘베이스라인 모델’ → ‘성능 개선’ 순으로 진행됩니다. 일반적인 머신러닝/딥러닝 문제를 해결할 때도 그대로 적용할 수 있습니다.
  • 경진대회 이해 단계에서는 대회의 취지와 문제 유형을 정확히 파악하고, 평가지표를 확인합니다.
  • 탐색적 데이터 분석 단계에서는 시각화를 포함한 각종 기법을 동원해 데이터를 분석하여, 피처 엔지니어링과 모델링 전략을 수립합니다.
  • 베이스라인 모델 단계에서는 본격적인 최적화에 앞서 기본 모델을 제작합니다. 유사한 문제를 풀 때 업계에서 흔히 쓰는 모델이나 직관적으로 떠오르는 모델을 선택합니다.
  • 성능 개선 단계에서는 베이스라인 모델보다 나은 성능을 목표로 각종 최적화를 진행합니다.
  • 타깃값이 정규분포에 가까울수록 회귀 모델의 성능이 좋습니다. 한쪽으로 치우친 타깃값은 로그변환하면 정규분포에 가까워지고, 결괏값을 지수변환하면 원래 타깃값 형태로 복원됩니다(타깃값 변환).
  • 훈련 데이터에서 이상치를 제거하면 일반화 성능이 높아질 수 있습니다(이상치 제거).
  • 기존 피처를 분해/조합하여 모델링에 도움되는 새로운 피처를 추가할 수 있습니다(파생 피처 추가).
  • 반대로 불필요한 피처를 제거해주면 성능도 좋아지고, 훈련 속도도 빨라집니다(피처 제거).
  • 선형 회귀, 릿지, 라쏘 모델은 회귀 문제를 푸는 대표적인 모델이지만, 너무 기본적이라 실전에서 단독으로 최상의 성능을 기대하기는 어렵습니다.
  • 랜덤 포레스트 회귀 모델은 여러 모델을 묶어 (대체로) 더 나은 성능을 이끌어내는 간단하고 유용한 기법입니다.
  • 그리드서치는 교차 검증으로 최적의 하이퍼파라미터 값을 찾아주는 기법입니다.
실전 문제
  • season vs. month. 어느 피처를 선택해야 할까요?피처 엔지니어링 과정에서 month 피처의 특성이 season에 녹아 있다는 이유로 month 피처는 삭제했습니다. 그런데 얼핏 생각해보면 season보다 month가 더 세분화되어 있으니 month를 기준으로 훈련하면 예측 정확도가 더 좋아지지 않을까요? 실제로 season 대신 month를 남긴 후 똑같이 훈련하여 RMSLE 값과 등수를 확인해보세요.
정답
  • RMSLE 값 : 0.1091
  • Private 점수 : 0.45625(898등)
  • Public 점수 : 0.45625(898등)
훈련 데이터 RMSLE 값은 살짝 좋아졌는데, 제출하니 등수가 확 떨어졌을 겁니다. 우선, 본문에서도 설명했듯이 훈련 데이터로 모델을 훈련하고, 다시 똑같은 훈련 데이터로 RMSLE 값을 구하는 건 올바른 모델 성능 검증 방법이 아닙니다. 따라서 season 대신 month 피처를 남겨 모델링했을 때 훈련 데이터 RMSLE 값이 더 좋아졌다고 해서 실제로도 성능이 좋아지는 건 아닙니다. 다시 말해, 여기서 구한 RMSLE 값을 전적으로 신뢰하기는 어렵다는 말입니다.

신백균
KAIST 산업및시스템공학과 졸업 후 한국생산성본부에서 직무교육 기획 및 운영을 담당하는 전문위원입니다. 세계 랭킹 0.18%의 캐글 노트북 엑스퍼트(Expert)이며, 월 평균 6만여 명이 방문하는 데이터 분석/머신러닝 관련 기술 블로그를 운영하고 있습니다. 참여자 1,200명 이상인 머신러닝 관련 오픈 채팅방의 운영진이기도 합니다.

Leave a Reply

©2020 GoldenRabbit. All rights reserved.
서울시 마포구 신촌로2길 19 마포출판문화진흥센터 Platform-P 302호 (우)04051
master@goldenrabbit.co.kr
개인정보처리방침