[kaggle] - 자전거 대여 수요 예측

Jeonghwan Kim·2022년 11월 18일
0

Intro

캐글의 플레이그라운드 대회 'Bike Sharing Demand' compeition에 참가해 간단한 회귀 모델을 연습해보았다. 워싱턴 DC의 Capital bikeshare 프로그램에서 과거 사용 기록과 날씨 데이터를 결합해 향후 자전거 대여 수요를 예측하는 대회이다. Capital bikeshare 프로그램은 서울시의 따릉이 시스템과 비슷하다고 생각하면 된다.

자전거 대여 기록과 당시의 날씨 정보로 구성된 2년치의 데이터가 제공된다. Train data는 매달 1~19일의 기록으로, Test data는 매달 20일부터 말일까지의 기록으로 구성된 것이 특징이며, Feature로는 대여 날짜, 시간, 요일, 계절, 날씨, 실제 온도, 체감온도, 습도, 풍속, 회원 여부가 있다.

예측할 값이 범주형 데이터로 제공되므로 회귀 문제이고, 간단한 회귀 모델로 문제를 해결해 보았다.

대략적인 진행 순서는 다음과 같다.

EDA -> 베이스라인 모델 (선형 회귀) -> 성능 개선 I (릿지 회귀) -> 성능 개선 II (라쏘 회귀) -> 성능 개선 III (랜덤 포레스트 회귀)


EDA

Import Library & Load Data

  • pandas의 Dataframe 형태로 train, test, sample submission 데이터를 불러온 후 train data, test data의 크기를 확인해 보았다.

    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 data의 열의 개수(feature의 개수)는 12개이고, test data의 열의 개수는 9개이므로 head()함수를 통해 각각 어떤 feature를 갖는지 확인해 보았다.

    train.head()

    test.head()

    submission.head()

    • train data에는 있는 casual, registered 열이 test data에는 빠져있으므로 모델을 훈련할 때도 train data의 casual과 registed 열을 제거해야 한다.
  • info() 함수를 통해 DataFrame의 결측값과 데이터 타입을 확인할 수 있다.

    train.info()

    test.info()

