본문 바로가기

python·Data Science

[Kaggle] 범주형 데이터 이진분류|Categorical Feature Encoding Challenge | logistic regressi

728x90

캐글 노트북
https://www.kaggle.com/code/rickyhouse/categorical-feature-encoding-hyperparmeter-optimiz

경진대회 이해

범주형 피처 23개를 활용해 해당 데이터가 타깃값 1에 속할 확률을 예측하는 것이 목표
 
데이터 둘러 보기

import pandas as pd

data_path = '/kaggle/input/cat-in-the-dat/'

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
((300000, 24), (200000, 23))

훈련 데이터는 300,000행 24열, 테스트 데이터는 200,000행 2열로 구성
 

train.head().T

 
피처 요약표 만들기

def resumetable(df):
    print(f'데이터 세트 형상: {df.shape}')
    summary = pd.DataFrame(df.dtypes, columns=['데이터 타입'])
    summary = summary.reset_index()
    summary = summary.rename(columns={'index' : '피처'})
    summary['결측값 개수'] = df.isnull().sum().values
    summary['고윳감 개수'] = df.nunique().values
    summary['첫 번째 값'] = df.loc[0].values
    summary['두 번째 값'] = df.loc[1].values
    summary['세 번째 값'] = df.loc[2].values
    
    return summary

resumetable(train)

 - 이진형 피처 : bin_0 ~ bin_4
  : 고윳값이 2개, 결측값 없음
  : bin_3은 T, F /  bin_4는 Y, N으로 구성되어 있음. 0과 1로 인코딩 해야 함
- 명목형 피처 : nom_0 ~ n0m_9
 : 모두 object 타입, 결측값 없음
- 순서형 피처 : ord_0 ~ord_5
: ord_0 피쳐만 int64 타입, 나머지는 object 타입, 결측값 없음
- 그외 피처 : day, month, target
 
순서형 피처 고유값 출력

for i in range(3):
    feature = 'ord_'+ str(i)
    print(f'{feature} 고윳값: {train[feature].unique()}')
ord_0 고윳값: [2 1 3]
ord_1 고윳값: ['Grandmaster' 'Expert' 'Novice' 'Contributor' 'Master']
ord_2 고윳값: ['Cold' 'Hot' 'Lava Hot' 'Boiling Hot' 'Freezing' 'Warm']

 

for i in range (3, 6):
    feature = 'ord_' + str(i)
    print(f'{feature} 고윳값: {train[feature].unique()}')
ord_3 고윳값: ['h' 'a' 'i' 'j' 'g' 'e' 'd' 'b' 'k' 'f' 'l' 'n' 'o' 'c' 'm']
ord_4 고윳값: ['D' 'A' 'R' 'E' 'P' 'K' 'V' 'Q' 'Z' 'L' 'F' 'T' 'U' 'S' 'Y' 'B' 'H' 'J'
 'N' 'G' 'W' 'I' 'O' 'C' 'X' 'M']
