La regresión lineal cerró la lección anterior chocando contra un muro: no sabe responder preguntas de sí o no. Y la pregunta estrella de MercaFresh es exactamente de ese tipo — ¿abandonará este cliente en los próximos 90 días? En esta lección transformamos la recta en una curva en forma de S que produce probabilidades, entendemos por qué esa transformación es necesaria y qué significan sus coeficientes en términos de odds, y entrenamos el primer clasificador del curso sobre el dataset de churn que construimos en el módulo 3, con su preprocesador incluido. A pesar de su nombre, la regresión logística es un algoritmo de clasificación — probablemente el más usado del mundo en producción.

Contenido

  1. Por qué la regresión lineal no sirve para clasificar
  2. La función sigmoide: de números a probabilidades
  3. Probabilidad y umbral de decisión
  4. Log-odds y coeficientes: interpretación con odds ratio
  5. La función de coste: log-loss
  6. Implementación: churn de MercaFresh con el preprocesador del módulo 3
  7. Clasificación multiclase: one-vs-rest y softmax

Por qué la regresión lineal no sirve para clasificar

Primera tentación: codificar churn como 0/1 y ajustar una regresión lineal. Parece razonable — la predicción sería "algo así como la probabilidad de churn". Falla por tres motivos:

  • Predicciones fuera de rango: un hiperplano no está acotado. Para un cliente muy inactivo predecirá 1.4, y para uno muy fiel, −0.3. ¿Probabilidad del 140%? Ninguna interpretación lo salva.
  • Sensibilidad absurda a casos extremos: recuerda el ejercicio 3 de 04-01 — los puntos lejanos tiran de la recta. Un cliente clarísimo de churn (recencia 300 días) desplaza la recta y empeora la clasificación de los casos dudosos, que son justo los que importan.
  • El error cuadrático castiga mal: penalizar con MSE que el modelo prediga 1.4 en un cliente que efectivamente abandonó (etiqueta 1) es castigar por exceso de razón.

La solución no es abandonar la combinación lineal $z = w_0 + w_1 x_1 + \dots + w_n x_n$ — que sigue siendo el corazón del modelo — sino pasarla por una función que la comprima al intervalo (0, 1).

La función sigmoide: de números a probabilidades

Esa función es la sigmoide (o función logística, de ahí el nombre del algoritmo):

$$\sigma(z) = \frac{1}{1 + e^{-z}}$$

Sus propiedades son exactamente las que necesitamos:

Entrada $z$ Salida $\sigma(z)$ Lectura
$z \to -\infty$ → 0 Certeza de "no churn"
$z = -2$ 0.12 Probablemente se queda
$z = 0$ 0.50 Máxima incertidumbre
$z = +2$ 0.88 Probablemente abandona
$z \to +\infty$ → 1 Certeza de "churn"
import numpy as np
import matplotlib.pyplot as plt

z = np.linspace(-6, 6, 200)
sigma = 1 / (1 + np.exp(-z))

plt.plot(z, sigma)
plt.axhline(0.5, color="gray", linestyle="--")
plt.axvline(0, color="gray", linestyle="--")
plt.xlabel("z = w0 + w1*x1 + ... (puntuacion lineal)")
plt.ylabel("P(churn = 1)")
plt.title("La funcion sigmoide")
plt.show()

La curva tiene forma de S: plana en los extremos (donde el modelo ya está seguro, más evidencia apenas cambia nada) y empinada en el centro (donde cada décima de $z$ mueve mucho la probabilidad). El modelo completo es:

$$P(\text{churn} = 1 \mid \mathbf{x}) = \sigma(w_0 + w_1 x_1 + \dots + w_n x_n)$$

Fíjate en la notación: es una probabilidad condicionada, la misma criatura que manejamos en el teorema de Bayes (02-05). La regresión logística estima directamente $P(\text{clase} \mid \text{datos})$ — lo que allí llamábamos posterior — sin pasar por priors y verosimilitudes: aprende la probabilidad condicionada de golpe, ajustando pesos. En 04-06 veremos el enfoque contrario (Naive Bayes), que sí construye el posterior pieza a pieza como en 02-05.

