Post

3. 안전 운전자 예측

3. 안전 운전자 예측



난이도2   
경진대회명포르투 세구로 안전 운전자 예측 경진대회   
미션포르투 세구로 보험사에서 제공한 고객 데이터를 활용해
운전자가 보험을 청구할 확률 예측
   
문제유형이진분류평가지표정규화된 지니계수 
제출시 사용한 모델LightGBM와 XGBoost의 앙상블   
파이썬 버전3.7.10   
사용 라이브러리 버전- numpy == 1.19.5 - pandas == 1.3.2
- seaborn == 0.11.2 - matplotlib == 3.4.3
- sklearn == 0.32.2 - scipy == 1.7.1
- missingno == 0.4.2 - lightgbm == 3.2.1
- xgboost == 1.4.2. - bayes_opt == 1.2.0
   


학습 목표

  • 실제 기업 데이터를 활용한 안전 운전자 예측 경진대회에 참가
  • 베이스라인 모델에 시작해 성능이 좋은 모델로 발전
  • 실제로 많이 활용되는 여러 가지 고급 모델링 기법 학습


학습 순서

  1. 경진대회 이해
  2. 탐색적 데이터 분석
  3. 베이스라인 모델(LightGBM)
  4. 성능 개선 I (LightGBM + 피처 엔지니어링 강화, 하이퍼파라미터 최적화)
  5. 성능 개선 II (XGBoost)
  6. 성능 개선 III (앙상블)


학습 키워드

  • 유형 및 평가 지표 : 이진분류, 정규화된 지니계수
  • 탐색적 데이터 분석 : 피처 요약표 응용, 결측값 시각화, 결측값 처리
  • 머신러닝 모델 : OOF 예측, LightGBM, XGBoost, 앙상블
  • 피처 엔지니어링 : 창의적 피처 엔지니어링
  • 하이퍼파라미터 최적화 : 베이지안 최적화





1. 경진대회 이해

  • 포르투 세구로는 지난 20년 간 머신러니을 활용해왔지만, 자동차 보험과 관련해서 보다 정확한 예측 모델을 만들고자 본 대회를 열었다.

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

  • 데이터셋 특징

    • 운전자가 보험금을 청구할 확률을 정확히 예측하는 모델을 만드는게 본 경진대회의 목표이다.
    • 주어진 데이터에는 결측값이 많다. 결측값은 -1로 기록돼 있다.
    • 타겟값은 0 또는 1이다. 값이 0이면 운전자가 보험금을 청구하지 않는다는 뜻이고, 1이면 청구한다는 뜻이다.





2. 탐색적 데이터 분석

분석 과정은 아래와 같은 순서로 진행된다.



2.1 데이터 둘러보기

kaggle competitions download -c porto-seguro-safe-driver-prediction


  • 본 경진대회에서 예측해야 하는 값은 타겟값이 1일 확률이다.
  • 타겟값 0은 운전자가 보험금을 청구하지 않는 경우, 타겟값 1은 청구하는 경우를 의미한다.
  • 즉 운전자가 보험금을 청구할 확률이 얼마나 되는지를 예측한다.



데이터 로드 및 shape 확인

데이터를 읽어올 때 index_col 파라미터에 데이터가 가지고 있는 id를 전달한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pandas as pd 

data_path = 'datasets/'

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

train.shape, test.shape


'''
((595212, 58), (892816, 57))
'''



