Módulo / 2. Python para ML
0%
Módulo 2 · Fundamentos

Python para ML

Python es el idioma franco del ML por su ecosistema científico. Este módulo cubre lo esencial del lenguaje y las cuatro bibliotecas que usarás a diario: NumPy para cálculo numérico, Pandas para datos tabulares, Matplotlib y Seaborn para visualización.

7 lecciones 12 h estimadas Código ejecutable en Colab
0 de 7 lecciones completadas
Lección 2.1

Entorno, sintaxis y tipos de datos

18 minPython básicoPrimer programa

Python es un lenguaje interpretado, de tipado dinámico y con una sintaxis legible. Para ML no necesitas dominarlo entero: necesitas escribir funciones claras, manipular colecciones y usar bien las bibliotecas.

¿Dónde ejecutar el código?

EntornoVentajasCuándo usarlo
Google ColabGratis, GPU/TPU, cero instalaciónAprender, prototipos, notebooks compartidos
Jupyter NotebookLocal, flexible, ideal para EDAAnálisis exploratorio y experimentos
VS Code + PythonDepuración, refactor, GitProyectos y código de producción
Scripts .pyReproducibles, versionablesPipelines, entrenamiento, APIs

Recomendación: usa Colab para estudiar y VS Code + entorno virtual para proyectos. Instala las bibliotecas con pip install numpy pandas matplotlib seaborn scikit-learn.

Variables y tipos

En Python no declaras el tipo: se infiere del valor. Los tipos fundamentales son int, float, str, bool y None.

Pythontipos.py
nombre = "Ada"          # str
edad = 36               # int
altura = 1.68           # float
es_cientifica = True    # bool
sin_valor = None        # NoneType

print(type(edad))                 # 
print(f"{nombre} tiene {edad} años")   # f-string

# Conversión de tipos (casting)
print(int("42") + 8)      # 50
print(float("3.14") * 2)  # 6.28
print(str(2026) + "!")    # "2026!"

