In [1]:
#Necesario para que los plots de matplotlib aparezcan en el notebook
%matplotlib inline

Indicamos si queremos que cada variable categórica se convierta en varias binarias (tantas como categorías), indicamos binarizar = True, o si preferimos que cada variable categórica se convierta simplemente a una numérica (ordinal), binarizar = False.

In [2]:
binarizar = False

Leemos el conjunto de datos. Los valores perdidos notados como '?' se convierten a NaN, sino se consideraría '?' como una categoría más.

In [3]:
import pandas as pd

carpeta_datos="../data/"

if not binarizar:
    bank_orig = pd.read_csv(carpeta_datos+'bank-additional-full.csv', delimiter=';')
else:
    bank_orig = pd.read_csv(carpeta_datos+'bank-additional-full.csv',na_values="?", delimiter=';')
    
# print("------ Lista de características y tipos (object=categórica)")
# print(bank_orig.dtypes,"\n")

Si el dataset contiene variables categóricas con cadenas, es necesario convertirlas a numéricas antes de usar 'fit'. Si no las vamos a hacer ordinales (binarizar = True), las convertimos a variables binarias con get_dummies. Para saber más sobre las opciones para tratar variables categóricas: http://pbpython.com/categorical-encoding.html

In [4]:
# devuelve una lista de las características categóricas excluyendo la columna 'class' que contiene la clase
lista_categoricas = [x for x in bank_orig.columns if (bank_orig[x].dtype == object and bank_orig[x].name != 'y')]
binarizar = False
if not binarizar:
    bank = bank_orig
else:
    # reemplaza las categóricas por binarias
    bank = pd.get_dummies(bank_orig, columns=lista_categoricas)

Lista de atributos del dataset.

In [5]:
list(bank)
Out[5]:
['age',
 'job',
 'marital',
 'education',
 'default',
 'housing',
 'loan',
 'contact',
 'month',
 'day_of_week',
 'duration',
 'campaign',
 'pdays',
 'previous',
 'poutcome',
 'emp.var.rate',
 'cons.price.idx',
 'cons.conf.idx',
 'euribor3m',
 'nr.employed',
 'y']

Separamos el DataFrame en dos arrays numpy, uno con las características (X) y otro con la clase (y). Si la última columna es la que contiene la clase se puede separar así:

In [6]:
from sklearn import preprocessing
    
le = preprocessing.LabelEncoder() 
# LabelEncoder codifica los valores originales entre 0 y el número de valores - 1
# Se puede usar para normalizar variables o para transformar variables no-numéricas en numéricas

X = bank.values[:,0:len(bank.columns)-1]
y = bank.values[:,len(bank.columns)-1]
y_bin = le.fit_transform(y)

print("X", X)
print("y", y)
X [[56 'housemaid' 'married' ... -36.4 4.857 5191.0]
 [57 'services' 'married' ... -36.4 4.857 5191.0]
 [37 'services' 'married' ... -36.4 4.857 5191.0]
 ...
 [56 'retired' 'married' ... -50.8 1.028 4963.6]
 [44 'technician' 'married' ... -50.8 1.028 4963.6]
 [74 'retired' 'married' ... -50.8 1.028 4963.6]]
y ['no' 'no' 'no' ... 'no' 'yes' 'no']

También se puede separar indicando los nombres de las columnas.

In [7]:
columns = ['age', 'job', 'marital', 'education', 'default', 'housing', 'loan',
       'contact', 'month', 'day_of_week', 'duration', 'campaign', 'pdays',
       'previous', 'poutcome', 'emp.var.rate', 'cons.price.idx',
       'cons.conf.idx', 'euribor3m', 'nr.employed']
X = bank[list(columns)].values
y = bank["y"].values
y_bin = le.fit_transform(y)

print("X", X)
print("y", y)
X [[56 'housemaid' 'married' ... -36.4 4.857 5191.0]
 [57 'services' 'married' ... -36.4 4.857 5191.0]
 [37 'services' 'married' ... -36.4 4.857 5191.0]
 ...
 [56 'retired' 'married' ... -50.8 1.028 4963.6]
 [44 'technician' 'married' ... -50.8 1.028 4963.6]
 [74 'retired' 'married' ... -50.8 1.028 4963.6]]
