Curso de ML EN

Capítulo 13 de 37 · intermedio

Regresión logística

Qué cubre este capítulo

Allá en el capítulo del threshold dibujamos una línea vertical sobre un solo feature y lo llamamos clasificador. Funcionó, fue honesto, y dejó siete de las ocho columnas tiradas en el piso. La regresión logística es el siguiente paso natural: conserva la frontera recta, pero deja que cada feature vote, y en vez de un sí/no duro en la línea, devuelve una probabilidad. Es el modelo al que recurro primero en cualquier problema de clasificación lineal, y la mayoría de las veces es el que termino poniendo en producción.

Lo construimos desde cero en NumPy — la sigmoide, el score lineal que envuelve, la pérdida de cross-entropy, el gradiente y el loop de descenso de gradiente que los une. Después dejamos que scikit-learn ajuste el mismo modelo y verificamos que los números coincidan. En el camino vas a poder ver el entrenamiento en vivo: una frontera de decisión deslizándose y rotando sobre un dataset 2-D mientras la curva de pérdida cae debajo de ella, cada frame un paso real de gradiente. Esta vez no hay forma cerrada, no hay argmax sobre una malla — un optimizador de verdad caminando cuesta abajo.

Los resultados corren sobre el mismo dataset de diabetes Pima de la semana 1, así que tenemos una referencia limpia. Un feature y un threshold sacaron 73% en aquel entonces. Ocho features y una sigmoide deberían hacerlo mejor. Veamos por cuánto.

Un poco de historia

La curva en el centro de todo esto es más vieja que la estadística como campo. Pierre François Verhulst escribió la función logística en 1838 para modelar el crecimiento poblacional — una curva en forma de S que arranca lento, acelera y luego se satura cuando se queda sin espacio. Necesitaba algo que viviera entre dos cotas y se moviera suavemente de una a la otra, que es exactamente la forma que quieres para una probabilidad.

La historia de la clasificación empieza un siglo después. Joseph Berkson, físico y estadístico en la Clínica Mayo, acuñó la palabra "logit" en 1944 — log-odds, la cosa en la que el modelo es realmente lineal — y pasó años argumentando que era una mejor herramienta que el probit, entonces dominante, para el trabajo de bioensayos. Tenía razón en su mayor parte, y el nombre se quedó. Luego, en 1958, David Cox planteó la regresión logística como un método general para resultados binarios, y Nelder y Wedderburn la integraron al marco de los modelos lineales generalizados en 1972. Ese linaje es la razón de que el modelo se sienta tan asentado: no es un truco de machine learning que dio la casualidad de funcionar, es un modelo estadístico con sesenta años de teoría detrás. Cuando la gente dice que la regresión logística es el clasificador lineal por defecto, ese es el peso detrás de la palabra defecto.

La intuición

Aquí hay un dataset pequeño en 2-D, dos clases, algo de traslape. Dos features en los ejes, color para la etiqueta verdadera. Este es el set de juguete con el que entrenaremos, para que de verdad puedas ver moverse la frontera.

Una línea recta puede separar en su mayoría estas dos nubes, igual que un solo corte separó en su mayoría la glucosa. La diferencia es que la línea ahora vive en dos dimensiones, así que puede inclinarse — no tiene que quedar perpendicular a un eje. En ocho dimensiones es un hiperplano que no puedes visualizar, pero la idea es idéntica: una frontera plana, orientada por un peso por feature.

La otra idea nueva es lo que pasa cerca de la línea. El clasificador de threshold era brutal — de un lado enfermo, del otro sano, sin puntos medios. La regresión logística lo suaviza. Lejos de la frontera, del lado positivo, dice "casi seguro clase 1"; lejos del otro lado, "casi seguro clase 0"; justo en la línea, "de verdad no sé, échalo a volado." Esa suavidad es todo el punto. Te da una probabilidad que puedes cortar donde quieras, y le da al entrenamiento algo suave que optimizar.

La matemática

