Julie의 Tech 블로그

Kaggle Case Study - (1) Santander Customer Transaction Prediction 본문

Tech/ML, DL

Kaggle Case Study - (1) Santander Customer Transaction Prediction

Julie's tech 2021. 5. 12. 23:46
728x90

본 카테고리는 Kaggle Case study를 통해 학습데이터 구성부터 모델 빌딩까지의 cycle 경험을 늘려보려고 한다.

참고한 코드는 아래 링크에서 확인 가능하다.

Santander Customer Transaction - EDA / FE / LGB

https://www.kaggle.com/gunesevitan/santander-customer-transaction-eda-fe-lgb

 

Santander Customer Transaction - EDA / FE / LGB

Explore and run machine learning code with Kaggle Notebooks | Using data from Santander Customer Transaction Prediction

www.kaggle.com


Contents

1. EDA

2. FE

3. Modeling - light BGM


1. EDA

아래와 같은 포맷으로 데이터에 대해 파악한 사실을 정리해볼 수 있다.

train, test 각각의 데이터 크기는 { } 행이다.

feature수는 각각 { } 개 이다.

target 변수는 { }형이며, { }개의 unique한 값이다.

데이터의 key는 { }변수이다.

{기타 feautre 에 대한 특징}

결측치는 { }개로 파악된다.

- 데이터 크기 파악 (cardinality, feature수)

data.shape

- 컬럼 분포 파악

아래 수행 결과를 통해 전부 수치형 변수임을 확인할 수 있고, 미리 processed된 데이터임을 결론낼 수 있다.

data.info()

- 결측치 확인

# 결측치 확인
data.isnull().sum()
# 결측지 비율 확인
data.isnull().sum() / len(data)

- target distribution

negative = data['target'].value_counts()[0]
positive = data['target'].value_counts()[1]
negative_ratio = negative / data.shape[0] * 100
positive_ratio = positive / data.shape[0] * 100

print('{} out of {} rows are POSITIVE and it is the {:.2f}% of the dataset.'.format(positive, data.shape[0], positive_ratio))
print('{} out of {} rows are NEGATIVE and it is the {:.2f}% of the dataset.'.format(negative, data.shape[0], negative_ratio))

plt.figure(figsize=(8, 6))
sns.countplot(data['target'])

plt.xlabel('Target Distribution')
plt.xticks((0, 1), ['Class 0 ({0:.2f}%)'.format(negative_ratio), 'Class 1 ({0:.2f}%)'.format(positive_ratio)])
plt.ylabel('Count')
plt.title('Target Distribution')

plt.show()

- 상관관계 확인

pandas corr() 함수의 default 값인 pearson 상관계수로 분석한다.

abs() -> unstack() 으로 dataframe을 만든다. 처음 접하게 된 코드였는데, 분포를 확인할 때 유용한 것 같다.

df_corr = data.corr().abs().unstack().sort_values(kind="quicksort").reset_index() ## correlation을 절대치로 확인하여 unstack함. 정렬
df_corr.rename(columns={"level_0": "Feature 1", "level_1": "Feature 2", 0: 'Correlation Coefficient'}, inplace=True)


df_corr.drop(df_corr.iloc[1::2].index, inplace=True) # index 1부터 step 2단위 (홀수만) drop함
df_corr_nd = df_corr.drop(df_corr[df_corr['Correlation Coefficient'] == 1.0].index) # 동일한 대상에 대한 상관관계 drop

DF를 생성한 뒤 홀수행만 제거하여 동일한 set은 제거하고, 상관관계가 1인 것은 자기자신과의 관계이니 제거한다.

- feature별 unique value count 확인

feature별 unique value count를 통해, 이미 Processed되어있는 데이터를 유추해볼 수 있다.

예를 들어 상대적으로 가장 unique value수가 적은 feature인 경우 카테고리형 변수로 유추해볼수도 있다.

- Target Distribution in Quartiles