1
train.head().T
id79131617
target0.0000000.0000000.0000000.0000000.000000
ps_ind_012.0000001.0000005.0000000.0000000.000000
ps_ind_02_cat2.0000001.0000004.0000001.0000002.000000
ps_ind_035.0000007.0000009.0000002.0000000.000000
ps_ind_04_cat1.0000000.0000001.0000000.0000001.000000
ps_ind_05_cat0.0000000.0000000.0000000.0000000.000000
ps_ind_06_bin0.0000000.0000000.0000001.0000001.000000
ps_ind_07_bin1.0000000.0000000.0000000.0000000.000000
ps_ind_08_bin0.0000001.0000001.0000000.0000000.000000
ps_ind_09_bin0.0000000.0000000.0000000.0000000.000000
ps_ind_10_bin0.0000000.0000000.0000000.0000000.000000
ps_ind_11_bin0.0000000.0000000.0000000.0000000.000000
ps_ind_12_bin0.0000000.0000000.0000000.0000000.000000
ps_ind_13_bin0.0000000.0000000.0000000.0000000.000000
ps_ind_140.0000000.0000000.0000000.0000000.000000
ps_ind_1511.0000003.00000012.0000008.0000009.000000
ps_ind_16_bin0.0000000.0000001.0000001.0000001.000000
ps_ind_17_bin1.0000000.0000000.0000000.0000000.000000
ps_ind_18_bin0.0000001.0000000.0000000.0000000.000000
ps_reg_010.7000000.8000000.0000000.9000000.700000
ps_reg_020.2000000.4000000.0000000.2000000.600000
ps_reg_030.7180700.766078-1.0000000.5809480.840759
ps_car_01_cat10.00000011.0000007.0000007.00000011.000000
ps_car_02_cat1.0000001.0000001.0000001.0000001.000000
ps_car_03_cat-1.000000-1.000000-1.0000000.000000-1.000000
ps_car_04_cat0.0000000.0000000.0000000.0000000.000000
ps_car_05_cat1.000000-1.000000-1.0000001.000000-1.000000
ps_car_06_cat4.00000011.00000014.00000011.00000014.000000
ps_car_07_cat1.0000001.0000001.0000001.0000001.000000
ps_car_08_cat0.0000001.0000001.0000001.0000001.000000
ps_car_09_cat0.0000002.0000002.0000003.0000002.000000
ps_car_10_cat1.0000001.0000001.0000001.0000001.000000
ps_car_11_cat12.00000019.00000060.000000104.00000082.000000
ps_car_112.0000003.0000001.0000001.0000003.000000
ps_car_120.4000000.3162280.3162280.3741660.316070
ps_car_130.8836790.6188170.6415860.5429490.565832
ps_car_140.3708100.3887160.3472750.2949580.365103
ps_car_153.6055512.4494903.3166252.0000002.000000
ps_calc_010.6000000.3000000.5000000.6000000.400000
ps_calc_020.5000000.1000000.7000000.9000000.600000
ps_calc_030.2000000.3000000.1000000.1000000.000000
ps_calc_043.0000002.0000002.0000002.0000002.000000
ps_calc_051.0000001.0000002.0000004.0000002.000000
ps_calc_0610.0000009.0000009.0000007.0000006.000000
ps_calc_071.0000005.0000001.0000001.0000003.000000
ps_calc_0810.0000008.0000008.0000008.00000010.000000
ps_calc_091.0000001.0000002.0000004.0000002.000000
ps_calc_105.0000007.0000007.0000002.00000012.000000
ps_calc_119.0000003.0000004.0000002.0000003.000000
ps_calc_121.0000001.0000002.0000002.0000001.000000
ps_calc_135.0000001.0000007.0000004.0000001.000000
ps_calc_148.0000009.0000007.0000009.0000003.000000
ps_calc_15_bin0.0000000.0000000.0000000.0000000.000000
ps_calc_16_bin1.0000001.0000001.0000000.0000000.000000
ps_calc_17_bin1.0000001.0000001.0000000.0000000.000000
ps_calc_18_bin0.0000000.0000000.0000000.0000001.000000
ps_calc_19_bin0.0000001.0000001.0000000.0000001.000000
ps_calc_20_bin1.0000000.0000000.0000000.0000000.000000