Empieza con el score lineal. Para un vector de features xix_i con DD entradas, un vector de pesos ww y un sesgo bb:

zi=wxi+b=j=1Dwjxij+bz_i = w^{\top} x_i + b = \sum_{j=1}^{D} w_j\, x_{ij} + b

Ese ziz_i es el log-odds — el logit. Corre de -\infty a ++\infty, lo cual no sirve como probabilidad, así que lo aplastamos con la sigmoide:

σ(z)=11+ez\sigma(z) = \frac{1}{1 + e^{-z}}

La sigmoide es la curva en S que escribió Verhulst. Mapea todo número real al intervalo (0,1)(0, 1), cruza 0.50.5 en z=0z = 0 y se aplana hacia los extremos. La predicción del modelo es ese aplastamiento aplicado al score:

pi=σ(wxi+b)=P(yi=1xi)p_i = \sigma(w^{\top} x_i + b) = P(y_i = 1 \mid x_i)

Para clasificar, corta la probabilidad en 0.50.5 — lo cual, como la sigmoide cruza 0.50.5 exactamente en z=0z = 0, es lo mismo que preguntar si ziz_i es positivo. La frontera es la superficie plana wx+b=0w^{\top} x + b = 0.

Ahora el entrenamiento. Necesitamos una pérdida que sea pequeña cuando pip_i está confiado y en lo correcto, y grande cuando está confiado y equivocado. Esa es la cross-entropy, también llamada log loss:

L(w,b)=1Ni=1N[yilogpi+(1yi)log(1pi)]\mathcal{L}(w, b) = -\frac{1}{N} \sum_{i=1}^{N} \Big[\, y_i \log p_i + (1 - y_i)\log(1 - p_i) \,\Big]

Cuando la verdad es yi=1y_i = 1 solo sobrevive el primer término, y es logpi-\log p_i — cero si dijiste 1.01.0, explotando conforme te acercas a 00. Cuando yi=0y_i = 0 el segundo término hace lo mismo al revés. Un acierto confiado sale casi gratis; un error confiado sale carísimo.

No hay forma cerrada para los ww y bb que minimizan esto, así que descendemos por el gradiente. Y aquí está la recompensa de emparejar la sigmoide con la cross-entropy — el gradiente colapsa en algo limpio:

Lw=1Ni=1N(piyi)xiLb=1Ni=1N(piyi)\frac{\partial \mathcal{L}}{\partial w} = \frac{1}{N}\sum_{i=1}^{N}(p_i - y_i)\,x_i \qquad \frac{\partial \mathcal{L}}{\partial b} = \frac{1}{N}\sum_{i=1}^{N}(p_i - y_i)

El error es simplemente piyip_i - y_i, la brecha entre la probabilidad predicha y la verdad, y el gradiente es ese error ponderado por los features. La derivada de la sigmoide y la derivada del logaritmo se cancelan perfectamente; no sobrevive nada feo. El descenso de gradiente entonces repite una línea — wwηL/ww \leftarrow w - \eta\, \partial\mathcal{L}/\partial w — hasta que la pérdida deja de bajar.

Esa cancelación también responde una pregunta que la gente hace temprano: ¿por qué no usar simplemente error cuadrático, como en regresión lineal? Dos razones. El error cuadrático encima de una sigmoide es no convexo, así que el descenso puede atorarse en valles locales. Y castiga los errores confiados solo tibiamente, porque la sigmoide aplana el gradiente justo donde más necesitas una corrección fuerte. La cross-entropy aquí es convexa y sigue empujando duro contra los errores confiados. Usa la pérdida para la que el modelo fue construido.

En qué es bueno, en qué no

La ventaja es un paquete difícil de superar por el precio. Obtienes probabilidades calibradas, no solo etiquetas — cuando el modelo dice 0.7 lo dice en serio, lo cual importa muchísimo en el momento en que una decisión posterior depende de qué tan probable, no solo de si lo es. Obtienes pesos que puedes leer: cada wjw_j es el cambio en log-odds por unidad del feature jj, así que el modelo te dice qué mediciones mueven la predicción y en qué dirección. Entrena rápido, apenas sufre overfitting con un poco de regularización, y es convexo, así que obtienes la misma respuesta cada vez. Como baseline fuerte en datos tabulares, es el número que tu gradient-boosted-lo-que-sea tiene que superar de verdad, no solo empatar.