ord_5 고윳값: ['kr' 'bF' 'Jc' 'kW' 'qP' 'PZ' 'wy' 'Ed' 'qo' 'CZ' 'qX' 'su' 'dP' 'aP'
 'MV' 'oC' 'RL' 'fh' 'gJ' 'Hj' 'TR' 'CL' 'Sc' 'eQ' 'kC' 'qK' 'dh' 'gM'
 'Jf' 'fO' 'Eg' 'KZ' 'Vx' 'Fo' 'sV' 'eb' 'YC' 'RG' 'Ye' 'qA' 'lL' 'Qh'
 'Bd' 'be' 'hT' 'lF' 'nX' 'kK' 'av' 'uS' 'Jt' 'PA' 'Er' 'Qb' 'od' 'ut'
 'Dx' 'Xi' 'on' 'Dc' 'sD' 'rZ' 'Uu' 'sn' 'yc' 'Gb' 'Kq' 'dQ' 'hp' 'kL'
 'je' 'CU' 'Fd' 'PQ' 'Bn' 'ex' 'hh' 'ac' 'rp' 'dE' 'oG' 'oK' 'cp' 'mm'
 'vK' 'ek' 'dO' 'XI' 'CM' 'Vf' 'aO' 'qv' 'jp' 'Zq' 'Qo' 'DN' 'TZ' 'ke'
 'cG' 'tP' 'ud' 'tv' 'aM' 'xy' 'lx' 'To' 'uy' 'ZS' 'vy' 'ZR' 'AP' 'GJ'
 'Wv' 'ri' 'qw' 'Xh' 'FI' 'nh' 'KR' 'dB' 'BE' 'Bb' 'mc' 'MC' 'tM' 'NV'
 'ih' 'IK' 'Ob' 'RP' 'dN' 'us' 'dZ' 'yN' 'Nf' 'QM' 'jV' 'sY' 'wu' 'SB'
 'UO' 'Mx' 'JX' 'Ry' 'Uk' 'uJ' 'LE' 'ps' 'kE' 'MO' 'kw' 'yY' 'zU' 'bJ'
 'Kf' 'ck' 'mb' 'Os' 'Ps' 'Ml' 'Ai' 'Wc' 'GD' 'll' 'aF' 'iT' 'cA' 'WE'
 'Gx' 'Nk' 'OR' 'Rm' 'BA' 'eG' 'cW' 'jS' 'DH' 'hL' 'Mf' 'Yb' 'Aj' 'oH'
 'Zc' 'qJ' 'eg' 'xP' 'vq' 'Id' 'pa' 'ux' 'kU' 'Cl']

 
day, month, target 고윳값 출력

print('day 고윳값:', train['day'].unique())
print('month 고윳값:', train['month'].unique())
print('target 고윳값:', train['target'].unique())
day 고윳값: [2 7 5 4 3 1 6]
month 고윳값: [ 2  8  1  4 10  3  7  9 12 11  5  6]
target 고윳값: [0 1]

day 피쳐 : 고윳값 7개, 요일을 나타낸다고 짐작
month  피쳐 : 고윳값이 1~12, 월을 나타낸다고 짐작
 

데이터 시각화

import seaborn as sns
import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline
mpl.rc('font', size=15)
plt.figure(figsize=(7, 6))

ax = sns.countplot(x='target', data=train)
ax.set(title='Target Distribution');

train['target']에서 고윳값별로 데이터가 몇개인지 카운트플롯으로 그려봄

타깃값 0은 20만개가 좀 넘고 타깃값 1은 9만 개 정도 있음
그래프 상단에 각 값의 비율 표시

rectangle= ax.patches[0]
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()
        percent = height/total_size*100
        
        ax.text(x=left_coord + width/2.0,
                y=height + total_size*0.001,
                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 비율임
이진 피쳐의 분포를 타깃별로 그려보기

import matplotlib.gridspec as gridspec

mpl.rc('font', size=12)
grid = gridspec.GridSpec(3, 2)
plt.figure(figsize=(10, 16))
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])
    
    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))

이진피처도 타깃값 0과 1이 약 7대 3 비율임
 
명목형 피쳐 분포와 피쳐별 타깃값 1의 비율

def get_crosstab(df, feature):
    crosstab = pd.crosstab(df[feature], df['target'], normalize='index')*100
    crosstab = crosstab.reset_index()
    return crosstab
def plot_pointplot(ax, feature, crosstab):
    ax2 = ax.twinx() 
    
    ax2 = sns.pointplot(x=feature, y=1, data=crosstab,
                        order=crosstab[feature].values, 
                        color='black')                   
    ax2.set_ylim(crosstab[1].min()-5, crosstab[1].max()*1.1) 
    ax2.set_ylabel('Target 1 Ratio(%)')
def plot_cat_dist_with_true_ratio(df, features, num_rows, num_cols, 
                                  size=(15, 20)):
    plt.figure(figsize=size)  
    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) 

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

