본문 바로가기
ML,DL

[머신러닝/딥러닝] 안전 운전자 예측 경진대회: 분석정리 및 시각화

by 공부기 2022. 11. 7.

 

이번 장에서는 실제 기업 데이터를 활용한 안전 운전자 예측 경진대회 문제를 풀어보았다.

 

경진대회명은 포르투 세구로 안전 운전자 예측 경진대회다.

보험회사에서는 사고를 낼 가능성이 낮은 안전운전자에게는 보험료를 적게 청구하고, 사고 가능성이 높은 난폭 운전자에게는 많은 보험료를 청구해야한다.

예측 모델이 부정확하다면 보험료를 잘못 부과하게 되어 고객 만족도와 회사 수익에 문제가 생긴다.

 

그러므로 이번 목표는 포르투 세구로 보험사에서 제공한 고객 데이터를 활용해서 운전자가 보험을 청구할 확률을 예측하는 것이다.

 

이 글에서는 모델을 만들기 전에 먼저 탐색적 데이터 분석으로 데이터를 살펴보고,

시각화를 통해서 모델링에 필요 없는 데이터를 찾아볼 것이다.

 

 

 

<대회 정보와 데이터>

Porto Seguro’s Safe Driver Prediction

https://www.kaggle.com/competitions/porto-seguro-safe-driver-prediction

 

Porto Seguro’s Safe Driver Prediction | Kaggle

 

www.kaggle.com

 

<참고 코드>

https://www.kaggle.com/code/bertcarremans/data-preparation-exploration/notebook

 

Data Preparation & Exploration

Explore and run machine learning code with Kaggle Notebooks | Using data from Porto Seguro’s Safe Driver Prediction

www.kaggle.com

 

 

 

 

 

 

주어진 데이터는 포르투 세구로가 보유한 고객 데이터다. 이 데이터에는 결측값이 꽤 많은데, 결측값은 -1로 기록되어있다고 한다. 타깃값은 0 또는 1로 이진분류 문제에 속한다.

 

0: 운전자가 보험금을 청구하지 않음

1: 운전자가 보험금을 청구함

 

 

 

 

 

 

데이터 둘러보기

 

데이터를 불러오고 살펴보자. index_col에 'id'를 전달하면 id열을 인덱스로 지정할 수 있다.

 

 

import pandas as pd

# 데이터 경로
data_path = '/kaggle/input/porto-seguro-safe-driver-prediction/'

train = pd.read_csv(data_path + 'train.csv', index_col = 'id')
test = pd.read_csv(data_path + 'test.csv', index_col = 'id')
submission = pd.read_csv(data_path + 'sample_submission.csv', index_col = 'id')

 

 

shape로 훈련 데이터와 테스트 데이터 크기도 확인해본다.

 

train.shape, test.shape

 

훈련 데이터: 약 59만 개

테스트 데이터: 약 89만 개

피처: 타깃값 제외 57개

 

 

 

훈련데이터, 테스트 데이터, 제출 샘플 데이터의 맨 위 다섯 행을 출력도 출력해본다.

 

train.head()

 

 

 

 

test.head()

 

 

 

 

 

submission.head()

 

 

제출 샘플 데이터를 보면 타깃값이 0.0364로 일괄 입력 되어있다.

저번 범주형 데이터 이진분류에서와 마찬가지로 예측해야 하는 값은 '타깃값이 1일 확률' 이다. 

즉, 운전자가 보험을 청구할 확률이 얼마나 되는지 예측해야 한다.

 

 

 

다음은 info()로 훈련 데이터를 살펴보았다.

 

train.info()

 

 

 

결과값이 많아 중간은 생략했다. 데이터 타입은 int64와 float64로 이루어져있다. 피처명을 보면 어떤 의미인지 잘 알 수 없으나 아래와 같은 일정한 형식이 보인다.

 

ps_[분류]_[분류별 일련번호]_[데이터 종류]

 

맨 처음은 모두 ps로 시작하고 분류로 ind, reg, car, calc가 나온다. 다음으로는 일련번호, 데이터 종류가 순서대로 나온다.

 

<데이터 종류>

bin: 이진피처

