Phase 02 - Lesson 09

Evaluación del modelo

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

Un modelo es tan bueno como la forma en que lo mides.

Tipo: Construcción Idiomas: Python Requisitos previos: Fase 1 (Probabilidad y distribuciones, Estadísticas para ML), Fase 2, Lecciones 1-8 Tiempo: ~90 minutos

Objetivos de aprendizaje

  • Implementar validación cruzada K-fold y K-fold estratificada desde cero y explicar por qué la estratificación es importante para datos desequilibrados
  • Calcule precisión, recuperación, F1, AUC-ROC y métricas de regresión (MSE, RMSE, MAE, R-cuadrado) desde cero
  • Interpretar curvas de aprendizaje para diagnosticar si un modelo sufre de alto sesgo o alta varianza.
  • Identificar errores de evaluación comunes, incluida la fuga de datos, la selección de métricas incorrecta y la contaminación del conjunto de pruebas.

El problema

Entrenaste a un modelo. Obtiene un 95% de precisión en sus datos. ¿Es bueno?

Tal vez. Quizás no. Si el 95% de sus datos pertenecen a una clase, un modelo que siempre predice esa clase obtiene un 95% de precisión y es completamente inútil. Si evaluó con los mismos datos con los que entrenó, el número del 95% no tiene sentido porque el modelo simplemente memorizó las respuestas. Si su conjunto de datos tiene un componente de tiempo y lo barajó aleatoriamente antes de dividirlo, es posible que su modelo esté usando datos futuros para predecir el pasado.

La evaluación de modelos es donde la mayoría de los proyectos de ML salen mal. Una métrica incorrecta hace que un mal modelo parezca bueno. La división incorrecta permite que un modelo haga trampa. Una comparación incorrecta te hace elegir el peor modelo. Hacer una evaluación correcta no es opcional. Es la diferencia entre un modelo que funciona en producción y uno que falla en el momento en que ve datos reales.

El concepto

Entrenamiento, Validación, Prueba

flowchart LR
    A[Full Dataset] --> B[Train Set 60-70%]
    A --> C[Validation Set 15-20%]
    A --> D[Test Set 15-20%]
    B --> E[Fit Model]
    E --> C
    C --> F[Tune Hyperparameters]
    F --> E
    F --> G[Final Model]
    G --> D
    D --> H[Report Performance]

Tres divisiones, tres propósitos:

  • Conjunto de entrenamiento: el modelo aprende de estos datos. Ve estos ejemplos durante el entrenamiento.
  • Conjunto de validación: se utiliza para ajustar hiperparámetros y seleccionar entre modelos. El modelo nunca se entrena con estos datos, pero sus decisiones están influenciadas por ellos.
  • Conjunto de prueba: se toca exactamente una vez, al final, para informar el rendimiento final. Si observa el rendimiento de la prueba y luego regresa para cambiar su modelo, ya no es un conjunto de prueba. Se ha convertido en un segundo conjunto de validación.

El conjunto de prueba es su garantía de que el rendimiento informado refleja cómo funcionará el modelo con datos realmente invisibles.

Validación cruzada de K-Fold

Con conjuntos de datos pequeños, una única división tren/validación desperdicia datos y proporciona estimaciones ruidosas. La validación cruzada K-fold utiliza todos los datos tanto para el entrenamiento como para la validación:

flowchart TB
    subgraph Fold1["Fold 1"]
        direction LR
        V1["Val"] --- T1a["Train"] --- T1b["Train"] --- T1c["Train"] --- T1d["Train"]
    end
    subgraph Fold2["Fold 2"]
        direction LR
        T2a["Train"] --- V2["Val"] --- T2b["Train"] --- T2c["Train"] --- T2d["Train"]
    end
    subgraph Fold3["Fold 3"]
        direction LR
        T3a["Train"] --- T3b["Train"] --- V3["Val"] --- T3c["Train"] --- T3d["Train"]
    end
    subgraph Fold4["Fold 4"]
        direction LR
        T4a["Train"] --- T4b["Train"] --- T4c["Train"] --- V4["Val"] --- T4d["Train"]
    end
    subgraph Fold5["Fold 5"]
        direction LR
        T5a["Train"] --- T5b["Train"] --- T5c["Train"] --- T5d["Train"] --- V5["Val"]
    end
    Fold1 --> R["Average scores"]
    Fold2 --> R
    Fold3 --> R
    Fold4 --> R
    Fold5 --> R
  1. Dividir los datos en K pliegues del mismo tamaño
  2. Para cada pliegue, entrene en los pliegues K-1 y valide en el pliegue restante.
  3. Promediar los puntajes de validación K