nom_0 피쳐 : 고윳값은 blue, Green, Red이며, 각각 32.1%,42.4%, 25.5%를 차지
꺽은 선 그래프는 포인트플롯 : 해당 고윳값 중 타깃값이 1인 비율을 나타냄
nom_0부터 nom_4 피처는 고윳값별로 타깃값 1의 비율이 서로 다름. 이는 타깃값에 대한 예측능력이 있음을 뜻함.
nom50부터 nom_9 피처는 고윳값이 너무 많고, 의미없는 문자로 이루어져 시각화가 어려움.
 
순서형 피처 분포도 확인
- plot_cat_dist_true_ratio() 함수 사용

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 피처 값들이 순서가 정렬되지 않았음
ord_1 피처: 'Novice', 'Contributor', 'Expert', 'Master', 'Grandmaster' 순으로 정렬
ord_2 피처: 'Freezing', 'Cold', 'Warm', 'Hot', 'Boiling Hot', 'Lava Hot'  순으로 정렬
- CategoricalDtype()을 정용해 ord_1과 ord_2 피처에 순서 지정

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비율도 비례해서 커짐
 

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

ord_4와 ord_5 모두 고윳값 순서에 따라 타깃값 1 비율이 증가함
날짜 피처 분포도

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

피처 엔지니어링

데이터 합치기
머신러닝 모델은 문자 데이터를 인식하지 못함. 문자를 숫자로 바꿔야 함.
훈련 데이터와 테스트 데이터에 동일한 인코딩을 적용하기 위해 데이터 합침.
힙친 데이터에서 drop() 함수로 타깃값 제거

all_data = pd.concat([train, test]) # 훈련 데이터와 테스트 데이터 합치기 
all_data = all_data.drop('target', axis=1) # 타깃값 제거
all_data

 
이진 피처 인코딩
bin_3은 T, F /  bin_4는 Y, N으로 구성되어 있음.
판다스 map() 함수를 사용해 0과 1로 인코딩 

all_data['bin_3'] = all_data['bin_3'].map({'F':0, 'T':1})
all_data['bin_4'] = all_data['bin_4'].map({'N':0, 'Y':1})

 
순서형 피처 인코딩

ord1dict = {'Novice':0, 'Contributor':1, 
            'Expert':2, 'Master':3, 'Grandmaster':4}
ord2dict = {'Freezing':0, 'Cold':1, 'Warm':2, 
            'Hot':3, 'Boiling Hot':4, 'Lava Hot':5}

all_data['ord_1'] = all_data['ord_1'].map(ord1dict)
all_data['ord_2'] = all_data['ord_2'].map(ord2dict)

 
ord_3, ord_4, ord_5는 사이킷런의 OrdinaEncider를 사용해 알파벳 순서대로 인코딩

from sklearn.preprocessing import OrdinalEncoder

ord_345 = ['ord_3', 'ord_4', 'ord_5']

ord_encoder = OrdinalEncoder() # OrdinalEncoder 객체 생성
# ordinal 인코딩 적용
all_data[ord_345] = ord_encoder.fit_transform(all_data[ord_345])

# 피처별 인코딩 순서 출력
for feature, categories in zip(ord_345, ord_encoder.categories_):
    print(feature)
    print(categories)
ord_3
['a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o']
ord_4
['A' 'B' 'C' 'D' 'E' 'F' 'G' 'H' 'I' 'J' 'K' 'L' 'M' 'N' 'O' 'P' 'Q' 'R'
 'S' 'T' 'U' 'V' 'W' 'X' 'Y' 'Z']