El límite es el mismo que tenía el threshold, elevado una dimensión: la frontera sigue siendo plana. La regresión logística dibuja un hiperplano, y si la frontera verdadera se curva o las clases se envuelven una a la otra, un plano no puede seguirla. Puedes recuperar algo de eso a mano — agrega features polinomiales o de interacción y el plano se dobla en el espacio original — pero en ese punto ya estás haciendo el feature engineering que los modelos basados en árboles hacen gratis. También se toma el score lineal al pie de la letra, así que features con escalas descontroladas y outliers fuertes pueden arrastrar los pesos. Pasa a otro modelo cuando la señal sea genuinamente no lineal y prefieras que sea el modelo, y no tú, quien encuentre las interacciones.

Los datos

Los mismos pacientes de la semana 1: el dataset Pima Indians Diabetes, 768 mujeres, ocho mediciones cada una — glucosa, IMC, edad, presión arterial, etcétera — y una etiqueta que indica si fueron diagnosticadas con diabetes. La vez pasada usamos una columna. Esta vez entran las ocho.

Hay un paso de preprocesamiento que importa lo suficiente como para mencionarlo. Estos features viven en escalas completamente distintas: la glucosa pasa de 120, el score de pedigrí de diabetes ronda el 0.5, los conteos de embarazos son de un dígito. El descenso de gradiente con una sola tasa de aprendizaje no puede servir a rangos tan distintos — el paso que le queda bien a la glucosa es mil veces demasiado grande para el score de pedigrí. Así que primero estandarizamos: restamos la media de cada columna, dividimos entre su desviación estándar, usando estadísticas del set de entrenamiento únicamente y aplicando el mismo desplazamiento al set de prueba. Después de eso, cada feature queda con media 0 y varianza unitaria, y una sola tasa de aprendizaje funciona para todos. También hace comparables los pesos, algo que vamos a cobrar al final.

Constrúyelo, una función a la vez

Siete piezas pequeñas, de abajo hacia arriba, en el orden en que las escribirías en una terminal. Empieza con el aplastamiento, porque todo depende de él.

def sigmoid(z):
    """Squash any real number into (0, 1).

    Written in the numerically stable branch form so a large negative z
    never overflows exp(). For z >= 0 use 1 / (1 + e^-z); for z < 0 use
    e^z / (1 + e^z). Both are the same function, algebraically.
    """
    z = np.asarray(z, dtype=float)
    out = np.empty_like(z)
    pos = z >= 0
    out[pos] = 1.0 / (1.0 + np.exp(-z[pos]))
    ez = np.exp(z[~pos])
    out[~pos] = ez / (1.0 + ez)
    return out

Esa forma con ramas es deliberada. La versión de libro de texto 1/(1+ez)1/(1+e^{-z}) se desborda para zz muy negativos — exp de un número positivo grande es inf — así que usamos la algebraicamente idéntica ez/(1+ez)e^{z}/(1+e^{z}) siempre que zz es negativo. Misma curva, sin NaNs. Es el tipo de cosa que nunca te muerde en datos de juguete y siempre te muerde en datos reales.

Luego el score lineal, el log-odds antes de aplastarlo:

def logit(X, w, b):
    """The linear part: z = Xw + b.

    X is (N, D), w is (D,), b is a scalar bias. Returns one score z per
    row — the log-odds of class 1 before the sigmoid squashes it.
    """
    return X @ w + b

Envuelve las dos piezas y tienes la salida de probabilidad del modelo:

def predict_proba(X, w, b):
    """P(y = 1 | x) for every row: sigmoid of the linear score."""
    return sigmoid(logit(X, w, b))

