캐글 노트북
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')