cat: 명목형 피처

생략: 순서형 또는 연속형 피처

 

 

이런 피처명에서 분류와 일련번호로는 의미를 알 수 없기 때문에 정보를 얻을 수 없다. 마지막에 나오는 데이터 종류에서만 유의미한 정보를 얻을 수 있다.

 

또 여기서 잘 봐야하는 부분은 결측값 개수다. 출력 결과에서는 모든 피처에 결측값이 없다고 나온다. 하지만 위에서 보았듯이 값이 누락된 곳에 -1이 입력되어있어서 결측값이 없다고 판단한 것이다. 

이런 경우에는 -1을 np.NaN으로 변환한 다음 개수를 세도록 한다.

 

 

피처 개수가 많으므로 결측값을 시각화 해서 한눈에 보는 것이 좋을 것 같다.

missingno 패키지를 사용하면 결측값을 시각화 할 수 있다.

missingno의 bar() 함수를 사용하면 훈련 데이터에 결측값이 얼마나 있는지 막대그래프로 보여준다.

 

import numpy as np
import missingno as msno

# 훈련 데이터 복사본에서 -1을 np.NaN으로 변환
train_copy = train.copy().replace(-1, np.NaN)

# 결측값 시각화 (처음 28개만)
msno.bar(df = train_copy.iloc[:, 1:29], figsize = (13, 6));

 

 

훈련데이터를 복사하고 replace로 -1을 np.NaN으로 바꾸어주었고, 피처가 많으므로 앞의 28개를 먼저 시각화한 결과다.

막대그래프의 높이가 낮을수록 결측값이 많다는 의미다. 그래프 위에는 정상 값이 몇 개인지 표시되어있다.

노란색으로 표시된 피처에 특히 결측값이 많다. 막대 높이가 가장 낮은 ps_car_03_cat 피처는 결측치가 3/5 이상으로 대부분이 결측값이다.

 

 

나머지 피처들의 결측값도 보겠다.

 

msno.bar(df=train_copy.iloc[:, 29:], figsize=(13, 6));

 

 

 

이 피처들 중에서는 ps_car_14에 결측값이 조금 있고 나머지 피처에는 거의 없다.

 

 

missingno의 matrix 함수로 결측값을 매트릭스 형태로도 시각화할 수 있다.

 

# 결측값을 매트릭스 형태로 시각화하기
msno.matrix(df = train_copy.iloc[:, 1:29], figsize = (13,6));

 

 

오른쪽 막대는 결측값의 상대적인 분포를 보여준다. 검은색으로 튀어나온 부분이 결측값이 몰려있는 행을 의미한다. 22는 결측값이 없는 열의 개수, 28은 전체 열 개수를 의미한다. 위의 매트릭스에서 흰색으로 표현된 빈칸들이 결측치인데 ps_car_03_cat에 가장 빈칸이 많아보인다. bar()로 보았을 때와 마찬가지로 matrix에서도 결측치가 많다는 것을 알 수 있다.

 

 

 

 

 

 

피처 요약표 만들기

 

이번에도 피처 요약표를 만들어 다양한 피처를 한눈에 파악할 수 있게 하는 것이 좋을 것이다.

저번 이진분류에서 만들었던 피처 요약표를 조금 수정하여 만들었다.

 

# 피처 요약표
def resumetable(df):
    print(f'데이터셋 형상: {df.shape}')
    summary = pd.DataFrame(df.dtypes, columns = ['데이터 타입'])
    summary['결측값 개수'] = (df == -1).sum().values  # 피처별 -1 개수
    summary['고윳값 개수'] = df.nunique().values
    summary['데이터 종류'] = None
    
    for col in df.columns:  
        if 'bin' in col or col == 'target' :
            summary.loc[col, '데이터 종류'] = '이진형'
        
        elif 'cat' in col:
            summary.loc[col, '데이터 종류'] = '명목형'
            
        elif df[col].dtype == float:
            summary.loc[col, '데이터 종류'] = '연속형'
            
        elif df[col].dtype == int:
            summary.loc[col, '데이터 종류'] = '순서형'
            
    return summary

 