1
test.head().T
id01234
ps_ind_010.0000004.0000005.0000000.0000005.000000
ps_ind_02_cat1.0000002.0000001.0000001.0000001.000000
ps_ind_038.0000005.0000003.0000006.0000007.000000
ps_ind_04_cat1.0000001.0000000.0000000.0000000.000000
ps_ind_05_cat0.0000000.0000000.0000000.0000000.000000
ps_ind_06_bin0.0000000.0000000.0000001.0000000.000000
ps_ind_07_bin1.0000000.0000000.0000000.0000000.000000
ps_ind_08_bin0.0000000.0000000.0000000.0000000.000000
ps_ind_09_bin0.0000001.0000001.0000000.0000001.000000
ps_ind_10_bin0.0000000.0000000.0000000.0000000.000000
ps_ind_11_bin0.0000000.0000000.0000000.0000000.000000
ps_ind_12_bin0.0000000.0000000.0000000.0000000.000000
ps_ind_13_bin0.0000000.0000000.0000000.0000000.000000
ps_ind_140.0000000.0000000.0000000.0000000.000000
ps_ind_1512.0000005.00000010.0000004.0000004.000000
ps_ind_16_bin1.0000001.0000000.0000001.0000001.000000
ps_ind_17_bin0.0000000.0000000.0000000.0000000.000000
ps_ind_18_bin0.0000000.0000000.0000000.0000000.000000
ps_reg_010.5000000.9000000.4000000.1000000.900000
ps_reg_020.3000000.5000000.0000000.2000000.400000
ps_reg_030.6103280.7713620.916174-1.0000000.817771
ps_car_01_cat7.0000004.00000011.0000007.00000011.000000
ps_car_02_cat1.0000001.0000001.0000001.0000001.000000
ps_car_03_cat-1.000000-1.000000-1.000000-1.000000-1.000000
ps_car_04_cat0.0000000.0000000.0000000.0000000.000000
ps_car_05_cat-1.0000000.000000-1.000000-1.000000-1.000000
ps_car_06_cat1.00000011.00000014.0000001.00000011.000000
ps_car_07_cat1.0000001.0000001.0000001.0000001.000000
ps_car_08_cat1.0000001.0000001.0000001.0000001.000000
ps_car_09_cat2.0000000.0000002.0000002.0000002.000000
ps_car_10_cat1.0000001.0000001.0000001.0000001.000000
ps_car_11_cat65.000000103.00000029.00000040.000000101.000000
ps_car_111.0000001.0000003.0000002.0000003.000000
ps_car_120.3162280.3162280.4000000.3741660.374166
ps_car_130.6695560.6063200.8962390.6521100.812914
ps_car_140.3521360.3583290.3984970.3814450.385097
ps_car_153.4641022.8284273.3166252.4494903.316625
ps_calc_010.1000000.4000000.6000000.1000000.900000
ps_calc_020.8000000.5000000.6000000.5000000.600000
ps_calc_030.6000000.4000000.6000000.5000000.800000
ps_calc_041.0000003.0000002.0000002.0000003.000000
ps_calc_051.0000003.0000003.0000001.0000004.000000
ps_calc_066.0000008.0000007.0000007.0000007.000000
ps_calc_073.0000004.0000004.0000003.0000001.000000
ps_calc_086.00000010.0000006.00000012.00000010.000000
ps_calc_092.0000002.0000003.0000001.0000004.000000
ps_calc_109.0000007.00000012.00000013.00000012.000000
ps_calc_111.0000002.0000004.0000005.0000004.000000
ps_calc_121.0000000.0000000.0000001.0000000.000000
ps_calc_131.0000003.0000002.0000000.0000000.000000
ps_calc_1412.00000010.0000004.0000005.0000004.000000
ps_calc_15_bin0.0000000.0000000.0000001.0000000.000000
ps_calc_16_bin1.0000000.0000000.0000000.0000001.000000
ps_calc_17_bin1.0000001.0000000.0000001.0000001.000000
ps_calc_18_bin0.0000001.0000000.0000000.0000000.000000
ps_calc_19_bin0.0000000.0000000.0000000.0000000.000000
ps_calc_20_bin1.0000001.0000000.0000000.0000001.000000



1
submission.head()
target
id
00.0364
10.0364
20.0364
30.0364
40.0364




2.2 데이터 분석

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
train.info()

'''
<class 'pandas.core.frame.DataFrame'>
Index: 595212 entries, 7 to 1488027
Data columns (total 58 columns):
 #   Column          Non-Null Count   Dtype  
---  ------          --------------   -----  
 0   target          595212 non-null  int64  
 1   ps_ind_01       595212 non-null  int64  
 2   ps_ind_02_cat   595212 non-null  int64  
 3   ps_ind_03       595212 non-null  int64  
 4   ps_ind_04_cat   595212 non-null  int64  
 5   ps_ind_05_cat   595212 non-null  int64  
 6   ps_ind_06_bin   595212 non-null  int64  
 7   ps_ind_07_bin   595212 non-null  int64  
 8   ps_ind_08_bin   595212 non-null  int64  
 9   ps_ind_09_bin   595212 non-null  int64  
 10  ps_ind_10_bin   595212 non-null  int64  
 11  ps_ind_11_bin   595212 non-null  int64  
 12  ps_ind_12_bin   595212 non-null  int64  
 13  ps_ind_13_bin   595212 non-null  int64  
 14  ps_ind_14       595212 non-null  int64  
 15  ps_ind_15       595212 non-null  int64  
 16  ps_ind_16_bin   595212 non-null  int64  
 17  ps_ind_17_bin   595212 non-null  int64  
 18  ps_ind_18_bin   595212 non-null  int64  
 19  ps_reg_01       595212 non-null  float64
 20  ps_reg_02       595212 non-null  float64
 21  ps_reg_03       595212 non-null  float64
 22  ps_car_01_cat   595212 non-null  int64  
 23  ps_car_02_cat   595212 non-null  int64  
 24  ps_car_03_cat   595212 non-null  int64  
 25  ps_car_04_cat   595212 non-null  int64  
 26  ps_car_05_cat   595212 non-null  int64  
 27  ps_car_06_cat   595212 non-null  int64  
 28  ps_car_07_cat   595212 non-null  int64  
 29  ps_car_08_cat   595212 non-null  int64  
 30  ps_car_09_cat   595212 non-null  int64  
 31  ps_car_10_cat   595212 non-null  int64  
 32  ps_car_11_cat   595212 non-null  int64  
 33  ps_car_11       595212 non-null  int64  
 34  ps_car_12       595212 non-null  float64
 35  ps_car_13       595212 non-null  float64
 36  ps_car_14       595212 non-null  float64
 37  ps_car_15       595212 non-null  float64
 38  ps_calc_01      595212 non-null  float64
 39  ps_calc_02      595212 non-null  float64
 40  ps_calc_03      595212 non-null  float64
 41  ps_calc_04      595212 non-null  int64  
 42  ps_calc_05      595212 non-null  int64  
 43  ps_calc_06      595212 non-null  int64  
 44  ps_calc_07      595212 non-null  int64  
 45  ps_calc_08      595212 non-null  int64  
 46  ps_calc_09      595212 non-null  int64  
 47  ps_calc_10      595212 non-null  int64  
 48  ps_calc_11      595212 non-null  int64  
 49  ps_calc_12      595212 non-null  int64  
 50  ps_calc_13      595212 non-null  int64  
 51  ps_calc_14      595212 non-null  int64  
 52  ps_calc_15_bin  595212 non-null  int64  
 53  ps_calc_16_bin  595212 non-null  int64  
 54  ps_calc_17_bin  595212 non-null  int64  
 55  ps_calc_18_bin  595212 non-null  int64  
 56  ps_calc_19_bin  595212 non-null  int64  
 57  ps_calc_20_bin  595212 non-null  int64  
dtypes: float64(10), int64(48)
memory usage: 267.9 MB
'''

  • 데이터 타입: int64, float64
  • 피처명은 비식별화 되어있어 각 피처가 어떤 의미인지 알 수 없지만 일정한 형식 있음
  • 맨 처음은 모두 ps로 시작
  • 분류: ind, reg, car, calc
  • 분류 후 해당 분류에서의 일련번호 나옴
  • 데이터 종류가 bin이면 이진 피처
  • cat이면 명목형 피처
  • 데이터 종류가 생략돼 있으면 순서형 피처 또는 연속형 피처