이 부분이 새로웠는데, 이해하기가 좀 어렵다.

각 feature별 사분위수 범위별로 target 1 (=정답데이터)가 얼마나 분포되어있는지를 확인하는 방법이다.

최종적으로 아래 표를 만드는 것인데,

Quartile 1 Positives

Quartile 2 Positives

Quartile 3 Positives

Quartile 4 Positives

Quartile 1 Positive Percentage

Quartile 2 Positive Percentage

Quartile 3 Positive Percentage

Quartile 4 Positive Percentage

Quartile Order

feature 1

4518

4472

4725

6383

9.04

8.94

9.45

12.77

4312

feature 1에 대해서는 1사분위 범위 내에 4,518개 값이 있고, 이 범위내에 매핑된 긍정정답지 비율은 9%이다.

각 1/2/3/4분위수에 대해 개수와 비율을 표기하여 비율이 높은 순서대로 Quartile Order을 매기는 것이다.

아래 코드 snippet에 주석을 조금 달아보았다.

df_qdist = pd.DataFrame(np.zeros((200, 9)), columns=['Quartile 1 Positives', 'Quartile 2 Positives', 'Quartile 3 Positives', 'Quartile 4 Positives',
                                                     'Quartile 1 Positive Percentage', 'Quartile 2 Positive Percentage', 'Quartile 3 Positive Percentage', 'Quartile 4 Positive Percentage',
                                                     'Quartile Order'])
## 빈 dataframe을 만들어 숫자를 전부 0으로 채워넣는다.

# feature에 해당하는 컬럼명을 리스트에 담는다
features = [col for col in train_data.columns.values.tolist() if col.startswith('var')]
# 위에서 만든 200개 feature의 index에 컬럼명을 넣어준다.
df_qdist.index = features
# 사분위수(0, 0.25, 0.5, 1)을 담은 array를 만든다
quartiles = np.arange(0, 1, 0.25)

for i, feature in enumerate(features): # 각 행번호, feature 별로
    for j, quartile in enumerate(quartiles): # 각 열번호, quartile 별로
        target_counts = train_data[np.logical_and(train_data[feature] >= train_data[feature].quantile(q=quartile), 
                                                train_data[feature] < train_data[feature].quantile(q=quartile + 0.25))].target.value_counts() # 0, 1에 대한 개수
        
        positive_ratio = target_counts[1] / (target_counts[0] + target_counts[1]) * 100 # 1 비율
        df_qdist.iloc[i, j] = target_counts[1] # 1행 ~ 4행
        df_qdist.iloc[i, j + 4] = positive_ratio # 5행 ~ 8 행

pers = df_qdist.columns.tolist()[4:-1]   # percentage 열만 선정
        
for i, index in enumerate(df_qdist.index): # 행번호, 행별
    order = df_qdist[pers].iloc[[i]].sort_values(by=index, ascending=False, axis=1).columns # 높은 column 부터 낮은 column까지 내림차순 정렬
    order_str = ''.join([col[9] for col in order])  # 순서대로 컬럼의 9번째 값(number)을 리스트로 반환하여 string으로 변환
    df_qdist.iloc[i, 8] = order_str  # 9번째 컬럼에 순서값을 insert 
                
df_qdist = df_qdist.round(2)
df_qdist.head(10)

위를 통해 대부분 변수의 분포에 따른 긍정정답지 비율이 비슷하게 발견되었을 경우, 그 원인을 winsorization으로 유추해볼 수도 있다고 한다.

* winsorization이란? : 극단치 조정 방법 중에 winsorize, truncate가 있는데, winsorize는 정상치 범위 내로 값을 변환하여 조정해주는 것이고, truncate는 아예 자르는 방법이다.