K=5 o K=10 son opciones estándar. Cada punto de datos se utiliza para la validación exactamente una vez. La puntuación promedio es una estimación más estable que cualquier división única.

Pliegue K estratificado: conserva la distribución de clases en cada pliegue. Si su conjunto de datos es 70% clase A y 30% clase B, cada pliegue tendrá aproximadamente la misma proporción. Esto es importante para conjuntos de datos desequilibrados donde una división aleatoria podría agrupar todas las muestras minoritarias en un solo pliegue.

Métricas de clasificación

Matriz de confusión: la base. Para clasificación binaria:

Positivo previsto Negativo previsto
Realmente Positivo Verdadero Positivo (TP) Falso Negativo (FN)
Realmente Negativo Falso Positivo (FP) Verdadero Negativo (TN)

De esta matriz, se siguen todas las demás métricas:

  • Precisión = (TP + TN) / (TP + TN + FP + FN). Fracción de predicciones correctas. Engañoso cuando las clases están desequilibradas.
  • Precisión = TP / (TP + FP). De todas las cosas que se pronosticaron positivas, ¿cuántas realmente lo fueron? Úselo cuando los falsos positivos sean costosos (por ejemplo, el filtro de spam marca el correo electrónico real como spam).
  • Recordar (sensibilidad) = TP / (TP + FN). De todos los casos positivos reales, ¿cuántos detectamos? Utilícelo cuando los falsos negativos sean costosos (p. ej., pruebas de detección de cáncer en las que no se detecta un tumor).
  • F1 puntuación = 2 * precisión * recuperación / (precisión + recuperación). Media armónica de precisión y recuperación. Equilibra a ambos cuando ninguno domina claramente.
  • AUC-ROC: Área bajo la curva característica de funcionamiento del receptor. Traza la tasa de verdaderos positivos frente a la tasa de falsos positivos en varios umbrales de clasificación. AUC = 0,5 significa adivinación aleatoria, AUC = 1,0 significa separación perfecta. Independiente del umbral: mide qué tan bien el modelo clasifica los aspectos positivos por encima de los negativos, independientemente del límite que elija.

Métricas de regresión

  • MSE (Error cuadrático medio) = media((y_true - y_pred)^2). Penaliza cuadráticamente los grandes errores. Sensible a valores atípicos.
  • RMSE (Error cuadrático medio) = sqrt(MSE). Mismas unidades que la variable objetivo. Más fácil de interpretar que MSE.
  • MAE (Error absoluto medio) = media(|y_true - y_pred|). Trata todos los errores de forma lineal. Más robusto a los valores atípicos que MSE.
  • R-cuadrado = 1 - SS_res / SS_tot, donde SS_res = suma((y_true - y_pred)^2) y SS_tot = suma((y_true - y_mean)^2). Fracción de varianza explicada por el modelo. R^2 = 1,0 es perfecto. R^2 = 0,0 significa que el modelo no es mejor que predecir siempre la media. R^2 puede ser negativo si el modelo es peor que la media.

Curvas de aprendizaje

Trazar puntuaciones de entrenamiento y validación en función del tamaño del conjunto de entrenamiento:

  • Sesgo alto (underfitting): ambas curvas convergen a una puntuación baja. Agregar más datos no ayudará. Necesitas un modelo más complejo.
  • Alta varianza (sobreajuste): la puntuación de entrenamiento es alta pero la puntuación de validación es mucho menor. La brecha entre ellos es grande. Agregar más datos debería ayudar.

Curvas de validación

Trazar puntuaciones de entrenamiento y validación en función de un hiperparámetro:

  • De baja complejidad: ambas puntuaciones son bajas (underfitting)
  • Con la complejidad adecuada: ambas puntuaciones son altas y muy juntas
  • En alta complejidad: la puntuación de entrenamiento se mantiene alta pero la puntuación de validación cae (sobreajuste)

El valor óptimo del hiperparámetro es donde la puntuación de validación alcanza su punto máximo.

Errores comunes de evaluación