여기서 분류와 일련번호는 비식별화되어 있어 각 분류가 무슨 의미인지 알 수 없다.

info 결과를 보면 모든 피처에 결측값이 없다고 나오지만, 실제로는 값이 누락된 곳에 -1이 입력되어 있다. 이런 경우엔 피처별 결측값 수는 -1 을 np.NaN으로 변환한 다음 갯수를 세면 된다.



훈련 데이터에 결측값이 얼마나 있는지 시각화

피처가 많다보니 결측값을 시각화해서 한눈에 보는게 편하다. 이럴 때 missingno 패키지를 사용한다.

missingno는 결측값을 시각화하는 패키지이다. missingno가 제공하는 bar() 함수를 사용해 훈련 데이터에 결측값이 얼마나 있는지 살펴보자. bar() 함수는 결측값을 막대 그래프 형태로 시각화해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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));


'''
불필요한 출력(리턴 값)을 숨기기 위해서 matplotlib 같은 시각화 함수 뒤에 ;를 붙인다.

맨 끝에 ;를 붙이면, 이 리턴값 출력만 숨겨주고 그래프는 정상 출력된다.
'''

먼저 -1을 np.NaN으로 바꿨다. 훈련 데이터 값을 직접 바꾸면 안 되기 때문에 중간에 Copy() 함수로 복사본을 만들어 진행한다. 피처가 57개나 되기 때문에 피처를 반으로 나눠 처음 28개를 시각화한다.

막대 그래프 높이가 낮을수록 결측값이 많다는 뜻이다. 그래프 아래 피처명이 개재돼 있고 그래프 위에는 정상 값이 몇 개인지가 표시되어 있다.

ps_reg_03, ps_car_03_cat, ps_car_05_cat 피처에 결측값이 많은것을 확인할 수 있다.



나머지 29개 이후 피처들의 결측값 확인

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

ps_car_14에 결측값이 좀 있고 나머지 피처에는 거의 없다.



결측값을 매트릭스 형태로 시각화

결측값을 매트릭스 형태로 시각화할 수도 있다. bar()대신 matrix() 함수를 사용하면 된다.

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

오른쪽 막대는 결측값의 상대적인 분포를 보여준다. 검은색으로 뾰족하게 튀어나온 부분이 결측값이 몰려있는 행을 의미한다. 왼쪽에 표시된 22는 결측값이 없는 열 개수를, 오른쪽의 28은 전체 열 개수를 뜻한다.



피처 요약표

피처 요약표가 있으면 데이터 관리도 편하고, 추후 그래프를 그릴 때도 활용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def resumetable(df):
    print(f'데이터셋 형상: {df.shape}')
    summary = pd.DataFrame(df.dtypes, columns=['데이터 타입'])
    summary['결측값 개수'] = (df == -1).sum().values # 피처별 -1 개수 [1]
    summary['고윳값 개수'] = df.nunique().values 
    summary['데이터 종류'] = None

    for col in df.columns: # [2]
        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 이므로 결측값 개수를 구하려면 -1의 개수를 구해야한다.