# Operadores
print(7 / 2)    # 3.5  (división real)
print(7 // 2)   # 3    (división entera)
print(7 % 2)    # 1    (módulo)
print(2 ** 10)  # 1024 (potencia)
División entera y módulo En ML, // y % aparecen al crear batches, dividir índices o comprobar si un número es par. Confundir / con // produce errores sutiles: 7/2 es 3.5, no 3.

Strings y formato

Pythonstrings.py
texto = "Machine Learning"
print(texto.lower())          # machine learning
print(texto.replace(" ", "_"))# Machine_Learning
print(texto.split())          # ['Machine', 'Learning']
print(texto[0:7])             # Machine
print(len(texto))             # 16

# Formato de métricas
accuracy = 0.9234
print(f"Accuracy: {accuracy:.2%}")       # Accuracy: 92.34%
print(f"Error:    {1 - accuracy:.4f}")   # Error:    0.0766

Para NLP, las operaciones con strings son el pan de cada día: limpieza, tokenización, normalización. En el Módulo 8 las retomaremos.

Entrada y salida

Pythonentrada.py
# En un notebook o script interactivo
# nombre = input("¿Cómo te llamas? ")
# print(f"Hola, {nombre}")

# Para ML, la salida estructurada es más útil que los prints sueltos
resultados = {"modelo": "Random Forest", "f1": 0.87, "auc": 0.91}
for clave, valor in resultados.items():
    print(f"{clave:>16}: {valor}")

Aplicación: registrar un experimento

Un buen hábito desde el primer día: guardar cada experimento con sus hiperparámetros y métricas. No confíes en la memoria ni en nombres de archivo como final_v2_bueno.ipynb.

Pythonexperimento.py
from datetime import datetime

experimento = {
    "fecha": datetime.now().isoformat(timespec="seconds"),
    "modelo": "logistic_regression",
    "lr": 0.01,
    "epochs": 100,
    "accuracy": 0.912,
    "notas": "features estandarizadas",
}
print(experimento)
# En producción: MLflow, Weights & Biases o un CSV versionado
Zen de Python «Explícito mejor que implícito; simple mejor que complejo; legible cuenta». Ejecuta import this para leer los 19 principios. El código de ML se lee mucho más de lo que se escribe.
Ejercicios 2.1
  1. ¿Qué imprime print(10 // 3, 10 % 3)? 3 1.
  2. Convierte la temperatura 98.6 °F a Celsius con la fórmula $C=(F-32)\times5/9$. Resultado: 37.0.
  3. Escribe un f-string que muestre una pérdida de 0.03456 con 3 decimales: f"loss={0.03456:.3f}".
  4. ¿Por qué "3" + 4 falla? Porque no se pueden concatenar str e int sin conversión explícita.
Lección 2.2

Condicionales, ciclos y comprensiones

16 minControl de flujoPython idiomático

Condicionales

Python usa indentación (4 espacios) para delimitar bloques. No hay llaves. Las comparaciones se encadenan de forma natural: 0 <= y <= 1.

Pythoncondicionales.py
def clasificar_imc(peso, altura):
    imc = peso / altura ** 2
    if imc < 18.5:
        categoria = "bajo peso"
    elif imc < 25:
        categoria = "normal"
    elif imc < 30:
        categoria = "sobrepeso"
    else:
        categoria = "obesidad"
    return round(imc, 1), categoria

print(clasificar_imc(70, 1.75))   # (22.9, 'normal')

# Operador ternario
estado = "aprobado" if 0.85 >= 0.7 else "reprobado"
print(estado)

# Operadores lógicos
es_adulto = True
tiene_dni = False
print(es_adulto and tiene_dni)   # False
print(es_adulto or tiene_dni)    # True
print(not tiene_dni)             # True

Ciclos

Pythonciclos.py
# for sobre un rango
for epoca in range(1, 4):          # 1, 2, 3
    perdida = 1.0 / epoca
    print(f"época {epoca}: loss={perdida:.3f}")

# for sobre una colección
modelos = ["lineal", "árbol", "knn"]
for i, modelo in enumerate(modelos, start=1):
    print(i, modelo)

# while con criterio de parada
x, paso = 4.0, 0
while abs(x) > 1e-3 and paso < 100:
    x = x - 0.2 * 2 * x
    paso += 1
print(f"convergió en {paso} pasos: x={x:.6f}")

# break / continue
for valor in [1, -2, 3, -4, 5]:
    if valor < 0:
        continue          # salta negativos
    if valor > 4:
        break             # corta el ciclo
    print(valor)
Cuidado con mutar mientras iteras No elimines elementos de una lista mientras la recorres. Crea una nueva lista o itera sobre una copia. Este error causa bugs difíciles de reproducir en el preprocesamiento de datos.

Comprensiones de listas

Son la forma idiomática de transformar o filtrar colecciones. Más legibles y rápidas que un for con append.

Pythoncomprensiones.py
numeros = [1, 2, 3, 4, 5, 6]

cuadrados = [x ** 2 for x in numeros]
pares = [x for x in numeros if x % 2 == 0]
pares_al_cuadrado = [x ** 2 for x in numeros if x % 2 == 0]

print(cuadrados)          # [1, 4, 9, 16, 25, 36]
print(pares)              # [2, 4, 6]
print(pares_al_cuadrado)  # [4, 16, 36]

# Diccionario por comprensión
longitudes = {palabra: len(palabra) for palabra in ["gato", "perro", "elefante"]}
print(longitudes)

# Con condición al final (ternario dentro de la comprensión)
etiquetas = ["positivo" if x > 0 else "negativo" for x in [3, -1, 0]]
print(etiquetas)          # ['positivo', 'negativo', 'negativo']

Aplicación: normalización de una lista

$$x_i' = \frac{x_i-\min(x)}{\max(x)-\min(x)}$$
Pythonminmax.py
datos = [10, 20, 30, 40, 50]
minimo, maximo = min(datos), max(datos)

normalizados = [(x - minimo) / (maximo - minimo) for x in datos]
print(normalizados)   # [0.0, 0.25, 0.5, 0.75, 1.0]

# Con NumPy es aún más directo (lo veremos en 2.4)
# (datos - datos.min()) / (datos.max() - datos.min())
Regla de estilo Si una comprensión ocupa más de dos líneas o anida tres for, usa un bucle normal. La legibilidad gana a la brevedad.
Ejercicios 2.2
  1. Crea una lista con los múltiplos de 3 entre 1 y 30: [x for x in range(1,31) if x % 3 == 0].
  2. Cuenta cuántos números de una lista son mayores que la media.
  3. Simula 10 épocas de un ciclo que imprime la pérdida $1/t$ y se detiene si baja de 0.15. ¿En qué época se detiene? En $t=7$ (1/7≈0.143).
  4. Convierte una lista de temperaturas en Fahrenheit a Celsius con una comprensión.
Lección 2.3

Funciones, estructuras de datos y POO

22 minPython intermedioBuenas prácticas

Funciones

Anatomía $$\texttt{def nombre(parametros): }\;\text{cuerpo}\; \texttt{return valor}$$
Pythonfunciones.py
def mse(y_real, y_pred):
    """Error cuadrático medio. Ambas listas deben tener igual longitud."""
    if len(y_real) != len(y_pred):
        raise ValueError("Las listas deben tener la misma longitud")
    errores = [(real - pred) ** 2 for real, pred in zip(y_real, y_pred)]
    return sum(errores) / len(errores)

print(mse([3, -0.5, 2, 7], [2.5, 0.0, 2, 8]))   # 0.375

# Argumentos con valor por defecto y por nombre
def entrenar(X, y, lr=0.01, epochs=100, verbose=False):
    """Firma típica de una función de entrenamiento."""
    historial = []
    for epoca in range(epochs):
        loss = 1 / (epoca + 1)          # simulación
        historial.append(loss)
        if verbose and epoca % 20 == 0:
            print(f"época {epoca}: {loss:.4f}")
    return historial

h = entrenar([], [], lr=0.1, epochs=50)

# *args y **kwargs
def resumen(*metricas, **opciones):
    print("métricas:", metricas)
    print("opciones:", opciones)

resumen(0.9, 0.8, 0.85, modelo="rf", seed=42)
Nunca uses listas como valor por defecto def f(x, acc=[]) comparte la misma lista entre llamadas y acumula resultados inesperados. Usa None y créala dentro: if acc is None: acc = [].

Listas, tuplas, diccionarios y conjuntos

EstructuraMutableOrdenadaUso típico
listSíSíSecuencias de ejemplos, batches
tupleNoSíPares (x, y), claves compuestas
dictSíSí (desde 3.7)Hiperparámetros, métricas, JSON
setSíNoVocabulario, eliminación de duplicados
Pythonestructuras.py
# Listas
edades = [23, 45, 31]
edades.append(29)
print(edades[-1], edades[1:3])     # 29 [45, 31]

# Tuplas: inmutables, útiles para datos fijos
punto = (2.5, -1.0)
x, y = punto                        # desempaquetado

# Diccionarios
modelo = {"tipo": "random_forest", "n_estimators": 200, "max_depth": 8}
print(modelo["tipo"])
modelo["random_state"] = 42
for clave, valor in modelo.items():
    print(f"{clave}: {valor}")

# Conjuntos
vocabulario = set(["gato", "perro", "gato", "ave"])
print(vocabulario)                  # {'gato', 'perro', 'ave'}
print("gato" in vocabulario)        # True

# Operaciones de conjuntos
A = {1, 2, 3}
B = {3, 4}
print(A | B)   # unión
print(A & B)   # intersección
print(A - B)   # diferencia

Programación orientada a objetos

En ML, las clases sirven para encapsular estado y comportamiento: un modelo con sus pesos, un dataset con sus transformaciones, un pipeline con sus pasos. Scikit-learn usa esta interfaz: fit, predict, score.

Pythonpoo.py
class StandardScaler:
    """Estandarización: (x - media) / desviación estándar."""

    def __init__(self):
        self.media_ = None
        self.sd_ = None

    def fit(self, X):
        n = len(X)
        self.media_ = sum(X) / n
        varianza = sum((x - self.media_) ** 2 for x in X) / n
        self.sd_ = varianza ** 0.5
        return self

    def transform(self, X):
        if self.media_ is None:
            raise RuntimeError("Llama a fit() antes de transform()")
        return [(x - self.media_) / self.sd_ for x in X]

    def fit_transform(self, X):
        return self.fit(X).transform(X)

scaler = StandardScaler()
datos = [10, 12, 14, 16, 18]
print(scaler.fit_transform(datos))
print("media aprendida:", scaler.media_)   # 14.0

El guion bajo final en media_ es una convención de Scikit-learn para atributos aprendidos durante fit. Distinguirlos de los hiperparámetros evita fugas de información.

Principio de responsabilidad única Una función debe hacer una cosa. Una clase debe tener una razón para cambiar. En un pipeline, separa carga de datos, preprocesamiento, entrenamiento y evaluación. Facilita probar y reemplazar piezas.

Módulos y organización de un proyecto

proyecto/
├── data/
│   ├── raw/            # datos originales (nunca modificar)
│   └── processed/      # datos limpios
├── notebooks/          # exploración
├── src/
│   ├── data.py         # carga y limpieza
│   ├── features.py     # ingeniería de características
│   ├── train.py        # entrenamiento
│   └── evaluate.py     # métricas
├── models/             # artefactos serializados
├── tests/
├── requirements.txt
└── README.md

Ejemplo integrador: clase de regresión lineal simple

Pythonregresion_oo.py
class RegresionLinealSimple:
    def __init__(self, lr=0.01, epochs=1000):
        self.lr = lr
        self.epochs = epochs
        self.w = 0.0
        self.b = 0.0
        self.historial = []

    def fit(self, X, y):
        n = len(X)
        for _ in range(self.epochs):
            y_pred = [self.w * x + self.b for x in X]
            error = [yp - yr for yp, yr in zip(y_pred, y)]
            dw = 2 / n * sum(e * x for e, x in zip(error, X))
            db = 2 / n * sum(error)
            self.w -= self.lr * dw
            self.b -= self.lr * db
            mse = sum(e ** 2 for e in error) / n
            self.historial.append(mse)
        return self

    def predict(self, X):
        return [self.w * x + self.b for x in X]

X = [1, 2, 3, 4]
y = [2, 4, 5, 4]
modelo = RegresionLinealSimple(lr=0.01, epochs=2000).fit(X, y)
print(f"y = {modelo.w:.3f}x + {modelo.b:.3f}")
print("predicciones:", [round(p, 2) for p in modelo.predict(X)])
Ejercicios 2.3
  1. Escribe una función accuracy(y_true, y_pred) que devuelva la proporción de aciertos. Prueba con [1,0,1,1] y [1,0,0,1]: 0.75.
  2. Implementa una clase MinMaxScaler con fit y transform.
  3. ¿Por qué una tupla puede ser clave de diccionario y una lista no? Porque las claves deben ser hashables e inmutables.
  4. Refactoriza un script de 100 líneas en tres funciones: cargar, entrenar, evaluar.
Lección 2.4

NumPy: cálculo vectorizado

24 minBiblioteca esencialBroadcasting

NumPy es la base de todo el ecosistema científico de Python. Introduce el objeto ndarray: un arreglo multidimensional homogéneo, almacenado de forma contigua y operado en C. La vectorización elimina bucles lentos de Python.

Creación de arreglos

Pythonnumpy_basico.py
import numpy as np

a = np.array([1, 2, 3])                 # 1D
b = np.array([[1, 2, 3], [4, 5, 6]])    # 2D (2 filas, 3 columnas)
print(b.shape)                          # (2, 3)
print(b.dtype)                          # int64

print(np.zeros((2, 3)))                 # matriz de ceros
print(np.ones(5))                       # vector de unos
print(np.arange(0, 10, 2))              # [0 2 4 6 8]
print(np.linspace(0, 1, 5))             # 5 puntos entre 0 y 1
print(np.eye(3))                        # identidad 3x3

rng = np.random.default_rng(42)         # generador moderno
print(rng.normal(0, 1, size=5))         # 5 normales
print(rng.integers(0, 10, size=(2, 3)))

Indexado y slicing

Pythonnumpy_index.py
X = np.arange(12).reshape(3, 4)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(X[0, 0])        # 0
print(X[1, :])        # fila 1: [4 5 6 7]
print(X[:, 2])        # columna 2: [2 6 10]
print(X[0:2, 1:3])    # submatriz

# Indexado booleano
print(X[X > 6])       # [7 8 9 10 11]
filas = X[:, 0] > 4
print(X[filas])       # filas donde la primera columna > 4

# Modificación
X[0, 0] = 99
print(X[0])

Operaciones vectorizadas y broadcasting

Las operaciones se aplican elemento a elemento sin bucles explícitos. El broadcasting permite combinar formas compatibles expandiendo dimensiones de tamaño 1.

Pythonnumpy_ops.py
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])

