1. 피처 엔지니어링

  • 원본 데이터로부터 도메인 지식 등을 바탕으로 문제를 해결하는데 도움이 되는 Feature를 생성, 변환하고 이를 머신러닝 모델에 적합한 형식으로 변환하는 작업

  • 일반 머신러닝에서는 성능을 올리는데 가장 중요한 핵심적인 단계

    • 딥러닝은 모델의 구조를 통해 데이터의 피처를 모델이 스스로 추출하지만 (end-to-end learning)
    • 머신러닝은 피처를 스스로 추출할 수 없기 때문에 사람이 직접 데이터를 이해해서 피처를 만들어야 함


1) groupby / aggregation

  • Pandas의 groupby, aggregation 함수를 적용하여 새로운 피처 생성

  • EDA를 위한 Feature 생성 코드 스크린샷 2023-05-16 오후 12 36 42


  • Feature 생성 스크린샷 2023-05-16 오후 12 39 02


  • 피처 엔지니어링 함수 정의
'''
    입력인자로 받는 year_month와 변수 prev_ym 기준으로 train, test 데이터를 생성 하고
    집계(aggregation) 함수를 사용하여 피처 엔지니어링을 하는 함수
'''
def feature_engineering1(df: pd.DataFrame, year_month: str):
    df = df.copy()
    
    # year_month 이전 월 계산
    d = datetime.datetime.strptime(year_month, "%Y-%m")
    prev_ym = d - dateutil.relativedelta.relativedelta(months=1)
    prev_ym = prev_ym.strftime('%Y-%m')
    
    # train, test 데이터 선택
    train = df[df['order_date'] < prev_ym]
    test = df[df['order_date'] < year_month]
    
    # train, test 레이블 데이터 생성
    train_label = generate_label(df, prev_ym)[['customer_id','year_month','label']]
    test_label = generate_label(df, year_month)[['customer_id','year_month','label']]
    
    # group by aggregation 함수 선언
    agg_func = ['mean','max','min','sum','count','std','skew']
    all_train_data = pd.DataFrame()
    
    for i, tr_ym in enumerate(train_label['year_month'].unique()):
        # group by aggretation 함수로 train 데이터 피처 생성
        train_agg = train.loc[train['order_date'] < tr_ym].groupby(['customer_id']).agg(agg_func)

        # 멀티 레벨 컬럼을 사용하기 쉽게 1 레벨 컬럼명으로 변경
        new_cols = []
        for col in train_agg.columns.levels[0]:
            for stat in train_agg.columns.levels[1]:
                new_cols.append(f'{col}-{stat}')

        train_agg.columns = new_cols
        train_agg.reset_index(inplace = True)
        
        train_agg['year_month'] = tr_ym
        
        all_train_data = all_train_data.append(train_agg)
    
    all_train_data = train_label.merge(all_train_data, on=['customer_id', 'year_month'], how='left')
    features = all_train_data.drop(columns=['customer_id', 'label', 'year_month']).columns
    
    # group by aggretation 함수로 test 데이터 피처 생성
    test_agg = test.groupby(['customer_id']).agg(agg_func)
    test_agg.columns = new_cols
    
    test_data = test_label.merge(test_agg, on=['customer_id'], how='left')

    # train, test 데이터 전처리
    x_tr, x_te = feature_preprocessing(all_train_data, test_data, features)
    
    print('x_tr.shape', x_tr.shape, ', x_te.shape', x_te.shape)
    
    return x_tr, x_te, all_train_data['label'], features


1) total-sum Feature

sample1 = df_all[(df_all['total-sum'] > -1000) & (df_all['total-sum'] < 5000)]
sns.distplot(sampel1.loc[sample1['label'] == 0, 'total-sum'], label = 'label=0')
sns.distplot(sampel1.loc[sample1['label'] == 1, 'total-sum'], label = 'label=1')
plt.legend()
plt.show()

스크린샷 2023-05-16 오후 12 41 49

  • total-sum : label별 분포가 확연히 다르기 때문에 모델에서 사용하기 좋은 feature


2) quantity-sum feature

sample1 = df_all[(df_all['quantity-sum'] > 0) & (df_all['quantity-sum'] < 1500)]
sns.distplot(sampel1.loc[sample1['label'] == 0, 'quantity-sum'], label = 'label=0')
sns.distplot(sampel1.loc[sample1['label'] == 1, 'quantity-sum'], label = 'label=1')
plt.legend()
plt.show()

