Preparar datos: limpieza, variables y pipelines
Los 19 bloques de código de la teoría, en el mismo orden. Cópialos y córrelos en tu Python o en Colab; en la ficha Teoría está la explicación de cada uno.
Los datos tal como llegan
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
titanic = sns.load_dataset("titanic")
print(titanic.shape)
print(titanic[["survived", "pclass", "sex", "age", "fare",
"embarked", "deck"]].head())from matplotlib.colors import LinearSegmentedColormap
cols = ["survived", "pclass", "sex", "age", "sibsp", "parch",
"fare", "embarked", "deck"]
faltan = titanic[cols].isna()
print((faltan.mean() * 100).round(1))
blanco_tinta = LinearSegmentedColormap.from_list("bt", ["white", "C0"])
plt.imshow(faltan, aspect="auto", cmap=blanco_tinta,
interpolation="nearest")
plt.xticks(range(len(cols)), cols, rotation=90)
plt.grid(False) # sin rejilla encima del mapa
plt.ylabel("Pasajero (fila)")
plt.title("Casillas vacías (oscuras)")
plt.show()columnas = ["pclass", "sex", "age", "sibsp", "parch", "fare",
"embarked"]
X = titanic[columnas]
y = titanic["survived"]
print(X.shape, " sobrevivió:", round(y.mean() * 100, 1), "%")Primero, dividir
from sklearn.model_selection import train_test_split
# stratify=y: la misma proporción de sobrevivientes en las dos partes
X_ent, X_prueba, y_ent, y_prueba = train_test_split(
X, y, test_size=0.25, stratify=y, random_state=0)
print("Entrenamiento:", X_ent.shape, " prueba:", X_prueba.shape)
print("Edades faltantes:", X_ent["age"].isna().sum(), "y",
X_prueba["age"].isna().sum())Valores faltantes
for col in ["age", "fare"]:
print(f"{col}: media = {X_ent[col].mean():5.2f}"
f" mediana = {X_ent[col].median():5.2f}"
f" máximo = {X_ent[col].max():6.2f}")from sklearn.impute import SimpleImputer
imputar_edad = SimpleImputer(strategy="median")
imputar_edad.fit(X_ent[["age"]]) # aprende con el entrenamiento
print("Mediana aprendida:", imputar_edad.statistics_)
edad_ent = imputar_edad.transform(X_ent[["age"]])
edad_prueba = imputar_edad.transform(X_prueba[["age"]])
print("Faltantes después:", np.isnan(edad_ent).sum(), "y",
np.isnan(edad_prueba).sum())
imputar_puerto = SimpleImputer(strategy="most_frequent")
imputar_puerto.fit(X_ent[["embarked"]])
print("Puerto más frecuente:", imputar_puerto.statistics_)Variables categóricas: one-hot
from sklearn.preprocessing import OneHotEncoder
puertos = pd.DataFrame({"embarked": ["S", "C", "Q", "S"]})
onehot = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
onehot.fit(puertos)
print(onehot.get_feature_names_out())
print(onehot.transform(puertos))
# Un puerto que no apareció en el entrenamiento queda todo en cero
nuevo = pd.DataFrame({"embarked": ["X"]})
print(onehot.transform(nuevo))Escalar: estandarizar y normalizar
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# Primero rellenamos la edad; la tarifa no tiene faltantes
edad_tarifa = pd.DataFrame(
{"edad": edad_ent[:, 0], "tarifa": X_ent["fare"].to_numpy()})
estandarizar = StandardScaler().fit(edad_tarifa)
print("Medias:", estandarizar.mean_.round(2))
print("Desviaciones:", estandarizar.scale_.round(2))
est = estandarizar.transform(edad_tarifa)
normalizar = MinMaxScaler().fit(edad_tarifa)
norm = normalizar.transform(edad_tarifa)
print("Estandarizadas: de", est.min(axis=0).round(2),
"a", est.max(axis=0).round(2))
print("Normalizadas: de", norm.min(axis=0).round(2),
"a", norm.max(axis=0).round(2))
fig, ejes = plt.subplots(2, 2, figsize=(4.8, 4.8))
ejes[0, 0].hist(edad_tarifa["edad"], bins=30)
ejes[0, 0].set_title("Edad (años)")
ejes[0, 1].hist(edad_tarifa["tarifa"], bins=30)
ejes[0, 1].set_title("Tarifa")
ejes[1, 0].hist(est[:, 0], bins=30, color="C2")
ejes[1, 0].set_title("Edad (puntaje z)")
ejes[1, 1].hist(est[:, 1], bins=30, color="C2")
ejes[1, 1].set_title("Tarifa (z)")
plt.show()Fuga de datos
# Con fuga: las cuentas usan también a los pasajeros de prueba
print(f"Mediana de la edad: todos = {X['age'].median()}"
f" solo entrenamiento = {X_ent['age'].median()}")
print(f"Media de la tarifa: todos = {X['fare'].mean():.2f}"
f" solo entrenamiento = {X_ent['fare'].mean():.2f}")from sklearn.feature_selection import SelectKBest
from sklearn.linear_model import LogisticRegression
rng = np.random.default_rng(0)
X_azar = rng.normal(size=(200, 5000)) # 5000 columnas de ruido
y_azar = rng.integers(0, 2, size=200) # respuestas al azar
X_nuevo = rng.normal(size=(1000, 5000)) # datos que llegan después
y_nuevo = rng.integers(0, 2, size=1000)
Xa_ent, Xa_prueba, ya_ent, ya_prueba = train_test_split(
X_azar, y_azar, test_size=0.25, random_state=0)
resultados = {}
for nombre, Xs, ys in [("Con fuga", X_azar, y_azar),
("Sin fuga", Xa_ent, ya_ent)]:
# La única diferencia: con qué filas se eligen las 20 columnas
elegir = SelectKBest(k=20).fit(Xs, ys)
modelo = LogisticRegression().fit(elegir.transform(Xa_ent), ya_ent)
resultados[nombre] = [
modelo.score(elegir.transform(Xa_prueba), ya_prueba),
modelo.score(elegir.transform(X_nuevo), y_nuevo)]
print(f"{nombre}: prueba = {resultados[nombre][0]:.2f}"
f" datos nuevos = {resultados[nombre][1]:.3f}")
alturas = resultados["Con fuga"] + resultados["Sin fuga"]
nombres = ["Prueba\ncon fuga", "Nuevos\ncon fuga",
"Prueba\nsin fuga", "Nuevos\nsin fuga"]
# Coral solo para el número engañoso
barras = plt.bar(nombres, alturas, color=["C1", "C2", "C0", "C2"])
plt.bar_label(barras, fmt="%.2f")
plt.axhline(0.5, color="C2", linestyle="--", linewidth=1)
plt.ylim(0, 1)
plt.ylabel("Exactitud")
plt.title("La fuga engaña en la prueba")
plt.show()Pipeline y ColumnTransformer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.neighbors import KNeighborsClassifier
numericas = ["pclass", "age", "sibsp", "parch", "fare"]
categoricas = ["sex", "embarked"]
prep_numericas = Pipeline([
("imputar", SimpleImputer(strategy="median")),
("escalar", StandardScaler())])
prep_categoricas = Pipeline([
("imputar", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore"))])
preparar = ColumnTransformer([
("num", prep_numericas, numericas),
("cat", prep_categoricas, categoricas)])
pipe_knn = Pipeline([("preparar", preparar),
("modelo", KNeighborsClassifier(n_neighbors=5))])
pipe_knn.fit(X_ent, y_ent)
print(pipe_knn.named_steps["preparar"].get_feature_names_out())
print("Forma lista para el modelo:",
pipe_knn.named_steps["preparar"].transform(X_ent).shape)from sklearn.base import clone
from sklearn.model_selection import StratifiedKFold, cross_val_score
pliegues = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
for escala in [StandardScaler(), "passthrough"]:
pipe = clone(pipe_knn).set_params(preparar__num__escalar=escala)
puntajes = cross_val_score(pipe, X_ent, y_ent, cv=pliegues)
print(f"Escalar = {str(escala):16} exactitud = "
f"{puntajes.mean():.3f} ± {puntajes.std():.3f}")# Con fuga: se prepara una vez con todas las filas del entrenamiento
X_preparado = clone(preparar).fit_transform(X_ent)
con_fuga = cross_val_score(KNeighborsClassifier(n_neighbors=5),
X_preparado, y_ent, cv=pliegues)
sin_fuga = cross_val_score(pipe_knn, X_ent, y_ent, cv=pliegues)
print(f"Con fuga: {con_fuga.mean():.3f}"
f" sin fuga: {sin_fuga.mean():.3f}")Crear variables nuevas
def agregar_variables(df):
"""Agrega variables nuevas calculadas fila por fila."""
df = df.copy()
df["familia"] = df["sibsp"] + df["parch"] + 1
df["tarifa_persona"] = df["fare"] / df["familia"]
df["es_nino"] = (df["age"] < 12).astype(int)
return df
X_ent_mas = agregar_variables(X_ent)
print(X_ent_mas[["sibsp", "parch", "familia", "fare",
"tarifa_persona", "age", "es_nino"]].head(4))def pipe_logistica(numericas):
"""Pipeline de regresión logística para unas columnas numéricas."""
prep = ColumnTransformer([
("num", prep_numericas, numericas),
("cat", prep_categoricas, categoricas)])
return Pipeline([("preparar", prep),
("modelo", LogisticRegression(max_iter=1000))])
pruebas = {"originales": [],
"+ familia y tarifa_persona": ["familia", "tarifa_persona"],
"+ es_nino": ["es_nino"]}
for nombre, extra in pruebas.items():
puntajes = cross_val_score(pipe_logistica(numericas + extra),
X_ent_mas, y_ent, cv=pliegues)
print(f"{nombre:27} {puntajes.mean():.3f} ± {puntajes.std():.3f}")Buscar hiperparámetros
from sklearn.model_selection import GridSearchCV
rejilla = {
"modelo__n_neighbors": [1, 3, 5, 9, 15, 25, 41],
"preparar__num__escalar": [StandardScaler(), MinMaxScaler(),
"passthrough"]}
busqueda = GridSearchCV(pipe_knn, rejilla, cv=pliegues)
busqueda.fit(X_ent, y_ent)
for nombre, valor in busqueda.best_params_.items():
print(f"Mejor {nombre}: {valor}")
print(f"Exactitud en validación: {busqueda.best_score_:.3f}")
# Los 21 puntajes, como tabla: una fila por escalador
tabla = busqueda.cv_results_["mean_test_score"].reshape(7, 3).T
escaladores = ["Estandarizar", "Normalizar", "Sin escalar"]
ks = rejilla["modelo__n_neighbors"]
fig, ax = plt.subplots(figsize=(4.8, 2.6))
sns.heatmap(tabla, annot=True, fmt=".2f", cmap=blanco_tinta,
cbar=False, xticklabels=ks, yticklabels=escaladores,
annot_kws={"fontsize": 11}, ax=ax)
ax.tick_params(axis="y", labelrotation=0)
ax.set_xlabel("Número de vecinos k")
ax.set_title("Exactitud en validación")
plt.show()from scipy.stats import loguniform
from sklearn.model_selection import RandomizedSearchCV
pipe_log = pipe_logistica(numericas + ["es_nino"])
distribuciones = {"modelo__C": loguniform(1e-3, 1e2)}
azar = RandomizedSearchCV(pipe_log, distribuciones, n_iter=20,
cv=pliegues, random_state=0)
azar.fit(X_ent_mas, y_ent)
print(f"Mejor C: {azar.best_params_['modelo__C']:.3f}")
print(f"Exactitud en validación: {azar.best_score_:.3f}")El pipeline del tema 10
# El pipeline del tema 10: el Titanic, de la tabla cruda a la predicción
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
NUMERICAS = ["pclass", "age", "sibsp", "parch", "fare",
"familia", "tarifa_persona", "es_nino"]
CATEGORICAS = ["sex", "embarked"]
def agregar_variables(df):
"""Agrega variables nuevas calculadas fila por fila."""
df = df.copy()
df["familia"] = df["sibsp"] + df["parch"] + 1
df["tarifa_persona"] = df["fare"] / df["familia"]
df["es_nino"] = (df["age"] < 12).astype(int)
return df
def crear_pipeline_titanic(modelo):
"""Prepara las columnas del Titanic y termina en el modelo dado."""
numericas = Pipeline([
("imputar", SimpleImputer(strategy="median")),
("escalar", StandardScaler())])
categoricas = Pipeline([
("imputar", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore"))])
preparar = ColumnTransformer([
("num", numericas, NUMERICAS),
("cat", categoricas, CATEGORICAS)])
return Pipeline([("preparar", preparar), ("modelo", modelo)])
titanic = agregar_variables(sns.load_dataset("titanic"))
X = titanic[NUMERICAS + CATEGORICAS]
y = titanic["survived"]
X_ent, X_prueba, y_ent, y_prueba = train_test_split(
X, y, test_size=0.25, stratify=y, random_state=0)
pipeline_titanic = crear_pipeline_titanic(
LogisticRegression(C=0.44, max_iter=1000))
pipeline_titanic.fit(X_ent, y_ent)
# La prueba se usa una sola vez, al final
pred = pipeline_titanic.predict(X_prueba)
exactitud = pipeline_titanic.score(X_prueba, y_prueba)
print(f"Exactitud en prueba: {exactitud:.3f}")
print(classification_report(y_prueba, pred,
target_names=["murió", "sobrevivió"]))nuevo = agregar_variables(pd.DataFrame([{
"pclass": 3, "sex": "female", "age": np.nan, "sibsp": 0,
"parch": 2, "fare": 15.0, "embarked": "S"}]))
prob = pipeline_titanic.predict_proba(nuevo[NUMERICAS + CATEGORICAS])
print(f"Probabilidad de sobrevivir: {prob[0, 1]:.2f}")