#Necesario para que los plots de matplotlib aparezcan en el notebook
%matplotlib inline
En este notebook vamos a ver un ejemplo de uso de eXtreme Gradient Boosting (XGB), LightGBM y Grid Search en Python.
XGBoost y LightGBM:
Grid Search:
Ejemplo en R:
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.
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.
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
# 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.
list(bank)
['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í:
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.
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.
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).
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
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
Probablemnte tengáis que instalar el paquete boruta: pip install boruta
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...
<matplotlib.axes._subplots.AxesSubplot at 0x1f098e0a048>
Realizamos una selección de características usando Random Forest como estimador.
Configuramos Random Forest
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.
feat_selector = BorutaPy(rf, n_estimators=200, verbose=2, max_iter=9, random_state=123456)
Lo aplicamos sobre nuestros datos.
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
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.
feat_selector.support_
array([ True, False, False, True, True, False, False, True, True, False, True, True, True, True, True, True, True, True, True, True])
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.
feat_selector.ranking_
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.
X_filtered = feat_selector.transform(X)
X.shape
(41188, 20)
X_filtered.shape
(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).
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
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
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
Varios enlaces que pueden ser de utilidad:
En general, para instalación de paquetes: pip install $\lt$paquete$\gt$