y ['no' 'no' 'no' ... 'no' 'yes' 'no']

Si las variables categóricas tienen muchas categorías, se generarán muchas variables y algunos algoritmos serán extremadamente lentos. Se puede optar por, como hemos comentado antes, convertirlas a variables numéricas (ordinales) sin binarizar.

Esto se haría si no se ha ejecutado pd.get_dummies() previamente. Además, no funciona is hay valores perdidos notados como NaN.

In [8]:
if not binarizar:    
    for i in range(0,X.shape[1]):
        if isinstance(X[0,i],str):
            X[:,i] = le.fit_transform(X[:,i])

Validación cruzada con particionado estratificado y control de la aleatoriedad (fijando la semilla).

In [9]:
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import accuracy_score
from sklearn.metrics import f1_score
from sklearn.metrics import roc_auc_score
from imblearn.metrics import geometric_mean_score
from sklearn import preprocessing
import numpy

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=123456)
le = preprocessing.LabelEncoder()

def validacion_cruzada(modelo, X, y, cv):
    y_test_all = []
    y_prob_all = []

    for train, test in cv.split(X, y):
        modelo = modelo.fit(X[train],y[train])
        y_pred = modelo.predict(X[test])
        y_prob = modelo.predict_proba(X[test])[:,1] #la segunda columna es la clase positiva '1' en bank-marketing
        y_test_bin = y[test]
        #y_test_bin = le.fit_transform(y[test]) #se convierte a binario para AUC: 'yes' -> 1 (clase positiva) y 'no' -> 0 en bank-marketing
        
        print("Accuracy: {:6.2f}%, F1-score: {:.4f}, G-mean: {:.4f}, AUC: {:.4f}".format(accuracy_score(y[test],y_pred)*100 , f1_score(y[test],y_pred,average='macro'), geometric_mean_score(y[test],y_pred,average='macro'), roc_auc_score(y_test_bin,y_prob)))
        y_test_all = numpy.concatenate([y_test_all,y_test_bin])
        y_prob_all = numpy.concatenate([y_prob_all,y_prob])

    print("")

    return modelo, y_test_all, y_prob_all

Extreme Gradient Boosting (XGB)

In [10]:
import xgboost as xgb

print("------ XGB...")
clf = xgb.XGBClassifier(n_estimators = 200)
clf, y_test_clf, y_prob_clf = validacion_cruzada(clf,X,y,skf)
------ XGB...
Accuracy:  91.85%, F1-score: 0.7737, G-mean: 0.7471, AUC: 0.9495
Accuracy:  91.53%, F1-score: 0.7677, G-mean: 0.7448, AUC: 0.9500
Accuracy:  91.50%, F1-score: 0.7648, G-mean: 0.7400, AUC: 0.9494
Accuracy:  91.60%, F1-score: 0.7654, G-mean: 0.7386, AUC: 0.9478
Accuracy:  91.73%, F1-score: 0.7732, G-mean: 0.7497, AUC: 0.9497

Light Gradient Boosting

Probablemnte tengáis que instalar el paquete boruta: pip install boruta

In [11]:
import lightgbm as lgb

print("------ LightGBM...")
lgbm = lgb.LGBMClassifier(objective='binary',n_estimators=200,num_threads=2)
lgbm, y_test_lgbm, y_prob_lgbm = validacion_cruzada(lgbm,X,y,skf)

print("------ Importancia de las características...")
importances = list(zip(lgbm.feature_importances_, bank.columns))
importances.sort()
pd.DataFrame(importances, index=[x for (_,x) in importances]).plot(kind='barh',legend=False)
------ LightGBM...
Accuracy:  92.02%, F1-score: 0.7860, G-mean: 0.7669, AUC: 0.9492
Accuracy:  91.43%, F1-score: 0.7729, G-mean: 0.7579, AUC: 0.9504
Accuracy:  91.51%, F1-score: 0.7716, G-mean: 0.7527, AUC: 0.9499
Accuracy:  91.61%, F1-score: 0.7728, G-mean: 0.7523, AUC: 0.9485
Accuracy:  91.68%, F1-score: 0.7757, G-mean: 0.7560, AUC: 0.9490

------ Importancia de las características...
Out[11]:
<matplotlib.axes._subplots.AxesSubplot at 0x1f098e0a048>

