Introducción al Machine Learning con Python: conceptos, flujo y primeros modelos

📅 Actualizado en marzo 2026 📊 Nivel: Intermedio ⏱️ 22 min de lectura
The Analytical Engine has no pretensions whatever to originate anything. It can do whatever we know how to order it to perform.
«El Motor Analítico no pretende en absoluto originar nada. Solo puede hacer lo que sabemos cómo ordenarle que ejecute»
Ada Lovelace · Primera programadora · 1815 – 1852
Ada Lovelace escribió el primer algoritmo para la máquina de Babbage en 1843 y formuló una pregunta que todavía se debate: ¿puede una máquina originar algo, o solo ejecuta lo que le ordena su programador? El Machine Learning invierte la perspectiva que Ada conocía: en lugar de que el programador defina cada regla, el sistema infiere las reglas a partir de los datos. La máquina no origina —sigue siendo matemática aplicada— pero el origen de sus instrucciones no está en la mente del programador sino en los patrones estadísticos del conjunto de entrenamiento. Ada habría encontrado esa idea fascinante, y probablemente habría escrito el primer artículo crítico sobre sus límites.

El Machine Learning no es magia ni ciencia ficción: es estadística y álgebra lineal aplicadas a datos, con Python como herramienta. Un modelo de ML es una función matemática cuyos parámetros se ajustan automáticamente para que sus predicciones se acerquen lo máximo posible a los valores reales del conjunto de entrenamiento. Esta lección te da el mapa completo: qué es, cómo funciona, y cómo construir y evaluar tus primeros modelos con scikit-learn.

🗺️ Tipos de aprendizaje: supervisado, no supervisado y refuerzo

# ── APRENDIZAJE SUPERVISADO ───────────────────────────────────────────────────
# Los datos de entrenamiento incluyen la "respuesta correcta" (etiqueta)
# El modelo aprende a mapear entrada → salida

# Regresión: la salida es un valor continuo
# Ejemplos: precio de una casa, temperatura mañana, demanda de un producto

# Clasificación: la salida es una categoría
# Ejemplos: spam/no spam, diagnóstico médico, categoría de producto

# Notación estándar:
# X  → features (variables de entrada), shape (n_muestras, n_features)
# y  → target (variable objetivo), shape (n_muestras,)
# ŷ  → predicción del modelo

# ── APRENDIZAJE NO SUPERVISADO ────────────────────────────────────────────────
# Los datos NO tienen etiquetas — el modelo descubre estructura por sí solo

# Clustering: agrupar muestras similares
# Ejemplos: segmentación de clientes, agrupación de documentos

# Reducción de dimensionalidad: comprimir datos conservando información
# Ejemplos: PCA, t-SNE para visualización

# Detección de anomalías: identificar puntos que no siguen el patrón
# Ejemplos: fraude bancario, fallos de maquinaria

# ── APRENDIZAJE POR REFUERZO ─────────────────────────────────────────────────
# Un agente aprende interactuando con un entorno: acciones → recompensas
# Ejemplos: juegos (AlphaGo), robots, sistemas de recomendación

# ── CUADRO RESUMEN ────────────────────────────────────────────────────────────
tipos = {
    "Supervisado - Regresión":      ["Precio de vivienda", "Demanda de stock", "Temperatura"],
    "Supervisado - Clasificación":  ["Spam/No spam", "Diagnóstico", "Fraude"],
    "No supervisado - Clustering":  ["Segmentos de clientes", "Temas en documentos"],
    "No supervisado - Reducción":   ["Visualizar datos HD", "Comprimir features"],
    "Refuerzo":                     ["Juegos", "Robots autónomos", "Recomendación"],
}

🔄 El flujo completo de un proyecto ML

# Todo proyecto de Machine Learning sigue este ciclo:
#
# 1. DEFINIR EL PROBLEMA
#    ¿Qué quiero predecir? ¿Qué datos tengo? ¿Cuál es la métrica de éxito?
#
# 2. RECOPILAR Y ENTENDER LOS DATOS
#    Exploración (EDA), visualización, estadísticas descriptivas
#
# 3. PREPARAR LOS DATOS
#    Limpiar (valores faltantes, outliers), transformar (encoding, scaling)
#    Dividir en entrenamiento / validación / test
#
# 4. ELEGIR Y ENTRENAR UN MODELO
#    Empezar simple, probar varios, ajustar hiperparámetros
#
# 5. EVALUAR EL MODELO
#    Métricas en el conjunto de validación, detectar over/underfitting
#
# 6. MEJORAR
#    Más datos, mejores features, otro modelo, ajuste de hiperparámetros
#
# 7. DESPLEGAR Y MONITORIZAR
#    Poner en producción, vigilar que el rendimiento no se degrada

# El error más frecuente: saltarse los pasos 2 y 3, ir directamente a entrenar.
# En la práctica, el 70-80% del tiempo se invierte en preparar datos bien.

# Importaciones estándar de cualquier proyecto ML
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing   import StandardScaler, LabelEncoder
from sklearn.pipeline        import Pipeline
from sklearn.metrics         import (mean_squared_error, r2_score,
                                     accuracy_score, classification_report,
                                     confusion_matrix)
