Queda una pregunta viva desde el cierre de M10, y es la que más dinero mueve: ¿cuánto stock pedir de cara a Sant Jordi? Las lecciones anteriores describieron el pasado (sábados fuertes, abril desbocado, Fausto escaparatista); pedir stock exige proyectar el futuro. Ese salto — de describir a predecir — es el aprendizaje automático (machine learning, ML), y scikit-learn es su librería de referencia en Python. Esta lección lo presenta de verdad y sin humo: qué significa que una máquina "aprenda", dos casos completos con los datos de Papyrus, y las advertencias que el marketing suele saltarse. Nivel introductorio honesto: un mapa fiable del territorio, no el territorio entero.

Contenido

  1. Qué es ML de verdad: reglas escritas vs patrones aprendidos
  2. Tipos de aprendizaje: supervisado y no supervisado
  3. El flujo scikit-learn: split → fit → predict → evaluar
  4. Caso completo 1 — regresión: predecir las ventas diarias
  5. La decisión: el pedido de Sant Jordi
  6. Caso completo 2 — clasificación: ¿comprará este socio este mes?
  7. Sobreajuste: memorizar no es aprender
  8. Lo que NO hemos cubierto (tu siguiente mapa)
  9. Una advertencia ética

Qué es ML de verdad

Todo el curso hasta aquí ha sido programación clásica: tú escribes las reglas. El vender() de M5 descontaba stock porque tú escribiste self.stock -= unidades; el clean_precio de Django (M10) rechazaba negativos porque tú lo decidiste; la tarifa de socio es una fórmula que tecleó Ana. Reglas explícitas, datos que entran, respuestas que salen.

El ML invierte el contrato: le das datos con sus respuestas, y la máquina deduce las reglas.

Programación clásica (M1-M10) Aprendizaje automático
Tú aportas Reglas + datos Datos + respuestas conocidas
La máquina produce Respuestas Reglas (un modelo)
Ejemplo Papyrus precio * 1.04 * 0.95 "los sábados se vende el doble" — deducido del CSV
Cuándo brilla La regla se conoce y es exacta La regla existe pero nadie sabe escribirla
Cuándo falla Casos no previstos por ti Futuro distinto del pasado, datos escasos o sucios

Nadie sabe escribir a mano la fórmula exacta de "cuánto vende Papyrus un martes de marzo". Pero el patrón está en ventas_2026.csv, y un modelo puede extraerlo. Eso — ajustar funciones a datos — es todo el truco. No hay comprensión ni intención: hay estadística aplicada con buena ingeniería.

Tipos de aprendizaje

Tipo Qué se aprende Ejemplo Papyrus
Supervisado — regresión Predecir un número a partir de ejemplos etiquetados ¿Cuántas unidades venderemos mañana? (caso 1)
Supervisado — clasificación Predecir una categoría ¿Este socio comprará este mes: sí/no? (caso 2)
No supervisado Estructura sin etiquetas (grupos, anomalías) Agrupar socios por hábitos de compra sin decirle qué grupos buscar

Este módulo practica los dos supervisados. Del no supervisado te llevas el concepto; y existe un cuarto mundo — el aprendizaje profundo (deep learning, redes neuronales: el motor de los chatbots y la visión por computador) — que honestamente queda fuera de este curso: exige más matemáticas, más datos y más máquina de lo que Papyrus necesita.

El flujo scikit-learn

pip install scikit-learn

Lo mejor de scikit-learn es que todos los modelos se usan igual — misma API para una regresión lineal que para un bosque aleatorio:

flowchart LR
    A["Datos<br>X (características)<br>y (respuesta)"] --> B["train_test_split<br>80% entrenar / 20% probar"]
    B --> C["fit(X_train, y_train)<br>el modelo aprende"]
    C --> D["predict(X_test)<br>predice datos que NUNCA vio"]
    D --> E["evaluar<br>MAE, accuracy...<br>¿nos fiamos?"]
    E -.->|no| A

La pieza que lo cambia todo es train_test_split: se aparta un 20 % de los datos antes de entrenar y se evalúa solo con ellos. ¿Por qué? Porque evaluar con los datos de entrenamiento es dejar que el estudiante se corrija su propio examen con las respuestas delante: nota alta garantizada, aprendizaje no garantizado. Volveremos sobre esto en el sobreajuste.

Caso completo 1 — regresión: predecir las ventas diarias

Pregunta: ¿cuántas unidades venderá Papyrus un día dado? Datos: ventas_2026.csv agregado por día (181 días), con características que las lecciones anteriores ya señalaron como relevantes: el mes, si es fin de semana (el sábado de 11-03) y si es Sant Jordi (el pico de 11-02/11-04).

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error