스크린샷 2023-05-16 오후 12 44 47

  • quantity-sum : label별 분포가 확연히 다르기 때문에 모델에서 사용하기 좋은 feature


3) price-sum feature

sample1 = df_all[(df_all['price-sum'] > 0) & (df_all['price-sum'] < 1000)]
sns.distplot(sampel1.loc[sample1['label'] == 0, 'price-sum'], label = 'label=0')
sns.distplot(sampel1.loc[sample1['label'] == 1, 'price-sum'], label = 'label=1')
plt.legend()
plt.show()

스크린샷 2023-05-16 오후 12 46 18

  • price-sum : label별 분포가 확연히 다르기 때문에 모델에서 사용하기 좋은 feature


4) total-count feature

sample1 = df_all[(df_all['total-count'] > 0) & (df_all['total-count'] < 200)]
sns.distplot(sampel1.loc[sample1['label'] == 0, 'total-count'], label = 'label=0')
sns.distplot(sampel1.loc[sample1['label'] == 1, 'total-count'], label = 'label=1')
plt.legend()
plt.show()

스크린샷 2023-05-16 오후 12 47 57

  • total-count : label별 분포가 확연히 다르기 때문에 모델에서 사용하기 좋은 feature


5) quantity-count feature

sample1 = df_all[(df_all['quantity-count'] > 0) & (df_all['quantity-count'] < 200)]
sns.distplot(sampel1.loc[sample1['label'] == 0, 'quantity-count'], label = 'label=0')
sns.distplot(sampel1.loc[sample1['label'] == 1, 'quantity-count'], label = 'label=1')
plt.legend()
plt.show()

스크린샷 2023-05-16 오후 12 49 35

  • quantity-count : label별 분포가 확연히 다르기 때문에 모델에서 사용하기 좋은 feature


6) price-std feature

sample1 = df_all[(df_all['price-std'] > 0) & (df_all['price-std'] < 10)]
sns.distplot(sampel1.loc[sample1['label'] == 0, 'price-std'], label = 'label=0')
sns.distplot(sampel1.loc[sample1['label'] == 1, 'price-std'], label = 'label=1')
plt.legend()
plt.show()

스크린샷 2023-05-16 오후 12 52 34

  • price-std : label별 분포가 큰 차이를 보이지 않음


2. Cross Validation을 이용한 Out of Fold 예측

  • 모델 훈련시 Cross Validation을 적용해서 Out of Fold Validation 성능 측정 및 Test 데이터 예측을 통해 성능 향상

스크린샷 2023-05-16 오후 12 54 23

  • Out Of Fold
    • Fold마다 학습한 모델로 테스트 데이터를 예측하고, 이를 평균 앙상블하여 최종 예측값으로 사용하는 방법
    • 대부분 이 기법 사용


  • LightGBM cross validation out of fold train/predict 함수 정의
'''
    학습 데이터(x_tr), 검증 데이터(x_val), 테스트 데이터(test)로 LightGBM 모델을
    학습, 교차(cross) 검증 및 테스트하고 사용된 피처들의 중요도를 반환하는 함수
'''