for i, col in enumerate(pers):    # 열 별, percentage 컬럼별
    print('There are {} features that have the highest positive target percentage in Quartile {}'.format(df_qdist[df_qdist['Quartile Order'].str.startswith(str(i + 1))].count()[0],
                                                                                                            i + 1)) # 1,2,3,4 숫자별 가장 높은 긍정정답지 비율을 가진 사분위수 수를 각각 사분위수 별로 count
    print('Quartile {} max positive target percentage = {}% ({})'.format(i + 1, df_qdist[col].max(), df_qdist[col].argmax())) # 가장 높은 값과 position 반환
    print('Quartile {} min positive target percentage = {}% ({})\n'.format(i + 1, df_qdist[col].min(), df_qdist[col].argmin())) #

위 코드를 통해 어떤 사분위수에 긍정 정답지 분포가 가장 많이 되었는지를 확인할 수 있고,

그 결과 해당 데이터셋은 1/4분위수에 긍정 정답지가 많이 분포되어있다고 한다. (2,3분위수는 2/3개만 해당)

2/3 사분위수 값보다 1/4사분위수 값이 긍정정답지일 확률이 2-3% 높다고도 확인할 수 있다.

- Target Distribution in features

변수 별로 target 값이 어떻게 분포되어있는지를 확인하는 것이다.

저자는 아래 값으로 튀는 구간이 확실하여 split 노드를 잘 잡을 수 있겠다고 추정하였고, 이에 따라 트리계열형 모델인 lightGBM으로 선정했다고 한다.

features = [col for col in train_data.columns.tolist() if col.startswith('var')]

# 200개 feature를 50x4 grid로 나누어 플라팅
nrows = 50
fig, axs = plt.subplots(nrows=50, ncols=4, figsize=(24, nrows * 5))

for i, feature in enumerate(features, 1):
    plt.subplot(50, 4, i)
    
    # feature별로 value를 -1, 1 범위 내로 standard scaling
    sns.distplot(StandardScaler().fit_transform(train_data[train_data['target'] == 0][feature].values.reshape(-1, 1)), label='Target=0', hist=True)
    sns.distplot(StandardScaler().fit_transform(train_data[train_data['target'] == 1][feature].values.reshape(-1, 1)), label='Target=1', hist=True)
    
    plt.tick_params(axis='x', which='major', labelsize=8)
    plt.tick_params(axis='y', which='major', labelsize=8)
    
    plt.legend(loc='upper right')
    plt.xlabel('')
    plt.title('Distribution of Target in {}'.format(feature))
    
plt.show()

()

feature별로 target에 대한 밀도를 알 수 있다.

* 추가로 sns.distplot(kde=True) 로 '커널 밀도 추정(kde) 분포도'를 그릴 수 있는데, 여기서 커널 밀도란, 본래 변수의 확률 분포를 추정하는 밀도 추정 방법 중 '커널 함수'를 사용한다는 의미이다. // 참고자료 : https://darkpgmr.tistory.com/147

** fit_transsform()함수는 fit()과 transform()을 순차적으로 수행하는 함수. fit()은 데이터를 통해 기반 셋팅을 먼저 하고, (최소값/최대값 파악 등) transform()을 통해 값을 변환하는 작업을 수행한다 // 참고자료 : https://www.inflearn.com/questions/19038


2. FE

- Data Augmentation

긍정/부정 정답지간의 Imbalance가 발생하는 데이터에 대해 over-sampling을 진행하여 정확도를 높일 수 있다.

- 학습데이터와 테스트 데이터간 분포 확인

feature 200개에 대해서 위 그래프와 유사하게 train / test으로 나누어 분포 차이를 확인해볼 수 있다.

Kaggle은 심사시에 test데이터 중 랜덤 샘플링하여 일부만 점수를 매긴다고 한다. 이에 따라 어떤 분포에도 높은 점수를 산출할 수 있는 방법을 연구한다고 한다. 이 괴리가 학습/테스트 데이터간 분포가 차이가 날 경우가 가장 크다고 한다.