Fuga de datos: la inentrenamiento del conjunto de prueba se filtra al entrenamiento. Ejemplos: ajustar un escalador al conjunto de datos completo antes de dividirlo, incluidos datos futuros en la predicción de series temporales, utilizando una característica derivada del objetivo. Siempre divida primero y luego preprocese.

Desequilibrio de clases: el 99% de las transacciones son legítimas, el 1% son fraude. Un modelo que siempre predice "legítimo" obtiene una precisión del 99%. Utilice precisión, recuperación, F1 o AUC-ROC en su lugar.

Métrica incorrecta: optimizar la precisión cuando debería optimizar el recuerdo (diagnóstico médico) u optimizar el RMSE cuando sus datos tienen grandes valores atípicos (use MAE en su lugar).

No utilizar divisiones estratificadas: con datos desequilibrados, una división aleatoria podría incluir muy pocas muestras minoritarias en el pliegue de validación, lo que generaría estimaciones inestables.

Probar con demasiada frecuencia: cada vez que observa el rendimiento de la prueba y realiza ajustes, se sobreajusta al conjunto de prueba. El equipo de prueba es de un solo uso.

Constrúyelo

Paso 1: División de entrenamiento/validación/prueba

import random
import math


def train_val_test_split(X, y, train_ratio=0.6, val_ratio=0.2, seed=42):
    random.seed(seed)
    n = len(X)
    indices = list(range(n))
    random.shuffle(indices)

    train_end = int(n * train_ratio)
    val_end = int(n * (train_ratio + val_ratio))

    train_idx = indices[:train_end]
    val_idx = indices[train_end:val_end]
    test_idx = indices[val_end:]

    X_train = [X[i] for i in train_idx]
    y_train = [y[i] for i in train_idx]
    X_val = [X[i] for i in val_idx]
    y_val = [y[i] for i in val_idx]
    X_test = [X[i] for i in test_idx]
    y_test = [y[i] for i in test_idx]

    return X_train, y_train, X_val, y_val, X_test, y_test

Paso 2: validación cruzada K-fold y K-fold estratificada

def kfold_split(n, k=5, seed=42):
    random.seed(seed)
    indices = list(range(n))
    random.shuffle(indices)

    fold_size = n // k
    folds = []

    for i in range(k):
        start = i * fold_size
        end = start + fold_size if i < k - 1 else n
        val_idx = indices[start:end]
        train_idx = indices[:start] + indices[end:]
        folds.append((train_idx, val_idx))

    return folds


def stratified_kfold_split(y, k=5, seed=42):
    random.seed(seed)

    class_indices = {}
    for i, label in enumerate(y):
        class_indices.setdefault(label, []).append(i)

    for label in class_indices:
        random.shuffle(class_indices[label])

    folds = [{"train": [], "val": []} for _ in range(k)]

    for label, indices in class_indices.items():
        fold_size = len(indices) // k
        for i in range(k):
            start = i * fold_size
            end = start + fold_size if i < k - 1 else len(indices)
            val_part = indices[start:end]
            train_part = indices[:start] + indices[end:]
            folds[i]["val"].extend(val_part)
            folds[i]["train"].extend(train_part)

    return [(f["train"], f["val"]) for f in folds]


def cross_validate(X, y, model_fn, k=5, metric_fn=None, stratified=False):
    n = len(X)

    if stratified:
        folds = stratified_kfold_split(y, k)
    else:
        folds = kfold_split(n, k)

    scores = []
    for train_idx, val_idx in folds:
        X_train = [X[i] for i in train_idx]
        y_train = [y[i] for i in train_idx]
        X_val = [X[i] for i in val_idx]
        y_val = [y[i] for i in val_idx]

        model = model_fn()
        model.fit(X_train, y_train)
        predictions = [model.predict(x) for x in X_val]

        if metric_fn:
            score = metric_fn(y_val, predictions)
        else:
            score = sum(1 for yt, yp in zip(y_val, predictions) if yt == yp) / len(y_val)
        scores.append(score)

    return scores

Paso 3: Matriz de confusión y métricas de clasificación

def confusion_matrix(y_true, y_pred):
    tp = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 1 and yp == 1)
    tn = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 0 and yp == 0)
    fp = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 0 and yp == 1)
    fn = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 1 and yp == 0)
    return tp, tn, fp, fn


def accuracy(y_true, y_pred):
    tp, tn, fp, fn = confusion_matrix(y_true, y_pred)
    total = tp + tn + fp + fn
    return (tp + tn) / total if total > 0 else 0.0