Pantalla de ordenador con gráficas de datos estadísticos, curvas de aprendizaje y visualizaciones de un modelo de machine learning
Un proyecto de Machine Learning es más análisis de datos que código. La mayor parte del tiempo se invierte en entender los datos, limpiarlos y transformarlos antes de entrenar el primer modelo. Las gráficas de curvas de aprendizaje revelan overfitting o underfitting antes de que afecten al sistema en producción. Fuente: Pexels (licencia libre).

🧰 El ecosistema: NumPy, Pandas, scikit-learn y Matplotlib

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris, load_wine, fetch_california_housing

# ── NUMPY: operaciones matriciales rápidas ────────────────────────────────────
# La base de todo el ecosistema científico Python

a = np.array([1, 2, 3, 4, 5])
b = np.array([10, 20, 30, 40, 50])

a + b               # [11, 22, 33, 44, 55]  — operaciones vectorizadas
a * 2               # [2, 4, 6, 8, 10]
np.dot(a, b)        # 550  — producto escalar
np.mean(a)          # 3.0
np.std(a)           # 1.414...

# Arrays 2D (matrices) — la estructura nativa de los datos ML
X = np.array([[1, 2], [3, 4], [5, 6]])  # shape (3, 2) → 3 muestras, 2 features
X.shape             # (3, 2)
X.T                 # transpuesta: shape (2, 3)
X[:, 0]             # primera columna (feature 0): [1, 3, 5]
X[1, :]             # segunda fila (muestra 1): [3, 4]


# ── PANDAS: manipulación de datos tabulares ───────────────────────────────────
# La herramienta para cargar, explorar y limpiar datasets

df = pd.DataFrame({
    "m2":       [50, 80, 120, 65, 95, 200, 45, 110],
    "hab":      [2,  3,   4,  2,  3,   5,  1,   3],
    "barrio":   ["Centro","Norte","Centro","Sur","Norte","Centro","Sur","Norte"],
    "precio":   [180, 240, 350, 170, 270, 580, 130, 310],
})

# Exploración básica — siempre lo primero
df.shape            # (8, 4)
df.dtypes           # tipos de cada columna
df.describe()       # estadísticas descriptivas
df.isnull().sum()   # valores faltantes por columna
df.value_counts("barrio")  # frecuencia de cada categoría

# Seleccionar features y target
X = df[["m2", "hab"]]      # DataFrame con las features
y = df["precio"]            # Serie con el target


# ── SCIKIT-LEARN: la API unificada ────────────────────────────────────────────
# Todos los modelos siguen la misma interfaz:
#   modelo.fit(X_train, y_train)   ← entrenar
#   modelo.predict(X_test)         ← predecir
#   modelo.score(X_test, y_test)   ← evaluar

from sklearn.linear_model import LinearRegression

modelo = LinearRegression()
modelo.fit(X, y)
modelo.predict([[100, 3]])   # predice precio para 100m² y 3 habitaciones


# ── MATPLOTLIB: visualización ─────────────────────────────────────────────────
# Indispensable para entender los datos antes de modelar

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# Distribución del target
axes[0].hist(y, bins=10, color="#4a9eff", edgecolor="white")
axes[0].set_title("Distribución de precios")
axes[0].set_xlabel("Precio (miles €)")

# Correlación feature-target
axes[1].scatter(df["m2"], y, alpha=0.7, color="#ff6b35")
axes[1].set_xlabel("Metros cuadrados")
axes[1].set_ylabel("Precio (miles €)")
axes[1].set_title("Precio vs m²")

plt.tight_layout()
plt.savefig("eda.png", dpi=150)
plt.show()

📈 Regresión lineal: predecir valores continuos

import numpy as np
import pandas as pd
from sklearn.datasets        import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing   import StandardScaler
from sklearn.linear_model    import LinearRegression, Ridge, Lasso
from sklearn.metrics         import mean_squared_error, r2_score, mean_absolute_error

# ── CARGAR DATOS ─────────────────────────────────────────────────────────────
housing = fetch_california_housing(as_frame=True)
X = housing.data        # 8 features: MedInc, HouseAge, AveRooms...
y = housing.target      # precio mediano en $100k

print(f"Dataset: {X.shape[0]:,} muestras, {X.shape[1]} features")
print(f"Target: precio medio = ${y.mean()*100:.0f}k, rango [{y.min():.2f}, {y.max():.2f}]")

# ── DIVIDIR EN ENTRENAMIENTO Y TEST ─────────────────────────────────────────
# test_size=0.2 → 20% para test, 80% para entrenamiento
# random_state → fija la semilla para reproducibilidad
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
print(f"Train: {len(X_train):,} muestras  |  Test: {len(X_test):,} muestras")

# ── ESCALAR FEATURES ─────────────────────────────────────────────────────────
# StandardScaler: media=0, std=1 — mejora estabilidad numérica
# IMPORTANTE: fit solo en train, transform en ambos (evita data leakage)
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)   # aprende media/std en train
X_test_s  = scaler.transform(X_test)        # aplica sin recalcular

# ── REGRESIÓN LINEAL ─────────────────────────────────────────────────────────
lr = LinearRegression()
lr.fit(X_train_s, y_train)

y_pred = lr.predict(X_test_s)