[1] 처럼 (df == -1).sum().values 코드로 피처별 -1 개수를 구할 수 있다.

[2] 에서는 for문을 순회하며 데이터 종류를 추가했다. 피처명에 ‘bin’이 포함돼 있거나 타겟 열이면 이진형 데이터이고, ‘cat’이 포함돼 있으면 명목형이다. 데이터 타입이 float면 연속형 데이터 이며, int면 순서형 데이터이다.


1
2
3
4
5
6
summary = resumetable(train)
summary

'''
데이터셋 형상: (595212, 58)
'''
데이터 타입결측값 개수고윳값 개수데이터 종류
targetint6402이진형
ps_ind_01int6408순서형
ps_ind_02_catint642165명목형
ps_ind_03int64012순서형
ps_ind_04_catint64833명목형
ps_ind_05_catint6458098명목형
ps_ind_06_binint6402이진형
ps_ind_07_binint6402이진형
ps_ind_08_binint6402이진형
ps_ind_09_binint6402이진형
ps_ind_10_binint6402이진형
ps_ind_11_binint6402이진형
ps_ind_12_binint6402이진형
ps_ind_13_binint6402이진형
ps_ind_14int6405순서형
ps_ind_15int64014순서형
ps_ind_16_binint6402이진형
ps_ind_17_binint6402이진형
ps_ind_18_binint6402이진형
ps_reg_01float64010연속형
ps_reg_02float64019연속형
ps_reg_03float641077725013연속형
ps_car_01_catint6410713명목형
ps_car_02_catint6453명목형
ps_car_03_catint644112313명목형
ps_car_04_catint64010명목형
ps_car_05_catint642665513명목형
ps_car_06_catint64018명목형
ps_car_07_catint64114893명목형
ps_car_08_catint6402명목형
ps_car_09_catint645696명목형
ps_car_10_catint6403명목형
ps_car_11_catint640104명목형
ps_car_11int6455순서형
ps_car_12float641184연속형
ps_car_13float64070482연속형
ps_car_14float6442620850연속형
ps_car_15float64015연속형
ps_calc_01float64010연속형
ps_calc_02float64010연속형
ps_calc_03float64010연속형
ps_calc_04int6406순서형
ps_calc_05int6407순서형
ps_calc_06int64011순서형
ps_calc_07int64010순서형
ps_calc_08int64011순서형
ps_calc_09int6408순서형
ps_calc_10int64026순서형
ps_calc_11int64020순서형
ps_calc_12int64011순서형
ps_calc_13int64014순서형
ps_calc_14int64024순서형
ps_calc_15_binint6402이진형
ps_calc_16_binint6402이진형
ps_calc_17_binint6402이진형
ps_calc_18_binint6402이진형
ps_calc_19_binint6402이진형
ps_calc_20_binint6402이진형



피처 요약표에서 명목형 피처 추출

1
2
3
4
5
6
7
8
9
summary[summary['데이터 종류'] == '명목형'].index

'''
Index(['ps_ind_02_cat', 'ps_ind_04_cat', 'ps_ind_05_cat', 'ps_car_01_cat',
       'ps_car_02_cat', 'ps_car_03_cat', 'ps_car_04_cat', 'ps_car_05_cat',
       'ps_car_06_cat', 'ps_car_07_cat', 'ps_car_08_cat', 'ps_car_09_cat',
       'ps_car_10_cat', 'ps_car_11_cat'],
      dtype='object')
'''



피처 요약표에서 데이터 타입이 실수형인 피처 추출

1
2
3
4
5
summary[summary['데이터 타입'] == 'float64'].index

'''
Index(['ps_reg_01', 'ps_reg_02', 'ps_reg_03', 'ps_car_12', 'ps_car_13', 'ps_car_14', 'ps_car_15', 'ps_calc_01', 'ps_calc_02', 'ps_calc_03'], dtype='object')
'''




2.3 데이터 시각화

타겟값 분포를 활용해 타겟값이 얼마나 불균형한지 알아본다. 또한 이진 피처, 명목형 피처, 순서형 피처의 고윳값별 타겟값 비유을 알아본다.

고윳값별 타겟값 비율을 보면 모델링 시 필요한 피처와 필요 없는 피처를 구분할 수 있다.


시각화 라이브러리 임포트

1
2
3
import seaborn as sns
import matplotlib as mpl
import matplotlib.pyplot as plt



타겟값 분포 확인

  • write_percent 함수 생성

    • 폰트 : 15

    • 전체 크기: 7x6

    • 비율 표시

    • 제목: Target Distribution


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def write_percent(ax, total_size):
    for patch in ax.patches:
        width = patch.get_width()
        height = patch.get_height()
        left_coord = patch.get_x()
        percent = height / total_size * 100

        ax.text(x = left_coord + width / 2,
                y = height + total_size * 0.001,
                s = '{:1.1f}%'.format(percent),
                ha = 'center')
        