결측값이 -1이었으므로 결측값 개수를 구하기 위해서 (df == -1).sum().values 로 피처별 -1의 개수를 구했다.

 

고윳값 갯수는 nunique() 함수로 구했다. (* unique(): 각 고윳값들을 출력, nunique(): 고윳값들의 갯수를 출력)

 

데이터 종류를 추가하기 위해서 for문을 사용해 각 피처를 순회하며 데이터 종류를 추가했다. 피처명에 'bin'이 포함되어 있거나 타깃 열이면 이진형, 'cat'이 포함되어 있으면 명목형, 데이터 타입이 float면 연속형, int면 순서형 데이터라고 주었다.

 

 

summary = resumetable(train)
summary

 

피처요약표(아래생략)

피처 요약표를 출력한 결과다. 피처 요약표를 통해 데이터 타입과 결측값 개수, 고윳값 개수, 데이터 종류를 한눈에 보기 좋아졌다. 이를 이용해서 원하는 피처를 추출할 수도 있다.

 

다음은 명목형 피처와 실수형인 피처를 추출하는 코드다.

 

summary[summary['데이터 종류'] == '명목형'].index

 

 

 

# 데이터 타입이 실수형인 피처
summary[summary['데이터 타입'] == 'float64'].index

 

 

 

 

 

 

 

 

데이터 시각화

 

이제 데이터 시각화를 통해서 모델링에 필요한 피처와 필요 없는 피처를 선별해보자.

타깃값 분포를 활용해서 타깃값이 얼마나 불균형한지 알아볼 수 있다.

 

이진 피처, 명목형 피처, 순서형 피처의 고윳값별 타깃값 비율을 막대그래프로 알아보았다.

고윳값별 타깃값 비율을 보면 모델링 시 어떤 피처를 제거해야할지 알 수 있게 된다.

 

# 시각화 라이브러리
import seaborn as sns
import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline

 

# 타깃값 분포
def write_percent(ax, total_size):
    '''도형 객체를 순회하며 막대 그래프 상단에 타깃값 비율 표시'''
    for patch in ax.patches:
        height = patch.get_height()     # 도형 높이(데이터 개수)
        width = patch.get_width()       # 도형 너비
        left_coord = patch.get_x()      # 도형 왼쪽 테두리의 x축 위치
        percent = height/total_size*100 # 타깃값 비율
        
        # (x, y) 좌표에 텍스트 입력
        ax.text(left_coord + width / 2.0,     # x축 위치
                height + total_size * 0.001,  # y축 위치
                '{:1.1f}%'.format(percent), # 입력 텍스트
                ha='center')                # 가운데 정렬
    
mpl.rc('font', size = 15)
plt.figure(figsize = (7, 6))

ax = sns.countplot(x = 'target', data = train)
write_percent(ax, len(train)) # 비율 표시
ax.set_title('Target Distribution');

 

플롯을 그린 후 각 값의 비율을 상단에 표시해주는 함수를 만들어서 비율을 표시해주었다. 

 

타깃값 0은 96.4%, 타깃값 1은 3.6%를 차지한다. 즉 전체 운전자 중 3.6%만 보험금을 청구했다는 것이다.

차 사고가 흔하게 나지는 않으므로 소수의 운전자만 보험금을 청구하게 되기 때문에 타깃값이 불균형하다. 

-> 타깃값이 불균형할 때 비율이 작은 타깃값 1을 잘 예측하는 것이 중요하다.

그러므로 각 피처의 분포보다 각 피처의 고윳값별 타깃값 1 비율을 알아보는 것이 좋겠다. 고윳값별 타깃값 1 비율을 통해서 해당 피처가 모델링에 필요한 피처인지 확인할 수 있다.

 

고윳값별로 타깃값 1 비율이나 통계적 유효성을 살펴볼 것인데, 통계적 유효성은 barplot()을 그릴 때 나타나는 신뢰구간으로 판단한다. 신뢰구간이 좁다면 통계적으로 어느 정도 유효하다고 보고, 넓다면 신뢰하기 어렵다고 본다.

 

 

  • 고윳값별로 타깃값에 대한 예측력이 없음 -> 타깃값 예측에 도움이 되지 않는피처(제거)
  • 신뢰구간이 커서 통계적 유효성이 떨어짐 -> 타깃값 예측에 도움이 되지 않는피처(제거)
  • 고윳값별로 타깃값에 대한 예측력도 있고, 통계적 유효성도 있음 -> 타깃값 예측에 도움이 되는 피처