df = pd.read_csv("datos/ventas_2026.csv", parse_dates=["fecha"])

# 1) De ventas sueltas a un dataset diario (pandas, 11-03)
diario = df.groupby("fecha")["unidades"].sum().reset_index()
diario["mes"] = diario["fecha"].dt.month
diario["es_finde"] = (diario["fecha"].dt.dayofweek >= 5).astype(int)
diario["es_sant_jordi"] = (diario["fecha"] == "2026-04-23").astype(int)

X = diario[["mes", "es_finde", "es_sant_jordi"]]   # características (features)
y = diario["unidades"]                              # lo que queremos predecir

# 2) Apartar el examen ANTES de estudiar
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 3) Aprender y 4) predecir sobre lo nunca visto
modelo = LinearRegression()
modelo.fit(X_train, y_train)
predicciones = modelo.predict(X_test)

# 5) Evaluar
print(f"MAE: {mean_absolute_error(y_test, predicciones):.2f} unidades/día")
# MAE: 1.10 unidades/día

El MAE (mean absolute error) dice que, de media, el modelo se equivoca en ±1.1 unidades al día — sobre una media real de unos 2.9, no es brillante, pero para dimensionar un pedido mensual basta, porque los errores diarios se compensan al sumar. Miremos qué reglas dedujo:

for nombre, coef in zip(X.columns, modelo.coef_):
    print(f"{nombre:>15}: {coef:+.2f}")
print(f"{'intercepto':>15}: {modelo.intercept_:+.2f}")
            mes: +0.11
       es_finde: +2.41
  es_sant_jordi: +70.93
     intercepto: +1.92

Interpretación con humildad, que es como se interpretan siempre los coeficientes:

  • Día laborable base: ~1.9 + 0.11·mes ≈ 2-3 unidades. Coherente con lo observado.
  • Fin de semana: +2.4 unidades/día. El sábado de 11-03, ahora cuantificado como regla.
  • Sant Jordi: +71 unidades. Cuidado: ese coeficiente se aprendió de un único día en todo el histórico. El modelo no "entiende" Sant Jordi; memorizó un punto. Con dos o tres años de datos sería una regla; con uno, es una anotación fiable pero frágil.
  • Y la advertencia general: coeficiente no es causa. es_finde no provoca ventas; captura que la gente pasea el sábado. Si el ayuntamiento peatonaliza la calle un martes, el modelo no se entera.

La decisión: el pedido de Sant Jordi

El modelo predice cualquier día futuro construyendo sus características. Abril 2027: 30 días, 9 de fin de semana, y Sant Jordi cae en viernes:

import numpy as np

abril_2027 = pd.DataFrame({
    "mes": [4] * 30,
    "es_finde": [1 if d in dias_finde_abril_2027 else 0 for d in range(1, 31)],
    "es_sant_jordi": [1 if d == 23 else 0 for d in range(1, 31)],
})
pred_diaria = modelo.predict(abril_2027)
print(f"Predicción abril 2027: {pred_diaria.sum():.0f} unidades")
# Predicción abril 2027: 172 unidades

172 unidades — la misma zona que el abril real de 2026 (168), que es lo razonable con un solo año de histórico. Y aquí la parte más importante de la lección: el modelo no decide; Ana decide. Ana coge la predicción, le añade un margen de seguridad (~10 %, porque quedarse sin libros el 23 de abril cuesta más que sobrestockar unos ejemplares) y la reparte según la cuota que cada título tuvo en abril (11-03):

Título Cuota abril Pedido (sobre 190)
El Quijote 35 % 67
La Odisea 30 % 57
Hamlet 23 % 44
Fausto 12 % 22

Pedido final: 190 unidades. La pregunta que abrió el módulo, cerrada: no con una corazonada, sino con un histórico limpio (11-03), un pico visualizado (11-04) y una proyección con su error conocido (±1.1/día). El criterio del margen, el reparto y la última palabra siguen siendo humanos — así debe ser.

Caso completo 2 — clasificación: ¿comprará este socio este mes?

Marta quiere saber a qué socios enviar el boletín del club de lectura. Pregunta de clasificación: socio × mes → ¿compra (1) o no (0)? El dataset socios_meses.csv sale de cruzar las fichas de socios (LUIS-001, MARTA-002, PAU-003...) con ventas_2026.csv y los logs de la web: 40 socios × 6 meses = 240 filas, con compro_mes_anterior, visitas_web_mes, meses_como_socio y la etiqueta compra — que solo es 1 en 36 filas (15 %): la mayoría de los meses, la mayoría no compra. Ese desequilibrio es la clave de todo lo que sigue.

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, recall_score