rmse = mean_squared_error(y_test, y_pred, squared=False)
mae  = mean_absolute_error(y_test, y_pred)
r2   = r2_score(y_test, y_pred)

print(f"
── Regresión Lineal ──")
print(f"RMSE:  {rmse:.4f}  (error cuadrático medio raíz)")
print(f"MAE:   {mae:.4f}  (error absoluto medio)")
print(f"R²:    {r2:.4f}  (1.0 = perfecto, 0.0 = predice la media)")

# Coeficientes — qué peso tiene cada feature
for feat, coef in zip(X.columns, lr.coef_):
    print(f"  {feat:<20} {coef:+.4f}")

# ── REGULARIZACIÓN: RIDGE Y LASSO ────────────────────────────────────────────
# Ridge (L2): penaliza coeficientes grandes — reduce overfitting
# Lasso (L1): penaliza y pone algunos coeficientes exactamente a 0 (selección de features)
# alpha: intensidad de la regularización (mayor alpha → más penalización)

for nombre, modelo in [("Ridge (α=1)", Ridge(alpha=1)),
                       ("Ridge (α=10)", Ridge(alpha=10)),
                       ("Lasso (α=0.1)", Lasso(alpha=0.1))]:
    modelo.fit(X_train_s, y_train)
    r2 = r2_score(y_test, modelo.predict(X_test_s))
    n_cero = sum(abs(c) < 1e-6 for c in modelo.coef_)
    print(f"{nombre:<20} R²={r2:.4f}  coefs_cero={n_cero}")
💡 Data leakage: Uno de los errores más graves en ML. Ocurre cuando información del conjunto de test "contamina" el entrenamiento. El caso más frecuente: hacer scaler.fit_transform(X_completo) antes de dividir. La media y la desviación típica del scaler incluirían información de los datos de test, dando métricas artificialmente optimistas. Regla: todo fit solo sobre los datos de entrenamiento.

🎯 Clasificación: predecir categorías

import numpy as np
from sklearn.datasets        import load_iris
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing   import StandardScaler
from sklearn.linear_model    import LogisticRegression
from sklearn.tree            import DecisionTreeClassifier
from sklearn.ensemble        import RandomForestClassifier
from sklearn.svm             import SVC
from sklearn.metrics         import accuracy_score, classification_report, confusion_matrix

# ── DATASET IRIS ─────────────────────────────────────────────────────────────
iris = load_iris()
X, y = iris.data, iris.target
# 150 flores, 4 features (longitud/anchura de sépalos y pétalos), 3 clases

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y  # stratify: mantiene proporción de clases
)

scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s  = scaler.transform(X_test)

# ── COMPARAR VARIOS CLASIFICADORES ───────────────────────────────────────────
clasificadores = {
    "Regresión Logística": LogisticRegression(max_iter=200),
    "Árbol de Decisión":   DecisionTreeClassifier(max_depth=4, random_state=42),
    "Random Forest":       RandomForestClassifier(n_estimators=100, random_state=42),
    "SVM (RBF)":           SVC(kernel="rbf", C=1.0),
}

print(f"{'Modelo':<25} {'Acc. Train':>10} {'Acc. Test':>10} {'CV 5-fold':>10}")
print("─" * 60)

for nombre, clf in clasificadores.items():
    clf.fit(X_train_s, y_train)

    acc_train = clf.score(X_train_s, y_train)
    acc_test  = clf.score(X_test_s, y_test)

    # Cross-validation: estimación más robusta del rendimiento real
    cv_scores = cross_val_score(clf, X_train_s, y_train, cv=5, scoring="accuracy")

    print(f"{nombre:<25} {acc_train:>10.3f} {acc_test:>10.3f} "
          f"{cv_scores.mean():>7.3f}±{cv_scores.std():.3f}")

# ── ANÁLISIS DETALLADO DEL MEJOR MODELO ──────────────────────────────────────
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train_s, y_train)
y_pred = rf.predict(X_test_s)

