Yours Ever, Data Chronicles

[kaggle] 범주형 데이터 분석 - 변수 인코딩 & Baseline model 본문

Data Science/Kaggle

[kaggle] 범주형 데이터 분석 - 변수 인코딩 & Baseline model

Everly. 2022. 8. 4. 15:48

저번 EDA 포스팅을 통해 범주형 변수들이 어떤 카테고리 값을 갖고 있는지와, target과의 관계를 파악하였다.

그리고 주어진 모든 범주형 변수가 target과 유의미한 관계가 있다는 것을 알 수 있었다. (하지만 명목형 변수 nom_5~nom_9는 카테고리가 워낙 많아 검증이 필요하다.)

 

이번에는 베이스라인 모델을 만들어본다. 주어진 범주형 변수들을 활용해 target을 예측하는 모델을 만들어 볼 것이며, 베이스라인 모델이므로 가장 간단한 형태로 만들 것이다.

이번 포스팅에서는 범주형 변수를 전처리하는 변수 인코딩(Encoding) 방법과, 아주 간단한 로지스틱 회귀 모델을 만들어 보고 score가 얼마가 나오는지 알아보자.

 

✔Table of Contents

     

    1. 범주형 변수 인코딩(Encoding)

    사실 이 프로젝트의 메인이 바로 이 인코딩이다.

    수치형 변수(numerical data)의 경우, 전처리를 할 땐 숫자니까 스케일링(Scaling)을 하여 변수 간 범위를 맞춰준다.

    범주형 변수(categorical data)를 전처리할 때는 인코딩(Encoding)을 한다.

     

    수치형 변수의 전처리를 다룰 기회는 많지만, 범주형 변수는 생소한 경우가 많다. 그래서 더욱 인코딩하는 방법을 알 필요가 있다. 

    특히 범주형 변수는 숫자가 아닌 문자로 되어 있는 경우가 많은데, 이렇게 문자로 되어 있으면 머신러닝 모델에 아예 넣을 수 없기 때문에 반드시 숫자로 바꿔주는 과정이 필요하다.

     

    먼저 데이터를 가져오고, 각 범주형 변수별로 어떤 인코딩을 적용해야 하는지 알아보자.

    개념 위주로 설명하는 포스팅이 될 예정이라, 전체 코드를 알고 싶은 경우는 이 깃허브에서 전체 Notebook 파일을 다운받으면 볼 수 있다 :)

     

    import pandas as pd
    import numpy as np
    import warnings
    warnings.filterwarnings("ignore")
    
    train = pd.read_csv('train.csv', index_col = 'id')
    test = pd.read_csv('test.csv', index_col = 'id')
    
    display(train.head())
    display(test.head())

     

    train, test 데이터를 불러왔다. 저번 포스팅에서도 봤듯이 범주형 변수의 개수가 워낙 많기 때문에(총 23개) 중간에 잘려서 나온다.

    변수들은 전부 범주형 변수이다. 얘네를 정리하자면 다음과 같다.

     

    변수명 bin_* ord_* nom_* day, month
    의미 이진 변수 순서 변수 명목 변수 날짜 변수
    인코딩 방법 굳이 할 필요 X 순서를 보존하여
    숫자 형태로 인코딩
    원-핫 인코딩 원-핫 인코딩

     

    그리고 train, test 데이터를 모두 모아 한꺼번에 인코딩을 적용시킨 뒤 다시 분할해줄 것이므로 다음의 코드를 실행한다.

     

    # 인코딩은 train과 test에 모두 해줄 것이므로 먼저 두개를 이어붙인다. target은 따로 빼서 저장
    all_data = pd.concat([train, test], ignore_index = True)
    all_data.drop(columns = 'target', axis = 1, inplace = True)
    
    # target
    train_target = train['target']

    1) 이진 변수(bin_*) 인코딩

     

    이진 변수의 경우에는 딱히 인코딩을 해줄 것이 없다. 0과 1로만 구성되어 있기 때문! 

    다만 T, F나 Y, N 처럼 문자로 되어 있는 경우에는 머신러닝 모델을 적용할 수 없으므로 이것을 각각 1과 0으로 변환해주자.

     

    all_data['bin_3'] = all_data['bin_3'].apply(lambda x: 1 if x == 'T' else 0)
    all_data['bin_4'] = all_data['bin_4'].apply(lambda x: 1 if x == 'Y' else 0)
    
    # bin_* 변수만 떼어내기
    bin_cols = [f'bin_{i}' for i in range(0, 5)]
    bin_data = all_data[bin_cols]
    bin_data.head(3)

     

    이렇게 'bin_data' 가 만들어졌다.

     


    2) 순서 변수(ord_*) 인코딩

     

    순서 변수는 말 그대로 순서(순위)가 있는 데이터를 의미한다.

    예를 들어 ord_1 변수는 캐글 등급을 뜻하는 변수인데, Novice(초보자)보다 GrandMaster(그랜드마스터) 등급이 더 순위가 높다. 이런 순위를 보존하지 않고 그냥 일괄적으로 처리해버린다면 예측을 제대로 할 수 없을 것이다. 

     

    그래서 저번 EDA 포스팅에서도 했었지만, 순위를 보존해준다. 여기서는 map 함수를 활용하여 순위대로 숫자를 부여할 것이다.

     

    # ord_* 변수만 떼어내기
    ord_cols = [f'ord_{i}' for i in range(0, 6)] 
    ord_data = all_data[ord_cols]
    
    # 순서 설정이 필요한 ord_1, ord_2를 숫자값 순서로 설정해준다.
    ord1_enc = {'Novice': 0, 'Contributor': 1, 'Expert': 2,'Master': 3, 'Grandmaster': 4}
    ord2_enc = {'Freezing': 0 , 'Cold': 1, 'Warm': 2, 'Hot': 3, 'Boiling Hot': 4, 'Lava Hot': 5}
    
    ord_data.loc[:, 'ord_1'] = ord_data.loc[:, 'ord_1'].map(ord1_enc)
    ord_data.loc[:, 'ord_2'] = ord_data.loc[:, 'ord_2'].map(ord2_enc)
    ord_data.head()

     

    ord_0 변수는 이미 숫자로 되어 있는 데이터이므로 건들지 않는다.

    문자로 되어 있었던 ord_1, ord_2 변수에 순위별로 숫자를 부여하였다.

     

    그렇다면 ord_3, ord_4, ord_5 변수는 뭘까? 바로 알파벳이다.

    알파벳은 이미 알파벳 순서가 있다. 그래서 따로 위처럼 딕셔너리를 만들어 인코딩을 해주지 않아도 된다. 

    사이킷런에서 제공하는 LabelEncoder를 사용하면 쉽게 알파벳 순서대로 숫자를 부여해준다.

     

    # ord_3, ord_4, ord_5는 이미 알파벳이므로 labelEncoder로 숫자만 순서대로 부여한다.
    from sklearn.preprocessing import LabelEncoder
    encoder = LabelEncoder()
    ord_data['ord_3'] = encoder.fit_transform(ord_data['ord_3'].values)
    ord_data['ord_4'] = encoder.fit_transform(ord_data['ord_4'].values)
    ord_data['ord_5'] = encoder.fit_transform(ord_data['ord_5'].values)
    
    ord_data.head()

     

    ord_* 변수를 모두 숫자로 변경 완료하였다! 

    하지만 결과를 보면 숫자와 숫자 간 간격이 너무 차이가 나고 있다. 다른 변수들은 0과 1의 값을 가지는데, ord_5의 경우엔 136, 158 이런 너무 큰 값을 가진다. 

    이렇게 숫자의 스케일의 차이가 크면 머신러닝 모델은 ord_5 변수가 가장 중요하다고 인식하게 된다. (실제로는 전혀 그렇지 않더라도!)

    그래서 나는 사이킷런에서 제공하는 표준화 스케일러인 StandardScaler를 사용해 위 변수들을 정규분포를 따르도록 변형하였다. 

     

    # 그런데 위처럼 진행하면 숫자의 스케일이 너무 넓어지므로, 정규분포를 따르도록 표준화 스케일링
    from sklearn.preprocessing import StandardScaler
    scaler = StandardScaler()
    ord_data_scaled = scaler.fit_transform(ord_data)
    
    pd.DataFrame(ord_data_scaled, columns = ord_data.columns).head()

     

    [Q. StandardScaler(표준화)와 MinMaxScaler(정규화)의 차이] 

    • StandardScaler: 평균이 0, 분산이 1인 정규분포를 따르도록 값을 변형함. 그래서 모든 값은 -1과 1 사이 값을 가짐
    • MinMaxScaler: 모든 값을 0과 1 사이 값으로 변형함. 

     

    사실 위의 케이스의 경우, 표준화를 시키든 정규화를 시키든 별 상관이 없다.

    다른 범주형 변수들이 0, 1의 이진 값을 가지니까 MinMaxScaler를 써도 되고,

    나처럼 변수들을 정규분포로 만들고 싶으면 StandardScaler를 써도 된다. 

     

    나는 베이스라인 모델로 이진 분류에 좋은 성능을 보이는 로지스틱 회귀 모델을 쓸 것인데, 로지스틱 회귀의 경우 선형 회귀와 같이 데이터가 가우시안 분포를 갖고 있다고 가정하고 만들어진 것이므로 데이터를 정규분포 형태로 변형한 것이다.

    (그리고 내가 두개 다 써서 결과를 뽑아봤는데 성능이 차이가 없었다.)

     

    아무튼, 이렇게 만들어진 'bin_data'와 'ord_data_scaled' 두 데이터프레임을 합쳐 'all_data2' 라는 이름으로 저장한다.

     

    all_data2 = pd.concat([bin_data, pd.DataFrame(ord_data_scaled, columns = ord_data.columns)], axis = 1)

     


    3) 명목 변수(nom_*) & 날짜 변수(day, month) 인코딩

    이번에 살펴볼 위 변수들은 모두 명목형 변수들이다. 이전 포스팅에서도 소개했지만, 명목형 변수는 따로 순위(순서)를 갖지 않는 데이터를 의미한다.

    예를 들어 month를 보면, 1월과 4월은 어떤 차이가 있는가? 아무런 차이가 없다. 4월이 1월보다 더 값이 크다고 설정할 근거가 없으니까. 

    그래서 이런 명목형 변수들에는 통상적으로 원-핫 인코딩(One-Hot Encoding)을 많이 사용한다.

     

    One-Hot Encoding(원-핫 인코딩)은 특정 항목에 대해서만 1, 다른 것은 0으로 변환하는 것을 의미한다.

    그래서 만일 어떤 범주형 변수가 N개의 카테고리를 가지고 있다면, 열의 개수가 N개로 늘어난다. 

    위의 그림처럼, color라는 범주형 변수가 Green, Blue, Yellow 이렇게 3개의 카테고리를 갖고 있을 때 3개의 열이 생성되고, Green에 해당하는 것은 'col_Green' 변수 값이 1, 나머지는 0이 된다.

     

    원-핫 인코딩은 사이킷런의 OneHotEncoder를 사용하였으며 다음의 코드로 손쉽게 원-핫 인코딩이 가능하다.

     

    # nom_* & 날짜 변수만 떼어내기
    nom_cols = [f'nom_{i}' for i in range(0, 10)] 
    nm_list = ['day', 'month']
    nom_cols.extend(nm_list)
    nom_data = all_data[nom_cols]
    nom_data.head(3)

    # 변수가 엄청 늘어날 예정. 사이킷런의 OneHotEncoder 사용
    from sklearn.preprocessing import OneHotEncoder
    encoder = OneHotEncoder()
    nom_data_enc = encoder.fit_transform(nom_data)
    
    # 희소행렬 생성 완료! (데이터프레임으로 출력은 불가)
    nom_data_enc

     

    원-핫 인코딩을 적용하여 'nom_data_enc' 변수가 만들어졌다.

    열의 개수가 원래 12개에서 → 16,295개로 매우 늘어난 것을 알 수 있다.

    이렇게 만들어진 nom_data_enc 변수는 데이터프레임으로 출력은 불가능하다. 희소 행렬(Sparse Matrix) 형태이기 때문이다.

    (엄밀히 말하자면 데이터프레임으로 형태 변환을 하면 출력을 할 수는 있는데, 열의 수가 워낙 많다보니 출력하는 데 시간이 매우 오래 걸린다. 어차피 데이터프레임 형태로 꼭 봐야 할 필요가 없으니 굳이 바꾸지 않았다.)


    4) 인코딩된 변수들 합치기

    이제 앞서 만들어진 'all_data2' 라는 데이터프레임과,

    원-핫 인코딩된 'nom_data_enc' 를 합쳐보자. 

     

    이 두가지는 형태가 서로 다르기 때문에 그냥 붙이려고 하면 붙여지지 않는다.

    all_data2를 scipy 라이브러리의 sparse를 사용해 CSR 희소행렬 형태로 바꾼 후, hstack을 사용해 붙여야 한다.

    hstack을 사용하면 데이터들이 수평으로 붙여지게 되는데 쉽게 말해서 column-bind 형태로 붙는다는 뜻!

     

    from scipy import sparse
    all_sprs = sparse.hstack([sparse.csr_matrix(all_data2), nom_data_enc],
                                 format = 'csr')
    all_sprs

     

    이렇게 잘 붙여졌고, 최종 만들어진 'all_sprs' 데이터의 열 개수는 16,306개로 아까보다 더 늘어났다.

    이 데이터는 원래 train data와 test data를 모두 합친 후 전처리한 것이므로 다시 쪼개주자.

     

    # all_sprs는 앞 부분은 train / 뒷 부분은 test 데이터이므로 분할
    train_sprs = all_sprs[:len(train), :]
    test_sprs = all_sprs[len(train):, :]
    print(train_sprs.shape, test_sprs.shape)

     

    이렇게 train data 30만 개, test data 20만 개로 잘 분할되었다.

     


    2. 베이스라인 모델 - 로지스틱 회귀

    베이스라인 모델로는 로지스틱 회귀 모델을 선택하였다. 그 이유는 로지스틱 회귀가 이진 분류에 사용하기 좋은 가벼운 모델이기 때문이다.

    베이스라인 모델이니까, 가장 간단한 기본 로지스틱 회귀모형에 만들어진 데이터셋을 학습시켜 결과를 도출해보았다.

     

    # train / val set split 
    from sklearn.model_selection import train_test_split
    
    X_train, X_val, y_train, y_val = train_test_split(train_sprs, train_target, stratify = train_target, random_state = 99)
    print(X_train.shape, X_val.shape)

     

    보통 나는 train data를 학습시킬 때, train_test_split을 사용해 train set과 validation set을 나누기보다는 k-fold를 애용한다.

    하지만 이 데이터에선 train data가 30만 개나 있기 때문에 굳이 k-fold를 수고스럽게 사용할 필요가 없는 것 같아서 간단하게 train_test_split을 사용하였다.

     

    참고로 train_test_split의 옵션 중 stratify가 있는데, EDA 편에서도 살펴봤지만 target의 값 0과 1의 비율이 7:3이기 때문에 1의 개수가 적은 불균형 데이터(imbalanced data)라고 볼 수 있다.

    그래서 stratify를 target으로 설정해주면 0과 1의 비율에 맞춰서 데이터를 분할해준다. 불균형 데이터일 땐 이런 옵션을 사용하는 게 유용하다!

     

    from sklearn.linear_model import LogisticRegression
    
    model = LogisticRegression(random_state = 99)
    model.fit(X_train, y_train)
    y_pred = model.predict_proba(X_val)[:, 1]
    
    # 성능 확인(AUC) - 주의점. predict_proba로 뽑힌 확률값으로 비교해야 함
    from sklearn.metrics import roc_auc_score
    roc_auc_score(y_val, y_pred)

     

    사이킷런의 LogisticRegression 모델을 불러와 학습시켰다.

    이 컴퍼티션의 성능 지표(metric)는 AUC score이므로, 사이킷런의 roc_auc_score를 사용해 결과를 도출했다.

    AUC score는 0.799로 나쁘지 않은 편! (참고로 AUC 값은 1에 가까울 수록 좋다)

     

    이제 test data에 대해서 예측하고 캐글에 제출해보자.

     

    sub = pd.read_csv('sample_submission.csv')
    
    # 전체 데이터로 학습
    model = LogisticRegression(random_state = 99, max_iter = 1000)
    model.fit(train_sprs, train_target)
    
    # test data에 대해 예측 
    y_pred = model.predict_proba(test_sprs)
    del sub['target']
    sub['target'] = y_pred[:, 1]
    
    sub.to_csv('sub_sy_base.csv', index = False)

     

    [Test Score]

    • public: 0.80231, private: 0.79764
    • 앞에서 validation set에 대한 스코어가 0.799였음을 감안하면, test data에 대한 스코어가 더 잘 나왔다! (보통은 test data에 대한 스코어가 더 안 나오는게 일반적이다)

     

    안 좋은 성능은 아니지만, 리더보드를 보면 이 컴퍼티션이 대체로 성능이 잘 나오는 대회인 것 같다.

    1등의 성능이 0.808이므로 좀 더 성능을 향상시켜보자! 다음 포스팅에선 성능을 개선해서 public score를 0.808로 끌어올리는 과정을 알아보자 :)

    반응형