mpl.rc('font', size=15)
plt.figure(figsize=(7, 6))
ax = sns.countplot(data=train, x='target')
write_percent(ax, len(train))
ax.set_title('Target Distribution');

타겟값 0은 96.4%를 차지하며 1은 단 3.6% 만 차지한다. 전체 운전자 중 3.6%만 보험금을 청구했다는 뜻이다. 차 사고가 그리 흔하게 나진 않기 때문에 소수의 운전자만 보험금을 청구했다.(타겟값 불균형)



타겟값이 불균형하기 때문에 비율이 작은 타겟값 1을 잘 예측하는 게 중요하다. 따라서 이번에는 각 피처의 분포를 알아보기 보다는, 각 피처의 고윳값별 타겟값 1 비율을 알아본다. 고윳값별 타겟값 1 비율을 통해 해당 피처가 모델링에 필요한 피처인지 확인할 수 있다.

타겟값의 비율 차이가 크면 비율이 작은 타겟값을 잘 예측하는게 중요하다.



예를들어 피처 A에 고윳값 a, b 가 있다고 하자. 이때 고윳값 a, b별로 타겟값 1 비율이 얼마나 되는지 살펴보려는 것이다. 고윳값별로 타겟값 1 비율이 똑같거나 통계적 유효성이 떨어지면, 즉 통계적으로 유의미한 차이가 없다면 피처 A로는 무언가를 분별하기 어려우므로 예측에 도움이 되지 않는다. 즉 고윳값에 따라 타겟값 비율이 달라야 그 피처가 타겟값 예측에 도움을 준다.

각각의 고윳값마다 타겟값 비율이 다른 피처여야 모델링에 도움이 된다.



타겟값 1 비율의 통계적 유효성이 떨어져도 불필요한 피처가 될 수 있다. 통계적 유효성은 barplot()을 그릴 때 나타나는 신뢰구간으로 판단한다. 신뢰구간이 좁다면 통계적으로 어느 정도 유효하다고 보고, 구간이 넓다면 신뢰하기 어렵다고 보는것이다.

A의 키는 170 ~ 175 정도 돼 vs A의 키는 150 ~ 200 정도 돼

통계적 유효성이 높아야(신뢰구간이 좁아야) 모델링에 도움이 된다.



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



이진 피처 타겟값 분포 확인

위 내용에 유념해 이전 피처의 고윳값별 타겟값 비율을 구해본다.

  • plot_target_ratio_by_features 함수 생성

    • df, features, nrows, ncols, size=(12, 18)

    • 폰트: 9

    • 상하 좌우 여백 :0.3, 0.3

    • 색상: Set2

  • 데이터 종류가 이진형인 피처들만 그래프 출력

  • 범례 미표시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import matplotlib.gridspec as gridspec

def plot_target_ratio_by_features(df, features, nrows, ncols, size=(12, 18)):
    mpl.rc('font', size=9)
    plt.figure(figsize=size)
    grid = gridspec.GridSpec(nrows, ncols)
    plt.subplots_adjust(hspace=0.3, wspace=0.3)

    for idx, feature in enumerate(features):
        ax = plt.subplot(grid[idx])

        sns.barplot(data=df, x=feature, y='target', hue=feature, palette='Set2', legend=False, ax=ax);

bin_features = summary[summary['데이터 종류'] == '이진형'].index
plot_target_ratio_by_features(train, bin_features, 6, 3)

ps_ind_06_bin 피처 그래프를 먼저 살펴보면 이 피처는 값이 0일 때 타겟값이 1인 비율이 4%(0.04)정도 이다. 나머지 96%는 타겟값이 0이다. 값이 1일 때는 타겟값 1이 2.8% 정도 차지한다.

고윳값타겟값 1비율타겟값 0 비율
04%96%
12.8%97.2%


종합하면 ps_ind_06_bin 피처는 고윳값별로 타겟값 비율이 다르다. 따라서 타겟값을 추정하는 예측력이 있다. 추가로 신뢰구간도 좁아 모델링에 도움 되는 피처이다.



제거할 피처 표

피처명제거해야하는 이유
ps_ind_10_bin ~
ps_ind_13_bin
신뢰구간이 넓어 통계적 유효성이 떨어짐
ps_calc_15_bin ~
ps_calc_20_bin
고윳값별 타겟값 비율 차이가 없어 타겟값 예측력이 없음