Para convertir probabilidades en etiquetas, corta en 0.5:

def predict_proba(X, w, b):
    """P(y = 1 | x) for every row: sigmoid of the linear score."""
    return sigmoid(logit(X, w, b))

Nota que el threshold es un parámetro, no una constante mágica. 0.5 es la elección natural, pero es la misma perilla que el corte de la semana 1 — muévela para intercambiar positivos perdidos por falsas alarmas cuando un error cuesta más que el otro.

Ahora la pérdida. Esto es lo que el entrenamiento minimiza:

def cross_entropy(X, y, w, b, eps=1e-12):
    """Mean binary cross-entropy (log loss) over the dataset.

    For each row, -log(p) when the truth is 1 and -log(1 - p) when it's 0.
    A confident, correct probability costs almost nothing; a confident,
    wrong one costs a lot. Clipping keeps log() away from 0.
    """
    p = predict_proba(X, w, b)
    p = np.clip(p, eps, 1.0 - eps)
    return float(-np.mean(y * np.log(p) + (1 - y) * np.log(1 - p)))

El clip mantiene a log lejos del cero cuando el modelo se pone gallito y predice exactamente 0 o 1. Ahora el gradiente — la dirección que empeora la pérdida, contra la cual daremos el paso:

def gradient(X, y, w, b):
    """Gradient of the mean cross-entropy w.r.t. w and b.

    The algebra collapses to something clean: the error is just
    (p - y), and the gradient is that error dotted with the features.
    dw = X^T (p - y) / N, db = mean(p - y). No sigmoid derivative left
    over — cross-entropy and the sigmoid were built for each other.
    """
    p = predict_proba(X, w, b)
    err = p - y                      # shape (N,)
    dw = X.T @ err / len(y)          # shape (D,)
    db = float(np.mean(err))         # scalar
    return dw, db

Mira qué poco hay ahí. Todo el gradiente es (p - y) en producto punto con los features. Todo el cálculo de la sección de matemáticas se reduce a una resta y una multiplicación de matrices, y no es coincidencia — es la sigmoide y la cross-entropy cancelándose, la misma cancelación que hizo del error cuadrático la elección equivocada.

Última pieza, el loop de entrenamiento. Arranca los pesos en cero y da pasos cuesta abajo:

def fit(X, y, lr=0.1, n_iter=300, record=False):
    """Train by batch gradient descent: step downhill on the log loss.

    Start the weights at zero, and on every iteration compute the
    gradient over the whole training set and take one step against it.
    With `record=True` we also return the per-iteration (w, b, loss)
    history so the chapter can animate the descent.
    """
    N, D = X.shape
    w = np.zeros(D)
    b = 0.0
    history = []
    for t in range(n_iter):
        if record:
            history.append((w.copy(), b, cross_entropy(X, y, w, b)))
        dw, db = gradient(X, y, w, b)
        w -= lr * dw
        b -= lr * db
    if record:
        history.append((w.copy(), b, cross_entropy(X, y, w, b)))
        return w, b, history
    return w, b

Descenso de gradiente por lotes, la versión simple: cada iteración usa todo el set de entrenamiento para calcular un gradiente y dar un paso. La bandera record guarda los pesos y la pérdida en cada iteración para que podamos reproducir el descenso en un minuto. Esa reproducción es toda la razón por la que el loop se ve como se ve.

Míralo trabajar

Este es el entrenamiento mismo, corriendo sobre el set de juguete 2-D para que cada punto sea visible. El panel superior son los datos; la línea naranja es la frontera de decisión actual, el lugar donde la probabilidad del modelo es exactamente 0.5. Un punto recibe un anillo rojo en el momento en que la frontera lo deja del lado equivocado. El panel inferior traza la pérdida de cross-entropy, un punto por iteración.

Dale play. Cada frame es un paso real de gradiente salido de fit — los mismos pesos que produjo el código, sin suavizado. Reinicia y córrelo de nuevo las veces que quieras.