정리하면, 고윳값별 타깃값 1 비율이 충분히 차이가 나고 신뢰구간도 작은 피처여야 모델링에 도움이 된다. 그렇지 않은 피처는 제거하는 것이 좋다.

 

 

 

 

 

 

이진 피처

 

이진 피처의 고윳값별 타깃값 비율을 구해보자. 저번 이진분류에서 교차분석표를 활용해 카운트플롯과 포인트플롯을 그리던  함수 plot_cat_dist_with_true_ratio()와 유사하다. 이번에는 막대그래프를 그린다는 점에서 다르다.

 

import matplotlib.gridspec as gridspec

def plot_target_ratio_by_features(df, features, num_rows, num_cols, 
                                  size=(12, 18)):
    mpl.rc('font', size = 9) 
    plt.figure(figsize = size)                     # 전체 Figure 크기 설정
    grid = gridspec.GridSpec(num_rows, num_cols) # 서브플롯 배치
    plt.subplots_adjust(wspace = 0.3, hspace = 0.3)  # 서브플롯 좌우/상하 여백 설정

    for idx, feature in enumerate(features):
        ax = plt.subplot(grid[idx])
        # ax축에 고윳값별 타깃값 1 비율을 막대 그래프로 그리기
        sns.barplot(x = feature, y = 'target', data = df, palette = 'Set2', ax = ax)

 

bin_features = summary[summary['데이터 종류'] == '이진형'].index # 이진 피처

# 이진 피처 고윳값별 타깃값 1 비율을 막대 그래프로 그리기
plot_target_ratio_by_features(train, bin_features, 6, 3) # 6행 3열 배치

 

고윳값별 타깃값 비율을 막대그래프로 표시해주는 함수를 만들어 그래프를 그렸다. 이진피처가 18개이므로 6행 3열로 배치했다.

 

 

이진 피처기 때문에 고윳값이 0과 1  두 개다. 첫 번째(0행0열) 그래프는 타깃값 그래프이므로 두 번째 (0행 1열) 그래프부터 본다.

두 번째 그래프 ps_ind_06_bin 피처를 살펴보면 값이 0일 때 타깃값이 1인 비율이 약 4% 정도이고, 나머지 96%는 타깃값 0이다. 값이 1일 때는 타깃값 1이 2.8% 정도고 타깃값 0 비율은 97.2% 정도가 될 것이다.

이 피처는 고윳값별로 타깃값 비율이 다르므로 타깃값을 추정하는 예측력이 있다. 게다가 신뢰구간도 좁기 때문에 모델링할 때 도움이 되는 피처라고 판단할 수 있다.

 

이런 식으로 모델링 시 어떤 피처를 사용하고 제거해야하는지 찾으면 다음 표와 같다.

 

서브플롯 위치 피처명 제거해야 하는 이유
6~9 (1행 2열 ~ 2행 2열) ps_ind_10_bin ~
ps_ind_13_bin
신뢰구간이 넓어 통계적 유효성이 떨어짐
13~ 18 (4행 0열 ~ 5행 2열) ps_calc_15_bin ~
ps_calc_20_bin
고유값별 타깃값 비율 차이가 없어서 타깃값 예측력이 없음

 

 

* calc 분류를 보면 모두 타깃값 비율에 차이가 없다. calc 분류의 다른 피처도 차이가 있는지 없는지 알아볼 필요가 있다.

 

 

 

 

 

 

 

명목형 피처

 

명목형 피처의 고윳값 개수도 그래프를 그려 보자.

 

# 명목형 피처
nom_features = summary[summary['데이터 종류'] == '명목형'].index # 명목형 피처

plot_target_ratio_by_features(train, nom_features, 7, 2) # 7행 2열

 

명목형피처의 고윳값 개수 (아래생략)