print(a + b)          # [11 22 33]
print(a * b)          # [10 40 90]
print(a ** 2)         # [1 4 9]
print(np.sqrt(a))     # [1. 1.414 1.732]

# Broadcasting: matriz (3,1) + vector (3,)
columna = np.array([[1], [2], [3]])
fila = np.array([10, 20, 30])
print(columna + fila)
# [[11 21 31]
#  [12 22 32]
#  [13 23 33]]

# Normalización por columnas
X = np.array([[1., 2.], [3., 4.], [5., 6.]])
media = X.mean(axis=0)        # [3. 4.]
sd = X.std(axis=0)            # [1.633 1.633]
print((X - media) / sd)

# Producto matricial
W = np.array([[1., 2.], [3., 4.]])
print(X @ W)                  # producto matricial
print(X.T @ X)                # matriz de Gram
axis axis=0 opera a lo largo de las filas (colapsa filas → resultado por columna); axis=1 colapsa columnas. Es la fuente número uno de confusión. Verifica con shape.

Agregaciones y estadística

Pythonnumpy_stats.py
X = rng.normal(50, 10, size=(100, 3))

print("media:", X.mean(axis=0))
print("desv :", X.std(axis=0))
print("min  :", X.min(axis=0))
print("max  :", X.max(axis=0))
print("argmax por columna:", X.argmax(axis=0))