Observa el primer movimiento. Los pesos empiezan en cero, así que no hay línea en absoluto y el modelo llama a todo una sola clase — la mitad de los puntos traen anillo rojo. Luego el gradiente entra en acción y aparece una frontera, tosca al principio, cortando el plano más o menos a la mitad. Desde ahí rota y se desliza, y los anillos rojos se desprenden por tandas conforme encuentra la orientación que separa las nubes. La curva de pérdida debajo cuenta la misma historia desde el otro lado: una caída empinada al principio mientras se corrigen los errores grandes, luego un largo planeo casi horizontal mientras afina detalles. Al final la frontera se ha asentado en el hueco entre las clases y la pérdida casi ha tocado fondo, aterrizando en 96% de accuracy en este set de juguete. Ese aplanamiento es la cara de la convergencia — el gradiente encogiéndose hacia cero porque queda poco por arreglar.

Lo único que el thresholding no podía hacer está justo ahí, en la inclinación de la línea. No es vertical, no es horizontal, está en ángulo — porque ambos features cargan señal y los pesos encontraron la mezcla. Ese ángulo es la dimensión extra de libertad que la regresión logística te compra sobre un solo corte.

Si quieres ver la función de aplastamiento por sí sola, aquí está: la sigmoide a lo largo del rango de scores que el modelo produce. Plana cerca de los extremos, empinada por el centro, cruzando 0.5 en cero.

La banda empinada del centro es la zona de incertidumbre cerca de la frontera; las colas planas son donde el modelo está confiado. Un punto lejos de la línea tiene un z|z| grande y se sienta en una cola, y por eso el modelo puede estar tan seguro de él.

La implementación completa

El archivo entero, de arriba abajo — el modelo, la pérdida, el gradiente, el loop, más los helpers de estandarización y accuracy que usan los traces. Este es el código que la animación de arriba realmente corrió:

"""Logistic regression, built from scratch.

A linear model that outputs a probability. Push the features through a
weighted sum, squash that sum to (0, 1) with the sigmoid, and call it
P(class = 1). Training is gradient descent on the cross-entropy loss — no
closed form, no library, just the gradient and a step size.

Pure NumPy (+ pandas for loading). Every function below appears in the
chapter one step at a time; the `# region:` markers are what the book's
include directives pull in.
"""

import numpy as np
import pandas as pd


# region: sigmoid
def sigmoid(z):
    """Squash any real number into (0, 1).

    Written in the numerically stable branch form so a large negative z
    never overflows exp(). For z >= 0 use 1 / (1 + e^-z); for z < 0 use
    e^z / (1 + e^z). Both are the same function, algebraically.
    """
    z = np.asarray(z, dtype=float)
    out = np.empty_like(z)
    pos = z >= 0
    out[pos] = 1.0 / (1.0 + np.exp(-z[pos]))
    ez = np.exp(z[~pos])
    out[~pos] = ez / (1.0 + ez)
    return out
# endregion


# region: logit
def logit(X, w, b):
    """The linear part: z = Xw + b.

    X is (N, D), w is (D,), b is a scalar bias. Returns one score z per
    row — the log-odds of class 1 before the sigmoid squashes it.
    """
    return X @ w + b
# endregion


# region: predict_proba
def predict_proba(X, w, b):
    """P(y = 1 | x) for every row: sigmoid of the linear score."""
    return sigmoid(logit(X, w, b))
# endregion


# region: predict
def predict(X, w, b, threshold=0.5):
    """Turn probabilities into 0/1 labels at a decision threshold.

    0.5 is the natural cutoff — predict class 1 when it's more likely than
    not — but the threshold is a knob you can move to trade the two kinds
    of mistake, exactly like the cutoff in week 1.
    """
    return (predict_proba(X, w, b) >= threshold).astype(int)
# endregion


