본문 바로가기
ML.DL

[ML경진대회] 범주형 데이터 이진분류-탐색적 데이터 분석(2)

by 권미정 2022. 10. 2.

책 <머신러닝, 딥러닝 문제해결 전략> 7장을 실습한 내용입니다.


지난 실습에 이어서, 이번에는 데이터를 시각화하여 어떤 피처가 중요하고 어떤 고윳값이 타깃값에 영향을 많이 주는지 알아보자!

2-2. 데이터 시각화

먼저 시각화 라이브러리를 불러오고, 그래프를 그려보자.

import seaborn as sns
import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline



① 타깃값 분포

분포도 중 하나인 카운트플롯으로 타깃값 0과 1의 개수를 파악하자. 카운트플롯은 범주형 데이터의 개수를 확인할 때 주로 사용한다.

mpl.rc('font', size=15) #폰트 크기 설정
plt.figure(figsize=(7,6)) #Figure 크기 설정

#타깃값 분포 카운트플롯
ax=sns.countplot(x='target', data=train)
ax.set_title('Target Distribution');

countplot()의 x 파라미터에 타깃값('target')을 전달했고, data 파라미터에 훈련 데이터(train)를 전달했다. 그러면 train['target']에서 고윳값별로 데이터가 몇 개인지 카운트플롯으로 그려준다.

타깃값 0이 타깃값 1보다 훨씬 많은 걸 볼 수 있다!

이번에는 각 값의 비율을 그래프 상단해 표시해서 더 유용한 그래프를 그려볼 것이다. 비율을 표시하려면 글자를 쓸 위치를 구해야 하는데, ax.patches를 이용해 사각형의 높이, 너비, 왼쪽 테두리의 x축의 위치를 먼저 구해야 한다.

rectangle=ax.patches[0] #첫 번째 Rectangle 객체
print('사각형 높이:',rectangle.get_height())
print('사각형 너비:',rectangle.get_width())
print('사각형 왼쪽 테두리의 x축 위치:',rectangle.get_x())


이번에는 비율을 표시할 위치를 계산해보자. 막대 바로 위에 가운데 정렬하여 표시하려고 한다.

print('텍스트 위치의 x좌표:', rectangle.get_x()+rectangle.get_width()/2.0)
print('텍스트 위치의 y좌표:', rectangle.get_height()+len(train)*0.001)


텍스트 표시 위치를 구하는 방법을 알았으니, 이제 비율을 표시해주는 코드를 함수로 구현한 후, 그 함수를 사용해 카운트플롯을 다시 그려보자!

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(x=left_coord+width/2.0, #x축 위치
                y=height+total_size*0.001, #y축 위치
                s=f'{percent:1.1f}%', #입력 텍스트
                ha='center') #가운데 정렬
        
plt.figure(figsize=(7,6))

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

각 막대 위에 비율이 표시돼서 타깃값 0과 1이 약 7대 3 비율인 것을 바로 파악할 수 있게 되었다!


② 이진피처 분포

이번에는 이진 피처의 분포를 타깃값별로 따로 그려볼 것이다. 범주형 피처의 타깃값 분포를 고윳값별로 구분해 그려보는 건 분류 문제에서 종종 쓰는 방법인데, 특정 고윳값이 특정 타깃값에 치우치는지 확인할 수 있기 때문이다.

피처별로 총 5개의 그래프가 출력될 것이기 때문에, 여러 그래프를 격자 형태로 배치하는 GridSpec을 사용해서 그려보자!

import matplotlib.gridspec as gridspec #여러 그래프를 격자 형태로 배치 ⓐ
#3행 2열 틀(Figure) 준비
mpl.rc('font',size=12)
grid=gridspec.GridSpec(3,2) #그래프(서브플롯)를 3행 2열로 배치
plt.figure(figsize=(10,16)) #전체 Figure 크기 설정
plt.subplots_adjust(wspace=0.4, hspace=0.3) #서브플롯 간 좌우/상하 여백 설정 ⓑ

#서브플롯 그리기
bin_features=['bin_0','bin_1','bin_2','bin_3','bin_4'] #피처 목록 ⓒ

for idx, feature in enumerate(bin_features): # ⓓ
    ax=plt.subplot(grid[idx]) # ⓔ
    
    #ax축에 타깃값 분포 카운트플롯 그리기 ⓕ
    sns.countplot(x=feature,
                  data=train,
                  hue='target',
                  palette='pastel', #그래프 색상 설정
                  ax=ax)
    
    ax.set_title(f'{feature} Distribution by Target') #그래프 제목 설정 ⓖ
    write_percent(ax, len(train)) #비율 표시 ⓗ

