Data Science/Kaggle

[kaggle] 범주형 데이터 분석 - 머신러닝 1편 (변수 선택)

Everly. 2022. 8. 5. 12:38

이전 포스팅에서는 범주형 변수를 전처리하기 위한 인코딩을 하고, 기본적인 베이스라인 모델을 만들어 제출까지 해보았다.

이번 포스팅부터는 성능을 개선하기 위한 여러 방법들을 시도해보자! 

내가 시도해본 방법은 다음과 같다.

 

  • 1) 명목형 변수인 nom_*의 변수 개수 조정 (nom_5~ nom_9번 변수는 의미있는 변수인지 모르니까)
  • 2) 로지스틱 회귀의 하이퍼 파라미터 튜닝

 

이번 포스팅에선 1번을 해본다. 참고로 전체 코드는 나의 깃허브에서 다운받을 수 있다 :)

 

✔Table of Contents

     

    1. 데이터 불러오기

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

     

    데이터가 어떻게 생겼는지는 앞의 포스팅에서도 많이 다루었으므로 별다른 설명 없이 넘어간다.

    데이터 인코딩을 위해 주어진 train data, test data를 모두 합쳐 'all_data'로 만들었다.

    그리고 train data에 있는 target 변수는 따로 빼서 'train_target' 이라는 이름으로 저장하였다.

     

    # target
    train_target = train['target']

    2. 인코딩 & 머신러닝 부분 함수화 (nom_* 변수 개수 조정)

    이번 포스팅에서는 명목형 변수인 nom_* 에서 어떤 값을 포함시키고, 어떤 값을 뺄 것인지가 가장 중요한 부분이다.

    특히 nom_* 변수들 중에서 nom_0 부터 nom_4까지는 EDA를 통해 중요한 변수라는 것을 알았지만,

    nom_5부터 nom_9까지는 갖고 있는 카테고리의 개수가 너무 많아 EDA를 할 수 없었다.

     

    그래서 직접 nom_5부터 nom_9 까지의 변수를 포함 or 제거해서 스코어가 어떻게 나오는지를 알아보았다.

    즉, nom_9만 빼보고 스코어를 구해보고,

    nom_8과 nom_9 두개를 빼서 스코어를 구해보고,

    nom_7, 8, 9 세개를 빼서 스코어를 구해보고, ... 이런 식으로 구해본다는 것이다.

     

    근데 이걸 위해 계속 같은 인코딩 과정을 거쳐야 하는 점이 매우 귀찮게 느껴졌다.(필요한 건 몇번째 nom 변수를 제거할까? 만 알면 되는데!)

    그래서 인코딩 부분을 다음과 같이 함수화했다.

     

    from sklearn.preprocessing import LabelEncoder
    from sklearn.preprocessing import StandardScaler
    from sklearn.preprocessing import OneHotEncoder
    from scipy import sparse
    
    ### 범주형 데이터 전처리 함수
    def cat_preprocessing(df, n_nom):
        # df: 전처리할 데이터
        # n_nom: 원하는 nom_* 변수의 개수 (만일 10을 넣으면 nom_0 ~ nom_9까지 포함하므로 베이스라인 모델과 동일함)
        # 반환되는 값은 train_sprs, test_sprs
        
        print('nom_* 변수는 nom_0 부터 nom_{0} 까지 사용되었습니다'.format(n_nom-1))
        print(' ')
        
        # bin_* 변수 전처리
        bin_cols = [f'bin_{i}' for i in range(0, 5)]
        bin_data = df[bin_cols]
        bin_data['bin_3'] = bin_data['bin_3'].apply(lambda x: 1 if x == 'T' else 0)
        bin_data['bin_4'] = bin_data['bin_4'].apply(lambda x: 1 if x == 'Y' else 0)
        
        # ord_* 변수 전처리
        ord_cols = [f'ord_{i}' for i in range(0, 6)] 
        ord_data = df[ord_cols]
        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)
            ## 숫자 순위 부여
        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)
            ## 숫자 스케일링
        scaler = StandardScaler()
        ord_data_scaled = scaler.fit_transform(ord_data)
        
        # 앞에서 만든 bin_data & ord_data 합침
        df2 = pd.concat([bin_data, pd.DataFrame(ord_data_scaled, columns = ord_data.columns)], axis = 1)
        
        #--------------- nom_* 변수에서 어떤 변수를 포함할지 선택------------------
        nom_cols = [f'nom_{i}' for i in range(0, n_nom)] # 변수 개수 조정 
        nm_list = ['day', 'month']
        nom_cols.extend(nm_list)
        nom_data = df[nom_cols]
       
        encoder = OneHotEncoder()
        nom_data_enc = encoder.fit_transform(nom_data)
        
        # df2와 nom_data_enc를 결합
        all_sprs = sparse.hstack([sparse.csr_matrix(df2), nom_data_enc], format = 'csr')
        
        # all_sprs는 앞 부분은 train / 뒷 부분은 test 데이터이므로 분할
        train_sprs = all_sprs[:len(train), :]
        test_sprs = all_sprs[len(train):, :]
        print('train data의 shape: ', train_sprs.shape)
        print('test data의 shape: ', test_sprs.shape)
        
        return train_sprs, test_sprs

     

    위처럼 'cat_preprocessing' 함수를 만들어 보았다.

    input으로는 전처리할 데이터 df와, 명목형 변수 몇 개를 포함할 것인지를 의미하는 n_nom 값이다.

    예를 들어 n_nom = 10을 넣으면 nom_0부터 nom_9까지를 포함하게 되고, n_nom = 9를 넣으면 nom_0부터 nom_8 까지만 포함하게 된다.

     

    이렇게 해서 반환되는 output은 집어넣은 df를 인코딩까지 완료해서 반환하는 train_sprs, test_sprs 이렇게 2개이다.

     


    인코딩 함수를 만들고 나니, train_sprs와 test_sprs를 넣어서 또 로지스틱 회귀로 모델링 하는 부분도 중복되는 과정이라... 이것도 함수로 만들어 보았다! 

     

    ### train, val set을 나눈 후에 기본 로지스틱 회귀 모델로 모델링 후, val set의 AUC score 출력 함수
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LogisticRegression
    from sklearn.metrics import roc_auc_score
        
    def logistic_train(train_sprs, y, tsize = 0.2, n_iter = 100):
    
        # train, val set split
        X_train, X_val, y_train, y_val = train_test_split(train_sprs, y, stratify = y, random_state = 99, test_size = tsize)
        print('X_train의 shape: ', X_train.shape)
        print('X_val의 shape: ', X_val.shape)
        
        # 기본 모델링
        model = LogisticRegression(random_state = 99, max_iter = n_iter)
        model.fit(X_train, y_train)
        y_pred = model.predict_proba(X_val)[:, 1]
        
        # 성능 (AUC) 출력
        print(' ')
        print("val set's AUC score : {:.4f}".format(roc_auc_score(y_val, y_pred)))

     

    이렇게 위의 logistic_train 함수를 사용하면 집어넣은 train data에 대해 로지스틱 회귀 모델링을 하고 AUC score를 출력해준다.

     


    3. nom_* 변수에 따른 스코어 변화

    이제 nom_* 변수를 어디까지 넣을 것인가에 따라 값을 뽑아보았다.

     

    1) 베이스라인 모델과 동일한 경우

    # baseline model과 동일
    train_sprs, test_sprs = cat_preprocessing(all_data, 10)
    logistic_train(train_sprs, train_target)

     

    AUC score는 약 0.80으로 저번 포스팅에서 나온 값 0.799와 거의 같다.

     

    2) nom_9 변수를 제외한 경우

    # nom_9 제외
    train_sprs, test_sprs = cat_preprocessing(all_data, 9)
    logistic_train(train_sprs, train_target)

     

    3) nom_8, nom_9 변수를 제외한 경우

    # nom_8과 9 제외
    train_sprs, test_sprs = cat_preprocessing(all_data, 8)
    logistic_train(train_sprs, train_target)

    nom_9 하나만 제거했을 때가 0.801로 가장 높아졌고, 더 많이 뺄수록 스코어가 점점 낮아지고 있다.

     

    4) nom_7, nom_8, nom_9 변수를 제외한 경우

    # nom_7, 8, 9 제외
    train_sprs, test_sprs = cat_preprocessing(all_data, 7)
    logistic_train(train_sprs, train_target)

     

    이렇게 해서 변수를 더 많이 뺄수록 스코어가 계속 낮아지고 있어서, 여기서 중단하고 nom_9 변수 하나만 빼기로 했다.

    이번에는 logistic_train 함수의 옵션을 좀 수정해서 성능을 더 높여보았다.

     

    1) test_size를 줄여 학습에 사용되는 데이터를 늘리기

    # nom_9 제외, test_size 좀 더 줄여보기 
    train_sprs, test_sprs = cat_preprocessing(all_data, 9)
    logistic_train(train_sprs, train_target, tsize = 0.15)

    # nom_9 제외, test_size 좀 더 줄여보기 
    train_sprs, test_sprs = cat_preprocessing(all_data, 9)
    logistic_train(train_sprs, train_target, tsize = 0.1)

     

     

    이렇게 test_size를 0.1로 줄였을 때, AUC 값이 0.805로 가장 높아졌다.

    test_size를 더 줄이면 학습에 사용하는 데이터가 많아져 더 스코어가 높아질 수도 있겠지만, 과적합이 될 가능성도 높아지기 때문에 여기서 중단했다.

     

    2) n_iter(max_iter) 값을 늘려 모델 고도화하기

    # nom_9 제외, test_size = 0.1, n_iter 값 늘리기
    train_sprs, test_sprs = cat_preprocessing(all_data, 9)
    logistic_train(train_sprs, train_target, tsize = 0.1, n_iter = 200)

     

    원래 디폴트는 n_iter = 100 이다. 

    여기서 n_iter는 로지스틱 회귀의 max_iter 값을 의미하는데, 이는 Gradient Descent 방식을 반복해서 몇 번 수행할 것인가? 를 의미한다. 그래서 max_iter 값이 커질수록 더 모델 성능을 높일 수 있는데, 대신 그만큼 반복해서 수행하므로 시간이 더 오래 걸린다.

     

    이 데이터에선 특이하게도 이 값을 200으로 늘렸더니 0.804로 성능이 더 떨어졌다. 한 번만 더 해보자.

     

    train_sprs, test_sprs = cat_preprocessing(all_data, 9)
    logistic_train(train_sprs, train_target, tsize = 0.1, n_iter = 300)

     

    300으로 늘렸을 때에도 여전히 성능이 좋아지지 않았다. 그래서 max_iter 값은 디폴트인 100을 유지하는 걸로!

     


    4. 최종 제출 결과

    최종적으로 nom_9 변수를 제외하고, max_iters는 100을 사용하기로 했다.

    이제 train data 전체를 다 사용하여 모델을 만들어보자.

    앞에서 validation set에 대한 AUC 성능은 0.805였으므로 성능이 얼마까지 향상할 수 있을지 기대해보자!

     

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

     

    [Test Score]

    • public: 0.80421 / private: 0.79823
    • 이전 포스팅에서 베이스라인 모델에 대한 public 성능이 0.802였음을 감안하면 → 0.804로 꽤 성능 향상을 보였다!

     

    이전 포스팅에서도 봤었지만 train AUC 값과 test AUC 값이 큰 차이를 보이지 않는다는 점도 주목할 만 하다. 보통은 train 스코어에 비해 test 스코어가 더 낮아지는 것이 일반적이기 때문.. (인공적으로 만든 데이터이기 때문인가?!) 

     

    이번에는 nom_9 라는 변수를 하나 제거하는 것만으로 0.002의 성능 향상을 이뤄냈다. 

    다음 포스팅에선 로지스틱 회귀 모델을 튜닝하여 더 성능 향상을 기대해보자 :)

    반응형