def precision(y_true, y_pred):
    tp, tn, fp, fn = confusion_matrix(y_true, y_pred)
    return tp / (tp + fp) if (tp + fp) > 0 else 0.0


def recall(y_true, y_pred):
    tp, tn, fp, fn = confusion_matrix(y_true, y_pred)
    return tp / (tp + fn) if (tp + fn) > 0 else 0.0


def f1_score(y_true, y_pred):
    p = precision(y_true, y_pred)
    r = recall(y_true, y_pred)
    return 2 * p * r / (p + r) if (p + r) > 0 else 0.0


def roc_curve(y_true, y_scores):
    thresholds = sorted(set(y_scores), reverse=True)
    tpr_list = []
    fpr_list = []

    total_positives = sum(y_true)
    total_negatives = len(y_true) - total_positives

    for threshold in thresholds:
        y_pred = [1 if s >= threshold else 0 for s in y_scores]
        tp = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 1 and yp == 1)
        fp = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 0 and yp == 1)

        tpr = tp / total_positives if total_positives > 0 else 0.0
        fpr = fp / total_negatives if total_negatives > 0 else 0.0

        tpr_list.append(tpr)
        fpr_list.append(fpr)

    return fpr_list, tpr_list, thresholds


def auc_roc(y_true, y_scores):
    fpr_list, tpr_list, _ = roc_curve(y_true, y_scores)

    pairs = sorted(zip(fpr_list, tpr_list))
    fpr_sorted = [p[0] for p in pairs]
    tpr_sorted = [p[1] for p in pairs]

    area = 0.0
    for i in range(1, len(fpr_sorted)):
        width = fpr_sorted[i] - fpr_sorted[i - 1]
        height = (tpr_sorted[i] + tpr_sorted[i - 1]) / 2
        area += width * height

    return area

Paso 4: Métricas de regresión

def mse(y_true, y_pred):
    n = len(y_true)
    return sum((yt - yp) ** 2 for yt, yp in zip(y_true, y_pred)) / n


def rmse(y_true, y_pred):
    return math.sqrt(mse(y_true, y_pred))


def mae(y_true, y_pred):
    n = len(y_true)
    return sum(abs(yt - yp) for yt, yp in zip(y_true, y_pred)) / n


def r_squared(y_true, y_pred):
    mean_y = sum(y_true) / len(y_true)
    ss_res = sum((yt - yp) ** 2 for yt, yp in zip(y_true, y_pred))
    ss_tot = sum((yt - mean_y) ** 2 for yt in y_true)
    if ss_tot == 0:
        return 0.0
    return 1.0 - ss_res / ss_tot

Paso 5: Curvas de aprendizaje

def learning_curve(X, y, model_fn, metric_fn, train_sizes=None, val_ratio=0.2, seed=42):
    random.seed(seed)
    n = len(X)
    indices = list(range(n))
    random.shuffle(indices)

    val_size = int(n * val_ratio)
    val_idx = indices[:val_size]
    pool_idx = indices[val_size:]

    X_val = [X[i] for i in val_idx]
    y_val = [y[i] for i in val_idx]

    if train_sizes is None:
        train_sizes = [int(len(pool_idx) * r) for r in [0.1, 0.2, 0.4, 0.6, 0.8, 1.0]]

    train_scores = []
    val_scores = []

    for size in train_sizes:
        subset = pool_idx[:size]
        X_train = [X[i] for i in subset]
        y_train = [y[i] for i in subset]

        model = model_fn()
        model.fit(X_train, y_train)

        train_pred = [model.predict(x) for x in X_train]
        val_pred = [model.predict(x) for x in X_val]

        train_scores.append(metric_fn(y_train, train_pred))
        val_scores.append(metric_fn(y_val, val_pred))

    return train_sizes, train_scores, val_scores

Paso 6: Un clasificador simple para pruebas, más la demostración completa

class SimpleLogistic:
    def __init__(self, lr=0.1, epochs=100):
        self.lr = lr
        self.epochs = epochs
        self.weights = None
        self.bias = 0.0

    def sigmoid(self, z):
        z = max(-500, min(500, z))
        return 1.0 / (1.0 + math.exp(-z))

    def fit(self, X, y):
        n_features = len(X[0])
        self.weights = [0.0] * n_features
        self.bias = 0.0

        for _ in range(self.epochs):
            for xi, yi in zip(X, y):
                z = sum(w * x for w, x in zip(self.weights, xi)) + self.bias
                pred = self.sigmoid(z)
                error = yi - pred
                for j in range(n_features):
                    self.weights[j] += self.lr * error * xi[j]
                self.bias += self.lr * error

    def predict_proba(self, x):
        z = sum(w * xi for w, xi in zip(self.weights, x)) + self.bias
        return self.sigmoid(z)

    def predict(self, x):
        return 1 if self.predict_proba(x) >= 0.5 else 0


class SimpleLinearRegression:
    def __init__(self, lr=0.001, epochs=200):
        self.lr = lr
        self.epochs = epochs
        self.weights = None
        self.bias = 0.0

    def fit(self, X, y):
        n_features = len(X[0])
        self.weights = [0.0] * n_features
        self.bias = 0.0
        n = len(X)

        for _ in range(self.epochs):
            for xi, yi in zip(X, y):
                pred = sum(w * x for w, x in zip(self.weights, xi)) + self.bias
                error = yi - pred
                for j in range(n_features):
                    self.weights[j] += self.lr * error * xi[j] / n
                self.bias += self.lr * error / n

    def predict(self, x):
        return sum(w * xi for w, xi in zip(self.weights, x)) + self.bias


def standardize(values):
    n = len(values)
    mean = sum(values) / n
    var = sum((v - mean) ** 2 for v in values) / n
    std = math.sqrt(var) if var > 0 else 1.0
    return [(v - mean) / std for v in values], mean, std


def make_classification_data(n=300, seed=42):
    random.seed(seed)
    X = []
    y = []
    for _ in range(n):
        x1 = random.gauss(0, 1)
        x2 = random.gauss(0, 1)
        label = 1 if (x1 + x2 + random.gauss(0, 0.5)) > 0 else 0
        X.append([x1, x2])
        y.append(label)
    return X, y


def make_regression_data(n=200, seed=42):
    random.seed(seed)
    X = []
    y = []
    for _ in range(n):
        x1 = random.uniform(0, 10)
        x2 = random.uniform(0, 5)
        target = 3 * x1 + 2 * x2 + random.gauss(0, 2)
        X.append([x1, x2])
        y.append(target)
    return X, y


def make_imbalanced_data(n=300, minority_ratio=0.05, seed=42):
    random.seed(seed)
    X = []
    y = []
    for _ in range(n):
        if random.random() < minority_ratio:
            x1 = random.gauss(3, 0.5)
            x2 = random.gauss(3, 0.5)
            label = 1
        else:
            x1 = random.gauss(0, 1)
            x2 = random.gauss(0, 1)
            label = 0
        X.append([x1, x2])
        y.append(label)
    return X, y