ord_5
['AP' 'Ai' 'Aj' 'BA' 'BE' 'Bb' 'Bd' 'Bn' 'CL' 'CM' 'CU' 'CZ' 'Cl' 'DH'
 'DN' 'Dc' 'Dx' 'Ed' 'Eg' 'Er' 'FI' 'Fd' 'Fo' 'GD' 'GJ' 'Gb' 'Gx' 'Hj'
 'IK' 'Id' 'JX' 'Jc' 'Jf' 'Jt' 'KR' 'KZ' 'Kf' 'Kq' 'LE' 'MC' 'MO' 'MV'
 'Mf' 'Ml' 'Mx' 'NV' 'Nf' 'Nk' 'OR' 'Ob' 'Os' 'PA' 'PQ' 'PZ' 'Ps' 'QM'
 'Qb' 'Qh' 'Qo' 'RG' 'RL' 'RP' 'Rm' 'Ry' 'SB' 'Sc' 'TR' 'TZ' 'To' 'UO'
 'Uk' 'Uu' 'Vf' 'Vx' 'WE' 'Wc' 'Wv' 'XI' 'Xh' 'Xi' 'YC' 'Yb' 'Ye' 'ZR'
 'ZS' 'Zc' 'Zq' 'aF' 'aM' 'aO' 'aP' 'ac' 'av' 'bF' 'bJ' 'be' 'cA' 'cG'
 'cW' 'ck' 'cp' 'dB' 'dE' 'dN' 'dO' 'dP' 'dQ' 'dZ' 'dh' 'eG' 'eQ' 'eb'
 'eg' 'ek' 'ex' 'fO' 'fh' 'gJ' 'gM' 'hL' 'hT' 'hh' 'hp' 'iT' 'ih' 'jS'
 'jV' 'je' 'jp' 'kC' 'kE' 'kK' 'kL' 'kU' 'kW' 'ke' 'kr' 'kw' 'lF' 'lL'
 'll' 'lx' 'mb' 'mc' 'mm' 'nX' 'nh' 'oC' 'oG' 'oH' 'oK' 'od' 'on' 'pa'
 'ps' 'qA' 'qJ' 'qK' 'qP' 'qX' 'qo' 'qv' 'qw' 'rZ' 'ri' 'rp' 'sD' 'sV'
 'sY' 'sn' 'su' 'tM' 'tP' 'tv' 'uJ' 'uS' 'ud' 'us' 'ut' 'ux' 'uy' 'vK'
 'vq' 'vy' 'wu' 'wy' 'xP' 'xy' 'yN' 'yY' 'yc' 'zU']

 
명목형 피처 인코딩
명목형 피처는 순서를 무시해도 되기 때문에 원-핫 인코딩 적용
지능형 리스트를 활용해 명목형 피처 리스트를 만든다
지능형 리스트는 코드 한 줄로 새로운 리스트를 만드는 문법 구조다.

nom_features = ['nom_' + str(i) for i in range(10)] # 명목형 피처

nom_0 부터 nom_9까지 총 10개의 원소를 갖는 리스트가 새로 생성 됨
이 명목형 피처를 원-핫 인코딩해 별도 행렬에 저장하고, all_data에서 명목형 피처를 삭제한다. 

from sklearn.preprocessing import OneHotEncoder

onehot_encoder = OneHotEncoder() # OneHotEncoder 객체 생성
# 원-핫 인코딩 적용
encoded_nom_matrix = onehot_encoder.fit_transform(all_data[nom_features])

encoded_nom_matrix
all_data = all_data.drop(nom_features, axis=1) # 기존 명목형 피처 삭제

 
날짜 피처에도 원-핫 인코딩 적용

date_features  = ['day', 'month'] # 날짜 피처

# 원-핫 인코딩 적용
encoded_date_matrix = onehot_encoder.fit_transform(all_data[date_features])

all_data = all_data.drop(date_features, axis=1) # 기존 날짜 피처 삭제

encoded_date_matrix

 
순서형 피처 스케일링
피처 스케일링은 서로 다른 피처들의 값의 범위가 일치하도록 조정하는 작업
피처 스케일링은 수치형 피처들의 유효 값 범위가 서로 다르면 훈련이 제대로 안될 수 있기 때문에 필요함
이진, 명목형, 날짜 피처를 모두 0과 1로 인코딩 했지만, 순서형 피처는 여전히 여러 가지 값을 가지고 있음
순서형 피처의 값 범위도 0~1 사이가 되도록 스케일링함.
피처의 값을 0~1로 조정하는 min-max 정규화를 적용