print(np.percentile(X, [25, 50, 75], axis=0))
print("correlación:\n", np.corrcoef(X, rowvar=False))

# Álgebra lineal
A = np.array([[2., 1.], [1., 3.]])
print("det:", np.linalg.det(A))
print("inv:\n", np.linalg.inv(A))
print("eig:", np.linalg.eigvalsh(A))
print("norma:", np.linalg.norm([3, 4]))   # 5.0

Por qué la vectorización importa

1 Bucle vs. vector

Calcular el cuadrado de un millón de números con un bucle de Python tarda del orden de 50–100 ms; con NumPy, del orden de 1–3 ms. La diferencia no es solo velocidad: el código es más corto y menos propenso a errores.

En entrenamiento de modelos, esto se multiplica por miles de iteraciones y millones de parámetros. La vectorización es una necesidad, no un lujo.

Aplicación: regresión lineal con NumPy

Pythonregresion_numpy.py
import numpy as np

# Datos: x = [1,2,3,4], y = [2,4,5,4]
X = np.array([1., 2., 3., 4.])
y = np.array([2., 4., 5., 4.])

# Añadimos columna de unos para el intercepto
Xb = np.column_stack([np.ones_like(X), X])
w = np.linalg.solve(Xb.T @ Xb, Xb.T @ y)
print("b0, b1 =", w)                 # [2.0 0.7]

y_pred = Xb @ w
residuos = y - y_pred
sse = (residuos ** 2).sum()
sst = ((y - y.mean()) ** 2).sum()
r2 = 1 - sse / sst
print("R² =", r2)                    # 0.5158

# Descenso del gradiente vectorizado
w_gd = np.zeros(2)
lr = 0.05
for _ in range(2000):
    grad = 2 / len(X) * Xb.T @ (Xb @ w_gd - y)
    w_gd -= lr * grad
print("gradiente:", w_gd)            # ≈ [2.0 0.7]
Ejercicios 2.4
  1. Crea una matriz 4×4 con valores 0..15 y extrae la submatriz de las filas 1–2 y columnas 2–3.
  2. Genera 1000 números normales con media 5 y desviación 2; calcula media y desviación muestral.
  3. Estandariza una matriz por columnas usando broadcasting; verifica que cada columna tenga media ≈0 y sd ≈1.
  4. Calcula la norma L2 de cada fila de una matriz sin usar bucles: np.linalg.norm(X, axis=1).
Lección 2.5

Pandas: datos tabulares

26 minAnálisis de datosEDA

Pandas introduce dos estructuras: Series (columna etiquetada) y DataFrame (tabla con índice y columnas). Es la herramienta estándar para cargar, limpiar, explorar y transformar datos.

Cargar y explorar

Pythonpandas_basico.py
import pandas as pd

# df = pd.read_csv("datos.csv")
# Para el ejemplo, creamos un DataFrame
df = pd.DataFrame({
    "edad": [25, 32, 47, 51, 29, 38, 41, 60],
    "ingreso": [25_000, 42_000, 58_000, 61_000, 33_000, 47_000, 52_000, 70_000],
    "ciudad": ["Lima", "Lima", "Arequipa", "Cusco", "Lima", "Arequipa", "Cusco", "Lima"],
    "compro": [0, 1, 1, 1, 0, 1, 1, 0],
})

print(df.head(3))
print(df.shape)          # (8, 4)
print(df.dtypes)
print(df.info())
print(df.describe())     # estadísticas de columnas numéricas
print(df["ciudad"].value_counts())
print(df.isna().sum())   # valores faltantes por columna

Selección y filtrado

Pythonpandas_seleccion.py
# Por etiqueta (loc) y por posición (iloc)
print(df.loc[0, "edad"])            # 25
print(df.iloc[0, 0])                # 25
print(df.loc[2:4, ["edad", "ciudad"]])

# Filtrar con condiciones
mayores = df[df["edad"] >= 40]
print(mayores)

# Varias condiciones: & (and), | (or), ~ (not). Paréntesis obligatorios.
filtro = (df["ciudad"] == "Lima") & (df["compro"] == 1)
print(df[filtro])

# Crear una columna
df["ingreso_miles"] = df["ingreso"] / 1000
df["grupo_edad"] = pd.cut(df["edad"], bins=[0, 30, 45, 100],
                          labels=["joven", "adulto", "mayor"])
print(df.head())
SettingWithCopyWarning Si filtras y luego modificas, Pandas puede operar sobre una copia y no sobre el original. Usa .copy() al filtrar si vas a modificar: sub = df[df.edad > 40].copy().

Agrupación y agregación

Pythonpandas_groupby.py
# Media de ingreso por ciudad
print(df.groupby("ciudad")["ingreso"].mean())

# Varias agregaciones
resumen = df.groupby("ciudad").agg(
    n=("edad", "size"),
    edad_media=("edad", "mean"),
    ingreso_medio=("ingreso", "mean"),
    tasa_compra=("compro", "mean"),
).reset_index()
print(resumen)

# Tabla cruzada
print(pd.crosstab(df["ciudad"], df["compro"], normalize="index"))

# Ordenar
print(df.sort_values("ingreso", ascending=False).head(3))

Valores faltantes y duplicados

Pythonpandas_limpieza.py
df2 = df.copy()
df2.loc[1, "ingreso"] = None
df2.loc[3, "ciudad"] = None

print(df2.isna().sum())
print(df2.dropna().shape)                         # elimina filas incompletas
print(df2["ingreso"].fillna(df2["ingreso"].median()))
print(df2["ciudad"].fillna("Desconocida"))

# Duplicados
df2 = pd.concat([df2, df2.iloc[[0]]], ignore_index=True)
print(df2.duplicated().sum())
df2 = df2.drop_duplicates()

# Renombrar y convertir tipos
df2 = df2.rename(columns={"ingreso": "salario"})
df2["compro"] = df2["compro"].astype(bool)

Unir tablas

Pythonpandas_merge.py
clientes = pd.DataFrame({
    "id": [1, 2, 3],
    "nombre": ["Ana", "Luis", "Eva"],
})
compras = pd.DataFrame({
    "id": [1, 1, 3, 4],
    "monto": [100, 250, 80, 500],
})

# inner: solo ids presentes en ambas
print(clientes.merge(compras, on="id", how="inner"))

# left: conserva todos los clientes
print(clientes.merge(compras, on="id", how="left"))

Aplicación: EDA mínimo de un dataset

Pythoneda.py
def resumen_eda(df):
    print("Forma:", df.shape)
    print("\nTipos:\n", df.dtypes.value_counts())
    faltantes = df.isna().mean().sort_values(ascending=False)
    print("\n% faltantes:\n", (faltantes * 100).round(1).head())
    print("\nDuplicados:", df.duplicated().sum())
    numericas = df.select_dtypes("number")
    print("\nResumen numérico:\n", numericas.describe().T)
    if len(numericas.columns) > 1:
        print("\nCorrelaciones:\n", numericas.corr().round(2))

resumen_eda(df)
Antes de modelar Pregunta siempre: ¿qué representa cada fila? ¿hay una fila por paciente, por transacción o por día? ¿el dataset contiene el futuro? ¿las etiquetas se generaron antes o después de las features? Estas preguntas evitan fugas de información, el error más costoso en ML.
Ejercicios 2.5
  1. Carga el dataset Titanic desde una URL pública y muestra las 5 primeras filas, la tasa de supervivencia por sexo y el porcentaje de nulos por columna.
  2. Crea una columna es_mayor que valga True si la edad es ≥18.
  3. Agrupa por clase y calcula la tarifa media y la supervivencia media.
  4. Detecta y elimina duplicados; documenta cuántos había.
Lección 2.6

Matplotlib y Seaborn

20 minVisualizaciónComunicar hallazgos

Un gráfico bien hecho revela patrones que una tabla oculta. Matplotlib da control total; Seaborn simplifica los gráficos estadísticos. La regla es simple: elige el gráfico según la pregunta, no según lo que se ve bonito.

Matplotlib: figura y ejes

Pythonmatplotlib_basico.py
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 10, 100)

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

axes[0].plot(x, np.sin(x), label="sin(x)", color="#3355c8")
axes[0].plot(x, np.cos(x), label="cos(x)", color="#0e7c6b", linestyle="--")
axes[0].set_title("Funciones trigonométricas")
axes[0].set_xlabel("x"); axes[0].set_ylabel("f(x)")
axes[0].legend(); axes[0].grid(alpha=.3)

axes[1].scatter(x, np.sin(x) + np.random.default_rng(0).normal(0, .15, 100),
                s=12, alpha=.6)
axes[1].set_title("Dispersión con ruido")
axes[1].grid(alpha=.3)

fig.tight_layout()
plt.show()
# plt.savefig("figura.png", dpi=200, bbox_inches="tight")

Qué gráfico para qué pregunta

PreguntaGráficoFunción
¿Cómo se distribuye una variable?Histograma / KDEhistplot
¿Hay relación entre dos variables?Dispersiónscatterplot
¿Cómo cambia una métrica en el tiempo?Línealineplot
¿Hay diferencias entre grupos?Boxplot / violinboxplot, violinplot
¿Cómo se relacionan muchas variables?Matriz de correlaciónheatmap
¿Cómo se distribuye una clase?Barrascountplot

Seaborn

Pythonseaborn_demo.py
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

sns.set_theme(style="whitegrid", palette="deep")

# Dataset de ejemplo incluido en Seaborn
iris = sns.load_dataset("iris")

fig, axes = plt.subplots(1, 3, figsize=(14, 4))

sns.histplot(data=iris, x="sepal_length", hue="species",
             kde=True, ax=axes[0])
axes[0].set_title("Distribución por especie")

sns.scatterplot(data=iris, x="sepal_length", y="petal_length",
                hue="species", ax=axes[1])
axes[1].set_title("Relación entre medidas")

sns.boxplot(data=iris, x="species", y="petal_width", ax=axes[2])
axes[2].set_title("Petal width por especie")
axes[2].tick_params(axis="x", rotation=15)

fig.tight_layout(); plt.show()

# Matriz de correlación
plt.figure(figsize=(6, 4))
sns.heatmap(iris.select_dtypes("number").corr(),
            annot=True, fmt=".2f", cmap="vlag", center=0,
            square=True, cbar_kws={"shrink": .8})
plt.title("Correlaciones")
plt.show()

Curvas de aprendizaje

Pythoncurvas.py
epocas = np.arange(1, 51)
train_loss = 0.9 * np.exp(-epocas / 12) + 0.08
val_loss = 0.9 * np.exp(-epocas / 12) + 0.08 + 0.12 * (epocas / 50) ** 2

plt.figure(figsize=(7, 4))
plt.plot(epocas, train_loss, label="entrenamiento", color="#3355c8")
plt.plot(epocas, val_loss, label="validación", color="#b42318")
plt.axvline(epocas[np.argmin(val_loss)], color="gray", linestyle=":", label="mejor época")
plt.xlabel("época"); plt.ylabel("pérdida"); plt.legend(); plt.grid(alpha=.3)
plt.title("Diagnóstico de overfitting")
plt.show()

Si la pérdida de entrenamiento sigue bajando mientras la de validación sube, el modelo está memorizando. Si ambas son altas, hay underfitting. Estas dos curvas son el diagnóstico más rápido y útil de un entrenamiento.

Buenas prácticas de visualización Etiqueta siempre los ejes y las unidades; usa escalas logarítmicas cuando los valores abarcan varios órdenes de magnitud; no cortes el eje Y para exagerar diferencias; incluye intervalos de confianza cuando compares grupos; y no uses gráficos 3D si un 2D comunica mejor.