ⓐ 서브플롯을 3행 2열로 배치한 GridSpec 객체를 grid 변수에 할당한다. 나중에 grid[0], grid[1],... 과 같이 원하는 서브플롯을 지정할 수 있다.
subplots_adjust()를 활용해 좌우, 상하 간격을 따로 조정할 수 있다.
ⓒ 이진 피처의 목록을 bin_features에 담은 후, ⓓ for문을 활용해 각각의 카운트플롯을 그린다.
ⓔ grid에서 이번 서브플롯을 그릴 위치를 ax축으로 지정하고, ⓕ ax축에 타깃값 분포 카운트플롯을 그린 다음, ⓖ 그래프의 제목을 설정하고, ⓗ 비율을 표시한다.

ⓕ에서 각 파라미터의 의미는 아래와 같다.

  • x: 피처
  • data: 전체 데이터셋
  • hue: 세부적으로 나눠 그릴 기준 피처인데, 여기서는 타깃값을 전달했다.
  • palette: 그래프 색상맵이다. pastel은 파스텔톤이다.
  • ax: 그래프를 그릴 축



③ 명목형 피처 분포

이번엔 명목형 피처 분포와 명목형 피처별 타깃값 1의 비율을 살펴보자! 이전 글에서 피처 요약표를 만들었을 때 nom_5~9는 고윳값 개수가 많고 의미를 알 수 없는 값이 입력돼 있었기 때문에, 여기서는 nom_0~4까지만 시각화할 것이다.

1단계. 교차분석표 생성 함수 만들기

교차분석표는 범주형 데이터 2개를 비교 분석하는 데 사용되는 표로, 각 범주형 데이터의 빈도나 통계량을 행과 열로 결합해놓은 표를 말한다. 여기서는 교차분석표를 활용해 2개의 범주형 데이터, 즉 명목형 피처와 타깃값을 비교 분석하고, 그 결과를 이용해 그래프를 그릴 것이다.

판다스의 crosstab() 함수로 nom_0과 타깃값(target) 간 교차분석표를 만들어보자.

pd.crosstab(train['nom_0'],train['target'])

nom_0의 고윳값은 Blue, Green, Red이고 고윳값별 타깃값 0과 1이 몇 개인지 알 수 있다.

비율로 표현하는 게 한눈에 이해하기 쉽기 때문에 normalize 파라미터를 추가해 정규화해보자. normalize 파라미터에 'index'를 전달해서 인덱스를 기준으로 정규화한다. 이를 다시 백분율로 표현하기 위해 100을 곱해야 한다.

#정규화 후 비율을 백분율로 표현
crosstab=pd.crosstab(train['nom_0'],train['target'],normalize='index')*100
crosstab

백분율로 잘 정리가 되었다!

이번에는 피처가 열로 설정돼 있어야 그래프를 그리기 편하기 때문에, 인덱스를 재설정해서 피처를 열로 가져오는 작업을 해볼 것이다.

crosstab=crosstab.reset_index() #인덱스 재설정
crosstab


교차분석표를 계속 사용할 것이므로 함수로 만들어놓고 다음 단계를 진행하자!

def get_crosstab(df,feature):
    crosstab=pd.crosstab(df[feature],df['target'],normalize='index')*100
    crosstab=crosstab.reset_index()
    return crosstab

 

2단계. 포인트플롯 생성 함수 만들기

앞에서 구한 교차분석표를 사용해서 타깃값 1의 비율을 나타내는 포인트플롯을 그리는 함수를 만들 것이다.
plot_pointplot()은 이미 카운트플롯이 그려진 축에 포인트플롯을 중복으로 그려준다.

def plot_pointplot(ax,feature,crosstab):
    ax2=ax.twinx() #x축은 공유하고 y축은 공유하지 않는 새로운 축 생성 ⓐ
    #새로운 축에 포인트플롯 그리기 ⓑ
    ax2=sns.pointplot(x=feature, y=1, data=crosstab,
                      order=crosstab[feature].values, #포인트플롯 순서
                      color='black',
                      legend=False) #범례 미표시
    ax2.set_ylim(crosstab[1].min()-5, crosstab[1].max()*1.1) #y축 범위 설정 ⓒ
    ax2.set_ylabel('Target 1 Ratio(%)')