Selección de características (Feature Selection)

Realizamos una selección de características usando Random Forest como estimador.

Configuramos Random Forest

In [12]:
from boruta import BorutaPy
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_jobs=-1, class_weight='balanced', max_depth=5)

Configuramos BorutaPy para la selección de características en función de la configuración hecha para Random Forest. En este caso, a partir de la novena iteración ya no se obtienen mejoras y debido a la implementación de BorutaPy aparece un warning originado por numpy. Esta es la razón de que en este caso concreto indiquemos max_iter = 9.

In [13]:
feat_selector = BorutaPy(rf, n_estimators=200, verbose=2, max_iter=9, random_state=123456)

Lo aplicamos sobre nuestros datos.

In [14]:
feat_selector.fit(X, y)
Iteration: 	1 / 9
Confirmed: 	0
Tentative: 	20
Rejected: 	0
Iteration: 	2 / 9
Confirmed: 	0
Tentative: 	20
Rejected: 	0
Iteration: 	3 / 9
Confirmed: 	0
Tentative: 	20
Rejected: 	0
Iteration: 	4 / 9
Confirmed: 	0
Tentative: 	20
Rejected: 	0
Iteration: 	5 / 9
Confirmed: 	0
Tentative: 	20
Rejected: 	0
Iteration: 	6 / 9
Confirmed: 	0
Tentative: 	20
Rejected: 	0
Iteration: 	7 / 9
Confirmed: 	0
Tentative: 	20
Rejected: 	0
Iteration: 	8 / 9
Confirmed: 	15
Tentative: 	3
Rejected: 	2


BorutaPy finished running.

Iteration: 	9 / 9
Confirmed: 	15
Tentative: 	1
Rejected: 	2
Out[14]:
BorutaPy(alpha=0.05,
     estimator=RandomForestClassifier(bootstrap=True, class_weight='balanced',
            criterion='gini', max_depth=5, max_features='auto',
            max_leaf_nodes=None, min_impurity_decrease=0.0,
            min_impurity_split=None, min_samples_leaf=1,
            min_samples_split=2, min_weight_fraction_leaf=0.0,
            n_estimators=200, n_jobs=-1, oob_score=False,
            random_state=<mtrand.RandomState object at 0x000001F098C8BBD0>,
            verbose=0, warm_start=False),
     max_iter=9, n_estimators=200, perc=100,
     random_state=<mtrand.RandomState object at 0x000001F098C8BBD0>,
     two_step=True, verbose=2)

Comprobar las características (variables) seleccionadas.

In [15]:
feat_selector.support_
Out[15]:
array([ True, False, False,  True,  True, False, False,  True,  True,
       False,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True])
In [16]:
all_features = numpy.array(list(bank)[0:-1])
selected_features = all_features[feat_selector.support_]
print(all_features)
print(selected_features)
['age' 'job' 'marital' 'education' 'default' 'housing' 'loan' 'contact'
 'month' 'day_of_week' 'duration' 'campaign' 'pdays' 'previous' 'poutcome'
 'emp.var.rate' 'cons.price.idx' 'cons.conf.idx' 'euribor3m' 'nr.employed']
['age' 'education' 'default' 'contact' 'month' 'duration' 'campaign'
 'pdays' 'previous' 'poutcome' 'emp.var.rate' 'cons.price.idx'
 'cons.conf.idx' 'euribor3m' 'nr.employed']

Comprobar el ranking de características.

In [17]:
feat_selector.ranking_
Out[17]:
array([1, 2, 4, 1, 1, 5, 6, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])

Aplicamos transform() a X para filtrar las características y dejar solo las seleccionadas.

In [18]:
X_filtered = feat_selector.transform(X)
In [19]:
X.shape
Out[19]:
(41188, 20)
In [20]:
X_filtered.shape
Out[20]:
(41188, 15)

Ejecutamos LGBM sobre el conjunto de datos resultante de la selección de características con validación cruzada estratificada (las mismas particiones que para los demás algoritmos).