이 둘 간 분포 차이에 대한 트래킹 원인과, 해결 방법에 대해서는 아래 링크로 갈음하겠다.

* 데이터간 분포 차이 확인이 필요한 이유 : https://taeguu.tistory.com/16

* 해당 대회의 코드에서 사용된 분리 방법 : https://www.kaggle.com/yag320/list-of-fake-samples-and-public-private-lb-split/comments

이번 대회에서는 feature별 value unique count에 커트라인을 주어 구분하였는데,

하나의 feature내에서 value값이 모두 unique할 때, 이 변수를 synthetic으로 분류하였다.

하나 이상의 value값이 겹칠 경우 realistic record로 분류하는 것이다.


3. Model - lightGBM

lightGBM 모델의 이론적 배경에 대해서는 아래 링크를 통해 다룰 것이다.

 

// 모델에 사용된 파라미터
gbdt_param = {
    // Core Parameters
    'objective': 'binary',
    'boosting': 'gbdt',
    'learning_rate': 0.01,
    'num_leaves': 15,
    'tree_learner': 'serial',
    'num_threads': 8,
    'seed': SEED,
    
    // Learning Control Parameters
    'max_depth': -1,
    'min_data_in_leaf': 50,
    'min_sum_hessian_in_leaf': 10,  
    'bagging_fraction': 0.6,
    'bagging_freq': 5,
    'feature_fraction': 0.05,
    'lambda_l1': 1.,
    'bagging_seed': SEED,
    
    // Others
    'verbosity ': 1,
    'boost_from_average': False,
    'metric': 'auc',
}

파라미터 값을 초기에 셋팅한 뒤, 아래와 같이 K-fold 방법을 적용하여 모델을 학습한다.

predictors = train_data.columns.tolist()[2:]
X_test = test_data[predictors]

n_splits = 5
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)

a = train_data[['ID_code', 'target']]
a['predict'] = 0
predictions = test_data[['ID_code']]
val_aucs = []
feature_importance_df = pd.DataFrame()
for fold, (train_ind, val_ind) in enumerate(skf.split(train_data, train_data.target.values)):
    
    X_train, y_train = train_data.iloc[train_ind][predictors], train_data.iloc[train_ind]['target']
    X_valid, y_valid = train_data.iloc[val_ind][predictors], train_data.iloc[val_ind]['target']

    N = 1
    p_valid, yp = 0, 0
        
    for i in range(N):
        print('\nFold {} - N {}'.format(fold + 1, i + 1))
        
        X_t, y_t = augment(X_train.values, y_train.values)
        weights = np.array([0.8] * X_t.shape[0])
        weights[:X_train.shape[0]] = 1.0
        print('Shape of X_train after augment: {}\nShape of y_train after augment: {}'.format(X_t.shape, y_t.shape))
        
        X_t = pd.DataFrame(X_t)
        X_t = X_t.add_prefix('var_')
    
        trn_data = lgb.Dataset(X_t, label=y_t, weight=weights)
        val_data = lgb.Dataset(X_valid, label=y_valid)
        evals_result = {}
        
        lgb_clf = lgb.train(gbdt_param, trn_data, 100000, valid_sets=[trn_data, val_data], early_stopping_rounds=5000, verbose_eval=1000, evals_result=evals_result)
        p_valid += lgb_clf.predict(X_valid)
        yp += lgb_clf.predict(X_test)
        
    fold_importance_df = pd.DataFrame()
    fold_importance_df["feature"] = predictors
    fold_importance_df["importance"] = lgb_clf.feature_importance()
    fold_importance_df["fold"] = fold + 1
    feature_importance_df = pd.concat([feature_importance_df, fold_importance_df], axis=0)
    
    a['predict'][val_ind] = p_valid / N
    val_score = roc_auc_score(y_valid, p_valid)
    val_aucs.append(val_score)
    
    predictions['fold{}'.format(fold + 1)] = yp / N
반응형