Capítulo 6 Regresión Logística
El nombre de este modelo es: Regresión Bernoulli con liga logit, pero todo mundo la conoce solo por regresión logística. Es importante saber que la liga puede ser elegida dentro de un conjunto de ligas comunes, por lo que puede dejar de ser logit y seguir siendo regresión Bernoulli, pero ya no podría ser llamada “logística”.
Al igual que en regresión lineal, existe la regresión simple y regresión múltiple. La regresión logística simple se utiliza una variable independiente, mientras que cuando se utiliza más de una variable independiente, el proceso se denomina regresión logística múltiple.
Objetivo: Estimar la probabilidad de pertenecer a la categoría positiva de una variable de respuesta categórica. Posteriormente, se determina el umbral de probabilidad a partir del cual se clasifica a una observación como positiva o negativa.
6.1 Función sigmoide
Si una variable cualitativa con dos categorías se codifica como 1 y 0, matemáticamente es posible ajustar un modelo de regresión lineal por mínimos cuadrados. El problema de esta aproximación es que, al tratarse de una recta, para valores extremos del predictor, se obtienen valores de \(Y\) menores que 0 o mayores que 1, lo que entra en contradicción con el hecho de que las probabilidades siempre están dentro del rango [0,1].
Para evitar estos problemas, la regresión logística transforma el valor devuelto por la regresión lineal empleando una función cuyo resultado está siempre comprendido entre 0 y 1. Existen varias funciones que cumplen esta descripción, una de las más utilizadas es la función logística (también conocida como función sigmoide):
\[\sigma(Z)=\frac{e^{Z}}{1+e^{Z}}\] Función sigmoide:
Para valores de \(Z\) muy grandes, el valor de \(e^{Z}\) tiende a infinito por lo que el valor de la función sigmoide es 1. Para valores de \(Z\) muy negativos, el valor \(e^{Z}\) tiende a cero, por lo que el valor de la función sigmoide es 0.
Sustituyendo la \(Z\) de la función sigmoide por la función lineal \(\beta_0+\beta_1X\) se obtiene que:
\[\pi=P(Y=k|X=x)=\frac{e^{\beta_0+\beta_1X}}{1+e^{\beta_0+\beta_1X}}\]
donde \(P(Y=k|X=x)\) puede interpretarse como: la probabilidad de que la variable cualitativa \(Y\) adquiera el valor \(k\), dado que el predictor \(X\) tiene el valor \(x\).
6.2 Ajuste del modelo
Esta función, puede ajustarse de forma sencilla con métodos de regresión lineal si se emplea su versión logarítmica:
\[logit(\pi)= ln(\frac{\pi}{1-\pi}) = ln(\frac{p(Y=k|X=x)}{1−p(Y=k|X=x)})=\beta_0+\beta_1X\]
\[P(Y=k|X=x)=\frac{e^{\beta_0+\beta_1x_1+\beta_2x_2+...+\beta_ix_i}}{1+e^{\beta_0+\beta_1x_1+\beta_2x_2+...+\beta_ix_i}}\]
La combinación óptima de coeficientes \(\beta_0\) y \(\beta_1\) será aquella que tenga la máxima verosimilitud (maximum likelihood), es decir el valor de los parámetros \(\beta_0\) y \(\beta_1\) con los que se maximiza la probabilidad de obtener los datos observados.
El método de maximum likelihood está ampliamente extendido en la estadística aunque su implementación no siempre es trivial.
Otra forma para ajustar un modelo de regresión logística es empleando descenso de gradiente. Si bien este no es el método de optimización más adecuado para resolver la regresión logística, está muy extendido en el ámbito de machine learning para ajustar otros modelos.
6.3 Clasificación
Una de las principales aplicaciones de un modelo de regresión logística es clasificar la variable cualitativa en función de valor que tome el predictor. Para conseguir esta clasificación, es necesario establecer un threshold de probabilidad a partir de la cual se considera que la variable pertenece a uno de los niveles. Por ejemplo, se puede asignar una observación al grupo 1 si \(p̂ (Y=1|X)>0.3\) y al grupo 0 si ocurre lo contrario.
Es importante mencionar que el punto de corte no necesariamente tiene que ser 0.5, este puede ser seleccionado a conveniencia de la métrica a optimizar.
6.4 Implementación en python
Ajustaremos un modelo de regresión logística usando la receta antes vista. El primer paso es cargar las librerías necesarias y los datos a explotar.
Lectura de datos
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
import pandas as pd
from siuba import *
from plydata.one_table_verbs import pull
pd.set_option('display.max_columns', 4)
telco = pd.read_csv("data/Churn.csv")
telco.info()
## <class 'pandas.core.frame.DataFrame'>
## RangeIndex: 7043 entries, 0 to 7042
## Data columns (total 21 columns):
## # Column Non-Null Count Dtype
## --- ------ -------------- -----
## 0 customerID 7043 non-null object
## 1 gender 7043 non-null object
## 2 SeniorCitizen 7043 non-null int64
## 3 Partner 7043 non-null object
## 4 Dependents 7043 non-null object
## 5 tenure 7043 non-null int64
## 6 PhoneService 7043 non-null object
## 7 MultipleLines 7043 non-null object
## 8 InternetService 7043 non-null object
## 9 OnlineSecurity 7043 non-null object
## 10 OnlineBackup 7043 non-null object
## 11 DeviceProtection 7043 non-null object
## 12 TechSupport 7043 non-null object
## 13 StreamingTV 7043 non-null object
## 14 StreamingMovies 7043 non-null object
## 15 Contract 7043 non-null object
## 16 PaperlessBilling 7043 non-null object
## 17 PaymentMethod 7043 non-null object
## 18 MonthlyCharges 7043 non-null float64
## 19 TotalCharges 7043 non-null object
## 20 Churn 7043 non-null object
## dtypes: float64(1), int64(2), object(18)
## memory usage: 1.1+ MB
Se separa el conjunto de datos completo, comenzando por la variable de respuesta y los explicativos. Posteriormente, se crean los conjuntos de training & testing.
Segmentación de datos
telco_y = telco >> pull("Churn") # telco[["Churn"]]
telco_x = select(telco, -_.Churn, -_.customerID) # telco.drop('Churn', axis=1)
telco_x_train, telco_x_test, telco_y_train, telco_y_test = train_test_split(
telco_x, telco_y,
train_size = 0.80,
random_state = 195,
stratify = telco_y
)
6.4.1 Pipeline de transformación de datos
La selección de variables y la ingeniería de características se lleva a cabo mediante el pipeline de transformación.
# pip install mlxtend==0.23.0
from mlxtend.feature_selection import ColumnSelector
# Seleccionamos las variales numéricas de interés
num_cols = ["MonthlyCharges"]
# Seleccionamos las variables categóricas de interés
cat_cols = ["PaymentMethod", "Dependents"]
# Juntamos todas las variables de interés
columnas_seleccionadas = num_cols + cat_cols
pipe = ColumnSelector(columnas_seleccionadas)
telco_x_train_selected = pipe.fit_transform(telco_x_train)
telco_train_selected = pd.DataFrame(
telco_x_train_selected,
columns = columnas_seleccionadas
)
telco_train_selected.info()
## <class 'pandas.core.frame.DataFrame'>
## RangeIndex: 5634 entries, 0 to 5633
## Data columns (total 3 columns):
## # Column Non-Null Count Dtype
## --- ------ -------------- -----
## 0 MonthlyCharges 5634 non-null object
## 1 PaymentMethod 5634 non-null object
## 2 Dependents 5634 non-null object
## dtypes: object(3)
## memory usage: 132.2+ KB
# ColumnTransformer para aplicar transformaciones
preprocessor = ColumnTransformer(
transformers = [
('scaler', StandardScaler(), num_cols),
('onehotencoding', OneHotEncoder(drop='first', sparse_output=False), cat_cols)
],
verbose_feature_names_out = False,
remainder = 'passthrough' # Mantener las columnas restantes sin cambios
)
transformed_data = preprocessor.fit_transform(telco_train_selected)
new_column_ntelco = preprocessor.get_feature_names_out()
transformed_df = pd.DataFrame(
transformed_data,
columns=new_column_ntelco
)
transformed_df
## MonthlyCharges PaymentMethod_Credit card (automatic) ... \
## 0 0.49 0.00 ...
## 1 -1.47 0.00 ...
## 2 -0.16 0.00 ...
## 3 0.44 1.00 ...
## 4 -0.03 1.00 ...
## ... ... ... ...
## 5629 -1.48 0.00 ...
## 5630 0.74 0.00 ...
## 5631 -1.00 0.00 ...
## 5632 0.56 0.00 ...
## 5633 -1.48 1.00 ...
##
## PaymentMethod_Mailed check Dependents_Yes
## 0 0.00 1.00
## 1 1.00 1.00
## 2 0.00 1.00
## 3 0.00 1.00
## 4 0.00 0.00
## ... ... ...
## 5629 1.00 1.00
## 5630 0.00 1.00
## 5631 1.00 0.00
## 5632 0.00 0.00
## 5633 0.00 0.00
##
## [5634 rows x 5 columns]
## <class 'pandas.core.frame.DataFrame'>
## RangeIndex: 5634 entries, 0 to 5633
## Data columns (total 5 columns):
## # Column Non-Null Count Dtype
## --- ------ -------------- -----
## 0 MonthlyCharges 5634 non-null float64
## 1 PaymentMethod_Credit card (automatic) 5634 non-null float64
## 2 PaymentMethod_Electronic check 5634 non-null float64
## 3 PaymentMethod_Mailed check 5634 non-null float64
## 4 Dependents_Yes 5634 non-null float64
## dtypes: float64(5)
## memory usage: 220.2 KB
6.4.2 Creación y ajuste de modelo
En este curso se comenzará con una regresión logística sin parametrización, tal como en el caso de la regresión lineal, sin embargo, posteriormente será posible agregar parámetros que permitirán crear mejores modelos a cambio de complejidad adicional.
6.4.3 Predicción con nuevos datos
Una vez creado el modelo, se procede a hacer predicciones. Estas predicciones deben hacerse sobre datos que el modelo no haya usado para ser creado, de lo contrario, se incurrirá en overfitting.
y_pred = pipeline.predict(telco_x_test)
telco_test = (
telco_x_test >>
mutate(Churn_Pred = y_pred, Churn = telco_y_test)
)
(
telco_test >>
select(_.Churn, _.Churn_Pred)
)
## Churn Churn_Pred
## 5058 No No
## 2466 No No
## 1740 No No
## 2977 No No
## 1726 No No
## ... ... ...
## 4068 No No
## 4648 No No
## 3996 No No
## 4076 No No
## 2031 No No
##
## [1409 rows x 2 columns]
6.5 Métricas de desempeño
Existen distintas métricas de desempeño para problemas de clasificación, debido a que contamos con la respuesta correcta podemos contar cuántos aciertos tuvimos y cuántos fallos tuvimos.
Primero, por simplicidad ocuparemos un ejemplo de clasificación binaria, Cancelación (1) o No Cancelación (0).
En este tipo de algoritmos definimos cuál de las categorías será nuestra etiqueta positiva y cuál será la negativa. La positiva será la categoría que queremos predecir -en nuestro ejemplo, Cancelación- y la negativa lo opuesto -en el caso binario- en nuestro ejemplo, no cancelación.
Dadas estas definiciones tenemos 4 posibilidades:
True positives: Nuestra predicción dijo que la transacción es fraude y la etiqueta real dice que es fraude.
False positives: Nuestra predicción dijo que la transacción es fraude y la etiqueta real dice que no es fraude.
True negatives: Nuestra predicción dijo que la transacción es no fraude y la etiqueta real dice que no es fraude.
False negatives: Nuestra predicción dijo que la transacción es no fraude y la etiqueta real dice que es fraude.
Matriz de confusión
Esta métrica corresponde a una matriz en donde se plasma el conteo de los aciertos y los errores que haya hecho el modelo, esto es: los verdaderos positivos (TP), los verdaderos negativos (TN), los falsos positivos (FP) y los falsos negativos (FN).
Normalmente los renglones representan las etiquetas predichas, ya sean positivas o negativas, y las columnas a las etiquetas reales, aunque esto puede cambiar en cualquier software.
- Accuracy
Número de aciertos totales entre todas las predicciones.
\[accuracy = \frac{TP + TN}{ TP+FP+TN+FN}\]
La métrica más utilizada, en conjuntos de datos no balanceados esta métrica no nos sirve, al contrario, nos engaña. Adicionalmente, cuando la identificación de una categoría es más importante que otra es mejor recurrir a otras métricas.
- Precision: Eficiencia
De los que identificamos como clase positiva, cuántos identificamos correctamente. ¿Qué tan eficientes somos en la predicción?
\[precision = \frac{TP}{TP + FP}\]
¿Cuándo utilizar precisión?
Esta es la métrica que ocuparás más, pues en un contexto de negocio, donde los recursos son finitos y tiene un costo asociado, ya sea monetario o de tiempo o de recursos, necesitarás que las predicciones de tu etiqueta positiva sean muy eficientes.
Al utilizar esta métrica estaremos optimizando el modelo para minimizar el número de falsos positivos.
- Recall o Sensibilidad: Cobertura
Del universo posible de nuestra clase positiva, cuántos identificamos correctamente.
\[recall = \frac{TP}{TP + FN }\]
Esta métrica la ocuparás cuando en el contexto de negocio de tu problema sea más conveniente aumentar la cantidad de positivos o disminuir los falsos negativos. Esto se realiza debido al impacto que estos pueden tener en las personas en quienes se implementará la predicción.
- Especificidad
Es el número de observaciones correctamente identificados como negativos fuera del total de negativos.
\[Specificity = \frac{TN}{TN+FP}\]
- F1-score
Combina precision y recall para optimizar ambos.
\[F = 2 *\frac{precision * recall}{precision + recall} \]
Se recomienda utilizar esta métrica de desempeño cuando quieres balancear tanto los falsos positivos como los falsos negativos. Aunque es una buena solución para tomar en cuenta ambos errores, pocas veces hay problemas reales que permiten ocuparla, esto es porque en más del 90% de los casos tenemos una restricción en recursos.
Algo importante a tomar en cuenta a la hora de calcular las métricas de desempeño, es que estas deben hacerse sobre el conjunta de prueba.
from sklearn.metrics import confusion_matrix
matriz_confusion = confusion_matrix(telco_y_test, y_pred)
matriz_confusion
## array([[932, 103],
## [276, 98]])
si queremos apreciar de una manera más legible y elegante la salida de una matriz de confusión, puede lograrse mediante el apoyo de la librería seaborn, la cual permite crear múltiples gráficos de manera simple.
import seaborn as sns
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
# Crear un DataFrame a partir de la matriz de confusión
confusion_df = pd.DataFrame(
matriz_confusion,
columns=['Predicción Negativa', 'Predicción Positiva'],
index=['Real Negativa', 'Real Positiva']
)
# Crear una figura utilizando Seaborn
sns.heatmap(confusion_df, annot=True, fmt='d', cmap='Blues', cbar=False);
plt.title('Matriz de Confusión');
plt.xlabel('Predicción');
plt.ylabel('Realidad');
plt.show();
Tómese en cuenta que esta matriz de confusión es creada a partir de un punto de corte arbirtrario en las probabilidades de predicción. Por default, python asigna un corte de porbabilidad en 0.5, sin embargo, es posible realizar otro corte, según la conveniencia para el estudio.
Teniendo esto en mente podemos definir las siguientes métricas:
- AUC y ROC: Area Under the Curve y Receiver operator characteristic
Una curva ROC es un gráfico que muestra el desempeño de un modelo de clasificación en todos los puntos de corte.
AUC significa “Área bajo la curva ROC”. Es decir, AUC mide el área debajo de la curva ROC.
Para poder determinar cual es el mejor punto de corte, es indispensable conocer el comportamiento y efecto de los diferentes puntos de corte.
Veamos un ejemplo visual en nuestra aplicación de Shiny: ConfusionMatrixShiny
6.6 Estimación de probabilidades
Para poder tomar la mejor decisión del punto de corte, deberemos hacer predicciones de probabilidades y posteriormente usar el threshold que mejor sirva a los propósitos del estudio.
import numpy as np
y_pred = pipeline.predict_proba(telco_x_test)[:,0]
Churn_Pred = np.where(y_pred >= 0.7, "No", "Yes")
results = (
telco_x_test >>
mutate(
Churn_Prob = y_pred,
Churn_Pred = Churn_Pred,
Churn = telco_y_test) >>
select(_.Churn_Prob, _.Churn_Pred, _.Churn)
)
results
## Churn_Prob Churn_Pred Churn
## 5058 0.77 No No
## 2466 0.91 No No
## 1740 0.94 No No
## 2977 0.83 No No
## 1726 0.58 Yes No
## ... ... ... ...
## 4068 0.91 No No
## 4648 0.53 Yes No
## 3996 0.82 No No
## 4076 0.77 No No
## 2031 0.90 No No
##
## [1409 rows x 3 columns]
## Churn_Pred n
## 0 No 961
## 1 Yes 448
confusion_df = pd.DataFrame(
confusion_matrix(telco_y_test, Churn_Pred),
columns=['Predicción Negativa', 'Predicción Positiva'],
index=['Real Negativa', 'Real Positiva']
)
# Crear una figura utilizando Seaborn
plt.plot();
sns.heatmap(confusion_df, annot=True, fmt='d', cmap='Blues', cbar=False);
plt.title('Matriz de Confusión');
plt.xlabel('Predicción');
plt.ylabel('Realidad');
plt.show();
from sklearn.metrics import roc_curve, roc_auc_score, precision_recall_curve, average_precision_score
from plotnine import *
fpr, tpr, thresholds = roc_curve(
y_true = np.where(telco_y_test == "Yes", 0, 1),
y_score = y_pred
)
roc_thresholds = pd.DataFrame({
'thresholds': thresholds,
'tpr': tpr,
'fpr': fpr}
)
(
roc_thresholds >>
ggplot(aes(x = fpr, y = tpr)) +
geom_path(size = 1.2) +
geom_abline(colour = "gray") +
xlab("Tasa de falsos positivos") +
ylab("Sensibilidad") +
ggtitle("Curva ROC")
)
## <Figure Size: (640 x 480)>
## 0.7081944767366762
precision, recall, thresholds = precision_recall_curve(
y_true = np.where(telco_y_test == "Yes", 0, 1),
probas_pred = y_pred
)
pr_thresholds = pd.DataFrame({
'thresholds': np.append(0, thresholds),
'precision': precision,
'recall': recall}
)
(
pr_thresholds >>
ggplot(aes(x = recall, y =precision)) +
geom_path(size = 1.2) +
geom_abline(colour = "gray", intercept = 1, slope = -1) +
xlim(0, 1) + ylim(0, 1) +
xlab("Recall") +
ylab("Precision") +
ggtitle("Curva PR")
)
## <Figure Size: (640 x 480)>
## 0.8662177360427568
6.7 Validación cruzada
Un apaso fundamental al momento de crear modelos de ML es conocer la variación en el desempeño de un modelo. Queremos conocer cuánto suele fluctuar el desepeño cuando el modelo presenta perturbaciones en los datos a lo largo del tiempo.
El esquema de validación cruzada permite usar datos de prueba distintos en cada iteración, de tal manera que es posible estimar la variación en el desempeño.
from sklearn.model_selection import KFold, cross_val_score
from sklearn.metrics import make_scorer, roc_auc_score, average_precision_score
from sklearn.model_selection import cross_validate
from sklearn.preprocessing import LabelEncoder
# Definir el objeto K-Fold Cross Validator
kf = KFold(n_splits=10, shuffle=True, random_state=42)
# Definir las métricas de desempeño que deseas calcular como funciones de puntuación
scoring = {
'roc_auc': make_scorer(roc_auc_score, greater_is_better=True),
'average_precision': make_scorer(average_precision_score, greater_is_better=True)
}
label_encoder = LabelEncoder()
y = label_encoder.fit_transform(telco_y_train)
# Realizar la validación cruzada y calcular métricas de desempeño utilizando cross_val_score
results = cross_validate(
pipeline,
telco_train_selected, 1-y,
cv=kf,
scoring=scoring
)
auc_roc_scores = results['test_roc_auc']
auc_roc_scores
## array([0.64418544, 0.59368071, 0.58210621, 0.61630695, 0.5852977 ,
## 0.57726774, 0.61213223, 0.60262944, 0.57838112, 0.5928815 ])
## array([0.81737309, 0.76634381, 0.73688596, 0.78742837, 0.74429755,
## 0.78635671, 0.80363038, 0.77942919, 0.73790323, 0.79105159])
# Calcular estadísticas resumidas (media y desviación estándar) de las métricas
mean_roc = np.mean(auc_roc_scores)
std_roc = np.std(auc_pr_scores)
mean_pr = np.mean(auc_pr_scores)
std_pr = np.std(auc_pr_scores)
## ROC AUC: 0.5984869040092843 +/- 0.026499546449472944
## PR AUC: 0.7750699890025018 +/- 0.026499546449472944