ax.twinx()로 새로운 축 ax2를 만들었다. ax는 카운트플롯을 그리기 위한 축이고, ax2는 포인트플롯을 그리기 위한 축이다. ax와 ax2는 x축을 서로 공유하지만, y축은 서로 다르다.
pointplot()의 x 파라미터에는 피처, y 파라미터에는 '타깃값이 1인 비율'을 나타내는 1, data 파라미터에는 교차분석표를 전달했다. order 파라미터에 전달한 것의 의미는 교차분석표의 피처(열) 순서대로 그리겠다는 뜻이다.
ⓒ 포인트플롯을 더 보기 좋게 하기 위해 y축의 범위를 설정했다.

3단계. 피처 분포도 및 피처별 타깃값 1의 비율 포인트플롯 생성 함수 만들기

이제 마지막 단계! get_crosstab()과 plot_pointplot() 함수를 활용해 최종 그래프를 그리는 함수를 만들어볼 것이다.

def plot_cat_dist_with_true_ratio(df,features,num_rows,num_cols,size=(15,20)):
    plt.figure(figsize=size) #전체 Figure 크기 설정
    grid=gridspec.GridSpec(num_rows, num_cols) #서브플롯 배치 ⓐ
    plt.subplots_adjust(wspace=0.45, hspace=0.3) #서브플롯 좌우/상하 여백 설정
    
    for idx, feature in enumerate(features): # ⓑ
        ax=plt.subplot(grid[idx])
        crosstab=get_crosstab(df,feature) #교차분석표 생성 ⓒ
        
        #ax축에 타깃값 분포 카운트플롯 그리기 ⓓ
        sns.countplot(x=feature, data=df,
                      order=crosstab[feature].values,
                      color='skyblue',
                      ax=ax)
        
        write_percent(ax,len(df)) #비율 표시 ⓔ
        
        plot_pointplot(ax,feature,crosstab) #포인트플롯 그리기 ⓕ
        
        ax.set_title(f'{feature} Distribution') #그래프 제목 설정 ⓖ

이 함수는 인수로 받는 features 피처마다 타깃값별로 분포도를 그린다. 전체 Figure 크기의 기본값을 (15,20)으로 설정했다.
ⓐ 서브플롯들을 격자 형태로 배치하기 위해 GridSpec을 사용하였다.
ⓑ for문으로 features를 순회하며 서브플롯들을 하나씩 그린다.
ⓒ 각각 서브플롯에 대해 해당 피처와 타깃값의 교차분석표를 만든다.
ⓓ ax축에 카운트플롯을 그린다.
ⓔ 카운트플롯에 비율을 표시한다.
ⓕ 카운트플롯과 같은 축에 포인트플롯을 덧그리고, ⓖ 그래프의 제목을 달았다.

nom_features=['nom_0','nom_1','nom_2','nom_3','nom_4'] #명목형 피처
plot_cat_dist_with_true_ratio(train, nom_features,num_rows=3,num_cols=2)

결과 그래프가 잘 나타났다! 이 그림에서 카운트플롯은 피처별 고윳값의 비율을 나타내고, 꺾은 선 그래프는 포인트플롯으로 해당 고윳값 중 타깃값 1의 비율을 나타낸다. 예를 들어 5번째 그래프를 보면, 고윳값 Piano의 비율은 28.2%고 그중 타깃값이 1인 데이터는 30% 정도인 걸 알 수 있다. 자연스럽게 타깃값이 0인 데이터는 70% 정도인 걸 알 수 있다.

이 그래프들을 보면 nom_0~4 피처는 고윳값별로 타깃값 1의 비율이 다른데, 이는 '타깃값에 대한 예측 능력이 있음'을 뜻한다. 따라서 모두 모델링에 사용해야 하는 피처들이다.
또, 명목형 피처는 순서를 무시해도 되고 고윳값 개수도 적기 때문에, 나중에 원-핫 인코딩을 할 것이다.
간단히 원-핫 인코딩의 개념을 복습하자면, 여러 값 중 하나(one)만 활성화(hot)하는 인코딩이다. 레이블 인코딩의 문제를 해결하지만, 피처의 고윳값이 많으면 그만큼 열 개수가 늘어나 모델 훈련 속도가 느려질 우려가 있다.
이럴 때는 비슷한 고윳값끼리 그룹화하거나 다른 인코딩을 적용하는 방법이 있는데, nom_5~9처럼 전체 데이터 양이 별로 많지 않을 때는 열 개수가 늘어나도 모델 훈련 속도에 크게 영향을 주지 않기 때문에 그냥 원-핫 인코딩을 적용하기도 한다.