Probabilidad y umbral de decisión

El modelo devuelve probabilidades; el negocio necesita decisiones. El puente es el umbral: por defecto, si $P(\text{churn}) \geq 0.5$ se predice churn, si no, permanencia. Geométricamente, $\sigma(z) = 0.5$ ocurre cuando $z = 0$: la ecuación $w_0 + w_1 x_1 + \dots = 0$ define una frontera de decisión lineal — un hiperplano, como en 04-01, pero ahora separando clases en lugar de ajustar valores.

Lo importante: el umbral 0.5 es una convención, no una ley. Para MercaFresh, dejar escapar un cliente valioso (falso negativo) cuesta mucho más que enviar un cupón innecesario (falso positivo) — el mismo dilema de costes asimétricos que el detector de fraude de 02-05. Bajando el umbral a 0.3, la campaña de retención captura más clientes en riesgo a cambio de más cupones desperdiciados:

probs = modelo.predict_proba(X_test)[:, 1]   # probabilidad de churn
decision_estandar = probs >= 0.5
decision_prudente = probs >= 0.3             # retener ante la duda

Cómo elegir el umbral óptimo y cómo medir ese compromiso con rigor es materia de la curva ROC (06-04) y las métricas de clasificación (06-02); aquí basta con saber que la probabilidad es la salida rica y la decisión es una capa encima, ajustable al coste de negocio.

Log-odds y coeficientes: interpretación con odds ratio

En regresión lineal, "subir $x_1$ en una unidad suma $w_1$ euros". ¿Y aquí? La sigmoide complica la lectura directa en probabilidades, pero hay una reformulación elegante. Despejando $z$:

$$\ln\left(\frac{P}{1-P}\right) = w_0 + w_1 x_1 + \dots + w_n x_n$$

El cociente $\frac{P}{1-P}$ son los odds (momios): con P = 0.75, los odds son 3 — "3 a 1 a favor del churn". El modelo es lineal en el logaritmo de los odds (log-odds). De ahí sale la interpretación práctica:

Subir $x_i$ en una unidad multiplica los odds por $e^{w_i}$ (el odds ratio), manteniendo el resto constante.

Coeficiente $w_i$ Odds ratio $e^{w_i}$ Lectura MercaFresh (ejemplo)
+0.9 (recencia escalada) 2.46 Cada unidad más de recencia multiplica los odds de churn por ~2.5
0.0 1.00 La feature no aporta
−0.7 (frecuencia escalada) 0.50 Cada unidad más de frecuencia reduce a la mitad los odds de churn

Dos cautelas heredadas de 04-01: las features escaladas (nuestro preprocesador usa RobustScaler) hacen que "una unidad" sea una cantidad robusta tipo-IQR, no una unidad natural; y con features correlacionadas los coeficientes individuales se reparten el efecto de forma inestable — la lectura es orientativa, no un veredicto causal (02-03).

La función de coste: log-loss

No podemos entrenar minimizando el MSE: combinado con la sigmoide produce un paisaje de error con valles falsos donde el descenso de gradiente se atasca. La función de coste natural para probabilidades es la log-loss (o entropía cruzada binaria). Su lógica, caso por caso:

  • Si la etiqueta real es 1 (churn), el coste es $-\ln(p)$: predecir $p = 0.9$ cuesta poco (0.105); predecir $p = 0.01$ cuesta muchísimo (4.6).
  • Si la etiqueta real es 0, el coste es $-\ln(1-p)$: lo simétrico.

$$\text{LogLoss} = -\frac{1}{n}\sum_{i=1}^{n} \left[ y_i \ln(p_i) + (1 - y_i)\ln(1 - p_i) \right]$$