# region: cross_entropy
def cross_entropy(X, y, w, b, eps=1e-12):
    """Mean binary cross-entropy (log loss) over the dataset.

    For each row, -log(p) when the truth is 1 and -log(1 - p) when it's 0.
    A confident, correct probability costs almost nothing; a confident,
    wrong one costs a lot. Clipping keeps log() away from 0.
    """
    p = predict_proba(X, w, b)
    p = np.clip(p, eps, 1.0 - eps)
    return float(-np.mean(y * np.log(p) + (1 - y) * np.log(1 - p)))
# endregion


# region: gradient
def gradient(X, y, w, b):
    """Gradient of the mean cross-entropy w.r.t. w and b.

    The algebra collapses to something clean: the error is just
    (p - y), and the gradient is that error dotted with the features.
    dw = X^T (p - y) / N, db = mean(p - y). No sigmoid derivative left
    over — cross-entropy and the sigmoid were built for each other.
    """
    p = predict_proba(X, w, b)
    err = p - y                      # shape (N,)
    dw = X.T @ err / len(y)          # shape (D,)
    db = float(np.mean(err))         # scalar
    return dw, db
# endregion


# region: fit
def fit(X, y, lr=0.1, n_iter=300, record=False):
    """Train by batch gradient descent: step downhill on the log loss.

    Start the weights at zero, and on every iteration compute the
    gradient over the whole training set and take one step against it.
    With `record=True` we also return the per-iteration (w, b, loss)
    history so the chapter can animate the descent.
    """
    N, D = X.shape
    w = np.zeros(D)
    b = 0.0
    history = []
    for t in range(n_iter):
        if record:
            history.append((w.copy(), b, cross_entropy(X, y, w, b)))
        dw, db = gradient(X, y, w, b)
        w -= lr * dw
        b -= lr * db
    if record:
        history.append((w.copy(), b, cross_entropy(X, y, w, b)))
        return w, b, history
    return w, b
# endregion


def accuracy(X, y, w, b, threshold=0.5):
    """Fraction of rows the model labels correctly at a threshold."""
    return float((predict(X, w, b, threshold) == y).mean())


def standardize(X, mean=None, std=None):
    """Center and scale each column to mean 0, unit variance.

    Gradient descent on raw features with wildly different ranges crawls;
    standardizing puts every feature on the same footing so one learning
    rate works for all of them. Returns the transformed X plus the mean
    and std, so the exact same shift can be applied to test data.
    """
    if mean is None:
        mean = X.mean(axis=0)
    if std is None:
        std = X.std(axis=0)
        std = np.where(std == 0, 1.0, std)
    return (X - mean) / std, mean, std


def load_data(path="../data/diabetes.csv"):
    """Pima Indians Diabetes dataset: 768 patients, 8 features, Outcome."""
    return pd.read_csv(path)

La versión de librería

No implementarías esto a mano en producción, y una vez que lo entiendes no lo necesitas. La LogisticRegression de scikit-learn es el mismo modelo — score lineal, sigmoide, cross-entropy — y te entrega los pesos ajustados y el sesgo de la misma manera:

def sklearn_logreg(Xtr, ytr, Xte, yte):
    """Fit sklearn's LogisticRegression on standardized features.

    Returns the learned weight vector, bias, and test accuracy. We hand it
    the already-standardized arrays so it's judged on the same footing as
    our from-scratch model.
    """
    clf = LogisticRegression(max_iter=1000)
    clf.fit(Xtr, ytr)
    w = clf.coef_[0]
    b = float(clf.intercept_[0])
    acc = float(clf.score(Xte, yte))
    return w, b, acc

Dos diferencias que vale la pena conocer. Primero, sklearn regulariza por defecto: agrega una penalización L2 sobre los pesos, que los empuja hacia cero y protege contra el overfitting. Nuestra versión desde cero no tiene penalización alguna, lo cual está bien en estos datos pero es lo primero que agregaría para cualquier cosa de mayor dimensión. Segundo, no usa descenso de gradiente simple — el solver por defecto es lbfgs, un método cuasi-Newton que usa la curvatura para dar pasos mucho más inteligentes, así que converge en unas cuantas docenas de iteraciones donde nuestro descenso de paso fijo se toma un par de miles. Mismo destino, camino más rápido. Le pasamos los arrays ya estandarizados para que la comparación sea honesta — mismos features, mismo escalado, mismo split.