Feature Engineering

  • 데이터 시각화를 진행하기 전, 일부 데이터는 시각화하기에 적합하지 않은 형태일 수 있으므로, 이를 분석하기 적합하게 변환해주어야 한다.

  • 연도, 월, 일, 시간, 분, 초로 구성된 datetime을 세부적으로 분석하기 위해 구성요소별로 나누어볼 수 있다. 이때는 split() 함수를 활용한다.

  • 앞에서 했던 방식을 적용해 date, year, month, day, hour, minute, second 열을 생성할 수 있다. 기존 feature에서 파생된 feature를 '파생피처', 혹은 '파생변수'라고 부른다.

    • apply()함수는 DataFrame의 데이터를 일괄 가공해주는데, lambda함수와 함께 자주 쓰인다. lambda함수를 DataFrame 축을 따라 적용하는 기능을 한다.

      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])```
      
  • 날짜 데이터를 활용해 요일 feature도 생성할 수 있다. calender와 datetime 라이브러리를 활용한다.

    from datetime import datetime # datatime 라이브러리 임포트
    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

    • 마찬가지로 apply()함수를 적용해 weekday 열을 추가한다.
      train['weekday'] = train['date'].apply(
          lambda dateString: 
          calendar.day_name[datetime.strptime(dateString,"%Y-%m-%d").weekday()])
  • season과 weather feature는 범주형 데이터인데 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, Thunder', 
                                             4: 'Heavy Snow, Rain, Thunder'})
  • 추가로, date 열의 정보는 모두 year, month, day 열에 있으므로 date feature를 제거한다. season 열과 month 열 또한 겹치므로 month 열을 제거한다. 세분화된 feature를 더 큰 분류로 묶으면 성능이 좋아지는 경우가 많기 때문이다.

Data Visualization

  • 데이터 분포나 테이터 간 관계를 파악하여 모델링에 도움이 되게 데이터를 시각화해준다.

  • 시각화에 사용되는 matplotlib, seaborn 라이브러리를 import한다.

    import seaborn as sns
    import matplotlib as mpl
    import matplotlib.pyplot as plt
    %matplotlib inline
    • %matplotlib inline을 추가해 주피터 노트북에서 바로 출력해줄 수 있다.

분포도 (Distribution Plot)

  • 수치형 데이터의 집계값(총 개수, 비율 등)을 나타내는 그래프이다.

    mpl.rc('font', size=15)      # 폰트 크기를 15로 설정 
    sns.displot(train['count']); # 분포도 출력

  • x축은 타깃값 count, y 축은 총 개수를 나타낸다. 분포도를 보면 타깃값이 0 근처에 몰려있는데, 이는 분포가 왼쪽으로 많이 편향되어 있는 것이다.

  • 회귀 모델이 좋은 성능을 내려면 데이터가 정규분포를 따라야하기에 데이터 분포를 정규분포에 가깝게 만들어주기위해 로그변환을 이용한다. 로그변환은 이와 같이 데이터가 왼쪽으로 편향되어 있을 때 사용한다.

    sns.displot(np.log(train['count']));

  • 타깃값 분포가 정규분포에 가까워졌으므로 타깃값을 log(count)로 변환해 사용하지만, 마지막에 지수변환을 하여 실제 타깃값인 count로 복원해주어야 한다. log를 취한 값을 지수변환 하면 원래 값으로 돌아온다.

막대 그래프 (Bar Plot)

  • 범주형 데이터인 '연도, 연, 월, 일 , 시 , 분, 초' feature를 각 데이터에 따라 평균 대여 수량이 어떻게 다른지 파악하기 위해 막대그래프를 그려준다.

    # 스텝 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)
    • 먼저 subplots() 함수로 m행 n열의 figure를 준비한다. plt.tight_layout()은 서브플롯 사이에 여백을 줘 간격을 넓혀준다.
    • 이후 각 그래프를 할당하고, ax 파라미터에는 위치를 전달한다.
    • 마지막으로 세부 설정을 해주는데, 서브플롯마다 제목을 달아주고 서브플롯들의 x축 라벨이 겹치지 않도록 서브플롯들의 라벨을 회전시켜준다.

    • 일별 평균 대여 수량 그래프를 보면 training 데이터이기 때문에 1일~19일의 데이터만 있고, test 데이터에 20~말일까지의 데이터가 있기에 training 과 test 데이터의 공통된 값이 없어 이 feature는 사용할 수 없다. 따라서 향후 day feature를 제거한다.

    • 맨 아래 그래프 2개는 minute, second feature로 값이 모두 0이라 아무 정보를 제공하지 않기에 향후 제거하여 모델 훈련에 사용하지 않도록 한다.

박스 플롯(Box Plot)

  • 범주형 데이터에 따른 수치형 데이터 정보를 나타내는 그래프

  • 계절, 날씨, 공휴일, 근무일별 대여 수량을 박스플롯으로 그려, 각 범주형 데이터에 따라 타깃값인 대여 수량이 어떻게 변하는지 알 수 있다.

  • 앞의 barplot 코드와 비슷하게 코드를 구성하면 된다.

# 스텝 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('x', labelrotation=10) # 10도 회전

  • 계절과 날씨와 관련된 대여량은 직관과 일치하고, 공휴일 여부에 따른 대여 수량과 근무일 여부에 따른 대여 수량인 아래 두 그래프는 공휴일이 아닐 때, 근무일일 때 이상치가 많다.

포인트 플롯 (Point Plot)

  • 막대 그래프와 동일한 정보이지만 한 화면에 여러 그래프를 그려 서로 비교하기에 적합하다.

  • 근무일, 공휴일, 요일, 계절, 날씨에 따른 시간대별 평균 대여 수량을 각각 비교해본다.

  • 모든 포인트플롯의 hue 파라미터에 비교하고 싶은 feature를 전달한다.

# 스텝 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]);

  • 첫번째 그래프를 보면 근무일에는 출퇴근 시간, 쉬는 날에는 오후12~2시에 가장 대여량이 많은데, 공휴일 여부, 요일에 따른 그래프도 비슷한 양상을 보인다.
  • 날씨에 따른 시간대별 포인트플롯인 마지막 그래프에서, 폭우, 폭설이 내릴 때 18시에 대여 건수가 있는데, 이는 이상치로 고려해 제거하는 것이 최종 모델 성능에 더 좋다. 따라서 'weather == 4'인 데이터를 제거한다.

회귀선을 포함한 산점도 그래프 (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'});

  • scatter_kws={'alpha': 0.2} 는 찍히는 점의 투명도를 조절하고, line_kws={'color': 'blue'} 는 회귀선의 색상을 선택하는 파라미터이다.

  • 회귀선의 기울기로 대략적인 추세를 파악할 수 있다.

  • 3번째 그래프에서는 0인 데이터가 많은데, 이는 결측치가 많아서 그런것으로 상관과계를 파악하기 힘드므로 windspeed feature를 제거하도록 한다.

히트맵

  • temp, atemp, humidity, windspeed, count는 수치형 데이터이고, 이들끼리의 상관관계를 알아보기 위해 corr() 함수로 feature간 상관계수를 알아본다.
train[['temp', 'atemp', 'humidity', 'windspeed', 'count']].corr()

  • 이를 한눈에 보기 편하게 heatmap()함수로 히트맵을 그려준다. corr()함수로 구한 상관관계 매트릭스 corrMat을 hetmap()함쉐 인수로 넣어준다. annot = True 로 하면 상관계수가 숫자로 표시된다.
# 피처 간 상관관계 매트릭스
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');

  • 타깃값이 count와의 상관관계가 중요한데, 다른 feature에 비해 windspeed feature는 상관계수가 0.1로 매우 낮기에 도움이 안되므로 제거하도록한다.

Baseline Model

  • 앞에서 EDA를 통해 추린 feature들을 활용해 Baseline Model을 훈련한다. scikitlearn의 기본 선형 회귀 모델을 Baseline Model로 사용할 예정이다. 먼저 데이터를 불러온다.
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')

Feature Engineering

  • 데이터를 변환하는 작업으로, Training data와 Test data에 모두 반영해야 하기에 두 데이터를 합쳐 feature engineering을 진행하고 다시 나누어준다.

이상치 제거

  • 그전에 Training data에서 아까 나온 weather가 4였던 이상치를 먼저 제거한다.

    # 훈련 데이터에서 weather가 4가 아닌 데이터만 추출
    train = train[train['weather'] != 4]

데이터 합치기

  • Traing data와 Test data에 같은 feature engineering을 해주기 위해 concat()함수를 이용해 데이터를 합친다.

    all_data_temp = pd.concat([train, test])
    all_data_temp

    • 총 17378행인데 인덱스가 6492까지밖에 없는데, 이는 Training data 0~10885까지 한 후 다시 Test data 0~6492를 매겼기 때문이다.

    • 원래 데이터의 인덱스를 무시하고 이어붙이려면 'ignore_index = True'를 전달한다.

    all_data = pd.concat([train, test], ignore_index=True)
    all_data

파생 feature(변수) 추가

  • 앞의 EDA 파트에서 진행했던 feature engineering 내용을 바탕으로 적용한다.

    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())
    • day, minute, second feature는 앞서 EDA에서 보았듯이 필요없으므로 생성하지 않았다.

필요없는 feature 제거

  • Test data에 없는 feature인 'casual', 'registered' feature를 제거하고, 정보가 겹치는 다른 feature를 가진 'datetime', 'date', 'month' feature를 제거하고, 타깃값과 낮은 상관계수를 가지던 'windspeed' feature도 제거한다.

    drop_features = ['casual', 'registered', 'datetime', 'date', 'month', 'windspeed']
    
    all_data = all_data.drop(drop_features, axis=1)
  • 이처럼 EDA 과정에서 얻은 내용으로 '모델링 시 데이터의 특징을 잘 나타내는 주요 feature만 선택하는 작업'을 Feature Selection(피처 선택)이라고 한다. 모델의 예측 성능을 높이기 위해 매우 중요하다.

데이터 나누기

  • Feature Engineering을 진행했으므로 다시 training data, test data를 나눈다.

    # 훈련 데이터와 테스트 데이터 나누기
    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'] # 타깃값
    • 타깃값이 있으면 training data, 없으면 test data이다. all_data['count']가 타깃값이므로 pd.isnull() 함수로 확인하였다. 앞에 붙인 ~는 not을 의미한다.

    • 이렇게 나눈 X_train과 X_test에는 타깃값인 count가 포함되어 있으므로 제거하고, 타깃값인 train['count']는 변수 y에 따로 할당했다.

    X_train.head()

평가지표 계산 함수 작성

  • 평가지표 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는 입력 데이터를 지수변환할지 정하는 파라미터이다. exp()함수로 지수변환을 해준다. 지수변환을 해주는 이유는 타깃값으로 log(count)를 사용할 것이기 때문이고, 타깃값이 정규분포라 그냥 count를 사용한다면 지수변환은 안해줘도 된다.
      np.nan_to_num()함수는 결측값을 모두 0으로 바꾸는 기능을 한다.

모델 훈련

from sklearn.linear_model import LinearRegression

linear_reg_model = LinearRegression()

log_y = np.log(y)  # 타깃값 로그변환
linear_reg_model.fit(X_train, log_y) # 모델 훈련
  • 훈련: 독립변수(feature)와 종속변수(타깃값)이 주어졌을 때 최적의 가중치(회귀계수)를 찾는 과정

  • 예측: 최적의 가중치를 아는 상태(훈련된 모델)에서 새로운 독립변수(데이터)가 주어졌을 때 타깃값을 추정하는 과정

모델 성능 검증

preds = linear_reg_model.predict(X_train)

print (f'선형회귀의 RMSLE 값 : {rmsle(log_y, preds, True):.4f}')
  • 훈련된 선형 회귀 모델이 X_train feature를 기반으로 타깃값을 예측한다. 원래 훈련 데이터로 예측을 하진 않는데 이 문제는 연습차원에서 훈련 데이터로 예측을 한 것이므로 참고할 필요가 있다. 다른 문제는 이렇게 풀지 않도록 한다.

예측 및 결과 제출

  • 베이스라인 모델로 예측한 결과를 제출한다. 예측값이 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로 설정해 인덱스를 제외하고 저장한다.

성능 개선 1: 릿지 회귀 모델

  • 릿지 회귀 모델은 L2 규제를 적용한 선형 회귀 모델로, 오버피팅이 적은 모델이다.
  • 모델의 성능을 개선할 때는 Feature Engineering을 보다 자세하게 수행하며 훈련 단계에서 하이퍼파라미터를 최적화한다.

하이퍼파라미터 최적화 (모델 훈련 단계)

  • Grid Search: 하이퍼파라미터를 격자(grid)처럼 촘촘하게 순회하며 최적의 하이퍼파라미터 값을 찾는 기법

  • 각 하이퍼파라미터를 적용한 모델마다 교차 검증(cross-validation)하며 성능을 측정하여 최종적으로 성능이 가장 좋았을 때의 하이퍼파라미터 값을 찾아준다.

  • '모델 생성 -> 그리드서치 객체 생성 -> 그리드서치 수행' 의 순서로 진행된다

  • 모델 생성

    from sklearn.linear_model import Ridge
    from sklearn.model_selection import GridSearchCV
    from sklearn import metrics
    
    ridge_model = Ridge()
  • 그리드서치 객체 생성

    1. 비교 검증해볼 하이퍼파라미터 값 목록
    2. 대상 모델
    3. 교차 검증용 평가수단(평가 함수)
    • 그리드서치를 진행하려면 다음의 세가지를 알고 있어야 하는데, 릿지 모델에서 중요한 하이퍼파라미터는 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)                    # 교차검증 분할 수
    • estimator: 분류 및 회귀 모델

    • param_grid: 딕셔너리 형태로 모델의 하이퍼파라미터명과 여러 하이퍼파라미터 값을 지정한다.

    • scoring: 평가지표

    • cv: 교차 검증 분할 개수

    • 그리드서치 객체는 param_grid로 전달된 모든 하이퍼파라미터를 대입해 교차 검증으로 모델 성능점수를 계산하여 어떤 값일 때 점수가 가장 좋은지 찾아준다. 교차 검증 시에는 해당 경진대회의 평가지표를 그대로 사용해야 한다.

  • 그리드서치 수행

log_y = np.log(y) # 타깃값 로그변환
gridsearch_ridge_model.fit(X_train, log_y) # 훈련(그리드서치)

  • fit()을 실행하면 객체 생성시 param_grid에 전달된 값들을 순회하면서 교차 검증으로 평가지표 점수를 계산하고, 가장 좋은 성능을 보인 값을 best_params_ 속성에 저장하며 이 값으로 훈련한 모델을 best_estimator_속성에 저장한다.

    print('최적 하이퍼파라미터 :', gridsearch_ridge_model.best_params_)

    • alpha가 0.1이고 max_iter가 3000일 때 가장 좋은 성능을 낸다는 사실을 알 수 있다.

성능 검증

  • 그리드 서치를 완료하면 best_estimator_ 속성에 최적예측기가 저장되어 있고, 따라서 예측은 그에 저장된 모델로 수행하면 된다.

    # 예측
    preds = gridsearch_ridge_model.best_estimator_.predict(X_train) 
    
    # 평가
    print(f'릿지 회귀 RMSLE 값 : {rmsle(log_y, preds, True):.4f}') 

성능 개선 2: 라쏘 회귀 모델

  • 라쏘 회귀 모델은 L1 규제를 적용한 선형 회귀모델로, 앞의 릿지 회귀 모델과 구현이 비슷하다.

하이퍼파라미터 최저화 (모델 훈련 단계)

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_)

성능 검증

# 예측
preds = gridsearch_lasso_model.best_estimator_.predict(X_train)

# 평가
print(f'라쏘 회귀 RMSLE 값 : {rmsle(log_y, preds, True):.4f}')
  • 결과 또한 비슷하다.

성능 개선 3: 랜덤 포레스트 회귀 모델

  • 앞의 릿지, 라쏘 회귀에서 성능 개선이 이루어지지않았기에 랜덤 포레스트 회귀 모델을 사용해본다.

  • 훈련 데이터를 랜덤하게 샘플링한 모델 n개를 각각 훈련하여 결과를 평균하는 방법이다.

하이퍼파라미터 최적화 (모델 훈련 단계)

  • 랜덤 포레스트 회귀 모델로 그리드 서치를 수행하고 최적 하이퍼파라미터 값을 출력해본다.

    from sklearn.ensemble import RandomForestRegressor
    
    # 모델 생성
    randomforest_model = RandomForestRegressor()
    # 그리드서치 객체 생성
    rf_params = {'random_state':[42], 'n_estimators':[100, 120, 140]}
    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는 랜덤 포레스트를 구성하는 결정 트리 개수를 의미한다.

모델 성능 검증

# 예측
preds = gridsearch_random_forest_model.best_estimator_.predict(X_train)

# 평가
print(f'랜덤 포레스트 회귀 RMSLE 값 : {rmsle(log_y, preds, True):.4f}')

  • 앞의 모델에 비해 값이 매우 개선된 것을 볼 수 있다. 4개의 모델 중 가장 성능이 좋은 모델은 랜덤 포레스트로, 이를 적용해 예측 결과를 제출하면 된다.

예측 및 결과 제출

  • 본 대회에서는 훈련 데이터로 성능 측정을 해보았기에, 테스트 데이터도 분포가 비슷하여 성능이 좋길 바라면서 먼저 훈련 데이터 타깃값과 테스트 데이터 타깃 예측값의 분포를 살펴본다.

  • histplot() 함수는 ax 파라미터를 사용해 여러 축에 그래프를 그릴 수 있다.

    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)

최종

  • score: 0.39595로 3243명의 참가자 중 195위를 차지하였다.(2022.11.18 기준)
  • 간단한 피처엔지니어링과 단순한 랜덤포레스트 모델만으로 상위 16% 정도의 순위에 오를 수 있었기에 좀 더 고도화된 모델로 순위를 더 올려볼 수 있겠다.
  • github에 해당 코드를 올려두었다.

참고: 머신러닝·딥러닝 문제해결 전략 (캐글 수상작 리팩터링으로 배우는 문제해결 프로세스와 전략)

참고: https://www.kaggle.com/code/viveksrinivasan/eda-ensemble-model-top-10-percentile/notebook

0개의 댓글