La propiedad clave: el coste de una equivocación segura tiende a infinito. Decir "97% seguro de que se queda" de un cliente que abandona sale carísimo; decir "60%" del mismo cliente, mucho menos. La log-loss no premia acertar sin más: premia probabilidades honestas. Con ella, el paisaje de error vuelve a ser un cuenco con un único mínimo, y el descenso de gradiente de 04-01 lo encuentra sin fórmula cerrada (aquí no existe la ecuación normal: el entrenamiento es siempre iterativo).

Implementación: churn de MercaFresh con el preprocesador del módulo 3

Momento de cobrar la inversión del módulo 3: el ColumnTransformer de 03-06 se encadena con el clasificador en un único Pipeline. Un solo objeto hace todo el viaje datos crudos → predicción:

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import (OneHotEncoder, OrdinalEncoder,
                                   PowerTransformer, RobustScaler)
from sklearn.linear_model import LogisticRegression

# --- El preprocesador construido en 03-06 (resumido) ---
rama_numerica = Pipeline([
    ("imputar", SimpleImputer(strategy="median", add_indicator=True)),
    ("desasimetrizar", PowerTransformer(method="yeo-johnson")),
    ("escalar", RobustScaler()),
])
preprocesador = ColumnTransformer([
    ("num", rama_numerica,
     ["edad", "satisfaccion", "recencia_dias", "pedidos_por_mes",
      "gasto_medio_pedido", "ratio_inactividad", "tendencia"]),
    ("cat_nominal", OneHotEncoder(sparse_output=False, handle_unknown="ignore"),
     ["ciudad"]),
    ("cat_ordinal", OrdinalEncoder(categories=[["basico", "estandar", "premium"]]),
     ["plan"]),
])

# --- Dataset de churn simulado con la estructura de 03-06 ---
rng = np.random.default_rng(7)
n = 800
df = pd.DataFrame({
    "edad": rng.integers(18, 75, n),
    "satisfaccion": np.where(rng.random(n) < 0.1, np.nan, rng.integers(1, 11, n)),
    "recencia_dias": rng.integers(1, 180, n),
    "pedidos_por_mes": rng.gamma(3, 1.2, n).round(1),
    "gasto_medio_pedido": rng.gamma(9, 5, n).round(2),
    "ciudad": rng.choice(["Valencia", "Madrid", "Barcelona"], n),
    "plan": rng.choice(["basico", "estandar", "premium"], n, p=[0.5, 0.3, 0.2]),
})
df["ratio_inactividad"] = (df["recencia_dias"] / 365).round(3)
df["tendencia"] = rng.uniform(0.1, 2.5, n).round(2)
# El churn depende sobre todo de recencia alta y tendencia a la baja
logits = 0.03 * df["recencia_dias"] - 1.5 * df["tendencia"] - 0.4 * df["pedidos_por_mes"] + 1.0
df["churn"] = (rng.random(n) < 1 / (1 + np.exp(-logits))).astype(int)

X = df.drop(columns="churn")
y = df["churn"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42)

# --- Pipeline completo: preprocesar + clasificar ---
clf = Pipeline([
    ("prep", preprocesador),
    ("modelo", LogisticRegression(max_iter=1000)),
])
clf.fit(X_train, y_train)   # fit del preprocesador Y del modelo, solo con train

accuracy = clf.score(X_test, y_test)   # % de aciertos, como primera referencia
print(f"Accuracy en test: {accuracy:.2%}")

# Probabilidades para la campana de retencion
en_riesgo = clf.predict_proba(X_test)[:, 1]
print(X_test.assign(p_churn=en_riesgo.round(2))
            .sort_values("p_churn", ascending=False)
            .head(3)[["recencia_dias", "tendencia", "p_churn"]])