socios = pd.read_csv("datos/socios_meses.csv")
X = socios[["compro_mes_anterior", "visitas_web_mes", "meses_como_socio"]]
y = socios["compra"]

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,
                                                    random_state=42, stratify=y)
clf = LogisticRegression()        # clasificación pese al nombre; misma API: fit/predict
clf.fit(X_train, y_train)
pred = clf.predict(X_test)

print(f"Accuracy: {accuracy_score(y_test, pred):.2f}")   # Accuracy: 0.88

¡88 % de aciertos! Antes de celebrarlo, la trampa del accuracy: como el 85 % de las filas son "no compra", un "modelo" que responda siempre no acierta el 85 % sin aprender nada. Nuestro 88 % es apenas mejor que esa piedra. La métrica que destapa el engaño:

print(f"Recall: {recall_score(y_test, pred):.2f}")       # Recall: 0.43

De los socios que compraron, el modelo solo detectó el 43 %. Para el boletín de Marta —donde no detectar a un comprador probable es lo caro— este modelo aún no sirve tal cual. Lección permanente: con clases desbalanceadas, el accuracy miente; pide siempre recall/precision (y sus remedios: class_weight="balanced", más datos, mejores características). DecisionTreeClassifier se entrena idéntico (misma API) y además se puede leer como reglas si-entonces — buen siguiente experimento.

Sobreajuste: memorizar no es aprender

Dos estudiantes ante el examen de M9: uno memorizó las soluciones de los ejercicios resueltos; el otro entendió los conceptos. Con las preguntas exactas del libro, el memorión saca un 10; con preguntas nuevas, se hunde — y el que entendió, aprueba. El sobreajuste (overfitting) es el modelo memorión: clava los datos de entrenamiento ajustándose incluso a su ruido, y fracasa con datos nuevos, que es lo único que importa.

  • Cómo se detecta: gran diferencia entre el rendimiento en train y en test (por eso existe train_test_split). Un árbol de decisión sin límite de profundidad puede dar 100 % en train y desplomarse en test.
  • Qué lo favorece: modelos muy flexibles + pocos datos. Nuestro coeficiente de Sant Jordi, aprendido de un solo día, es un pequeño sobreajuste asumido y documentado.
  • Qué lo mitiga: más datos, modelos más simples, y las técnicas de la tabla siguiente.

Lo que NO hemos cubierto: tu siguiente mapa

Honestidad de cierre — esta lección es la puerta, no la casa:

Tema Qué resuelve Búscalo como
Validación cruzada Evaluar sin depender de la suerte de un split cross_val_score, k-fold
Ingeniería de características Buenas variables valen más que buenos modelos feature engineering, OneHotEncoder
Escalado y pipelines Encadenar preprocesado + modelo sin fugas de datos StandardScaler, Pipeline
Ajuste de hiperparámetros Elegir la configuración del modelo con método GridSearchCV
Modelos de conjunto Más precisión combinando árboles RandomForest, gradient boosting
Aprendizaje profundo Imágenes, texto, voz — otra escala de datos y cómputo redes neuronales, PyTorch/TensorFlow

Una advertencia ética

El caso 2 predice el comportamiento de personas, y ahí el listón sube: un modelo que decide a quién se contacta, a quién se le ofrece un descuento o — en dominios serios — a quién se le concede un crédito, hereda los sesgos de sus datos y se equivoca con nombre y apellido. La regla en Papyrus y fuera de ella: los modelos recomiendan, las personas deciden; toda decisión con efectos sobre personas requiere supervisión humana, y quien la sufre merece poder preguntar por qué. Que Marta revise la lista antes de enviar el boletín no es burocracia: es la diferencia entre usar una herramienta y delegar en ella.

Errores Comunes y Consejos

  • Evaluar sobre los datos de entrenamiento. El error fundacional: nota inflada garantizada. El split va antes de cualquier fit, siempre.
  • Fiarse del accuracy con clases desbalanceadas. El 88 % que esconde un recall del 43 %. Compara siempre contra la línea base tonta (predecir siempre la clase mayoritaria) y mira recall/precision.
  • Leer coeficientes como causas. es_finde: +2.41 describe una asociación en estos datos; no es una ley física ni un mecanismo. Cambia el contexto y cambia el coeficiente.
  • Extrapolar sin decirlo. Predecir abril 2027 con datos de un solo año es una apuesta razonable declarada como tal. Predecir diciembre (campaña de Navidad que el modelo jamás vio) sería inventar.
  • Olvidar random_state. Sin fijarlo, cada ejecución da un split distinto y tus resultados no son reproducibles — el equivalente ML de los tests no deterministas de M9.
  • Empezar por el modelo. El 80 % del resultado está en la pregunta y los datos (11-01 a 11-03). Un LinearRegression con buenas características gana a un modelo sofisticado con datos sucios.

