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
- Qué es ML de verdad: reglas escritas vs patrones aprendidos
- Tipos de aprendizaje: supervisado y no supervisado
- El flujo scikit-learn: split → fit → predict → evaluar
- Caso completo 1 — regresión: predecir las ventas diarias
- La decisión: el pedido de Sant Jordi
- Caso completo 2 — clasificación: ¿comprará este socio este mes?
- Sobreajuste: memorizar no es aprender
- Lo que NO hemos cubierto (tu siguiente mapa)
- 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
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íaEl 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}")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_findeno 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 unidades172 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:
De los socios que sí 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.41describe 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
LinearRegressioncon buenas características gana a un modelo sofisticado con datos sucios.
Ejercicios
- Añade al caso 1 la característica
es_sabado(separada dees_finde, que pasa a seres_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? - 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 realmenteLogisticRegression? - 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
-
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 quediario["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óngroupbymostró en 11-03. Buena característica nueva = conocimiento del negocio codificado. -
base = (y_test == 0).mean() print(f"Línea base 'siempre no': {base:.2f}") # 0.85y_test == 0es una máscara booleana (11-02) y su media es la proporción deTrue. 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. - 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
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