Puntos que merecen lectura lenta:

  • stratify=y: mantiene la proporción de churn en train y test — importante cuando una clase es minoritaria (más en 06-01).
  • El Pipeline resuelve la fuga de datos: al hacer clf.fit(X_train, ...), el preprocesador aprende medianas y cuartiles solo del train; al predecir sobre test, aplica esos mismos parámetros. Exactamente la disciplina de 03-02 y 03-05, ahora automatizada.
  • predict_proba: la salida de negocio real. La lista ordenada por p_churn es la lista de llamadas del equipo de retención — conecta con la feature valor_en_riesgo del ejercicio de 03-06.
  • score devuelve la accuracy (fracción de aciertos): nos sirve como primera referencia, pero es una métrica engañosa con clases desbalanceadas; su crítica y sus alternativas llegan en 06-02.
  • max_iter=1000: el entrenamiento es iterativo (descenso de gradiente y variantes); con features mal escaladas o pocas iteraciones no converge — otra razón por la que escalar (03-05) importa aquí, a diferencia de 04-01.

Como en la regresión lineal, existe una versión regularizada que penaliza pesos grandes — de hecho LogisticRegression la trae activada por defecto (parámetro C); entenderla a fondo es materia de 07-01.

Clasificación multiclase: one-vs-rest y softmax

¿Y si las clases no son dos? MercaFresh podría querer predecir la categoría favorita de un cliente (frescos / despensa / hogar). Dos estrategias:

  • One-vs-rest (OvR): entrena un clasificador binario por clase ("frescos contra el resto", "despensa contra el resto"...) y elige la clase cuyo modelo dé mayor probabilidad. Simple y paralelizable.
  • Softmax (regresión logística multinomial): generaliza la sigmoide a K clases de golpe — calcula una puntuación $z_k$ por clase y las convierte en K probabilidades que suman 1. Es lo que LogisticRegression usa por defecto en multiclase, y reaparecerá como capa de salida de las redes neuronales (04-07).
# Nada cambia en el codigo: sklearn detecta las clases automaticamente
# modelo = LogisticRegression(max_iter=1000)   # softmax si y tiene 3+ clases
# modelo.predict_proba(X)  ->  una columna de probabilidad por clase

Para el resto del curso nos basta el caso binario, que es el del churn.

Errores Comunes y Consejos

  • Leer predict cuando el negocio necesita predict_proba. La etiqueta dura pierde información: un cliente con p = 0.51 y otro con p = 0.99 son "churn" los dos, pero no merecen la misma llamada. Trabaja con probabilidades y decide el umbral al final.
  • Interpretar coeficientes como probabilidades. Un coeficiente de 0.9 no significa "+90% de probabilidad": significa odds multiplicados por e^0.9 ≈ 2.5. El efecto en probabilidad depende del punto de partida (la S es plana en los extremos).
  • Olvidar el escalado. A diferencia de LinearRegression, aquí el entrenamiento es iterativo y regularizado por defecto: sin escalar, converge mal y la penalización castiga arbitrariamente a las features de escala grande. Nuestro Pipeline lo hace imposible de olvidar — úsalo siempre.
  • Fiarse de una accuracy alta con clases desbalanceadas. Con 90% de clientes fieles, un modelo que siempre dice "se queda" acierta el 90%... y es inútil. La solución completa, en 06-02.
  • Consejo: la regresión logística es la línea base obligada de cualquier clasificación, como la lineal lo era de la regresión. Interpretable, rápida, con probabilidades bien calibradas. Los modelos de las próximas lecciones deben ganársela.

Ejercicios

Ejercicio 1. Sin ejecutar código: un modelo de churn tiene $w_0 = -1$ y un único coeficiente $w_1 = 0.5$ sobre recencia_escalada. Calcula $P(\text{churn})$ para clientes con recencia escalada 0, 2 y 6. ¿A partir de qué recencia escalada el modelo predice churn con el umbral 0.5?

Ejercicio 2. Con el Pipeline de la lección entrenado, extrae los coeficientes del modelo (clf.named_steps["modelo"].coef_) junto a los nombres de las features transformadas (clf.named_steps["prep"].get_feature_names_out()). Ordena por valor absoluto y traduce las dos features más influyentes a odds ratio con una frase de negocio cada una.