Ejercicios

  1. Añade al caso 1 la característica es_sabado (separada de es_finde, que pasa a ser es_domingo), reentrena y compara el MAE. Con lo que sabes de 11-03 (sábado 130 vs domingo 97), ¿qué esperas que ocurra con los coeficientes?
  2. Para el caso 2, calcula la línea base: el accuracy de un "modelo" que siempre predice "no compra", sobre el mismo y_test. Escríbelo sin scikit-learn, con pandas o NumPy puro. ¿Cuánto mejora realmente LogisticRegression?
  3. Conceptual: Ana propone entrenar el modelo de regresión con TODOS los datos (sin apartar test) "porque así aprende de más ejemplos" y usarlo directamente para el pedido de 2027. Da un argumento a favor, el argumento en contra, y la práctica estándar que reconcilia ambos.

Soluciones

  1. diario["es_sabado"] = (diario["fecha"].dt.dayofweek == 5).astype(int)
    diario["es_domingo"] = (diario["fecha"].dt.dayofweek == 6).astype(int)
    X = diario[["mes", "es_sabado", "es_domingo", "es_sant_jordi"]]
    # ... mismo split/fit/evaluación
    
    El MAE baja ligeramente (≈1.10 → ≈1.02): separar sábado y domingo da al modelo libertad para asignarles pesos distintos, y los datos la aprovechan — coeficiente del sábado (~+3.1) mayor que el del domingo (~+1.8), exactamente la asimetría 130/97 que groupby mostró en 11-03. Buena característica nueva = conocimiento del negocio codificado.
  2. base = (y_test == 0).mean()
    print(f"Línea base 'siempre no': {base:.2f}")   # 0.85
    
    y_test == 0 es una máscara booleana (11-02) y su media es la proporción de True. La logística saca 0.88 frente a 0.85: 3 puntos de mejora real, no 88. Reportar modelos junto a su línea base debería ser ley.
  3. A favor: con datos escasos, cada fila cuenta, y un 20 % apartado es información desperdiciada para el modelo final. En contra: sin test no tienes ninguna medida honesta del error — no sabrías si el pedido se basa en un modelo decente o en uno sobreajustado; volar sin instrumentos. Práctica estándar: evaluar con split (o validación cruzada) para conocer el error esperado y, una vez validado el enfoque, reentrenar con todos los datos para producir el modelo definitivo. Se obtiene lo mejor de ambos: la medida y los datos.

Conclusión

El módulo 11 cumplió su promesa: las preguntas que la web de M10 dejó en el aire tienen hoy respuesta con datos. NumPy puso el motor (vectorización y máscaras booleanas), pandas lo convirtió en respuestas — sábado como rey de la semana con 130 unidades, La Odisea reinando en la web, Fausto con 210 visitas y un 11.9 % de conversión que señala a su precio —, Matplotlib lo hizo visible en el panel de la trastienda con la flecha clavada en Sant Jordi, y scikit-learn cerró el círculo: una regresión con MAE de ±1.1 unidades/día proyectó 172 unidades para abril de 2027, y Ana — no el modelo: Ana — firmó un pedido de 190 repartido por cuotas, con margen y con criterio. También te llevas las vacunas: el accuracy que miente con clases desbalanceadas, el sobreajuste del memorión, los coeficientes que no son causas y la regla de oro de que los modelos recomiendan y las personas deciden. Mira ahora la estantería completa: un paquete papyrus con modelos, errores y tests (M1-M9), una web con Flask y Django que lo sirve al mundo (M10) y un laboratorio de datos que lo entiende (M11). No falta ninguna pieza; falta ensamblarlas en un sistema que se sostenga entero, de la primera línea al informe final. Eso es el proyecto final, y es exactamente el módulo 12.

Curso de Programación en Python

Módulo 1: Introducción a Python

Módulo 2: Estructuras de Control

Módulo 3: Funciones y Módulos

Módulo 4: Estructuras de Datos

Módulo 5: Programación Orientada a Objetos

Módulo 6: Manejo de Archivos

Módulo 7: Manejo de Errores y Excepciones

Módulo 8: Temas Avanzados

Módulo 9: Pruebas y Depuración

Módulo 10: Desarrollo Web con Python

Módulo 11: Ciencia de Datos con Python

Módulo 12: Proyecto Final

© Copyright 2026. Todos los derechos reservados