print("
── Reporte de clasificación (Random Forest) ──")
print(classification_report(y_test, y_pred, target_names=iris.target_names))

# Matriz de confusión
cm = confusion_matrix(y_test, y_pred)
print("Matriz de confusión:")
print(cm)
# Filas = clase real, columnas = clase predicha
# Diagonal = predicciones correctas

# Importancia de features
print("
── Importancia de features ──")
for feat, imp in sorted(zip(iris.feature_names, rf.feature_importances_),
                         key=lambda x: -x[1]):
    barra = "█" * int(imp * 40)
    print(f"  {feat:<30} {imp:.3f}  {barra}")

# ── ÁRBOL DE DECISIÓN: INTERPRETABLE ─────────────────────────────────────────
# El árbol de decisión es el modelo más interpretable: cada nodo es una regla
dt = DecisionTreeClassifier(max_depth=3, random_state=42)
dt.fit(X_train_s, y_train)

# Exportar reglas como texto
from sklearn.tree import export_text
reglas = export_text(dt, feature_names=list(iris.feature_names))
print("
── Reglas del árbol (max_depth=3) ──")
print(reglas[:600])  # primeras reglas
Infografía del flujo completo de Machine Learning: desde datos crudos hasta evaluación del modelo, con los pasos de EDA, preprocesado, train/test split, entrenamiento, métricas y ajuste de hiperparámetros
El flujo estándar de un proyecto de Machine Learning con Python. La fase de preparación de datos (EDA + limpieza + encoding + scaling) ocupa la mayor parte del tiempo real. La división train/test/validation es el mecanismo fundamental para detectar overfitting antes de que el modelo llegue a producción. Infografía: Ciberaula.

⚖️ Validación: overfitting, underfitting y cross-validation

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets        import make_regression
from sklearn.model_selection import train_test_split, cross_val_score, learning_curve
from sklearn.tree            import DecisionTreeRegressor
from sklearn.linear_model    import Ridge

# ── OVERFITTING VS UNDERFITTING ───────────────────────────────────────────────
# Underfitting: modelo demasiado simple → error alto en train Y en test
# Overfitting:  modelo demasiado complejo → error bajo en train, alto en test
# Good fit:     error bajo en ambos

X, y = make_regression(n_samples=200, n_features=1, noise=20, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

print(f"{'max_depth':<12} {'RMSE Train':>12} {'RMSE Test':>12} {'Diagnóstico'}")
print("─" * 55)

for depth in [1, 2, 5, 10, None]:
    dt = DecisionTreeRegressor(max_depth=depth, random_state=42)
    dt.fit(X_train, y_train)

    rmse_train = mean_squared_error(y_train, dt.predict(X_train), squared=False)
    rmse_test  = mean_squared_error(y_test,  dt.predict(X_test),  squared=False)
    gap        = rmse_test - rmse_train

    if depth == 1:
        diag = "Underfitting"
    elif gap > 20:
        diag = "Overfitting ⚠️"
    else:
        diag = "OK ✅"

    depth_str = str(depth) if depth else "sin límite"
    print(f"{depth_str:<12} {rmse_train:>12.2f} {rmse_test:>12.2f}  {diag}")

# ── CROSS-VALIDATION (VALIDACIÓN CRUZADA) ────────────────────────────────────
# Divide los datos en K partes (folds). Entrena K veces, cada vez usando
# K-1 folds para train y 1 para validación. Devuelve K métricas.
# Ventaja: usa todos los datos para validar, estimación más robusta

from sklearn.model_selection import cross_val_score, KFold, StratifiedKFold
from sklearn.ensemble        import RandomForestClassifier
from sklearn.datasets        import load_wine

X_w, y_w = load_wine(return_X_y=True)

rf = RandomForestClassifier(n_estimators=100, random_state=42)

# CV estándar (5 folds)
scores = cross_val_score(rf, X_w, y_w, cv=5, scoring="accuracy")
print(f"
CV 5-fold accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
print(f"Scores por fold: {scores.round(3)}")

# StratifiedKFold — mantiene la proporción de clases en cada fold (recomendado)
skf    = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(rf, X_w, y_w, cv=skf, scoring="accuracy")
print(f"CV Stratified:   {scores.mean():.3f} ± {scores.std():.3f}")

# ── CURVAS DE APRENDIZAJE ─────────────────────────────────────────────────────
# Muestra cómo evoluciona el rendimiento con más datos de entrenamiento
# Útil para saber si más datos ayudarían

train_sizes, train_scores, val_scores = learning_curve(
    rf, X_w, y_w, cv=5,
    train_sizes=np.linspace(0.1, 1.0, 10),
    scoring="accuracy", random_state=42
)

# Interpretar:
# Gap grande entre train y val → overfitting (más datos o modelo más simple)
# Ambas curvas bajas → underfitting (modelo más complejo)
# Ambas curvas altas y juntas → buen ajuste
print("
Curva de aprendizaje (últimos 3 puntos):")
for ts, tr, vs in zip(train_sizes[-3:], train_scores[-3:], val_scores[-3:]):
    print(f"  {int(ts)} muestras train  → train={tr.mean():.3f}  val={vs.mean():.3f}")

📊 Métricas: cómo saber si tu modelo es bueno

from sklearn.metrics import (
    # Regresión
    mean_squared_error, mean_absolute_error, r2_score,
    # Clasificación
    accuracy_score, precision_score, recall_score, f1_score,
    classification_report, confusion_matrix, roc_auc_score
)
import numpy as np

# ── MÉTRICAS DE REGRESIÓN ────────────────────────────────────────────────────
y_real  = np.array([100, 200, 300, 400, 500])
y_pred  = np.array([110, 190, 320, 380, 510])

mse  = mean_squared_error(y_real, y_pred)
rmse = mean_squared_error(y_real, y_pred, squared=False)
mae  = mean_absolute_error(y_real, y_pred)
r2   = r2_score(y_real, y_pred)

print(f"MSE:  {mse:.2f}   (penaliza errores grandes al cuadrar)")
print(f"RMSE: {rmse:.2f}  (en las mismas unidades que y — más interpretable)")
print(f"MAE:  {mae:.2f}   (robusto a outliers, no penaliza tanto los errores grandes)")
print(f"R²:   {r2:.4f}  (proporción de varianza explicada: 1=perfecto, 0=media)")

# ── MÉTRICAS DE CLASIFICACIÓN ─────────────────────────────────────────────────
# Para clasificación binaria: spam/no spam, fraude/no fraude
y_real = np.array([0, 1, 1, 0, 1, 1, 0, 0, 1, 0])
y_pred = np.array([0, 1, 0, 0, 1, 1, 1, 0, 1, 0])

# Matriz de confusión
# [[TN, FP]       TN = verdaderos negativos (correctos clase 0)
#  [FN, TP]]      FP = falsos positivos (predijo 1, era 0) ← Error tipo I
#                 FN = falsos negativos (predijo 0, era 1) ← Error tipo II
#                 TP = verdaderos positivos (correctos clase 1)
cm = confusion_matrix(y_real, y_pred)
tn, fp, fn, tp = cm.ravel()
print(f"
Matriz de confusión:
{cm}")
print(f"TN={tn}  FP={fp}  FN={fn}  TP={tp}")

accuracy  = accuracy_score(y_real, y_pred)    # (TP+TN) / total
precision = precision_score(y_real, y_pred)   # TP / (TP+FP): de los que predije 1, ¿cuántos son 1?
recall    = recall_score(y_real, y_pred)      # TP / (TP+FN): de los que son 1, ¿cuántos predije 1?
f1        = f1_score(y_real, y_pred)          # media armónica de precision y recall

print(f"
Accuracy:  {accuracy:.3f}  (no sirve con clases desbalanceadas)")
print(f"Precision: {precision:.3f}  (optimiza si los FP son costosos: spam, alertas)")
print(f"Recall:    {recall:.3f}  (optimiza si los FN son costosos: cancer, fraude)")
print(f"F1-score:  {f1:.3f}  (equilibrio: cuando ambos importan)")

# ¿Cuándo usar cada métrica?
metricas_guia = {
    "Detección de cáncer":   "Recall alto (un FN puede costar una vida)",
    "Filtro de spam":        "Precision alta (un FP borra email importante)",
    "Scoring de crédito":    "F1 + AUC-ROC (clases desbalanceadas)",
    "Clasificación general": "Accuracy si clases balanceadas, F1 si no",
}
for caso, metrica in metricas_guia.items():
    print(f"  {caso:<28} → {metrica}")

🔧 Pipeline de scikit-learn: encadenar pasos

from sklearn.pipeline        import Pipeline
from sklearn.compose         import ColumnTransformer
from sklearn.preprocessing   import StandardScaler, OneHotEncoder, SimpleImputer
from sklearn.ensemble        import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.datasets        import fetch_openml
import pandas as pd
import numpy as np

# ── QUÉ ES UN PIPELINE ───────────────────────────────────────────────────────
# Encadena transformaciones + modelo en un objeto único.
# Ventajas:
# 1. Evita data leakage: fit solo en train, transform automático en test
# 2. Simplifica el código: .fit() y .predict() en un solo paso
# 3. Compatible con GridSearchCV: busca hiperparámetros en todo el pipeline
# 4. Serializable: guarda pipeline completo con joblib

# ── PIPELINE BÁSICO ───────────────────────────────────────────────────────────
from sklearn.linear_model import LogisticRegression
from sklearn.datasets     import load_iris

X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

pipeline = Pipeline([
    ("scaler", StandardScaler()),              # paso 1: normalizar
    ("modelo", LogisticRegression())           # paso 2: clasificar
])

pipeline.fit(X_train, y_train)                 # fit automáticamente en orden
print(f"Pipeline accuracy: {pipeline.score(X_test, y_test):.3f}")
# No hay riesgo de data leakage: StandardScaler solo aprende en X_train

# ── PIPELINE CON DATOS MIXTOS (numéricos + categóricos) ─────────────────────
# Caso real: dataset con columnas de distintos tipos

datos = pd.DataFrame({
    "m2":      [50, 80, np.nan, 65, 95, 200, 45, 110, 75, 130],
    "hab":     [2,   3,   4,   2,  3,   5,  1,   3,  2,   4],
    "barrio":  ["Centro","Norte","Centro","Sur","Norte",
                "Centro","Sur","Norte","Centro","Sur"],
    "precio":  [0, 1, 1, 0, 1, 1, 0, 1, 0, 1],  # 1=caro, 0=asequible
})

X = datos.drop("precio", axis=1)
y = datos["precio"]
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

# Transformadores por tipo de columna
num_features  = ["m2", "hab"]
cat_features  = ["barrio"]

num_transformer = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),  # rellenar NaN con mediana
    ("scaler",  StandardScaler()),                  # normalizar
])

cat_transformer = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),   # rellenar NaN con moda
    ("encoder", OneHotEncoder(handle_unknown="ignore")),    # codificar categorías
])

# ColumnTransformer aplica transformaciones distintas a columnas distintas
preprocesador = ColumnTransformer([
    ("num", num_transformer, num_features),
    ("cat", cat_transformer, cat_features),
])

# Pipeline completo: preprocesado + modelo
pipeline_completo = Pipeline([
    ("preprocesado", preprocesador),
    ("modelo", RandomForestClassifier(n_estimators=50, random_state=42)),
])

pipeline_completo.fit(X_train, y_train)
print(f"Pipeline completo accuracy: {pipeline_completo.score(X_test, y_test):.3f}")

# ── BÚSQUEDA DE HIPERPARÁMETROS CON GRIDSEARCHCV ─────────────────────────────
# Nombre del paso + __ + nombre del parámetro
param_grid = {
    "modelo__n_estimators": [50, 100],
    "modelo__max_depth":    [None, 5, 10],
    "modelo__min_samples_split": [2, 5],
}

grid_search = GridSearchCV(
    pipeline_completo, param_grid,
    cv=3, scoring="accuracy", n_jobs=-1, verbose=0
)
grid_search.fit(X_train, y_train)
print(f"Mejores parámetros: {grid_search.best_params_}")
print(f"Mejor CV accuracy:  {grid_search.best_score_:.3f}")

# Guardar el pipeline entrenado
import joblib
joblib.dump(grid_search.best_estimator_, "modelo_produccion.joblib")
# Cargar después:
# modelo = joblib.load("modelo_produccion.joblib")
# modelo.predict(nuevos_datos)
Ficha de referencia de scikit-learn: API común de modelos, flujo train-test-split, métricas de regresión y clasificación, y estructura de un Pipeline con ColumnTransformer
La API de scikit-learn en una imagen: todos los modelos comparten fit/predict/score; el Pipeline encadena transformaciones y modelo evitando data leakage; GridSearchCV optimiza hiperparámetros de todo el pipeline a la vez. Infografía: Ciberaula.

🛠️ Programa completo: predecir precios de viviendas

"""
Proyecto completo de ML: predicción de precios de viviendas.
Flujo: EDA → preprocesado → comparación de modelos → hiperparámetros → evaluación.
Dataset: California Housing (scikit-learn, 20.640 muestras, 8 features).
"""

import numpy as np
import pandas as pd
from sklearn.datasets        import fetch_california_housing
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV
from sklearn.preprocessing   import StandardScaler
from sklearn.pipeline        import Pipeline
from sklearn.linear_model    import LinearRegression, Ridge, Lasso
from sklearn.tree            import DecisionTreeRegressor
from sklearn.ensemble        import RandomForestRegressor, GradientBoostingRegressor
from sklearn.metrics         import mean_squared_error, mean_absolute_error, r2_score
import warnings
warnings.filterwarnings("ignore")

# ── 1. CARGAR Y EXPLORAR ──────────────────────────────────────────────────────
housing = fetch_california_housing(as_frame=True)
df = housing.frame

print("=" * 60)
print("PROYECTO ML — PREDICCIÓN DE PRECIOS DE VIVIENDAS")
print("=" * 60)
print(f"
1. EXPLORACIÓN")
print(f"   {df.shape[0]:,} muestras  ·  {df.shape[1]} columnas")
print(f"   Target: {housing.target_names[0]}  ({df['MedHouseVal'].describe()['mean']*100:.0f}k$ media)")
print(f"   Valores faltantes: {df.isnull().sum().sum()}")

# Correlaciones con el target
corr = df.corr()["MedHouseVal"].drop("MedHouseVal").sort_values(ascending=False)
print(f"
   Correlaciones con precio:")
for feat, c in corr.items():
    barra = ("+" if c > 0 else "") + "█" * int(abs(c) * 20)
    print(f"   {feat:<15} {c:+.3f}  {barra}")

# ── 2. PREPARAR DATOS ─────────────────────────────────────────────────────────
print(f"
2. PREPARACIÓN")
X = housing.data
y = housing.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
print(f"   Train: {len(X_train):,}  ·  Test: {len(X_test):,}")

# ── 3. COMPARAR MODELOS ───────────────────────────────────────────────────────
print(f"
3. COMPARACIÓN DE MODELOS")
print(f"   {'Modelo':<30} {'RMSE CV':>10} {'R² CV':>8}")
print(f"   {'─'*50}")

modelos = {
    "Regresión Lineal":       LinearRegression(),
    "Ridge (α=1)":            Ridge(alpha=1),
    "Árbol Decisión (d=5)":   DecisionTreeRegressor(max_depth=5, random_state=42),
    "Random Forest":          RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1),
    "Gradient Boosting":      GradientBoostingRegressor(n_estimators=100, random_state=42),
}

scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s  = scaler.transform(X_test)

resultados = {}
for nombre, modelo in modelos.items():
    # Cross-validation con neg_rmse (sklearn devuelve negativo)
    cv_rmse = -cross_val_score(
        modelo, X_train_s, y_train,
        cv=5, scoring="neg_root_mean_squared_error", n_jobs=-1
    )
    cv_r2 = cross_val_score(
        modelo, X_train_s, y_train, cv=5, scoring="r2", n_jobs=-1
    )
    resultados[nombre] = {"rmse_cv": cv_rmse.mean(), "r2_cv": cv_r2.mean()}
    print(f"   {nombre:<30} {cv_rmse.mean():>10.4f} {cv_r2.mean():>8.4f}")

# ── 4. AJUSTAR EL MEJOR MODELO ────────────────────────────────────────────────
mejor_nombre = min(resultados, key=lambda k: resultados[k]["rmse_cv"])
print(f"
4. AJUSTE DE HIPERPARÁMETROS — {mejor_nombre}")

pipeline_final = Pipeline([
    ("scaler", StandardScaler()),
    ("modelo", GradientBoostingRegressor(random_state=42)),
])

param_grid = {
    "modelo__n_estimators": [100, 200],
    "modelo__max_depth":    [3, 5],
    "modelo__learning_rate":[0.05, 0.1],
}

grid = GridSearchCV(
    pipeline_final, param_grid, cv=3,
    scoring="neg_root_mean_squared_error", n_jobs=-1, verbose=0
)
grid.fit(X_train, y_train)

print(f"   Mejores hiperparámetros: {grid.best_params_}")
print(f"   Mejor CV RMSE: {-grid.best_score_:.4f}")

# ── 5. EVALUACIÓN FINAL EN TEST ───────────────────────────────────────────────
print(f"
5. EVALUACIÓN FINAL EN TEST (datos nunca vistos)")
y_pred = grid.best_estimator_.predict(X_test)

rmse = mean_squared_error(y_test, y_pred, squared=False)
mae  = mean_absolute_error(y_test, y_pred)
r2   = r2_score(y_test, y_pred)

print(f"   RMSE: {rmse:.4f}  (~${rmse*100:.0f}k de error medio cuadrático)")
print(f"   MAE:  {mae:.4f}  (~${mae*100:.0f}k de error absoluto medio)")
print(f"   R²:   {r2:.4f}  ({r2*100:.1f}% de la varianza explicada)")

# Análisis de residuos
residuos = y_test - y_pred
print(f"
   Análisis de residuos:")
print(f"   Media:    {residuos.mean():.4f}  (cercana a 0 = sin sesgo sistemático)")
print(f"   Std:      {residuos.std():.4f}")
print(f"   Máx:      {residuos.max():.4f}  (peor sobreestimación)")
print(f"   Mín:      {residuos.min():.4f}  (peor subestimación)")

# Predicciones de ejemplo
print(f"
   EJEMPLOS DE PREDICCIÓN:")
print(f"   {'Real':>10} {'Predicho':>10} {'Error':>10} {'Error%':>8}")
idx_ejemplo = np.random.RandomState(42).choice(len(y_test), 5, replace=False)
for i in idx_ejemplo:
    real  = y_test.iloc[i] * 100
    pred  = y_pred[i] * 100
    error = pred - real
    pct   = error / real * 100
    print(f"   ${real:>8.0f}k  ${pred:>8.0f}k  {error:>+9.0f}k  {pct:>+7.1f}%")

print(f"
{'='*60}")
print(f"  RESULTADO FINAL: R² = {r2:.4f}  ·  RMSE = ${rmse*100:.0f}k")
print(f"  Modelo guardado listo para producción: pipeline_final.joblib")
print(f"{'='*60}
")

# Guardar
import joblib
joblib.dump(grid.best_estimator_, "pipeline_viviendas.joblib")

🐛 Errores clásicos en proyectos ML

1. Data leakage: el más peligroso y silencioso

# ❌ Escalar antes de dividir — el scaler "ve" los datos de test
scaler = StandardScaler()
X_todo_escalado = scaler.fit_transform(X)           # ← incorrecto
X_train, X_test = train_test_split(X_todo_escalado)

# ✅ Dividir primero, escalar solo en train
X_train, X_test = train_test_split(X, random_state=42)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)   # aprende en train
X_test  = scaler.transform(X_test)        # aplica sin aprender

# ✅✅ Mejor aún: Pipeline (garantiza esto automáticamente)
pipe = Pipeline([("scaler", StandardScaler()), ("modelo", RandomForestRegressor())])
pipe.fit(X_train_raw, y_train)   # scaler.fit solo en X_train_raw

2. Evaluar en los datos de entrenamiento

# ❌ Medir accuracy en train — siempre será alta, no indica nada útil
modelo.fit(X_train, y_train)
print(modelo.score(X_train, y_train))   # ← métricas infladas

# ✅ Siempre evaluar en datos no vistos durante el entrenamiento
print(modelo.score(X_test, y_test))     # ← métrica real

3. Ignorar el desbalanceo de clases

# En fraude bancario: 99% clase 0 (legítimo), 1% clase 1 (fraude)
# Un modelo que siempre predice 0 tiene accuracy del 99% — pero es inútil

# ✅ Usar métricas apropiadas: precision, recall, F1, AUC-ROC
from sklearn.metrics import classification_report
print(classification_report(y_test, y_pred))   # muestra las métricas por clase

# ✅ Usar class_weight para compensar
modelo = RandomForestClassifier(class_weight="balanced", random_state=42)

# ✅ Usar stratify en train_test_split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, stratify=y, random_state=42   # mantiene proporción de clases
)

4. No establecer random_state — resultados no reproducibles

# ❌ Sin semilla: cada ejecución da resultados distintos
X_train, X_test = train_test_split(X, y)
modelo = RandomForestClassifier()

# ✅ Con semilla fija: resultados reproducibles
X_train, X_test = train_test_split(X, y, random_state=42)
modelo = RandomForestClassifier(random_state=42)
# Ahora cualquier persona que ejecute el código obtiene el mismo resultado

5. Usar el conjunto de test para ajustar hiperparámetros

# ❌ Ajustar hiperparámetros mirando el test set
for alpha in [0.1, 1, 10]:
    modelo = Ridge(alpha=alpha)
    modelo.fit(X_train, y_train)
    if modelo.score(X_test, y_test) > mejor_score:   # ← estás usando test para decidir
        mejor_alpha = alpha

# ✅ Usar cross-validation o un conjunto de validación separado
from sklearn.model_selection import GridSearchCV
grid = GridSearchCV(Ridge(), {"alpha": [0.1, 1, 10]}, cv=5)
grid.fit(X_train, y_train)   # test nunca se toca durante la búsqueda
# Solo al final: grid.score(X_test, y_test)

✅ Resumen y próximos pasos

El Machine Learning con Python se asienta sobre cuatro pilares: NumPy para el álgebra matricial, Pandas para la manipulación de datos, scikit-learn para los modelos, y Matplotlib para visualizar. La API de scikit-learn —fit, predict, score— es uniforme para todos los modelos, y el Pipeline garantiza que el flujo de preprocesado no introduce data leakage.

Esta lección cierra el Módulo 8 y el curso completo de Python. Los pasos naturales desde aquí son profundizar en Deep Learning con PyTorch o TensorFlow, explorar el procesamiento de lenguaje natural (NLP) con Hugging Face Transformers, o especializarse en análisis de datos con el stack científico completo de Python.

🎓 ¿Quieres ir más lejos con Python e IA?

Ciberaula ofrece cursos bonificados de Python, Machine Learning e Inteligencia Artificial con tutor personal. Formación subvencionada por FUNDAE para trabajadores en activo.

Ver cursos de Python bonificados →

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre Introducción al Machine Learning con Python: conceptos, flujo y primeros modelos

Las dudas más comunes respondidas de forma clara y directa.

Para empezar a usar modelos con scikit-learn, no: la librería abstrae los detalles matemáticos y puedes entrenar y evaluar modelos con pocas líneas de código. Para entender por qué los modelos funcionan o fallan, necesitas álgebra lineal básica (vectores, matrices, producto escalar), estadística descriptiva (media, varianza, correlación) y cálculo elemental (derivadas para entender el gradiente descente). No necesitas ser matemático; necesitas suficiente intuición para interpretar los resultados. La progresión práctica es: aprende a usar scikit-learn → observa qué funciona y qué no → estudia la matemática de los modelos que más usas. Al revés (matemáticas primero) es más lento y más frustrante.
Son tres círculos concéntricos. La Inteligencia Artificial es el campo más amplio: cualquier técnica que hace que una máquina realice tareas que normalmente requieren inteligencia humana (incluye sistemas expertos basados en reglas, búsqueda, planificación). Machine Learning es un subconjunto de IA: sistemas que aprenden patrones a partir de datos sin ser explícitamente programados para cada caso. Deep Learning es un subconjunto de ML: redes neuronales con muchas capas que aprenden representaciones jerárquicas. Para la mayoría de problemas estructurados con tablas de datos, ML clásico (scikit-learn) funciona igual o mejor que Deep Learning, con muchísimo menos coste computacional. Deep Learning brilla con imágenes, texto, audio y señales donde los datos tienen estructura espacial o temporal compleja.
Depende del modelo y de la complejidad del problema. Como referencia orientativa: para regresión lineal con pocas variables, 100-500 muestras pueden ser suficientes. Para clasificación con scikit-learn (árboles, SVM, random forest), se suele necesitar a partir de 1.000 muestras por clase para resultados fiables. Para redes neuronales, se habla de decenas de miles como mínimo. Más importante que la cantidad es la calidad: datos representativos sin sesgo sistémático, bien etiquetados y sin demasiados valores faltantes. Con pocos datos de calidad, técnicas como cross-validation, regularización y transfer learning ayudan a sacar el máximo partido. La señal de alarma es cuando el modelo funciona muy bien en entrenamiento pero mal en datos nuevos (overfitting): suele indicar tanto un modelo demasiado complejo como pocos datos.
El overfitting (sobreajuste) ocurre cuando un modelo aprende los datos de entrenamiento tan bien que memoriza el ruido en lugar de aprender el patrón general. El resultado es un modelo con métricas excelentes en entrenamiento y malas en datos nuevos. Se detecta comparando las métricas en el conjunto de entrenamiento y en el de validación o test: si el error de entrenamiento es mucho menor que el de validación, hay overfitting. Las causas más comunes son un modelo demasiado complejo para la cantidad de datos (demasiadas features, árbol sin podar, red neuronal muy profunda) o datos de entrenamiento con ruido o sesgos. Las soluciones incluyen regularización (Ridge, Lasso, max_depth en árboles), más datos, reducción de features (PCA, selección de variables) y técnicas de ensemble (Random Forest, Gradient Boosting).
Porque la complejidad tiene costes. Un modelo más complejo no siempre generaliza mejor (paradoja del sobreajuste), tarda más en entrenar e inferir, consume más memoria, es más difícil de interpretar y depurar, y su comportamiento en casos límite es menos predecible. El principio de parsimonia (la navaja de Ockham aplicada al ML) dice: entre dos modelos con prestaciones similares, elige el más simple. En la práctica, la progresión recomendada es: empieza con un modelo base simple (regresión lineal, árbol de decisión pequeño), establece una métrica de referencia, y solo añade complejidad cuando hay evidencia de que el modelo simple tiene underfitting. Los modelos simples también son más fáciles de explicar a clientes o directivos, lo que en entornos empresariales es un criterio real.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Introducción al Machine Learning con Python: conceptos, flujo y primeros modelos? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!