def make_lgb_oof_prediction(train, y, test, features, categorical_features='auto', model_params=None, folds=10):
    x_train = train[features]
    x_test = test[features]
    
    # 테스트 데이터 예측값을 저장할 변수
    test_preds = np.zeros(x_test.shape[0])
    
    # Out Of Fold Validation 예측 데이터를 저장할 변수
    y_oof = np.zeros(x_train.shape[0])
    
    # 폴드별 평균 Validation 스코어를 저장할 변수
    score = 0
    
    # 피처 중요도를 저장할 데이터 프레임 선언
    fi = pd.DataFrame()
    fi['feature'] = features
    
    # Stratified K Fold 선언
    skf = StratifiedKFold(n_splits=folds, shuffle=True, random_state=SEED)

    for fold, (tr_idx, val_idx) in enumerate(skf.split(x_train, y)):
        # train index, validation index로 train 데이터를 나눔
        x_tr, x_val = x_train.loc[tr_idx, features], x_train.loc[val_idx, features]
        y_tr, y_val = y[tr_idx], y[val_idx]
        
        print(f'fold: {fold+1}, x_tr.shape: {x_tr.shape}, x_val.shape: {x_val.shape}')

        # LightGBM 데이터셋 선언
        dtrain = lgb.Dataset(x_tr, label=y_tr)
        dvalid = lgb.Dataset(x_val, label=y_val)
        
        # LightGBM 모델 훈련
        clf = lgb.train(
            model_params,
            dtrain,
            valid_sets=[dtrain, dvalid], # Validation 성능을 측정할 수 있도록 설정
            categorical_feature=categorical_features,
            verbose_eval=200
        )

        # Validation 데이터 예측
        val_preds = clf.predict(x_val)
        
        # Validation index에 예측값 저장 
        y_oof[val_idx] = val_preds
        
        # 폴드별 Validation 스코어 측정
        print(f"Fold {fold + 1} | AUC: {roc_auc_score(y_val, val_preds)}")
        print('-'*80)

        # score 변수에 폴드별 평균 Validation 스코어 저장
        score += roc_auc_score(y_val, val_preds) / folds
        
        # 테스트 데이터 예측하고 평균해서 저장
        test_preds += clf.predict(x_test) / folds
        
        # 폴드별 피처 중요도 저장
        fi[f'fold_{fold+1}'] = clf.feature_importance()

        del x_tr, x_val, y_tr, y_val
        gc.collect()
        
    print(f"\nMean AUC = {score}") # 폴드별 Validation 스코어 출력
    print(f"OOF AUC = {roc_auc_score(y, y_oof)}") # Out Of Fold Validation 스코어 출력
        
    # 폴드별 피처 중요도 평균값 계산해서 저장 
    fi_cols = [col for col in fi.columns if 'fold_' in col]
    fi['importance'] = fi[fi_cols].mean(axis=1)
    
    return y_oof, test_preds, fi


3. LightGBM Early Stopping 적용

  • Early Stopping
    • Iteration을 통해 반복학습이 가능한 머신러닝 모델에서 validation 성능 측정을 통해 validation 성능이 가장 좋은 하이퍼파라미터에서 학습을 조기 종료하는 regularization 기법
    • ex) Boosting 트리 모델의 트리 개수, 딥러닝의 Epoch 수
  • LightGBM Early Stopping
    • LightGBM에서 몇 개의 트리를 만들지 n_estimator라는 하이퍼파라미터로 설정하고 이 개수만큼 트리를 만들지만, 이 개수가 최적의 값이라고 볼 수 없음
    • Early Stopping은 validation 데이터가 있을 시, LightGBM의 트리 개수인 n_estimator는 충분히 크게 설정하고, early_stopping_rounds를 적절한 값으로 설정
    • 트리를 추가할 때마다 validation 성능을 측정하고, 이 성능이 early_stopping_rounds 값 이상 연속으로 성능이 좋아지지 않으면 더이상 트리를 만들지 않고, 가장 validation 성능이 좋은 트리 개수를 최종 트리 개수로 사용


  • LightGBM Early Stopping 적용
folds = 5
model_params = {'n_estimators':1000, ...}   # ligthgbm hyperparameter
x_train = ...  # train data
x_test = ...   # test data
y = ...        # train label

# validation out of fold 예측값을 저장할 변수
y_oof = np.zeros(x_train.shape[0])

# test data 예측값을 저장할 변수
y_preds = np.zeros(x_test.shape[0])

skf = StratifiedKFold(n_splits=folds, shuffle=True, ranndom_state=42)
# stratified k fold split 함수로 train index, validation index 가져옴
for fold, (tr_idx, val_idx) in enumerate(skf.split(x_train, y)):
    # train, validation index로 train data split
    x_tr, x_val = x_train.loc[tr_idx], x_train.loc[val_idx]
    y_tr, y_val = y[tr_idx], y[val_idx]
    dtrain = lgb.Dataset(x_Tr, label=y_tr)
    dvalid = lgb.Dataset(x_val, label=y_val)

    # early stopping으로 lightgbm 모델 train
    clf = lgv.train(model_params, dtrain, valid_sets=[dtrain, dvalid], early_stopping_rounds=100)

    # out of fold validation data 예측
    y_oof[val_idx] = clf.predict(x_val)
    # train된 모델로 test data 예측
    y_preds += clf.predict(x_test) / folds

# 전체 fold loop 순회 후 out of fold validation 성능 측정
print(f'OOF AUC = {roc_auc_score(y, y_oof)}')