In [21]:
lgbm, y_test_lgbm, y_prob_lgbm = validacion_cruzada(lgbm,X_filtered,y,skf)
Accuracy:  92.11%, F1-score: 0.7846, G-mean: 0.7613, AUC: 0.9491
Accuracy:  91.55%, F1-score: 0.7759, G-mean: 0.7605, AUC: 0.9494
Accuracy:  91.67%, F1-score: 0.7776, G-mean: 0.7602, AUC: 0.9505
Accuracy:  91.56%, F1-score: 0.7741, G-mean: 0.7563, AUC: 0.9494
Accuracy:  91.78%, F1-score: 0.7788, G-mean: 0.7594, AUC: 0.9485

In [22]:
from sklearn.metrics import make_scorer
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import RandomizedSearchCV

params_xgb = {'min_child_weight':[4,5], 'gamma':[i/10.0 for i in range(3,6)],  'subsample':[i/10.0 for i in range(6,11)],
          'colsample_bytree':[i/10.0 for i in range(6,11)], 'max_depth': [2,3,4], 'n_estimators':[50,100,200]}

print("------ Grid Search...")
params_lgbm = {'feature_fraction':[i/10.0 for i in range(3,6)], 'learning_rate':[0.05,0.1],
               'num_leaves':[30,50], 'n_estimators':[200]}
grid = GridSearchCV(lgbm, params_lgbm, cv=3, n_jobs=1, verbose=1, scoring=make_scorer(f1_score))
grid.fit(X,y_bin)

print("Mejores parámetros:")
print(grid.best_params_)

print("")
gs, y_test_gs, y_prob_gs = validacion_cruzada(grid.best_estimator_,X,y,skf)
------ Grid Search...
Fitting 3 folds for each of 12 candidates, totalling 36 fits
[Parallel(n_jobs=1)]: Using backend SequentialBackend with 1 concurrent workers.
[Parallel(n_jobs=1)]: Done  36 out of  36 | elapsed:   53.0s finished
Mejores parámetros:
{'feature_fraction': 0.4, 'learning_rate': 0.1, 'n_estimators': 200, 'num_leaves': 50}

Accuracy:  92.11%, F1-score: 0.7868, G-mean: 0.7660, AUC: 0.9495
Accuracy:  91.61%, F1-score: 0.7758, G-mean: 0.7585, AUC: 0.9498
Accuracy:  91.66%, F1-score: 0.7732, G-mean: 0.7517, AUC: 0.9504
Accuracy:  91.53%, F1-score: 0.7706, G-mean: 0.7505, AUC: 0.9479
Accuracy:  91.87%, F1-score: 0.7819, G-mean: 0.7632, AUC: 0.9504

In [23]:
print("------ Random Search...")
params_rnd_lgbm = {'feature_fraction':[i/10.0 for i in range(2,7)], 'learning_rate':[i/100.0 for i in range(5,20)],
               'num_leaves':[i*10 for i in range(2,6)], 'n_estimators':[200]}
rndsrch = RandomizedSearchCV(lgbm, params_rnd_lgbm, cv=3, n_iter=5, n_jobs=1, verbose=1, scoring=make_scorer(f1_score))
rndsrch.fit(X,y_bin)

print("Mejores parámetros:")
print(rndsrch.best_params_)

print("")
gs, y_test_gs, y_prob_gs = validacion_cruzada(rndsrch.best_estimator_,X,y,skf)
------ Random Search...
Fitting 3 folds for each of 5 candidates, totalling 15 fits
[Parallel(n_jobs=1)]: Using backend SequentialBackend with 1 concurrent workers.
[Parallel(n_jobs=1)]: Done  15 out of  15 | elapsed:   21.9s finished
Mejores parámetros:
{'num_leaves': 50, 'n_estimators': 200, 'learning_rate': 0.08, 'feature_fraction': 0.5}

Accuracy:  91.95%, F1-score: 0.7831, G-mean: 0.7632, AUC: 0.9506
Accuracy:  91.37%, F1-score: 0.7669, G-mean: 0.7477, AUC: 0.9499
Accuracy:  91.64%, F1-score: 0.7737, G-mean: 0.7534, AUC: 0.9510
Accuracy:  91.57%, F1-score: 0.7726, G-mean: 0.7531, AUC: 0.9486
Accuracy:  91.79%, F1-score: 0.7783, G-mean: 0.7581, AUC: 0.9493

Referencias complementarias

Varios enlaces que pueden ser de utilidad:

En general, para instalación de paquetes: pip install $\lt$paquete$\gt$