Estilo consistente para el curso

Pythonestilo.py
PALETA = ["#3355c8", "#0e7c6b", "#a86a12", "#6b3fa0", "#b42318"]

plt.rcParams.update({
    "figure.dpi": 120,
    "axes.spines.top": False,
    "axes.spines.right": False,
    "axes.grid": True,
    "grid.alpha": 0.25,
    "axes.titlesize": 12,
    "axes.labelsize": 10,
    "font.family": "sans-serif",
})
Ejercicios 2.6
  1. Grafica la distribución de la edad en el Titanic separando por supervivencia.
  2. Haz un scatter de precio vs. superficie en un dataset de viviendas y colorea por número de habitaciones.
  3. Grafica la curva de pérdida de tu modelo y marca la época de mejor validación.
  4. Crítica este gráfico: barras sin eje Y, colores arcoíris y título «Resultados». Propón una versión mejorada con unidades, escala y una conclusión en el título.
Lección 2.7

Jupyter, Colab y buenas prácticas

16 minFlujo de trabajoReproducibilidad

Un notebook es un laboratorio; un script es una fábrica. Saber cuándo usar cada uno y cómo mantener ambos reproducibles es una habilidad profesional.

Anatomía de un notebook

  • Celdas de código: se ejecutan individualmente; el estado vive en el kernel.
  • Celdas Markdown: documentación, ecuaciones LaTeX, imágenes y conclusiones.
  • Kernel: proceso que mantiene variables en memoria. Reiniciar y ejecutar todo es la prueba de reproducibilidad.
  • Outputs: pueden quedar obsoletos si ejecutas celdas fuera de orden. No confíes en un notebook con celdas saltadas.

Google Colab: inicio rápido

Pythoncolab_setup.py
# Verificar GPU (Entorno de ejecución → Cambiar tipo de entorno)
!nvidia-smi

# Instalar bibliotecas (en Colab ya vienen muchas)
!pip -q install scikit-learn xgboost

# Montar Google Drive (opcional)
# from google.colab import drive
# drive.mount("/content/drive")

# Descargar un dataset público
# !wget -q https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data

Semillas y reproducibilidad

Pythonreproducibilidad.py
import random
import numpy as np
import os

SEED = 42

def fijar_semillas(seed=SEED):
    random.seed(seed)
    np.random.seed(seed)
    os.environ["PYTHONHASHSEED"] = str(seed)
    # Para frameworks de deep learning:
    # import torch
    # torch.manual_seed(seed)
    # torch.cuda.manual_seed_all(seed)

fijar_semillas()

# Aun así, algunas operaciones en GPU no son deterministas.
# Documenta la versión de las librerías y el hardware.
import sklearn, pandas, numpy
print("numpy", numpy.__version__)
print("pandas", pandas.__version__)
print("sklearn", sklearn.__version__)
La semilla no garantiza reproducibilidad total Fijar la semilla ayuda, pero el orden de operaciones en GPU, las versiones de las librerías y el hardware pueden cambiar los resultados. Registra siempre versiones y configuración. Un resultado sin contexto no es ciencia.

Del notebook al script

  1. Explora en el notebook: gráficos, hipótesis, pruebas rápidas.
  2. Cuando una celda funciona, muévela a una función en src/.
  3. Escribe pruebas para las transformaciones críticas.
  4. Orquesta el pipeline con un script o un Makefile.
  5. Versiona datos y modelos con DVC o Git LFS si son grandes.
  6. Documenta cómo reproducir: python train.py --config config.yaml.

Checklist de un proyecto limpio

  • Semilla fijada y documentada.
  • Entorno reproducible (requirements.txt o environment.yml).
  • Datos crudos intactos; transformaciones en código.
  • Train/validación/test separados antes de cualquier preprocesamiento que aprenda parámetros.
  • Métricas reportadas con media y dispersión entre folds.
  • Ningún dato del test usado para elegir hiperparámetros.
  • Conclusiones separadas de los resultados.
  • Limitaciones y sesgos conocidos documentados.
Consejo de estudio No copies y pegues código sin ejecutarlo. Escribe variantes, rompe cosas, lee los errores. La intuición en programación se construye depurando, no leyendo. Dedica al menos el doble de tiempo a practicar que a mirar.