Ejercicio 3. El equipo de retención solo puede llamar al 10% de los clientes. Escribe el código que selecciona, del conjunto de test, el 10% con mayor probabilidad de churn, y explica por qué esto es mejor que usar predict con umbral 0.5.

Soluciones

Ejercicio 1

  • Recencia 0: $z = -1$, $\sigma(-1) = 1/(1+e^{1}) \approx 0.27$.
  • Recencia 2: $z = 0$, $\sigma(0) = 0.50$ — justo en la frontera.
  • Recencia 6: $z = 2$, $\sigma(2) \approx 0.88$.

La frontera está donde $z = 0$: $-1 + 0.5 \cdot x = 0 \Rightarrow x = 2$. Por encima de recencia escalada 2, el modelo predice churn. Observa que la frontera de decisión es un punto (en 1D), una recta (en 2D), un hiperplano en general: la logística sigue siendo un clasificador lineal.

Ejercicio 2

nombres = clf.named_steps["prep"].get_feature_names_out()
coefs = clf.named_steps["modelo"].coef_[0]
tabla = (pd.DataFrame({"feature": nombres, "coef": coefs})
           .assign(odds_ratio=lambda t: np.exp(t["coef"]).round(2))
           .reindex(pd.Series(coefs).abs().sort_values(ascending=False).index))
print(tabla.head())

Con los datos simulados, arriba aparecen num__recencia_dias (coeficiente positivo: cada unidad robusta de recencia extra multiplica los odds de churn por su odds ratio — el cliente que enmudece es el que se va) y num__tendencia (coeficiente negativo, odds ratio < 1: una tendencia de actividad creciente divide los odds de abandono). Las dos frases coinciden con la intuición de negocio que motivó esas features en 03-06 — buena señal: el modelo ha aprendido lo que esperábamos que aprendiera.

Ejercicio 3

probs = clf.predict_proba(X_test)[:, 1]
corte = np.quantile(probs, 0.90)          # percentil 90 (02-01)
lista_llamadas = X_test[probs >= corte]
print(f"Umbral efectivo: {corte:.2f} | clientes a llamar: {len(lista_llamadas)}")

Con umbral 0.5 la lista tendría un tamaño arbitrario — quizá 30% de los clientes (imposible de llamar), quizá 2% (capacidad desaprovechada). Ordenar por probabilidad y cortar por capacidad usa el recurso exacto disponible y lo concentra en los casos de mayor riesgo: el umbral lo dicta el negocio, no la convención matemática.

Conclusión

Has convertido la recta de 04-01 en un clasificador: la sigmoide comprime la puntuación lineal en una probabilidad, la log-loss castiga las equivocaciones seguras y premia la honestidad probabilística, los coeficientes se leen como odds ratios, y el umbral de decisión es una palanca de negocio, no una constante sagrada. Además has estrenado el patrón profesional definitivo: Pipeline(preprocesador, modelo), donde el trabajo del módulo 3 y el clasificador viajan juntos sin riesgo de fugas.

Pero la regresión logística tiene la misma alma que la lineal: su frontera de decisión es un hiperplano. Si los clientes que abandonan no se separan de los fieles con un corte recto — "churn si recencia alta y plan básico, pero también si gasto alto y tendencia desplomada" — necesitamos un modelo que piense en reglas y no en pesos. Eso es exactamente un árbol de decisión, y es la próxima lección.

Curso de Machine Learning

Módulo 1: Introducción al Machine Learning

Módulo 2: Fundamentos de Estadística y Probabilidad

Módulo 3: Preprocesamiento de Datos

Módulo 4: Algoritmos de Machine Learning Supervisado

Módulo 5: Algoritmos de Machine Learning No Supervisado

Módulo 6: Evaluación y Validación de Modelos

Módulo 7: Técnicas Avanzadas y Optimización

Módulo 8: Implementación y Despliegue de Modelos

Módulo 9: Proyectos Prácticos

Módulo 10: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados