Introducción al Machine Learning con Python: conceptos, flujo y primeros modelos
- Qué es el Machine Learning y qué no es
- Tipos de aprendizaje: supervisado, no supervisado y refuerzo
- El flujo completo de un proyecto ML
- El ecosistema: NumPy, Pandas, scikit-learn y Matplotlib
- Regresión lineal: predecir valores continuos
- Clasificación: predecir categorías
- Validación: overfitting, underfitting y cross-validation
- Métricas: cómo saber si tu modelo es bueno
- Pipeline de scikit-learn: encadenar pasos
- Programa completo: predecir precios de viviendas
- Errores clásicos en proyectos ML
- Preguntas frecuentes
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)
🧰 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}")
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
⚖️ 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)
🛠️ 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.
💬 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.
Todavía no hay mensajes. ¡Sé el primero en participar!