if __name__ == "__main__":
    X_clf, y_clf = make_classification_data(300)

    print("=== Train/Validation/Test Split ===")
    X_train, y_train, X_val, y_val, X_test, y_test = train_val_test_split(X_clf, y_clf)
    print(f"  Train: {len(X_train)}, Val: {len(X_val)}, Test: {len(X_test)}")
    print(f"  Train class distribution: {sum(y_train)}/{len(y_train)} positive")
    print(f"  Val class distribution: {sum(y_val)}/{len(y_val)} positive")

    model = SimpleLogistic(lr=0.1, epochs=200)
    model.fit(X_train, y_train)

    print("\n=== Classification Metrics ===")
    y_pred = [model.predict(x) for x in X_test]
    tp, tn, fp, fn = confusion_matrix(y_test, y_pred)
    print(f"  Confusion matrix: TP={tp}, TN={tn}, FP={fp}, FN={fn}")
    print(f"  Accuracy:  {accuracy(y_test, y_pred):.4f}")
    print(f"  Precision: {precision(y_test, y_pred):.4f}")
    print(f"  Recall:    {recall(y_test, y_pred):.4f}")
    print(f"  F1 Score:  {f1_score(y_test, y_pred):.4f}")

    y_scores = [model.predict_proba(x) for x in X_test]
    auc = auc_roc(y_test, y_scores)
    print(f"  AUC-ROC:   {auc:.4f}")

    print("\n=== K-Fold Cross-Validation (K=5) ===")
    cv_scores = cross_validate(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=200),
        k=5,
        metric_fn=accuracy,
    )
    mean_cv = sum(cv_scores) / len(cv_scores)
    std_cv = math.sqrt(sum((s - mean_cv) ** 2 for s in cv_scores) / len(cv_scores))
    print(f"  Fold scores: {[round(s, 4) for s in cv_scores]}")
    print(f"  Mean: {mean_cv:.4f} (+/- {std_cv:.4f})")

    print("\n=== Stratified K-Fold Cross-Validation (K=5) ===")
    strat_scores = cross_validate(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=200),
        k=5,
        metric_fn=accuracy,
        stratified=True,
    )
    strat_mean = sum(strat_scores) / len(strat_scores)
    strat_std = math.sqrt(sum((s - strat_mean) ** 2 for s in strat_scores) / len(strat_scores))
    print(f"  Fold scores: {[round(s, 4) for s in strat_scores]}")
    print(f"  Mean: {strat_mean:.4f} (+/- {strat_std:.4f})")

    print("\n=== Imbalanced Data: Why Accuracy Lies ===")
    X_imb, y_imb = make_imbalanced_data(300, minority_ratio=0.05)
    positives = sum(y_imb)
    print(f"  Class distribution: {positives} positive, {len(y_imb) - positives} negative ({positives/len(y_imb)*100:.1f}% positive)")

    always_negative = [0] * len(y_imb)
    print(f"  Always-negative baseline:")
    print(f"    Accuracy:  {accuracy(y_imb, always_negative):.4f}")
    print(f"    Precision: {precision(y_imb, always_negative):.4f}")
    print(f"    Recall:    {recall(y_imb, always_negative):.4f}")
    print(f"    F1 Score:  {f1_score(y_imb, always_negative):.4f}")

    X_tr_i, y_tr_i, X_v_i, y_v_i, X_te_i, y_te_i = train_val_test_split(X_imb, y_imb)
    model_imb = SimpleLogistic(lr=0.5, epochs=500)
    model_imb.fit(X_tr_i, y_tr_i)
    y_pred_imb = [model_imb.predict(x) for x in X_te_i]
    print(f"\n  Trained model on imbalanced data:")
    print(f"    Accuracy:  {accuracy(y_te_i, y_pred_imb):.4f}")
    print(f"    Precision: {precision(y_te_i, y_pred_imb):.4f}")
    print(f"    Recall:    {recall(y_te_i, y_pred_imb):.4f}")
    print(f"    F1 Score:  {f1_score(y_te_i, y_pred_imb):.4f}")

    print("\n=== Regression Metrics ===")
    X_reg, y_reg = make_regression_data(200)

    col0 = [x[0] for x in X_reg]
    col1 = [x[1] for x in X_reg]
    col0_s, m0, s0 = standardize(col0)
    col1_s, m1, s1 = standardize(col1)
    X_reg_scaled = [[col0_s[i], col1_s[i]] for i in range(len(X_reg))]

    X_tr_r, y_tr_r, X_v_r, y_v_r, X_te_r, y_te_r = train_val_test_split(X_reg_scaled, y_reg)
    reg_model = SimpleLinearRegression(lr=0.01, epochs=500)
    reg_model.fit(X_tr_r, y_tr_r)
    y_pred_r = [reg_model.predict(x) for x in X_te_r]

    print(f"  MSE:       {mse(y_te_r, y_pred_r):.4f}")
    print(f"  RMSE:      {rmse(y_te_r, y_pred_r):.4f}")
    print(f"  MAE:       {mae(y_te_r, y_pred_r):.4f}")
    print(f"  R-squared: {r_squared(y_te_r, y_pred_r):.4f}")

    mean_baseline = [sum(y_tr_r) / len(y_tr_r)] * len(y_te_r)
    print(f"\n  Mean baseline:")
    print(f"    MSE:       {mse(y_te_r, mean_baseline):.4f}")
    print(f"    R-squared: {r_squared(y_te_r, mean_baseline):.4f}")

    print("\n=== Learning Curve ===")
    sizes, train_sc, val_sc = learning_curve(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=200),
        metric_fn=accuracy,
    )
    print(f"  {'Size':>6} {'Train':>8} {'Val':>8}")
    for s, tr, va in zip(sizes, train_sc, val_sc):
        print(f"  {s:>6} {tr:>8.4f} {va:>8.4f}")

    print("\n=== Statistical Model Comparison ===")
    model_a_scores = cross_validate(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=100),
        k=5, metric_fn=accuracy,
    )
    model_b_scores = cross_validate(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=500),
        k=5, metric_fn=accuracy,
    )
    diffs = [a - b for a, b in zip(model_a_scores, model_b_scores)]
    mean_diff = sum(diffs) / len(diffs)
    std_diff = math.sqrt(sum((d - mean_diff) ** 2 for d in diffs) / len(diffs))
    t_stat = mean_diff / (std_diff / math.sqrt(len(diffs))) if std_diff > 0 else 0.0
    print(f"  Model A (100 epochs) mean: {sum(model_a_scores)/len(model_a_scores):.4f}")
    print(f"  Model B (500 epochs) mean: {sum(model_b_scores)/len(model_b_scores):.4f}")
    print(f"  Mean difference: {mean_diff:.4f}")
    print(f"  Paired t-statistic: {t_stat:.4f}")
    print(f"  (|t| > 2.78 for significance at p<0.05 with df=4)")