④ 순서형 피처 분포

plot_cat_dist_with_true_ratio() 함수를 사용해 순서형 피처 분포도 살펴보자! 이전 글 '순서형 피처 요약표'에서 확인했듯이 고윳값이 적은 ord_0~3은 2행 2열로, 고윳값이 많은 ord_4~5는 2행 1열로 그래프를 따로 그릴 것이다.

ord_features=['ord_0','ord_1','ord_2','ord_3'] #순서형 피처
plot_cat_dist_with_true_ratio(train, ord_features,
                              num_rows=2, num_cols=2, size=(15,12))

ord_1과 ord_2의 피처 값들의 순서가 정렬되지 않은 걸 볼 수 있다. CategoricalDtype()을 이용해 피처에 순서를 지정하고, 그래프를 다시 그려보자!

  • categories: 범주형 데이터 타입으로 인코딩할 값 목록
  • ordered: True로 설정하면 categories에 전달한 값의 순서가 유지된다.
from pandas.api.types import CategoricalDtype

ord_1_value=['Novice','Contributor','Expert','Master','Grandmaster']
ord_2_value=['Freezing','Cold','Warm','Hot','Boiling Hot','Lava Hot']

#순서를 지정한 범주형 데이터 타입
ord_1_dtype=CategoricalDtype(categories=ord_1_value, ordered=True)
ord_2_dtype=CategoricalDtype(categories=ord_2_value, ordered=True)

#데이터 타입 변경
train['ord_1']=train['ord_1'].astype(ord_1_dtype)
train['ord_2']=train['ord_2'].astype(ord_2_dtype)

 

plot_cat_dist_with_true_ratio(train, ord_features,
                              num_rows=2, num_cols=2, size=(15,12))

순서대로 잘 정렬된 결과 그래프를 보면, 고윳값 순서에 따라 타깃값 1 비율도 비례해서 커진다는 것을 확인할 수 있다.

이번엔 ord_4와 ord_5의 분포를 가로 길이를 늘려 2행 1열로 그려보자.

plot_cat_dist_with_true_ratio(train, ['ord_4','ord_5'],
                              num_rows=2, num_cols=1, size=(15,12))

ord_5는 고윳값 개수가 너무 많아 x축 라벨이 겹쳐졌지만, ord_4와 ord_5 둘 다 고윳값 순서에 따라 타깃값 1 비율이 증가한다는 건 알아볼 수 있다.
따라서 순서형 피처 모두 고윳값 순서에 따라 타깃값이 1인 비율이 증가하고, 모든 그래프에서 순서와 비율 사이에 상관관계가 있다는 걸 알 수 있다.


⑤ 날짜 피처 분포

마지막으로 날짜(요일과 월) 피처 분포도 살펴보자!

date_features=['day','month']
plot_cat_dist_with_true_ratio(train, date_features,
                              num_rows=2, num_cols=1, size=(10,10))

day 피처를 보면 1에서 4로 갈수록 타깃값 1 비율이 줄어들고, 4에서 7로 갈수록 비율이 늘어난다. month 피처는 day 피처와는 반대되는 것을 알 수 있다.
머신러닝 모델은 숫자 값을 가치의 크고 작음으로 해석해서, 12월과 다음해 1월이 한 달 차이지만 1월과 2월의 차이와 같다고 보지 않는다. 이럴 때는 원래 삼각함수 인코딩을 사용하는데, 이 경진대회는 데이터가 그리 크지 않아 명목형 피처와 같이 원-핫 인코딩을 적용할 것이다.

분석 정리

  1. 결측값이 없다.
  2. 모든 피처가 중요해서 제거할 피처가 없다.
  3. 이진 피처 인코딩: 값이 숫자가 아닌 이진 피처는 0과 1로 인코딩한다.
  4. 명목형 피처 인코딩: 전체 데이터가 크지 않기 때문에 원-핫 인코딩을 적용한다.
  5. 순서형 피처 인코딩: 고윳값들의 순서에 맞게 인코딩한다.
  6. 날짜 피처 인코딩: 원-핫 인코딩을 적용한다.

댓글