Desde cero contra la librería

Divide los datos Pima 70/30 — 537 pacientes para entrenar, 231 apartados — estandariza con las estadísticas de entrenamiento, ajusta ambos modelos y califica sobre el set apartado. Aquí está el enfrentamiento, con el clasificador de threshold de la semana 1 incluido como barra de referencia:

Nuestra regresión logística desde cero saca 0.7662 en el set apartado. Scikit-learn saca 0.7662 — el mismo número a cuatro decimales, con un sesgo de -0.888 contra el -0.881 de sklearn, que es lo más cerca que dos optimizadores distintos sobre la misma pérdida convexa necesitan llegar jamás. La convexidad es la razón: hay un solo mínimo, y ambos caminos llegan a él. Esa coincidencia es el punto del ejercicio. Cuando tu gradiente escrito a mano aterriza en la respuesta de la librería, entendiste el modelo, no solo lo llamaste.

Ahora la referencia. El stump de la semana 1 — un feature, un threshold sobre la glucosa — saca 0.7316 en el mismo split. La regresión logística, con los ocho features y los mismos pacientes apartados, saca 0.7662. Tres puntos y medio no son una paliza, y eso también es honesto: la glucosa realmente es la señal dominante en este dataset, así que un modelo de un solo feature nunca iba a quedar en ridículo. Pero los features extra sí cargan peso real. Estandarizados, los tres más pesados son glucosa con +1.07, IMC con +0.85 y conteo de embarazos con +0.39 — todos empujando hacia una predicción de diabetes, en un orden que un clínico aprobaría con la cabeza. Ese es el dividendo de la interpretabilidad: el modelo no solo clasificó mejor, te dijo por qué, en unidades que puedes rankear.

Las mismas dos advertencias de la semana 1 siguen aplicando. Las clases están desbalanceadas — cerca de un tercio de estas pacientes son positivas — así que un modelo que adivinara "sin diabetes" para todas ya rebasaría el 65%, y la accuracy por sí sola adula a todos los modelos de la gráfica. Y el split apartado es lo que mantiene honestos estos números; la accuracy de entrenamiento se leería más alta y significaría menos. La regresión logística no escapa de esas trampas. Solo te da una mejor frontera y una probabilidad sobre la cual razonar una vez que estás dentro de ellas.

Conclusiones

La regresión logística es el clasificador lineal que uso por defecto, y creo que tú también deberías. Es el siguiente peldaño honesto arriba del threshold: conserva la frontera recta, deja que cada feature vote, obtén una probabilidad calibrada en vez de una etiqueta pelona. En datos tabulares es el baseline que un modelo más pesado tiene que superar genuinamente antes de ganarse su lugar — y muchas veces no lo hace, que es exactamente lo que quieres descubrir en dos segundos de fit, no dos semanas dentro de un proyecto.

Las tres cosas para llevarte. Probabilidades, no solo etiquetas: el 0.7 significa 0.7, que es lo que te permite fijar un threshold acorde al costo de cada error en vez de aceptar 0.5 por defecto. Pesos legibles: estandariza los features y los coeficientes rankean tu señal gratis, dirección incluida. Y la lección del log-loss, que sobrevive a este capítulo — la sigmoide y la cross-entropy fueron construidas como pareja, sus gradientes se cancelan en un (p - y) limpio, y por eso no recurres al error cuadrático en un problema de clasificación. Esa misma pérdida de cross-entropy y ese mismo gradiente limpio son el motor debajo de las redes neuronales más adelante en el curso; una red es en gran parte este modelo apilado y doblado hasta que la frontera por fin puede curvarse. Que es lo único que la regresión logística no puede hacer, y la razón por la que el curso continúa. Pero es donde la clasificación lineal madura, y la mayoría de los días es todo lo que necesitas.