Úsalo

Con scikit-learn, la evaluación se integra en el flujo de trabajo:

from sklearn.model_selection import cross_val_score, StratifiedKFold, learning_curve
from sklearn.metrics import (
    accuracy_score, precision_score, recall_score, f1_score,
    roc_auc_score, confusion_matrix, mean_squared_error, r2_score,
)
from sklearn.linear_model import LogisticRegression

model = LogisticRegression()
scores = cross_val_score(model, X, y, cv=StratifiedKFold(5), scoring="f1")

Las versiones desde cero muestran exactamente qué hace la validación cruzada (sin magia, solo bucles for y seguimiento de índice), cómo se calcula cada métrica (solo contando TP/FP/TN/FN) y por qué es importante la estratificación (preservando las proporciones de clases en cada pliegue). Las versiones de la biblioteca agregan paralelismo, más opciones de puntuación e integración con canalizaciones.

Envíalo

Esta lección produce:

  • outputs/skill-evaluation.md - una habilidad que cubre la estrategia de evaluación para modelos de clasificación y regresión

Ejercicios

  1. Implementar curvas de recuperación de precisión: trazar precisión versus recuperación en diferentes umbrales. Calcule la precisión promedio (área bajo la curva PR). Compare la curva PR con la curva ROC en un conjunto de datos desequilibrado y explique cuándo cada una es más informativa.
  2. Cree un bucle de validación cruzada anidado: el bucle externo evalúa el rendimiento del modelo, el bucle interno ajusta los hiperparámetros. Úselo para comparar dos modelos de manera justa sin filtrar datos de validación en la evaluación.
  3. Implemente una prueba de permutación para comparar modelos: mezcle las etiquetas, vuelva a entrenar y mida el rendimiento. Repita 100 veces para crear una distribución nula. Calcule el valor p para el rendimiento observado del modelo frente a esta distribución.

Términos clave

Término Lo que dice la gente Lo que realmente significa
Sobreajuste "Memorizar los datos del entrenamiento" El modelo captura el ruido en los datos de entrenamiento y funciona bien en el entrenamiento pero mal en datos invisibles.
Validación cruzada "Pruebas en diferentes subconjuntos" Rotar sistemáticamente qué porción de datos se utiliza para la validación, promediando los resultados en todas las rotaciones
Precisión "Cuántos positivos previstos son correctos" TP / (TP + FP): la fracción de predicciones positivas que en realidad son positivas
Recordar "Cuántos aspectos positivos reales encontramos" TP / (TP + FN): la fracción de positivos reales que fueron identificados correctamente
AUC-ROC "Qué bien separa clases el modelo" El área bajo la curva de la tasa de verdaderos positivos frente a la tasa de falsos positivos en todos los umbrales, desde 0,5 (aleatorio) hasta 1,0 (perfecto)
R cuadrado "Cuánta varianza se explica" 1 - (suma de residuos al cuadrado / suma total de cuadrados): la fracción de la varianza objetivo capturada por el modelo
Fuga de datos "La modelo engañó" Usar inentrenamiento durante el entrenamiento que no estaría disponible en el momento de la predicción, lo que lleva a una evaluación optimista
Curva de aprendizaje "Cómo cambia el rendimiento con más datos" Un gráfico de puntuaciones de entrenamiento y validación frente al tamaño del conjunto de entrenamiento, que revela un ajuste insuficiente o excesivo
División estratificada "Mantener equilibrada la proporción de clases" Dividir datos para que cada subconjunto tenga la misma proporción de cada clase que el conjunto de datos completo

Lectura adicional

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).