위 표가 정답은 아니다. 신뢰구간이 얼마나 넓어야 제거할 피처로 볼지, 고윳값별 타겟값 비율이 얼마나 달라야 제거할 피처로 볼지는 사람마다 다르게 판단할 수 있기 때문이다. 또한 제거할 피처를 찾기 위한 다른 방법도 많다. 피처 선택(또는 피처 제거)에 꼭 한 가지 방법만 쓰는 게 아니다. ‘이런 논리로 제거할 피처를 골라낼 수 있구나’ 정도로 알아두면 된다.




명목형 피처 타겟값 분포 확인

명목형 피차 14개로 동일한 그래프를 그린다. 명목형 피처는 고윳값 개수가 이진 피처보다 많다. 고윳값이 많으면 그래프가 가로로 길어지기 때문에 서브플롯을 2열로 그린다.

  • 데이터 종류: 명목형
  • 7행 2열 배치
1
2
nom_features = summary[summary['데이터 종류'] == '명목형'].index
plot_target_ratio_by_features(train, nom_features, 7, 2)

-1은 결측값이다. 만약 결측값이 많지 않다면 다른 값으로 대체하고, 결측값이 많다면 해당 피처 자체를 제거한다. 하지만 결측값 자체가 타겟값 예측에 도움을 주는 경우도 있다.

ps_ind_02_cat 피처를 보면 결측값 -1이 다른 고윳값들보다 타겟값 1 비율이 크다. 신뢰구간이 넓다는 점을 감안해도 비율이 확실히 크다. 이런 결측값을 다른 값으로 대체하면 모델 성능이 더 나빠질 수 있다. 결측값 자체가 타겟값에 대한 예측력이 있기 때문이다. 따라서 결측값을 그래도 모델링한다.(-1도 하나의 고윳값이라 간주)



ps_car_02_cat 피처를 보면 -1일 때 타겟값 1 비율은 0%이다. 이 경우만 놓고볼 때 ps_car_02_cat 피처 값이 -1이면 타겟값이 0이라고 판단할 수 있다. 이 역시 결측값이 타겟값을 예측하는데 도움을 준다. 결측값을 포함하는 다른 피처도 비슷한 양상을 보인다.

결측값 자체에 타깃값 예측력이 있다면 고윳값으로 간주(결측값 처리 필요 없음)



ps_ind_02_cat, ps_ind_04_cat, ps_car_01_cat 그래프를 보면 -1을 제외하고 나머지 고윳값은 타겟값 1 비율이 비슷하며 -1일 때의 신뢰구간이 상대적으로 넓다.

그렇더라도 -1의 신뢰하한과 다른 고윳값들의 신뢰상한 간 차이가 크다. 따라서 -1의 신뢰 구간이 넓다는 점을 감안해도 -1과 다른 고윳값들 간 타겟값 1 비율에 차이가 있다. 즉, 고윳값 간 타겟값 1 비율에 차이가 있으므로 모델링에 필요한 피처이다.

그러므로 ps_ind_02_cat, ps_ind_04_cat, ps_car_01_cat 피처는 신뢰구간이 넓더라도 제거하지 않는다.



ps_car_10_cat 피처를 보면 세 고윳값은 타겟값 1의 평균 비율이 비슷하다. 그리고 고윳값 2의 신뢰구간이 유독 넓은것을 확인할 수 있다. 이 피처는 상위권 캐글러 중 ps_car_10_cat 피처를 제거한 사람도 있고, 그대로 둔 사람도 있다. 이럴 때는 해당 피처를 제거한 경우와 제거하지 않은 경우에 성능이 어떻게 다른지 비교해 보는것도 좋다.

여기서는 ps_car_10_cat 피처를 제거하지 않는다.

분석결과 : 명목형 피처는 모두 모델링에 이용




순서형 피처 타겟값 분포 확인

  • 데이터 종류: 순서형

  • 8행 2열

  • size = 12 x 20

1
2
ord_features = summary[summary['데이터 종류'] == '순서형'].index
plot_target_ratio_by_features(train, ord_features, 8, 2, (12, 20))

ps_ind_14 피처를 보면 고윳값 0, 1, 2, 3의 타겟값 비율은 큰 차이가 없고, 고윳값 4의 신뢰구간은 넓은것을 확인할 수 있다. 신뢰구간이 넓어 통계적 유효성이 떨어지므로 제거한다.



ps_calc_04 부터 ps_calc_14까지는 모두 고윳값별 타겟값 비율이 거의 비슷하다. 타겟값 비율이 다른 고윳값도 있긴 하겠지만, 그 고윳값은 신뢰구간이 넓어 통계적 유효성이 떨어진다. 따라서 이 피처들은 모두 제거한다.

앞서 ‘이진 피처’ 절에서 calc 분류에 속하는 이진 피처는 모두 제거하기로 했다. 순서형 피처도 마찬가지로 calc 분류의 모든 피처를 제거한다.


(분석 결과) 제거해야 할 순서형 피처

피처 명제거해야 하는 이유
ps_ind_14타겟값 비율의 신뢰구간이 넓어 통계적 유효성이 떨어짐
ps_calc_04 ~
ps_calc_14
고윳값별 타겟값 비율 차이가 없음. 타겟값 비율이 다르더라도 신뢰구간이 넓어 통계적 유효성이 떨어짐




연속형 피처 타겟값 분포 확인

연속형 피처는 연속된 값이므로 고윳값이 굉장히 많아 고윳값별 타겟값 1 비율을 구하기가 힘들다. 따라서 값을 몇 개의 구간으로 나눠서 구간별 타겟값 1 비율을 알아본다.

연속형 데이터를 구간으로 나누려면 판다스의 cut() 함수를 활용하면 딘다. 다음은 cut() 함수를 활용해 여러개의 값을 3개 구간으로 나누는 예이다.

고윳값이 아주 많을 때는 몇 개의 구간으로 나눠 구간별 비율을 알아볼 수 있다.


1
2
3
4
5
6
pd.cut([1.0, 1.5, 2.1, 2.7, 3.5, 4.0], 3)

'''
[(0.997, 2.0], (0.997, 2.0], (2.0, 3.0], (2.0, 3.0], (3.0, 4.0], (3.0, 4.0]]
Categories (3, interval[float64, right]): [(0.997, 2.0] < (2.0, 3.0] < (3.0, 4.0]]
'''

입력과 결과를 비교해보면 다음 그림처럼 변환된 것을 알 수 있다.

이 방식은 연속형 데이터를 범주형 데이터로 바꾸는 효과가 있다.



연속형 피처의 구간별 타겟값 1 비율을 구한다.
1
2
3
4
5
6
7
8
9
10
11
12
cont_features = summary[summary['데이터 종류'] == '연속형'].index

plt.figure(figsize=(12, 16))
grid = gridspec.GridSpec(5, 2)
plt.subplots_adjust(hspace=0.4, wspace=0.2)

for idx, cont_feature in enumerate(cont_features):
    train[cont_feature] = pd.cut(train[cont_feature], 5)
    ax = plt.subplot(grid[idx])

    sns.barplot(data=train, x=cont_feature, y='target', hue=cont_feature, legend=False, palette = 'Set2', ax=ax)
    ax.tick_params(axis='x', labelrotation=10)

(분석 결과) 제거해야 할 연속형 피처

피처명제거해야 하는 이유
ps_calc_01~ps_calc_03구간별 타겟값 비율 차이가 없음


clac 분류의 피처는 데이터 종류에 상관없이 모두 제거해야 한다는 결론이 났다. 이 피처들은 고윳값 혹은 구간별로 타겟값 비율이 거의 같다. 즉 타겟값을 구분하는 예측 능력이 떨어진다는 뜻이다.




연속형 피처 상관관계 확인

일반적으로 강한 상관관계를 보이는 두 피처가 있으면 둘 중 하나를 제거하는게 좋다. 상관관계가 강하면 타겟값 예측력도 비슷하다.


상관관계가 강한 피처들은 예측력도 비슷하므로 하나만 남겨두는게 좋다.



상관관계가 얼마나 강한 피처를 제거해야 하는가?

상관계수가 얼마 이상일 때 제거하는 게 좋은지 정해진 기준은 없다. 상황과 모델에 따라 다르다. 더불어, 강한 상관관계를 보이는 두 피처 중 하나를 제거한다고 해서 반드시 성능이 향상되는 것도 아니다.

고려해볼 만한 요소 정도로 생각하면 된다. 아래는 두 피처 간 상관계수에 따른 상관관계를 나타낸 표이다.


피어슨 상관계수상관관계
0.00 ~ 0.19아주 약함
0.20 ~ 0.39약함
0.40 ~ 0.59보통
0.60 ~ 0.79강함
0.80 ~ 1.0아주 강함


0.8 이상의 ‘아주 강한’ 상관관게를 보이는 피처가 있다면 제거를 고려해보는 것도 좋은 방법이다.



피처 간 상관관계를 파악하기 위해 히트맵을 그린다. 그 전에 현재 피처에 결측값이 있으므로 결측값을 제거해야 한다.

-1을 np.NaN 으로 변환한 train_copy에서 np.NaN을 제거하면 된다. dropna() 는 np.NaN을 제거해주는 함수이다.

1
train_copy = train_copy.dropna() # np.NaN값 삭제
1
2
3
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_car_14 피처를 제거하니 성능이 더 좋아졌다. 성능 향상을 위해 ps_car_14 피처를 추가로 제거한다.

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

분석 결과: 상관관계가 강한 피처 제거(ps_car_14)

This post is licensed under CC BY 4.0 by the author.