이번에는 결측값인 -1을 포함하는 피처가 많다. 결측값은 보통 많지 않으면 다른 값으로 대체하고, 많다면 해당 피처를 제거하는 등 적절하게 처리해야하지만, 결측값 자체가 타깃값 예측에 도움을 주는 경우도 있다.

 

첫 번째 그래프인 ps_ind_02_cat 피처에서 결측값 -1이 다른 고윳값들보다 타깃값 1 비율이 크다. 신뢰구간이 넓다는 점을 감안해도 비율이 크다. 이런 경우에 결측값을 다른 값으로 대체하면 모델 성능이 더 나빠질 수도 있다고 한다. 결측값 자체가 타깃값에 대한 예측력이 있기 때문이다. 그러므로 -1을 하나의 고윳값이라고 간주하며 그대로 두고 모델링한다.

 

다섯 번째 그래프의 ps_car_02_cat 피처에서는 -1일 때 타깃값 1의 비율은 0%다. 이 경우에서는 피처 값이 -1이면 타깃값이 0이라고 판단할 수 있다. 즉, 결측값이 타깃값을 예측하는 데 도움을 주는 경우다.

 

이런 이유로 명목형 피처 중에서 제거할 피처는 없는 것으로 판단했다. 하지만 맨 아래에 있는 ps_car_10_cat 피처에서는 타깃값 1의 평균 비율이 비슷하고, 고윳값 2의 신뢰구간이 넓다.

 

이 피처를 제거해야 할지 말지는 애매하다. 이런 경우 해당 피처를 제거한 경우와 제거하지 않은 경우를 비교하는 것도 좋은 방법이다. 교재에서 테스트해본 결과 제거하지 않은 경우에 성능이 더 좋았으므로 제거하지 않도록 한다.

 

 

 

 

 

 

순서형 피처

 

ord_features = summary[summary['데이터 종류'] == '순서형'].index # 순서형 피처

plot_target_ratio_by_features(train, ord_features, 8, 2, (12, 20)) # 8행 2열

 

순서형 피처들의 고윳값별 타깃값 1의 비율 (아래생략)

순서형 피처들의 고윳값별 타깃값 1의 비율을 표시한 그래프에서 1행 0열의 ps_ind_14 피처를 보면 0, 1, 2, 3의 타깃값 비율은 큰 차이가 없고, 고윳값 4의 신뢰구간은 넓으므로 통계적 유효성이 떨어지기 때문에 제거하도록 한다.

 

ps_calc_04부터 아래의 모든 피처들은 모두 고윳값별 타깃값 비율이 거의 비슷하거나 신뢰구간이 넓어 제거한다. 

이진 피처에서와 마찬가지로 calc 분류에 속하는 피처는 모두 제거하는 것으로 판단했다.

 

 

 

 

 

연속형 피처

 

마지막으로 연속형 피처의 구간별 타깃값 1의 비율을 볼 것이다. 연속형 피처는 연속된 값이므로 고윳값이 많다. 이 경우 고윳값별 타깃값 1 비율을 구하기가 어려우므로 몇 개의 구간으로 나누어 구간별 타깃값 1 비율을 알아본다.

 

 

구간을 나누려면 판다스의 cut() 함수를 사용하면 된다.

다음 코드는 cut() 함수를 사용해 여러 개의 값을 3개의 구간으로 나누는 코드다.

pd.cut([1.0, 1.5, 2.1, 2.7, 3.5, 4.0], 3)

 

함수의 첫 번째 인수에 값들의 리스트, 두 번째 인수에 나눌 구간의 개수를 입력한다.

 

실행 결과 세 구간으로 나누어졌다.

 

 

 

cont_features = summary[summary['데이터 종류'] == '연속형'].index # 연속형 피처

plt.figure(figsize=(12, 16))          # Figure 크기 설정
grid = gridspec.GridSpec(5, 2)        # GridSpec 객체 생성
plt.subplots_adjust(wspace=0.2, hspace=0.4) # 서브플롯 간 여백 설정

