CV and under sampling on a test fold

Viewed 334

I am a bit lost on building a ML classifier with imbalanced data (80:20). The dataset has 30 columns; the target is Label. I want to predict the major class. I am trying to reproduce the following steps:

  • Split the data on train/test
  • Perform CV on trains set
  • Apply undersampling only on a test fold
  • After the model has been chosen with the help of CV, undersample the train set and train the classifier
  • Estimate the performance on the untouched test set (recall)

What I have done is shown below:

    y = df['Label']
    X = df.drop('Label',axis=1)
    X.shape, y.shape

    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 12)
    X_train.shape, X_test.shape

    tree = DecisionTreeClassifier(max_depth = 5)

    tree.fit(X_train, y_train)

    y_test_tree = tree.predict(X_test)
    y_train_tree = tree.predict(X_train)

    acc_train_tree = accuracy_score(y_train,y_train_tree)
    acc_test_tree = accuracy_score(y_test,y_test_tree)

I have some doubts on how to perform CV on trains set, apply under sampling on a test fold and undersample the train set and train the classifier. Are you familiar with these steps? If you are, I would appreciate your help.

If I do as follows:

y = df['Label']
X = df.drop('Label',axis=1)
X.shape, y.shape

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 12)
X_train.shape, X_test.shape

tree = DecisionTreeClassifier(max_depth = 5)

tree.fit(X_train, y_train)

y_test_tree = tree.predict(X_test)
y_train_tree = tree.predict(X_train)

acc_train_tree = accuracy_score(y_train,y_train_tree)
acc_test_tree = accuracy_score(y_test,y_test_tree)
# CV
scores = cross_val_score(tree,X_train, y_train,cv = 3, scoring = "accuracy")
ypred = cross_val_predict(tree,X_train,y_train,cv = 3)

print(classification_report(y_train,ypred))
accuracy_score(y_train,ypred)
confusion_matrix(y_train,ypred)

I get this output

             precision    recall  f1-score   support

      -1       0.73      0.99      0.84       291
       1       0.00      0.00      0.00       105

accuracy                           0.73       396
macro avg       0.37      0.50      0.42       396
weighted avg       0.54      0.73      0.62       396

I guess I have missed something in the code above or doing something wrong.

Sample of data:

Have_0 Have_1 Have_2 Have_letters Label
1        0      1         1         1
0        0      0         1        -1 
1        1      1         1        -1
0        1      0         0         1
1        1      0         0         1
1        0      0         1        -1
1        0      0         0         1
2 Answers

Generally, the best way to create a cross-validation set is to simulate your test data. In your case, if we are going to divide your data into 3 sets (train, crossv., test), the best way to do it creating sets with the same proportion of true label/false label. That's what I did in the following function.

import numpy as np
import math
X=DF[["Have_0","Have_1","Have_2","Have_letters"]]
y=DF["Label"]
 


 
def create_cv(X,y):
    if type(X)!=np.ndarray:
        X=X.values
        y=y.values
 
    test_size=1/5
    proportion_of_true=y[y==1].shape[0]/y.shape[0]
    num_test_samples=math.ceil(y.shape[0]*test_size)
    num_test_true_labels=math.floor(num_test_samples*proportion_of_true)
    num_test_false_labels=math.floor(num_test_samples-num_test_true_labels)
    
    y_test=np.concatenate([y[y==0][:num_test_false_labels],y[y==1][:num_test_true_labels]])
    y_train=np.concatenate([y[y==0][num_test_false_labels:],y[y==1][num_test_true_labels:]])


    
    X_test=np.concatenate([X[y==0][:num_test_false_labels] ,X[y==1][:num_test_true_labels]],axis=0)
    X_train=np.concatenate([X[y==0][num_test_false_labels:],X[y==1][num_test_true_labels:]],axis=0)
    return X_train,X_test,y_train,y_test

    
    
X_train,X_test,y_train,y_test=create_cv(X,y)
X_train,X_crossv,y_train,y_crossv=create_cv(X_train,y_train)
    

By doing so we have sets with the following shapes (which all have the same proportion of true label/false label):

enter image description here

I am assuming that your test data is not representative as it is too small for this purpose (there isn't much left after splitting it a few times, as Cross-Validation is further splitting the dataset).

For under and over sampling there is a great library called imbalanced-learn. It also comes with good documentation, such as on under sampling.

Given your sample data:

from io import StringIO
import pandas as pd
from sklearn.model_selection import train_test_split
from imblearn.under_sampling import RandomUnderSampler

df = pd.read_csv(StringIO(
    """
    Have_0 Have_1 Have_2 Have_letters Label
    1        0      1         1         1
    0        0      0         1        -1 
    1        1      1         1        -1
    0        1      0         0         1
    1        1      0         0         1
    1        0      0         1        -1
    1        0      0         0         1
    """
), sep='\s+')

y = df['Label']
X = df.drop('Label',axis=1)

You could then under sample the training dataset:

under_sampler = RandomUnderSampler(random_state=0)
X_train_resampled, y_train_resampled = under_sampler.fit_resample(X_train, y_train)

You could pass that to the Cross-Validation. The downside is that it will be evaluated (as part of CV) based on the balanced dataset. That may be okay for model selection purpose though. Instead you could apply the sampling to the train split of each CV fold (as you would have to do for oversampling).

If your dataset is small, then you could benefit from using over sampling instead. In that case you need to be mindful that over sampled data is not split afterwards. That is because it would create data leakage (leading to wrong scores; see imbalance-learn's common pitfalls). That is easy to avoid with train_test_split, as you can just call the sampler after calling train_test_split. But using cross validation would lead to more splits (hidden in cross_val_score). Here the over sampling would need to happen after each CV split. You could do that using sklearn's KFold or StratifiedKFold class for example.

Something like this:

def get_train_sampled_cv_splits(train_test_indices_splits, sampler, y):
    for train_indices, test_indices in train_test_indices_splits:
        y_train_split = y.iloc[train_indices]
        train_indices_resampled, _ = sampler.fit_resample(train_indices.reshape(-1, 1), y_train_split)
        yield train_indices_resampled.reshape(-1), test_indices

over_sampler = RandomOverSampler(random_state=0)
kf = KFold(n_splits=2, shuffle=True, random_state=42)
resampled_train_test_indices_splits = get_train_sampled_cv_splits(
    kf.split(X_train, y_train),
    over_sampler,
    y
)
cross_val_score(tree, X_train, y_train, cv=resampled_train_test_indices_splits, scoring="f1")

You will also already be mindful of the metric for imbalanced datasets (accuracy is not usually a good one). Someone shared a chart Evaluation Metrics for Imbalanced Classification on Kaggle that might be useful.

Related