from sklearn.preprocessing import MinMaxScaler

ord_features = ['ord_' + str(i) for i in range(6)] # 순서형 피처
# min-max 정규화
all_data[ord_features] = MinMaxScaler().fit_transform(all_data[ord_features])

 
인코딩 및 스케일링된 피처 합치기
명목형 피처와 날짜 피처는 원-핫 인코딩되어 각각 encoded_nom_matrix와 encoded_data_matrix에 저장돼 있음
all_data는 DataFrame이고, encoded_nom_matrix와 encoded_data_matrix는 CSR 형식이 행렬임
형식이 서로 다르니 맞춰야 함
all_data를 CSR 형식으로 만들어서 합치겠음
사이파이가 제공하는 csr_ matrix()는 전달받은 데이터를 CSR 형식으로 바꿔줌

from scipy import sparse

# 인코딩 및 스케일링된 피처 합치기
all_data_sprs = sparse.hstack([sparse.csr_matrix(all_data),
                               encoded_nom_matrix,
                               encoded_date_matrix],
                              format='csr')

hstack() 은 행렬을 수평으로 합침
format='csr'을 전달하면 합친 결과를 CSR로 반환함

all_data_sprs
<500000x16306 sparse matrix of type '<class 'numpy.float64'>'
	with 9163718 stored elements in Compressed Sparse Row format>

인코딩 된 피처를 합친 all_data_sprs 를 출력해보면
500,000행 16,306열로 구성되어 있는 것을 알 수 있다. 
 
데이타 나누기

num_train = len(train) # 훈련 데이터 개수

# 훈련 데이터와 테스트 데이터 나누기
X_train = all_data_sprs[:num_train] # 0 ~ num_train - 1행
X_test = all_data_sprs[num_train:] # num_train ~ 마지막 행

y = train['target']

훈련 데이터와 테스트 데이터를 나눔. y는 모델 훈련시 필요한 타깃값임
 
하이퍼파라미터 최적화
그리드서치를 활용해 로지스틱 회귀 모델의 하이퍼파라미터를 최적화
탐색할 하이퍼파라미터는 C와 max_iter
C는 규제 강도를 조절하는 파라미터로 값이 작을수록 규제 강도가 세짐
%%time은 해당 셀 실행 후 소요 시간을 출력해줌

%%time

from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import LogisticRegression

# 로지스틱 회귀 모델 생성
logistic_model = LogisticRegression()

# 하이퍼파라미터 값 목록
lr_params = {'C':[0.1, 0.125, 0.2], 'max_iter':[800, 900, 1000], 
             'solver':['liblinear'], 'random_state':[42]}

# 그리드서치 객체 생성
gridsearch_logistic_model = GridSearchCV(estimator=logistic_model,
                                         param_grid=lr_params,
                                         scoring='roc_auc', # 평가지표
                                         cv=5)
# 그리드서치 수행
gridsearch_logistic_model.fit(X_train, y)

print('최적 하이퍼파라미터:', gridsearch_logistic_model.best_params_)
최적 하이퍼파라미터: {'C': 0.125, 'max_iter': 800, 'random_state': 42, 'solver': 'liblinear'}
CPU times: user 9min 20s, sys: 16min 10s, total: 25min 31s
Wall time: 6min 33s

6분 30초 정도 소요됨
최적 하이퍼파라미터는 C:0.125, max_iter: 800
예측 및 결과 제출

y_valid_preds = gridsearch_logistic_model.predict_proba(X_valid)[:, 1]
# 타깃값 1일 확률 예측
y_preds = gridsearch_logistic_model.best_estimator_.predict_proba(X_test)[:,1]

# 제출 파일 생성
submission['target'] = y_preds
submission.to_csv('submission.csv')
반응형