for idx, cont_feature in enumerate(cont_features):
    # 값을 5개 구간으로 나누기
    train[cont_feature] = pd.cut(train[cont_feature], 5)

    ax = plt.subplot(grid[idx])       # 분포도를 그릴 서브플롯 설정
    sns.barplot(x=cont_feature, y='target', data=train, palette='Set2', ax=ax)
    ax.tick_params(axis='x', labelrotation=10) # x축 라벨 회전

 

 

연속형 피처 '구간별' 타깃값 1 비율

출력 결과, 역시 구간별 타깃값 비율 차이가 없는 calc 분류의 피처들을 제거하도록 한다.

데이터 종류별 타깃값 비율을 확인한 결과 calc 분류의 피처들은 데이터 종류에 상관 없이 모두 제거해야 한다는 사실을 발견했다. 

 

 

 

 

이번에는 연속형 피처 간 '상관관계'를 파악해볼 것이다. 

일반적으로 강한 상관관계를 보이는 두 피처가 있으면 둘 중 하나는 제거하는 것이 좋다. 상관관계가 강하면 타깃값 예측 력도 비슷하고, 그런 피처가 모델 성능을 떨어트릴 수 있다고 한다.

 

상관관계 계수가 얼마 이상일 때 제거해야하는지 정해진 기준은 없다. 제거한다고 해서 성능이 반드시 향상되는것도 아니다. 그저 고려해볼 요소 정라고 보면 된다. 피어슨 상관계수가 0.8 이상의 아주 강한 상관관계를 보이는 피처가 있다면 고려해보는 것이 좋다.

 

피처간 상관관계를 파악하기 위해서 히트맵을 그려볼 것이다. 히트맵을 그리기 전에 현재 피처에 있는 결측값을 제거해주어야 한다.

 

# -1을 np.NaN으로 변환한 train_copy에서 결측값 제거
train_copy = train_copy.dropna() # np.NaN 값 삭제

 

히트맵은 시본의 heatmap() 함수로 그릴 수 있다. 

 

plt.figure(figsize = (10, 8))
cont_corr = train_copy[cont_features].corr() # 연속형 피처 간 상관관계 
sns.heatmap(cont_corr, annot = True, cmap = 'OrRd'); # 히트맵 그리기



 

 

ps_car_12와 ps_car_14 두 피처에서 0.77의 가장 강한 상관관계를 보인다. 제거해야 할 만큼의 강한 상관관계는 아니지만 제거하는 것이 성능이 더 좋았으므로 제거하도록 한다.

ps_reg_02와 pst_reg_03 두 피처 간 상관계수도 0.76으로 두 번째로 상관관계가 강한데 테스트 결과 둘 중 하나를 제거하면 오히려 성능이 떨어졌으므로 그대로 남겨둔다.

 

이렇게 상황별로 다르므로 비교해보는 것이 중요한 것 같다.

 

 

 

 

 

 

 

 


 

분석 정리

 

1. 저번 범주형 데이터 이진분류 경진대회에 비해 데이터가 크고 피처 수도 많다.

 

2. 피처명만으로 분류별, 데이터 종류별 피처들을 구분해 추출할 수 있다.

 

3. 결측값 자체에 타깃값 예측력이 있으면 고윳값으로 간주한다.

 

4. 피처 간 상관관계 분석은 결측값 제거 후 수행한다.

 

5. 신뢰구간이 넓으면 통계적 유효성이 떨어져 제거한다. (ps_ind_14, ps_calc_04 ~ ps_calc_14)

 

6. 고윳값별 타깃값 비율에 차이가 없다면 타깃값 예측력이 없으므로 제거한다. (ps_calc_04 ~ ps_calc_14)

 

7. 연속형 데이터에서 구간별 타깃값 차이가 거의 없다면 타깃값 예측력이 없으므로 제거한다. (ps_calc_01 ~ ps_calc_03)

 

8. 일반적으로 강한 상관관계를 보이는 두 피처가 있으면 둘 중 하나를 제거하는 것이 종을 수도 있다.(ps_car_14)

 

 

 

 

 

 

 

 

 

참고 교재

 

머신러닝·딥러닝 문제해결 전략 |  신백균

 

[머신러닝·딥러닝 문제해결 전략 - chapter8]