Equipo: WoodenSpoonNinjas
Comenzamos importando los paquetes y mĂ³dulos que vamos a utilizar
from collections import Counter
import numpy as np
import pandas as pd
import seaborn
import matplotlib.pyplot as plt
%matplotlib inline
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.metrics import roc_curve, roc_auc_score, precision_recall_curve, confusion_matrix, classification_report
from sklearn import metrics # MĂ¡s mĂ©tricas
from sklearn.model_selection import GridSearchCV # ValidaciĂ³n cruzada en rejilla de parĂ¡metros
import xgboost as xgb
from xgboost.sklearn import XGBClassifier
import itertools
Cargamos los datos de entrenamiento (previamente convertidos a csv) como un Pandas DataFrame y separamos las variables independientes X
y la variable dependiente y
. Al terminar, contamos el nĂºmero de registros en cada clase.
filename = 'pm_train.csv'
D = pd.read_csv(filename)
columns = D.columns
X = D.drop(["TARGET"],axis=1).values
y = D['TARGET'].values
print(Counter(y))
Como se puede ver, el nĂºmero de muestras en cada clase es muy dispar (sĂ³lo un 1.6% de ellas pertenecen a la clase 1). En este punto, tenemos 2 opciones:
En los criterios de evaluaciĂ³n del reto, se indica que clasificar correctamente ambas clases serĂ¡ tenido en consideraciĂ³n (se valorarĂ¡ especificidad y sensibilidad). Consecuentemente, seguiremos la aproximaciĂ³n 1, para que nuestros resultados no estĂ©n sesgados hacia la clase 0 debido al desbalance. De esta manera, conseguimos tambiĂ©n que los resultados sean robustos a futuros cambios en la probabilidad a priori de cliente pidiendo/no pidiendo prĂ©stamos.
method = "US"
if method == "US":
from imblearn.under_sampling import RandomUnderSampler
resamp = RandomUnderSampler(random_state=42)
X_res, y_res = resamp.fit_sample(X, y)
elif method == "OS":
from imblearn.over_sampling import SMOTE
resamp = SMOTE(random_state=42)
X_res, y_res = resamp.fit_sample(X, y)
sel = np.random.randint(0, y_res.shape[0], 20000)
X_res = X_res[sel,:]
y_res = y_res[sel]
else:
X_res, y_res = X, y
print(Counter(y_res))
X_sel = X_res # Seleccionamos para el entrenamiento todas las caracterĂsticas remuestreadas
Como modelo de clasificaciĂ³n utilizaremos un eXtreme Gradient Boosting model de la biblioteca xgboost
. Para realizar la selecciĂ³n de los hiperparĂ¡metros del modelo utilizaremos un sistema de validaciĂ³n cruzada secuencial ya que, por el tiempo (y potencia de cĂ¡lculo) limitada de que disponemos nos imposible realizar una bĂºsqueda exhaustiva en toda la rejilla de hiperparĂ¡metros.
En primer lugar fijamos todos los parĂ¡metros del modelo excepto el nĂºmero de estimadores y realizamos una validaciĂ³n cruzada de este Ăºltimo parĂ¡metro. Para ello, primero implementamos una funciĂ³n auxiliar modelfit
para realizar el barrido.
def modelfit(alg, predictors, target, useTrainCV=True, cv_folds=5, early_stopping_rounds=50):
if useTrainCV:
xgb_param = alg.get_xgb_params()
xgtrain = xgb.DMatrix(predictors, label=target)
cvresult = xgb.cv(xgb_param, xgtrain, num_boost_round=alg.get_params()['n_estimators'], nfold=cv_folds,
metrics='auc', early_stopping_rounds=early_stopping_rounds, verbose_eval=True)
alg.set_params(n_estimators=cvresult.shape[0])
# Fit the algorithm on the data
alg.fit(predictors, target, eval_metric='auc')
# Predict training set:
dtrain_predictions = alg.predict(predictors)
dtrain_predprob = alg.predict_proba(predictors)[:, 1]
# Print model report:
print("Model Report")
print("Accuracy : %.4g" % metrics.accuracy_score(target, dtrain_predictions))
print("AUC Score (Train): %f" % metrics.roc_auc_score(target, dtrain_predprob))
feat_imp = pd.Series(alg.booster().get_fscore()).sort_values(ascending=False)
plt.figure(figsize=(20,10))
feat_imp.plot(kind='bar', title='Feature Importances')
plt.ylabel('Feature Importance Score')
return alg
Ejecutamos el barrido y observamos que finaliza indicando que el nĂºmero optimo de estimadores es 385. A partir de este punto, fijaremos este hiperparĂ¡metro para optimizar el resto
xgb1 = XGBClassifier(
learning_rate=0.1,
n_estimators= 1000,
max_depth=5,
min_child_weight=1,
gamma=0,
subsample=0.8,
colsample_bytree=0.8,
objective= 'binary:logistic',
nthread=-1,
scale_pos_weight=1,
seed=27)
alg = modelfit(xgb1, X_sel, y_res)
# Guardamos el modelo entrenado
#from sklearn.externals import joblib
#joblib.dump(alg,'imbalanced_alg.pkl')
#joblib.dump(alg,'balanced_undersampled_alg.pkl')
#alg = joblib.load('imbalanced_alg.pkl')
#alg = joblib.load('balanced_undersampled_alg.pkl')
### SelecciĂ³n del nĂºmero de estimadores
param_test1 = {
'max_depth': range(3,10,2),
'min_child_weight': range(1,6,2)
}
gsearch1 = GridSearchCV(estimator=XGBClassifier(learning_rate=0.1, n_estimators=385, max_depth=5,
min_child_weight=1, gamma=0, subsample=0.8, colsample_bytree=0.8,
objective='binary:logistic', nthread=4, scale_pos_weight=1, seed=27),
param_grid = param_test1, scoring='roc_auc', n_jobs=-1, iid=False, cv=5, verbose=10)
gsearch1.fit(X_sel, y_res)
max_depth
y min_child_weight
¶Esta es la selecciĂ³n Ă³ptima obtenida por el proceso de validaciĂ³n cruzada:
gsearch1.best_params_ =
{'max_depth': 5, 'min_child_weight': 1}
param_test2 = {
'gamma':[i/10.0 for i in range(0,5)]
}
gsearch2 = GridSearchCV(estimator=XGBClassifier(learning_rate=0.1, n_estimators=385, max_depth=5,
min_child_weight=1, gamma=0, subsample=0.8, colsample_bytree=0.8,
objective= 'binary:logistic', nthread=4, scale_pos_weight=1,seed=27),
param_grid = param_test3, scoring='roc_auc', n_jobs=-1, iid=False, cv=5, verbose=10)
gsearch2.fit(X_sel, y_res)
gamma
¶Esta es la selecciĂ³n Ă³ptima obtenida por el proceso de validaciĂ³n cruzada:
gsearch2.best_params_ =
{'gamma': 0.0}
El resto de parĂ¡metros son seleccionados de la misma manera. Para limitar la extensiĂ³n de este documento, no se muestran aquĂ los detalles pero sĂ los resultados finales.
Tras las validaciones cruzadas terminamos con el modelo final indicado a continuaciĂ³n:
alg = XGBClassifier(
learning_rate=0.1,
n_estimators= 385,
max_depth=5, # mĂ¡xima profundidad de los Ă¡rboles base (mucha profundidad -> sobrejuste)
min_child_weight=1, # mĂnima suma de los pesos para todas las observaciones en un hijo
gamma=0, # mejora mĂnima para un nuevo split
subsample=0.8, # fracciĂ³n de las muestras para evitar sobreajuste
colsample_bytree=0.8, # fraccion de las columnas
objective= 'binary:logistic', # funciĂ³n de clasificaciĂ³n para clasificaciĂ³n binaria
nthread=-1, # utilizamos todos los cores disponibles
scale_pos_weight=1, # 1: alto desequilibrio entre classes
seed=27) # definimos una semilla para que los resultados sean reproducibles
Dividimos el conjunto de datos en test y train y hacemos una Ăºltima validaciĂ³n cruzada antes de aplicar el modelo a los datos e test. Hacemos esto para asegurarnos de que no existe sobreajuste y nuestras predicciones no estĂ¡n sesgadas por el desbalance entre clases.
X_train, X_test, y_train, y_test = train_test_split(X_sel, y_res, test_size=0.4, random_state=0, stratify=y_res)
alg = alg.fit(X_train, y_train)
y_pred = alg.predict(X_test)
pred_prob = alg.predict_proba(X_test)[:, 1]
fpr, tpr, _ = roc_curve(y_test, pred_prob)
plt.figure(figsize=(5,5))
plt.plot([0, 1], [0, 1], 'k--')
plt.plot(fpr, tpr, label='xgBoost')
plt.xlabel('False positive rate (`1 - specificity` or `1-TNR`)')
plt.ylabel('True positive rate (sensitivity or recall)')
plt.title('ROC curve')
plt.legend(loc='best')
plt.show()
#score = alg.score(X_test, y_test)
auc = roc_auc_score(y_test, pred_prob)
print("ROC-AUC = {}".format(auc))
print(confusion_matrix(y_test, y_pred))
def plot_confusion_matrix(cm, classes,
normalize=False,
title='Confusion matrix',
cmap=plt.cm.Blues):
"""
This function prints and plots the confusion matrix.
Normalization can be applied by setting `normalize=True`.
"""
if normalize:
cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
print("Normalized confusion matrix")
else:
print('Confusion matrix, without normalization')
print(cm)
plt.imshow(cm, interpolation='nearest', cmap=cmap)
plt.title(title)
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=45)
plt.yticks(tick_marks, classes)
thresh = cm.max() / 2.
for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
plt.text(j, i, "%.2f" % cm[i, j],
horizontalalignment="center",
color="white" if cm[i, j] > thresh else "black")
plt.colorbar()
plt.tight_layout()
plt.ylabel('True label')
plt.xlabel('Predicted label')
class_names = ["0", "1"]
# Compute confusion matrix
cnf_matrix = confusion_matrix(y_test, y_pred)
np.set_printoptions(precision=2)
# Plot non-normalized confusion matrix
plt.figure(figsize=(10,5))
plot_confusion_matrix(cnf_matrix, classes=class_names,
title='Confusion matrix, without normalization')
# Plot normalized confusion matrix
plt.figure(figsize=(10,5))
plot_confusion_matrix(cnf_matrix, classes=class_names, normalize=True,
title='Normalized confusion matrix')
plt.show()
filename = 'pm_test.txt' # leemos el fichero con los datos de test
D = pd.read_csv(filename, delimiter="|", decimal=",", index_col = 0)
# Sort columns by name
D = D.reindex_axis(sorted(D.columns), axis=1)
# Predecimos sobre el conjunto de test
X_test_final = D.values
y_pred_final = alg.predict(X_test_final)
Counter(y_pred_final) # contamos el nĂºmero de muestras en cada clase
D_resp = pd.Series(y_pred_final, index = D.index, name="Respuesta").to_frame()
D_resp.to_csv("submission.txt", sep="|")