Saltar a contenido

Ejercicios — Módulo 01: Fundamentos

AI Master Academy · Módulo 01-FUNDAMENTOS 30 ejercicios para consolidar todo lo aprendido en los 7 capítulos del módulo.


Instrucciones de uso

Antes de empezar, lee estas reglas. Son la diferencia entre aprender de verdad y creer que aprendiste:

  1. Intenta cada ejercicio durante al menos 15 minutos antes de mirar la solución. La lucha es donde ocurre el aprendizaje. Si lo resuelves en 2 minutos, perfecto; si no, insiste. Solo después de 15 minutos de intento honesto, abre el desplegable de la solución.
  2. Teclea el código, no lo copies y pegues. Tus dedos también aprenden. Copiar y pegar da la ilusión de dominio; teclear obliga a tu cerebro a procesar cada línea.
  3. Ejecuta todo el código. Cada bloque de código de este archivo es funcional. Crea un archivo ejercicios.py (o un notebook) y comprueba que obtienes los mismos resultados.
  4. En los ejercicios conceptuales, escribe tu respuesta antes de leer la propuesta. Escribir obliga a ordenar ideas. Comparar tu texto con la solución te muestra huecos que no sabías que tenías.
  5. Verifica los cálculos a mano. En la Sección B, primero calcula con papel y calculadora, y después verifica con NumPy. El objetivo es que entiendas qué hace la máquina, no que la máquina piense por ti.
  6. Anota tus dudas. Si tras leer una solución sigues sin entender algo, vuelve al capítulo correspondiente antes de continuar.

Nota

necesitas Python 3.10+ con numpy y pandas instalados (pip install numpy pandas). Todo lo demás es biblioteca estándar.

Niveles de dificultad

Símbolo Nivel Qué esperar
Básico Aplicación directa de un concepto del capítulo
Intermedio Combina 2-3 conceptos o requiere razonamiento propio
Avanzado Integra varios capítulos o exige pensamiento crítico

Criterio de aprobación del módulo

  • Un ejercicio cuenta como superado si lo resolviste correctamente por tu cuenta, o si tras ver la solución fuiste capaz de reproducirla al día siguiente sin mirarla.
  • Necesitas 25 de 30 ejercicios superados (≈83%) para dar por aprobado el módulo y pasar al Módulo 02.
  • Si no llegas a 25: identifica en qué sección fallaste más y repasa el capítulo asociado antes de reintentar.

Índice de ejercicios

# Ejercicio Sección Capítulo Dificultad
1 Explicar IA vs ML vs DL a un directivo A — Conceptual Cap. 1
2 Clasificar 5 casos reales por paradigma de aprendizaje A — Conceptual Cap. 6
3 ¿Este problema necesita IA o bastan reglas? A — Conceptual Cap. 1
4 Modelo propietario vs open source con requisitos de privacidad A — Conceptual Cap. 7
5 Ordenar hitos de la historia de la IA y justificar su importancia A — Conceptual Cap. 2
6 Los inviernos de la IA: causas y lecciones para hoy A — Conceptual Cap. 2
7 Entrenamiento vs inferencia: explicárselo al equipo de finanzas A — Conceptual Cap. 6
8 Overfitting explicado con analogías (y cómo detectarlo) A — Conceptual Cap. 6
9 Roles profesionales: ¿a quién contrato para mi equipo de IA? A — Conceptual Cap. 7
10 Evaluar el hype: ¿este anuncio revoluciona la IA o es humo? A — Conceptual Caps. 1, 2, 7
11 Producto punto a mano y con NumPy B — Matemáticas Cap. 3
12 Similitud coseno entre documentos de juguete B — Matemáticas Cap. 3
13 Normalizar vectores (norma L2) B — Matemáticas Cap. 3
14 Aplicar softmax e interpretar la temperatura B — Matemáticas Cap. 3
15 Un paso de descenso de gradiente a mano para f(x) = x² B — Matemáticas Cap. 3
16 Teorema de Bayes: el test médico que engaña B — Matemáticas Cap. 3
17 Media y desviación estándar de latencias con NumPy B — Matemáticas Caps. 3-4
18 Mini buscador semántico con similitud coseno B — Matemáticas Caps. 3-5
19 FizzBuzz con giro: monitor de lotes de entrenamiento C — Python Cap. 4
20 Funciones con type hints: calculadora de coste de tokens C — Python Cap. 4
21 Comprehensions sobre datos de clientes C — Python Cap. 4
22 Parsear el JSON de una API de IA ficticia C — Python Cap. 4
23 Clase Documento con métodos C — Python Cap. 4
24 Manejo de excepciones con datos sucios C — Python Cap. 4
25 NumPy: matrices y vectorización vs bucle (benchmark) C — Python Cap. 4
26 Pandas: análisis de ventas con CSV inline C — Python Cap. 4
27 Pipeline completo: limpiar datos y calcular estadísticas D — Integrador Caps. 3-5
28 Regresión lineal con descenso de gradiente desde cero D — Integrador Caps. 3-6
29 Clasificador de sentimiento por reglas y sus límites vs ML D — Integrador Caps. 1, 4, 6
30 Caso de empresa: ¿reglas, ML clásico o LLM? D — Integrador Todos

SECCIÓN A — Ejercicios conceptuales

Comprensión profunda de los capítulos 1, 2, 6 y 7. Escribe tus respuestas antes de mirar las soluciones.


Ejercicio 1 — Explicar IA vs ML vs DL a un directivo

Dificultad: · Capítulo relacionado: Cap. 1 (¿Qué es la IA?)

Contexto: Trabajas como consultor junior de IA. El director financiero (CFO) de una cadena de supermercados te dice en una reunión: "Todo el mundo habla de IA, machine learning y deep learning como si fueran lo mismo. ¿Me lo puedes explicar en dos minutos, sin tecnicismos, y decirme cuál necesito para predecir la demanda de productos frescos?"

Tu tarea: Escribe una explicación de máximo 200 palabras que: 1. Diferencie los tres conceptos con una analogía comprensible. 2. Deje clara la relación de inclusión entre ellos (cuál contiene a cuál). 3. Termine con una recomendación concreta para su caso de predicción de demanda.

Ver solución **Respuesta modelo (adáptala a tu estilo, lo importante son las ideas):** > "Piense en tres círculos, uno dentro de otro, como muñecas rusas. > > El círculo grande es la **Inteligencia Artificial**: cualquier sistema que hace tareas que asociamos con inteligencia humana — decidir, clasificar, predecir. Incluye desde reglas escritas a mano ('si el cliente gasta más de 500 €, es VIP') hasta sistemas que aprenden solos. > > Dentro está el **Machine Learning**: en vez de escribir las reglas nosotros, le damos a la máquina ejemplos históricos y *ella deduce las reglas*. Le enseñamos tres años de ventas y aprende qué factores mueven la demanda. > > Y dentro del ML está el **Deep Learning**: machine learning con redes neuronales de muchas capas. Brilla con datos masivos y complejos: imágenes, voz, texto. Es lo que hay detrás de ChatGPT. > > Para predecir demanda de frescos, no necesita deep learning: con **machine learning clásico** sobre su histórico de ventas, calendario y meteorología obtendrá resultados excelentes, más baratos y más fáciles de explicar al consejo." **Por qué esta respuesta funciona:** - **Analogía de círculos concéntricos / muñecas rusas:** transmite la relación de inclusión (DL ⊂ ML ⊂ IA) sin dibujar un diagrama de Venn formal. Este es el punto que más se suele evaluar: DL es un subconjunto de ML, y ML es un subconjunto de IA. Decir "IA o ML" como si fueran alternativas excluyentes es un error conceptual. - **Cero tecnicismos:** no aparece "gradiente", "red convolucional" ni "parámetros". Un directivo necesita el mapa, no el motor. - **Recomendación accionable y honesta:** un error típico de junior es recomendar deep learning para todo. La predicción de demanda con datos tabulares (ventas, fechas, clima) es terreno clásico de ML tradicional (regresiones, árboles de decisión): más barato, más interpretable, y con datasets medianos suele rendir igual o mejor que una red profunda. **Errores comunes a evitar:** 1. Presentar los tres términos como sinónimos o como tecnologías rivales. 2. No responder la pregunta de negocio (el CFO quería una recomendación, no solo una clase). 3. Superar las 200 palabras: la concisión es parte del ejercicio. Comunicar IA a negocio es una habilidad profesional en sí misma. **Consejo profesional:** practica esta explicación en voz alta hasta que dure 90 segundos. La pregunta "¿qué diferencia hay entre IA y ML?" aparece en la mayoría de entrevistas y reuniones con clientes.

Ejercicio 2 — Clasificar 5 casos reales por paradigma de aprendizaje

Dificultad: · Capítulo relacionado: Cap. 6 (Cómo aprende una máquina)

Contexto: Eres el nuevo responsable de IA de una consultora. Cinco clientes distintos te describen sus proyectos. Para asignar el equipo correcto, debes clasificar cada caso en uno de los tres paradigmas: aprendizaje supervisado, no supervisado o por refuerzo.

Casos:

  1. Un banco tiene 200 000 transacciones antiguas, cada una etiquetada por sus analistas como "fraude" o "legítima", y quiere un sistema que marque las transacciones nuevas.
  2. Una tienda online quiere descubrir "tipos de clientes" en su base de datos, pero no tiene ninguna categoría predefinida: quiere que el sistema encuentre los grupos por sí solo.
  3. Un estudio de videojuegos quiere un bot que aprenda a jugar a su juego de carreras probando acciones y recibiendo puntos por adelantar y penalizaciones por chocar.
  4. Un hospital tiene 50 000 radiografías, cada una revisada por radiólogos que anotaron "neumonía" o "sin neumonía", y quiere un modelo de apoyo al diagnóstico.
  5. Una plataforma de música quiere agrupar canciones "que suenan parecido" a partir de sus características de audio, sin géneros predefinidos, para descubrir estilos emergentes.

Tu tarea: Para cada caso, indica el paradigma y justifica en una frase qué pista del enunciado te lo revela.

Ver solución | Caso | Paradigma | Pista clave en el enunciado | |------|-----------|------------------------------| | 1. Fraude bancario | **Supervisado** (clasificación) | Hay datos históricos **etiquetados** ("fraude" / "legítima"): la máquina aprende de ejemplos con respuesta conocida. | | 2. Tipos de clientes | **No supervisado** (clustering) | "**No tiene ninguna categoría predefinida**": no hay etiquetas, el objetivo es descubrir estructura oculta. | | 3. Bot de carreras | **Por refuerzo** | El agente **prueba acciones** y recibe **recompensas y penalizaciones**: aprendizaje por interacción con un entorno. | | 4. Radiografías | **Supervisado** (clasificación) | Cada imagen tiene una **etiqueta puesta por expertos** ("neumonía" / "sin neumonía"). | | 5. Canciones parecidas | **No supervisado** (clustering) | Se buscan **grupos por similitud** "sin géneros predefinidos": de nuevo, ausencia de etiquetas. | **El razonamiento general (memoriza estas tres preguntas):** 1. **¿Tengo ejemplos con la respuesta correcta ya anotada?** → Supervisado. La palabra clave en los enunciados suele ser "etiquetado", "anotado", "clasificado por expertos", "histórico con resultados conocidos". 2. **¿Tengo datos sin respuesta y quiero descubrir estructura (grupos, patrones, anomalías)?** → No supervisado. Palabras clave: "descubrir", "agrupar", "segmentar", "sin categorías previas". 3. **¿Hay un agente que actúa en un entorno y recibe recompensas/castigos por sus acciones?** → Refuerzo. Palabras clave: "probar acciones", "puntos", "penalización", "aprender jugando". **Matices que distinguen a un profesional:** - Los casos 1 y 4 son ambos supervisados, pero fíjate: uno usa datos tabulares (transacciones) y otro imágenes (radiografías). El *paradigma* es el mismo; la *arquitectura* del modelo cambiará (ML clásico probablemente para el 1, deep learning para el 4). Paradigma ≠ tecnología. - En el caso 2, después de descubrir los grupos, el negocio podría etiquetarlos ("cazadores de ofertas", "compradores premium"...) y entrenar luego un clasificador supervisado que asigne clientes nuevos a esos grupos. Los paradigmas se encadenan en proyectos reales. - El caso 3 es refuerzo puro: no hay dataset previo; los "datos" se generan al interactuar. Este es el rasgo que más lo diferencia de los otros dos.

Ejercicio 3 — ¿Este problema necesita IA o bastan reglas?

Dificultad: · Capítulo relacionado: Cap. 1 (¿Qué es la IA?)

Contexto: Eres consultor. Una empresa de logística te plantea cuatro necesidades y quiere "ponerles IA a todas" porque lo pide su junta directiva. Tu trabajo (y tu reputación) consiste en recomendar IA solo donde aporta valor.

Necesidades:

  1. Cálculo de tarifas de envío: el precio depende del peso, el volumen, la distancia y si es urgente. Las fórmulas están documentadas en un PDF oficial de 3 páginas y cambian una vez al año.
  2. Clasificación de incidencias: llegan 2 000 correos diarios de clientes en lenguaje libre ("mi paquete llegó roto", "¿dónde está mi pedido?", "quiero factura") que hoy clasifican a mano 6 personas en 12 categorías.
  3. Validación de códigos postales: comprobar que el código postal introducido en un formulario existe y coincide con la provincia.
  4. Predicción de retrasos: estimar qué envíos llegarán tarde combinando tráfico histórico, meteorología, temporada y rendimiento de cada ruta.

Tu tarea: Para cada necesidad, decide entre sistema de reglas o IA/ML, y justifica con el criterio "¿puedo escribir las reglas explícitamente?".

Ver solución | Necesidad | Recomendación | Justificación | |-----------|---------------|---------------| | 1. Tarifas de envío | **Reglas** | La lógica está **completamente especificada** en un documento, es determinista y cambia una vez al año. Programarla directamente es más barato, exacto al 100%, auditable y trivial de mantener. Usar ML aquí introduciría errores donde hoy no los hay. | | 2. Clasificación de correos | **IA/ML** | El lenguaje natural libre **no se puede capturar con reglas**: hay infinitas formas de decir "paquete roto" (con erratas, ironía, mezcla de temas). Es el caso de uso clásico de clasificación de texto supervisada; además hay volumen (2 000/día) y coste humano (6 personas) que justifican la inversión. | | 3. Códigos postales | **Reglas** (una consulta a tabla) | Es una **verificación exacta contra una lista oficial finita**. Un `lookup` en una tabla resuelve el 100% de los casos. Meter ML aquí sería absurdo: no hay patrón que aprender, hay un dato que consultar. | | 4. Predicción de retrasos | **IA/ML** | La relación entre tráfico + clima + temporada + ruta y el retraso es **estadística, no determinista, y demasiado compleja para enumerarla a mano**. Hay datos históricos abundantes: terreno ideal para ML supervisado (regresión o clasificación "llegará tarde sí/no"). | **El criterio maestro (guárdalo para toda tu carrera):** > **Si un humano puede escribir todas las reglas de forma explícita, completa y estable → programa las reglas.** > **Si las reglas son desconocidas, difusas, cambiantes o hay demasiadas excepciones, pero tienes ejemplos históricos → machine learning.** **Señales de que un problema pide reglas:** lógica documentada, resultado exacto exigido (facturación, legal), pocos casos límite, cambia con poca frecuencia. **Señales de que un problema pide ML:** entrada en lenguaje natural o imágenes, patrones estadísticos, muchas excepciones, los propios expertos "lo saben hacer pero no saben explicar cómo", y existen datos históricos etiquetados o etiquetables. **El argumento de negocio que debes dar a la junta:** aplicar IA donde bastan reglas no es "innovación", es asumir coste, latencia, mantenimiento y errores probabilísticos a cambio de nada. El prestigio del consultor está en decir "aquí NO" con argumentos. Recomendar reglas en los casos 1 y 3 te da credibilidad para pedir presupuesto serio en los casos 2 y 4. **Advertencia:** "tiene que llevar IA porque lo pide la junta" es la primera frase de muchos proyectos fracasados. La tecnología se elige por el problema, no por la moda.

Ejercicio 4 — Modelo propietario vs open source con requisitos de privacidad

Dificultad: · Capítulo relacionado: Cap. 7 (Ecosistema y herramientas)

Contexto: Un hospital privado quiere un asistente interno que resuma historiales clínicos para sus médicos. Requisitos del cliente:

  • Los datos de pacientes no pueden salir de la infraestructura del hospital (normativa sanitaria y política interna estricta).
  • Presupuesto: tienen servidores propios con GPUs adquiridos el año pasado, y un equipo técnico pequeño (2 ingenieros).
  • La calidad del resumen debe ser alta, pero el volumen es moderado (unos 500 resúmenes al día).
  • Necesitan tenerlo funcionando en 4 meses.

Tu tarea: 1. Decide entre: (a) API de modelo propietario (p. ej. GPT/Claude/Gemini vía nube), (b) modelo open source (p. ej. Llama, Mistral, Qwen) desplegado en sus servidores, o (c) una solución híbrida. 2. Justifica con al menos 4 argumentos. 3. Menciona 2 riesgos de tu opción elegida y cómo mitigarlos.

Ver solución **Decisión recomendada: (b) modelo open source desplegado en la infraestructura del hospital (on-premise).** **Justificación (argumento por argumento):** 1. **El requisito de privacidad es eliminatorio.** "Los datos no pueden salir de la infraestructura" descarta de raíz cualquier API en la nube pública: al llamar a un modelo propietario, el historial clínico viaja a servidores de un tercero. Aunque los proveedores ofrezcan contratos de no-retención y certificaciones, la política interna del hospital lo prohíbe explícitamente. Cuando un requisito es eliminatorio, ordena toda la decisión. 2. **Ya tienen el hardware.** El mayor coste de la opción open source (GPUs) ya está amortizado. Un modelo open source de tamaño medio (7B-70B parámetros, según sus GPUs) corre en servidores propios con motores de inferencia como vLLM u Ollama. 3. **El volumen es moderado.** 500 resúmenes/día no exige la escala elástica de la nube; un despliegue local lo maneja de sobra. Si fueran 500 000/día, la conversación sería otra. 4. **Control y auditabilidad.** Con un modelo local, el hospital controla la versión exacta del modelo (no cambia sin avisar, cosa que sí ocurre con APIs), puede auditar registros internamente y cumple mejor ante inspecciones regulatorias. 5. **Resumir es una tarea "asequible".** El resumen de documentos es una de las tareas donde los modelos open source actuales rinden cerca de los propietarios, sobre todo si se ajusta el prompt o se hace un fine-tuning ligero con ejemplos del hospital. **Riesgos de la opción elegida y mitigaciones:** | Riesgo | Mitigación | |--------|------------| | **Equipo pequeño (2 ingenieros) para desplegar y mantener** un stack de inferencia, monitorización y actualizaciones. | Usar herramientas maduras "llave en mano" (vLLM, Ollama, contenedores preconfigurados), empezar con un solo modelo y una sola tarea, y contratar soporte externo puntual para el despliegue inicial. Los 4 meses de plazo son viables si el alcance se limita a resúmenes. | | **Calidad inferior al mejor modelo propietario** en casos clínicos complejos o jerga muy especializada. | Evaluación piloto con médicos antes de producción (comparar resúmenes del modelo vs resúmenes humanos en 100 casos), plantilla de prompt cuidada, y mantener SIEMPRE al médico como revisor final: el sistema asiste, no decide. | **¿Cuándo habría ganado la opción (a) o la (c)?** - **(a) API propietaria:** si el dato no fuera sensible o la normativa permitiera nubes certificadas, y no hubiera hardware propio: menor tiempo de puesta en marcha y máxima calidad sin gestionar infraestructura. - **(c) Híbrida:** patrón muy común en empresas reales: datos sensibles → modelo local; tareas sin datos personales (p. ej. redactar plantillas genéricas, documentación interna) → API propietaria. Aquí no hace falta porque el caso de uso es único y 100% sensible. **Lección del ejercicio:** en decisiones de ecosistema, primero identifica los requisitos *eliminatorios* (privacidad, normativa, presupuesto duro) y luego optimiza el resto (calidad, velocidad, comodidad). Muchos debates "propietario vs open source" se resuelven solos al mirar quién puede legalmente tocar los datos.

Ejercicio 5 — Ordenar hitos de la historia de la IA y justificar su importancia

Dificultad: · Capítulo relacionado: Cap. 2 (Historia de la IA)

Contexto: Preparas una charla introductoria para nuevos empleados de tu empresa. Tienes estas seis tarjetas desordenadas y debes colocarlas en una línea temporal correcta y explicar, en una frase por hito, por qué importó.

Tarjetas (desordenadas):

  • A. Deep Blue derrota a Garry Kasparov en ajedrez.
  • B. Conferencia de Dartmouth: se acuña el término "Artificial Intelligence".
  • C. Lanzamiento público de ChatGPT.
  • D. Alan Turing publica "Computing Machinery and Intelligence" (el test de Turing).
  • E. AlexNet gana ImageNet y detona la era del deep learning.
  • F. Se publica "Attention Is All You Need" (la arquitectura Transformer).

Tu tarea: Ordena cronológicamente (con el año aproximado) y escribe una frase de importancia por cada hito.

Ver solución **Orden cronológico correcto: D → B → A → E → F → C** | Orden | Hito | Año | Por qué importó | |-------|------|-----|-----------------| | 1º | **D. Test de Turing** | 1950 | Antes de que existieran los ordenadores modernos, Turing reformuló "¿pueden pensar las máquinas?" como una pregunta operativa y medible ("¿puede una máquina hacerse pasar por humana en una conversación?"), fundando el debate conceptual de todo el campo. | | 2º | **B. Conferencia de Dartmouth** | 1956 | Acto fundacional: se acuña el nombre "Artificial Intelligence" y la disciplina nace como campo de investigación formal con sus primeros investigadores y programas. | | 3º | **A. Deep Blue vence a Kasparov** | 1997 | Primera vez que una máquina derrota al campeón mundial de ajedrez: demostró que la búsqueda masiva + heurísticas superan al humano en dominios cerrados y bien definidos (aunque no "entendía" ajedrez). | | 4º | **E. AlexNet gana ImageNet** | 2012 | El momento "big bang" del deep learning moderno: una red neuronal profunda entrenada en GPUs pulverizó a todos los métodos clásicos de visión, demostrando que datos masivos + GPUs + redes profundas era la fórmula ganadora. | | 5º | **F. Paper del Transformer** | 2017 | "Attention Is All You Need" introdujo la arquitectura sobre la que se construyen TODOS los grandes modelos de lenguaje actuales (la T de GPT significa Transformer). Posiblemente el paper más influyente de la década. | | 6º | **C. Lanzamiento de ChatGPT** | 2022 (30 de noviembre) | No fue un avance científico radical sino de *producto y acceso*: puso un LLM potente en manos de cualquier persona, alcanzó 100 millones de usuarios en 2 meses y desató la actual ola de adopción masiva e inversión en IA. | **Claves para tu charla (el hilo narrativo que une los hitos):** 1. **1950-1956:** la IA nace como *pregunta filosófica* (Turing) y luego como *disciplina* (Dartmouth). 2. **1997:** la fuerza bruta computacional gana en mundos cerrados (ajedrez), pero el mundo abierto sigue lejos. 3. **2012:** cambio de paradigma: dejar de programar el conocimiento y dejar que las redes lo *aprendan* de datos masivos. Las GPUs (¡diseñadas para videojuegos!) resultan ser el hardware perfecto. 4. **2017-2022:** el Transformer permite escalar el aprendizaje sobre texto a escala de internet, y ChatGPT convierte esa tecnología en un fenómeno social. **Error frecuente:** situar AlexNet o el Transformer "cerca de ChatGPT" sin distinguirlos. Conviene recordar la cadena causal: sin Transformer (2017) no hay GPT; sin la demostración de AlexNet (2012) nadie habría apostado tan fuerte por las redes profundas; sin GPUs baratas, nada de lo anterior habría sido práctico.

Ejercicio 6 — Los inviernos de la IA: causas y lecciones para hoy

Dificultad: · Capítulo relacionado: Cap. 2 (Historia de la IA)

Contexto: Tu directora de estrategia ha leído que la IA ya vivió "inviernos" (períodos de desilusión y recortes de financiación) y te pregunta, preocupada por las inversiones de la empresa: "¿Qué causó los inviernos anteriores y qué señales deberíamos vigilar para saber si viene otro?"

Tu tarea: Redacta una respuesta que cubra: 1. Qué fue cada uno de los dos grandes inviernos de la IA (épocas aproximadas y causas). 2. El patrón común que los provocó. 3. Tres diferencias entre aquellas épocas y la situación actual. 4. Una señal de alerta razonable que vigilar hoy.

Ver solución **1. Los dos grandes inviernos:** - **Primer invierno (aprox. 1974-1980):** tras el optimismo desbordado de los años 50-60 ("en una generación tendremos máquinas tan inteligentes como humanos"), los resultados reales fueron mucho más modestos. Informes gubernamentales críticos (como el informe Lighthill en Reino Unido, 1973) concluyeron que la IA no había cumplido sus promesas, y la financiación pública se desplomó. Causas técnicas de fondo: potencia de cálculo ridícula para las ambiciones, y la "explosión combinatoria" — los métodos que funcionaban en problemas de juguete no escalaban al mundo real. - **Segundo invierno (aprox. 1987-1993):** el auge comercial de los **sistemas expertos** (programas con miles de reglas "si-entonces" escritas por expertos) creó una industria entera en los 80. Colapsó porque los sistemas eran carísimos de mantener (cada regla nueva podía romper otras), frágiles fuera de su dominio exacto, y el hardware especializado (máquinas Lisp) fue arrasado por los PC genéricos. Empresas quebradas, inversores quemados, y "IA" se convirtió en palabra maldita en las propuestas de financiación durante años. **2. El patrón común (esta es la parte importante):** > **Expectativas infladas muy por encima de la capacidad técnica real → promesas incumplidas → pérdida de confianza → retirada masiva de financiación.** El ciclo es: hype → decepción → invierno. No fue que la tecnología "fallara" de repente: fue que se prometió lo que no se podía entregar. **3. Tres diferencias relevantes con la situación actual:** 1. **La IA actual ya genera ingresos reales y masivos.** Los inviernos anteriores golpearon a una IA que era esencialmente investigación con demos; hoy la IA está en producción en buscadores, recomendadores, traducción, programación asistida, atención al cliente... Hay valor de negocio verificable, no solo promesas. 2. **La infraestructura es genérica y barata en comparación.** El segundo invierno mató un ecosistema de hardware especializado y frágil; hoy la IA corre sobre nubes y GPUs de propósito general con un ecosistema de software abierto gigantesco (PyTorch, Hugging Face...). 3. **Adopción masiva por usuarios finales.** Cientos de millones de personas usan herramientas de IA directamente. Los inviernos anteriores ocurrieron en tecnologías que el público general nunca llegó a tocar. **4. Señal de alerta a vigilar hoy:** La distancia entre **lo prometido y lo facturado**. Si las inversiones multimillonarias en IA generativa no encuentran modelos de negocio que las justifiquen en un plazo razonable (es decir: si los costes de entrenar e inferir crecen más rápido que los ingresos que generan), puede producirse una corrección fuerte de inversión — quizá no un "invierno" total, pero sí un enfriamiento sectorial. La lección operativa para tu empresa: **invertir en casos de uso con ROI medible y a corto plazo, no en "tener IA" como fin en sí mismo.** Esa disciplina es exactamente lo que faltó antes de los dos inviernos.

Ejercicio 7 — Entrenamiento vs inferencia: explicárselo al equipo de finanzas

Dificultad: · Capítulo relacionado: Cap. 6 (Cómo aprende una máquina)

Contexto: El equipo de finanzas de tu empresa está revisando el presupuesto del proyecto de IA y no entiende dos partidas: una enorme y puntual llamada "entrenamiento del modelo" y otra pequeña pero recurrente llamada "inferencia". El responsable te pregunta: "¿Por qué pagamos dos veces por lo mismo? ¿No basta con entrenar el modelo una vez?"

Tu tarea: 1. Explica la diferencia entre entrenamiento e inferencia con una analogía no técnica. 2. Explica por qué tienen perfiles de coste tan distintos (puntual y caro vs recurrente y barato por unidad). 3. Indica qué fase usa cada equipo: ¿quién entrena y quién "solo" infiere? 4. Responde a su pregunta: ¿de verdad se entrena una sola vez?

Ver solución **1. La analogía (respuesta modelo):** > "Piense en formar a un médico. El **entrenamiento** es la carrera de Medicina: años de estudio, miles de casos revisados, un coste enorme que se paga una vez. La **inferencia** es cada consulta que ese médico pasa después: usa lo que ya aprendió para atender a un paciente nuevo en minutos. No pagamos dos veces por lo mismo: pagamos una vez por *crear la capacidad* y luego pagamos un poco cada vez que *la usamos*." **2. Por qué los perfiles de coste son tan distintos:** | | Entrenamiento | Inferencia | |---|---------------|------------| | **Qué ocurre** | El modelo ajusta millones de parámetros viendo todos los datos históricos muchas veces (épocas), calculando errores y corrigiéndose con descenso de gradiente. | El modelo, ya con los parámetros congelados, recibe UNA entrada nueva y produce UNA salida. Solo hay un "pase hacia delante". | | **Cómputo** | Masivo: días o semanas de GPUs trabajando en paralelo. | Mínimo por petición: milisegundos o segundos. | | **Frecuencia** | Puntual (o periódica: reentrenos). | Continua: cada usuario, cada petición, 24/7. | | **Perfil de coste** | Tipo inversión: grande y de una vez. | Tipo coste operativo: pequeño por unidad, pero se multiplica por el volumen de uso. | **Nota:** aunque la inferencia sea barata *por unidad*, a gran escala puede superar con mucho el coste total del entrenamiento. Un modelo que atiende millones de peticiones al día gasta más en inferencia acumulada que lo que costó entrenarlo. Finanzas debe presupuestar la inferencia como coste variable ligado al uso. **3. Quién hace cada cosa:** - **Entrenan:** los laboratorios/equipos de data science o ML engineering (o directamente el proveedor, si usas un modelo como GPT/Claude vía API: tú jamás entrenas ese modelo). - **Infieren:** las aplicaciones de producción y, por tanto, indirectamente cada usuario final. Cuando tu app llama a la API de un LLM, estás pagando inferencia. **4. ¿Se entrena una sola vez?** No exactamente. El mundo cambia y los datos con él (a esto se le llama *deriva de datos* o data drift): los patrones de fraude evolucionan, aparecen productos nuevos, el lenguaje de los clientes cambia. Por eso los modelos se **reentrenan periódicamente** (cada semana, mes o trimestre según el dominio). La respuesta honesta a finanzas: "el entrenamiento es puntual pero no único; presupuesten reentrenos periódicos más un coste de inferencia proporcional al uso". **Consejo profesional:** en las facturas de APIs de LLMs, el precio por token *es* el precio de la inferencia. Entender esta distinción te permite estimar costes de proyecto: (nº peticiones/día) × (tokens por petición) × (precio por token). Harás este cálculo decenas de veces en tu carrera.

Ejercicio 8 — Overfitting explicado con analogías (y cómo detectarlo)

Dificultad: · Capítulo relacionado: Cap. 6 (Cómo aprende una máquina)

Contexto: Una startup te contrata para auditar su modelo de predicción de impagos. El equipo está eufórico: "¡Nuestro modelo acierta el 99,2% en los datos de entrenamiento!". Pero al desplegarlo con clientes reales, el acierto cae al 63%. El CEO, que no es técnico, exige una explicación de qué ha pasado y cómo evitarlo.

Tu tarea: 1. Diagnostica el problema y dale nombre. 2. Explícaselo al CEO con DOS analogías distintas (para que al menos una le encaje). 3. Explica cómo se detecta a tiempo (antes de desplegar). 4. Propón tres medidas concretas para corregirlo.

Ver solución **1. Diagnóstico:** el modelo sufre **overfitting (sobreajuste)**: ha memorizado los datos de entrenamiento en lugar de aprender los patrones generales que hay detrás. Por eso brilla en los datos que ya vio (99,2%) y fracasa con datos nuevos (63%). La diferencia enorme entre ambas cifras es la firma inconfundible del overfitting. **2. Las dos analogías para el CEO:** - **El estudiante que memoriza exámenes:** "Imagine un estudiante que, en vez de entender la asignatura, memoriza las respuestas de los exámenes de años anteriores. En el simulacro (preguntas que ya vio) saca un 10. En el examen real, con preguntas nuevas, suspende. Nuestro modelo hizo exactamente eso: memorizó los casos históricos en lugar de aprender qué hace que un cliente impague." - **El traje a medida extrema:** "Es como un traje cosido tan exactamente al cuerpo de una persona concreta — cada lunar, cada asimetría — que no le sirve a nadie más. Queríamos un patrón de tallas que sirviera a la mayoría; hicimos un traje para un solo maniquí: nuestros datos históricos." **3. Cómo se detecta a tiempo (la práctica que faltó):** Nunca se evalúa un modelo con los mismos datos con los que se entrenó. El procedimiento estándar: 1. **Dividir los datos ANTES de entrenar** en conjunto de entrenamiento (p. ej. 80%) y conjunto de prueba/test (20%) que el modelo jamás ve durante el entrenamiento (el famoso *train/test split* del Capítulo 5). 2. **Comparar rendimiento en ambos conjuntos.** Si entrenamiento ≫ test (como 99% vs 63%), hay overfitting. Si ambos son similares y buenos, el modelo generaliza. 3. Durante el entrenamiento, monitorizar además un conjunto de **validación**: cuando el error de validación empieza a subir mientras el de entrenamiento sigue bajando, es el momento de parar (*early stopping*). **4. Tres medidas correctoras concretas:** 1. **Más datos (o datos más variados):** cuantos más ejemplos reales vea el modelo, más difícil le resulta memorizar y más se ve obligado a generalizar. 2. **Simplificar el modelo:** un modelo con demasiada capacidad (demasiados parámetros para tan pocos datos) memoriza. Reducir complejidad o aplicar **regularización** (penalizar parámetros extremos) fuerza a aprender solo lo esencial. 3. **Validación rigurosa como proceso:** train/test split estricto, validación cruzada, y una regla de oro organizativa: **ninguna cifra de rendimiento se comunica a dirección si viene del conjunto de entrenamiento.** **Advertencia:** el error de esta startup es dolorosamente común: reportar métricas de entrenamiento como si fueran métricas reales. Cuando te presenten un "99% de acierto", tu primera pregunta profesional debe ser siempre: *"¿medido sobre qué datos?"*.

Ejercicio 9 — Roles profesionales: ¿a quién contrato para mi equipo de IA?

Dificultad: · Capítulo relacionado: Cap. 7 (Ecosistema y herramientas)

Contexto: Una empresa mediana de e-commerce va a crear su primer equipo de IA con presupuesto para 3 contrataciones. Su plan de proyectos para el primer año: (1) un dashboard de métricas de negocio con análisis de datos, (2) un sistema de recomendación de productos, y (3) un chatbot de atención al cliente construido sobre la API de un LLM comercial. La directora de RRHH te enseña las descripciones de puesto que ha encontrado por internet — Data Analyst, Data Scientist, Data Engineer, ML Engineer, AI Engineer, MLOps Engineer, Prompt Engineer — y te pregunta: "¿Cuáles tres contrato y por qué? ¿Qué hace cada uno?"

Tu tarea: 1. Define en una frase cada uno de los 7 roles. 2. Elige las 3 contrataciones para ESTA empresa y ESTOS proyectos, justificando. 3. Explica qué proyectos cubre cada contratación.

Ver solución **1. Los 7 roles en una frase:** | Rol | Qué hace | |-----|----------| | **Data Analyst** | Analiza datos históricos y construye informes/dashboards para responder preguntas de negocio (SQL, BI, estadística descriptiva). | | **Data Scientist** | Explora datos y construye modelos (prototipos) para predecir o clasificar; fuerte en estadística y experimentación. | | **Data Engineer** | Construye y mantiene las tuberías de datos: recolección, limpieza, almacenamiento; sin él, nadie tiene datos fiables con los que trabajar. | | **ML Engineer** | Lleva los modelos a producción: los convierte de notebook a servicio robusto, escalable y monitorizado. | | **AI Engineer** | Construye aplicaciones sobre modelos ya existentes (sobre todo LLMs vía API): integración, prompts, RAG, agentes. Rol emergente post-ChatGPT. | | **MLOps Engineer** | Automatiza el ciclo de vida del ML (despliegue, versionado, monitorización, reentrenos); es al ML lo que DevOps al software. | | **Prompt Engineer** | Especialista en diseñar y optimizar instrucciones para LLMs; en la práctica, cada vez más una *habilidad* dentro de otros roles que un puesto independiente. | **2. Las 3 contrataciones recomendadas:** 1. **Data Engineer** — la contratación menos glamurosa y más crítica. Los tres proyectos dependen de datos limpios y accesibles (ventas para el dashboard, historial de compras para el recomendador, catálogo y FAQs para el chatbot). Sin esta base, los otros dos contratados perderán el 70% de su tiempo peleándose con datos rotos. Regla del sector: los proyectos de IA fracasan más por datos que por modelos. 2. **Data Scientist (con perfil híbrido hacia analista)** — cubre el dashboard (proyecto 1) y construye el sistema de recomendación (proyecto 2), el proyecto de ML "clásico" del año. En una empresa pequeña, un buen data scientist absorbe el trabajo de data analyst. 3. **AI Engineer** — el chatbot (proyecto 3) no requiere entrenar ningún modelo: se construye SOBRE la API de un LLM comercial. Eso es exactamente el perfil AI Engineer: integración de APIs, diseño de prompts, conexión con la base de conocimiento de la empresa y evaluación de respuestas. El prompt engineering lo aporta como habilidad, no como puesto aparte. **3. Cobertura de proyectos:** | Proyecto | Responsable principal | Apoyo | |----------|----------------------|-------| | Dashboard de métricas | Data Scientist | Data Engineer (pipelines) | | Recomendador de productos | Data Scientist | Data Engineer (datos), y a futuro un ML Engineer para escalar | | Chatbot sobre LLM | AI Engineer | Data Engineer (base de conocimiento) | **¿Y los descartados?** ML Engineer y MLOps son roles de *escala*: imprescindibles cuando tienes varios modelos en producción con tráfico serio, prematuros para el año 1 (el propio equipo puede cubrir un despliegue sencillo). Prompt Engineer como puesto aislado rara vez se justifica: es una competencia del AI Engineer. **Consejo profesional:** cuando dimensiones equipos de IA, pregunta siempre primero "¿en qué estado están los datos?". La respuesta decide si la primera contratación es un perfil de datos o un perfil de modelos. Casi siempre es de datos.

Ejercicio 10 — Evaluar el hype: ¿este anuncio revoluciona la IA o es humo?

Dificultad: · Capítulos relacionados: Caps. 1, 2 y 7

Contexto: Tu CEO te reenvía una nota de prensa con el asunto "¿¿Deberíamos comprar esto??":

"La startup NeuroQuantum AI presenta OmniMind-X, el primer sistema de Inteligencia Artificial General (AGI) del mercado. OmniMind-X 'piensa como un humano', 'entiende cualquier industria sin necesidad de datos' y 'elimina el 100% de los errores humanos'. Según su fundador, 'los modelos tradicionales quedarán obsoletos en 6 meses'. La empresa, fundada hace 8 meses, no ha publicado benchmarks pero afirma que sus resultados internos 'superan a todos los modelos existentes'. Disponible mediante licencia anual de 400 000 €."

Tu tarea: Escribe un análisis crítico para tu CEO que: 1. Identifique al menos 5 señales de alarma (red flags) concretas en el texto. 2. Explique qué relación tiene esto con la historia del hype en IA (Cap. 2). 3. Proponga una lista de 4 preguntas de verificación ("due diligence") antes de considerar cualquier compra. 4. Termine con una recomendación clara.

Ver solución **1. Señales de alarma, una por una:** | # | Red flag | Por qué es alarmante | |---|----------|----------------------| | 1 | **"Primer sistema de AGI del mercado"** | La AGI (inteligencia artificial general, capaz de cualquier tarea intelectual humana) **no existe hoy**; ningún laboratorio serio — con miles de investigadores y miles de millones en cómputo — afirma haberla logrado. Que lo afirme una startup de 8 meses es extraordinariamente inverosímil, y las afirmaciones extraordinarias exigen pruebas extraordinarias. | | 2 | **"Entiende cualquier industria sin necesidad de datos"** | Contradice el fundamento mismo del machine learning: los modelos aprenden DE datos. Un sistema sin datos de tu dominio no puede conocer tu dominio. Es una imposibilidad técnica presentada como ventaja. | | 3 | **"Elimina el 100% de los errores"** | Los sistemas de ML son estadísticos: SIEMPRE tienen una tasa de error. Prometer el 100% de acierto es la mentira más fácil de detectar de todo el marketing de IA. | | 4 | **Sin benchmarks públicos, solo "resultados internos"** | El estándar del sector es publicar evaluaciones en benchmarks reconocidos y reproducibles, o someterse a evaluación independiente. "Confía en nuestras cifras secretas" + precio de 400 000 € = combinación tóxica. | | 5 | **"Los modelos tradicionales quedarán obsoletos en 6 meses"** | Predicción apocalíptica con plazo concreto: retórica clásica de presión comercial (miedo a quedarse fuera), no de comunicación científica. | | 6 | *(extra)* **Lenguaje antropomórfico: "piensa como un humano"** | Vender mecanismo humano en lugar de rendimiento medible es marketing; los sistemas actuales hacen procesamiento estadístico de patrones, no "piensan como humanos". | **2. Conexión con la historia (Cap. 2):** Este anuncio es un ejemplar de manual del ciclo **hype → decepción** que provocó los dos inviernos de la IA. En los años 60 se prometió "inteligencia humana en una generación"; en los 80, los sistemas expertos se vendieron como solución universal a precios astronómicos. Ambas burbujas compartían los mismos rasgos que OmniMind-X: promesas ilimitadas, plazos cortos, precios altos y ausencia de evidencia verificable. La historia no dice que toda innovación sea falsa; dice que **la carga de la prueba está en quien promete**, y que las promesas sin evidencia acaban en facturas sin retorno. **3. Las 4 preguntas de due diligence:** 1. **"Muéstrenme resultados en benchmarks públicos y reproducibles, o permitan una evaluación independiente con NUESTROS datos y casos de uso."** (Si se niegan, fin de la conversación.) 2. **"¿Qué arquitectura y qué datos de entrenamiento usa el sistema, y qué limitaciones documentadas tiene?"** (Todo sistema real tiene limitaciones documentadas; solo los humos no tienen ninguna.) 3. **"¿Qué clientes reales lo usan en producción y podemos hablar con dos de ellos?"** (Referencias verificables, no logos en una web.) 4. **"¿Ofrecen una prueba piloto acotada y medible antes de la licencia anual?"** (Un proveedor serio acepta demostrar valor en pequeño.) **4. Recomendación al CEO (respuesta modelo):** > "Recomiendo no avanzar. El anuncio contiene afirmaciones técnicamente imposibles (funcionar sin datos, 0% de errores) y la afirmación central — AGI — no la sostiene hoy ningún actor del sector. No hay evidencia verificable y el patrón retórico coincide con las burbujas que precedieron a los inviernos de la IA. Si el área de negocio que motivó tu interés es real, definamos el problema concreto y evaluemos soluciones establecidas y medibles con un piloto. El presupuesto de 400 000 € da para mucho valor real." **Consejo profesional:** desarrolla el reflejo de traducir todo anuncio de IA a tres preguntas: *¿qué tarea concreta hace?, ¿cómo se mide?, ¿quién lo verificó?* Si el anuncio no permite responder ninguna, no estás ante tecnología: estás ante marketing.

SECCIÓN B — Matemáticas con código

Los conceptos del Capítulo 3, primero a mano y después verificados con NumPy. No te saltes la parte manual: es donde se construye la intuición.


Ejercicio 11 — Producto punto a mano y con NumPy

Dificultad: · Capítulo relacionado: Cap. 3 (Matemáticas para IA)

Contexto: En un sistema de recomendación, el "interés" de un usuario por un producto se calcula como el producto punto entre el vector de gustos del usuario y el vector de características del producto. Tienes:

  • Usuario: u = [2, 1, 3] (cuánto le gustan: acción, romance, ciencia ficción)
  • Película: p = [4, 0, 2] (cuánto tiene de: acción, romance, ciencia ficción)

Tu tarea: 1. Calcula u · p a mano, escribiendo cada paso. 2. Verifica el resultado con NumPy de dos formas distintas (np.dot y el operador @). 3. Explica en una frase qué significa el número obtenido en el contexto del recomendador.

Ver solución **1. Cálculo a mano:** El producto punto multiplica los vectores componente a componente y suma los resultados:
u · p = (2 × 4) + (1 × 0) + (3 × 2)
      =    8    +    0    +    6
      = 14
**2. Verificación con NumPy:**
import numpy as np                      # Importamos NumPy con su alias estándar

u = np.array([2, 1, 3])                 # Vector de gustos del usuario
p = np.array([4, 0, 2])                 # Vector de características de la película

resultado_dot = np.dot(u, p)            # Forma 1: función np.dot
resultado_arroba = u @ p                # Forma 2: operador @ (recomendado en Python moderno)

print(resultado_dot)                    # 14
print(resultado_arroba)                 # 14
print(resultado_dot == resultado_arroba)  # True: ambas formas son equivalentes
**3. Interpretación:** El 14 es una **puntuación de afinidad**: mide cuánto "coinciden" los gustos del usuario con el contenido de la película. Observa el mecanismo: la componente de acción aporta mucho (2×4=8) porque *ambos* valores son altos; la de romance aporta cero porque a la película no tiene nada de romance (1×0=0). El producto punto premia las dimensiones donde ambos vectores son grandes a la vez — por eso es la operación básica de similitud en IA, desde recomendadores hasta el mecanismo de atención de los Transformers. **Nota:** el producto punto de dos vectores siempre devuelve **un solo número** (un escalar), no otro vector. Este detalle cae en todas las entrevistas.

Ejercicio 12 — Similitud coseno entre documentos de juguete

Dificultad: · Capítulo relacionado: Cap. 3 (Matemáticas para IA)

Contexto: El equipo de soporte quiere agrupar tickets parecidos. Como prototipo, representas cada ticket contando cuántas veces aparece cada palabra del vocabulario ["modelo", "precio", "envío"]:

  • Ticket A: [2, 1, 0] (habla 2 veces de "modelo", 1 de "precio", 0 de "envío")
  • Ticket B: [1, 2, 0]
  • Ticket C: [0, 1, 2]

Tu tarea: 1. Calcula a mano la similitud coseno entre A y B, y entre A y C. Fórmula: cos(v, w) = (v · w) / (||v|| · ||w||). 2. Verifica con NumPy escribiendo una función similitud_coseno(v, w). 3. ¿Qué par de tickets es más parecido y por qué tiene sentido? 4. Pregunta extra: ¿por qué usamos coseno y no simplemente el producto punto?

Ver solución **1. Cálculo a mano:** Primero las normas (longitudes) de cada vector:
||A|| = √(2² + 1² + 0²) = √5
||B|| = √(1² + 2² + 0²) = √5
||C|| = √(0² + 1² + 2²) = √5
(Los tres vectores tienen la misma longitud √5 ≈ 2.236 — elegido a propósito para que el cálculo sea limpio.) Similitud A-B:
A · B = (2×1) + (1×2) + (0×0) = 4
cos(A, B) = 4 / (√5 × √5) = 4 / 5 = 0.8
Similitud A-C:
A · C = (2×0) + (1×1) + (0×2) = 1
cos(A, C) = 1 / (√5 × √5) = 1 / 5 = 0.2
**2. Verificación con NumPy:**
import numpy as np

def similitud_coseno(v: np.ndarray, w: np.ndarray) -> float:
    """Devuelve la similitud coseno entre dos vectores."""
    producto_punto = v @ w                        # Numerador: producto punto
    norma_v = np.linalg.norm(v)                   # ||v||: longitud del primer vector
    norma_w = np.linalg.norm(w)                   # ||w||: longitud del segundo vector
    return producto_punto / (norma_v * norma_w)   # Fórmula del coseno

A = np.array([2, 1, 0])
B = np.array([1, 2, 0])
C = np.array([0, 1, 2])

print(f"cos(A, B) = {similitud_coseno(A, B):.2f}")   # 0.80
print(f"cos(A, C) = {similitud_coseno(A, C):.2f}")   # 0.20
**3. Interpretación:** A y B son mucho más parecidos (0.8) que A y C (0.2). Tiene sentido: A y B hablan de "modelo" y "precio" (mismo tema, distinta proporción), mientras que C habla sobre todo de "envío", tema que A ni menciona. La similitud coseno vale 1 para vectores que apuntan en la misma dirección, 0 para vectores sin nada en común (perpendiculares) y valores intermedios para parecidos parciales. **4. ¿Por qué coseno y no producto punto a secas?** Porque el producto punto crece con la **longitud** de los vectores: un documento larguísimo tendría producto punto enorme con todo, aunque hablara de otro tema. Al dividir por las normas, el coseno mide solo el **ángulo** (la dirección temática), ignorando el tamaño del documento. Es la métrica estándar para comparar embeddings precisamente por eso. **Consejo profesional:** este ejercicio en miniatura es EXACTAMENTE lo que hace un sistema RAG moderno al buscar contexto: convierte textos en vectores y los compara con similitud coseno. Solo cambia la forma de obtener los vectores (embeddings neuronales en lugar de conteo de palabras).

Ejercicio 13 — Normalizar vectores (norma L2)

Dificultad: · Capítulo relacionado: Cap. 3 (Matemáticas para IA)

Contexto: Las bases de datos vectoriales suelen almacenar los embeddings normalizados (longitud 1) para acelerar las búsquedas: con vectores unitarios, la similitud coseno se reduce a un simple producto punto.

Tu tarea: 1. Normaliza a mano el vector v = [3, 4] (divide cada componente entre la norma L2). 2. Haz lo mismo con w = [1, 1, 1, 1]. 3. Escribe una función NumPy normalizar(v) y verifica que los vectores resultantes tienen norma 1. 4. Comprueba con código la afirmación del contexto: que con vectores normalizados, producto punto = similitud coseno.

Ver solución **1. A mano, `v = [3, 4]`:**
||v|| = √(3² + 4²) = √(9 + 16) = √25 = 5
v_normalizado = [3/5, 4/5] = [0.6, 0.8]
Comprobación: √(0.6² + 0.8²) = √(0.36 + 0.64) = √1 = 1 ✅
(El clásico triángulo 3-4-5: por eso este ejemplo aparece en todos los libros.) **2. A mano, `w = [1, 1, 1, 1]`:**
||w|| = √(1 + 1 + 1 + 1) = √4 = 2
w_normalizado = [0.5, 0.5, 0.5, 0.5]
Comprobación: √(4 × 0.25) = √1 = 1 ✅
**3. Función NumPy:**
import numpy as np

def normalizar(v: np.ndarray) -> np.ndarray:
    """Devuelve el vector con la misma dirección y norma 1."""
    norma = np.linalg.norm(v)         # Calculamos la longitud (norma L2)
    if norma == 0:                    # Caso límite: el vector cero no se puede normalizar
        raise ValueError("No se puede normalizar el vector cero")
    return v / norma                  # NumPy divide TODAS las componentes a la vez (broadcasting)

v = np.array([3.0, 4.0])
w = np.array([1.0, 1.0, 1.0, 1.0])

v_n = normalizar(v)
w_n = normalizar(w)

print(v_n)                    # [0.6 0.8]
print(w_n)                    # [0.5 0.5 0.5 0.5]
print(np.linalg.norm(v_n))    # 1.0  ← norma unitaria confirmada
print(np.linalg.norm(w_n))    # 1.0
**4. Producto punto = coseno cuando las normas son 1:**
a = normalizar(np.array([2.0, 1.0, 0.0]))
b = normalizar(np.array([1.0, 2.0, 0.0]))

producto = a @ b                                    # Producto punto de los normalizados
coseno = (a @ b) / (np.linalg.norm(a) * np.linalg.norm(b))  # Fórmula completa del coseno

print(f"{producto:.4f}")   # 0.8000
print(f"{coseno:.4f}")     # 0.8000  ← idénticos: al dividir entre 1 × 1 no cambia nada
**Razonamiento:** la fórmula del coseno divide el producto punto entre las normas; si ambas normas son 1, la división es entre 1 y desaparece. Por eso las bases vectoriales normalizan al indexar: se ahorran millones de raíces cuadradas en cada búsqueda. **Advertencia:** fíjate en el caso límite del vector cero — dividir entre norma 0 produce `nan` silenciosos que corrompen pipelines enteros. Controlarlo con un `if` explícito es marca de código profesional.

Ejercicio 14 — Aplicar softmax e interpretar la temperatura

Dificultad: · Capítulo relacionado: Cap. 3 (Matemáticas para IA)

Contexto: Un modelo de lenguaje, antes de elegir la siguiente palabra, produce puntuaciones brutas (logits) para cada candidata. Para la frase "El gato se subió al...", el modelo produce:

Palabra Logit
tejado 2.0
árbol 1.0
coche 0.1

Tu tarea: 1. Aplica softmax a mano (con calculadora: softmax(zᵢ) = e^zᵢ / Σe^zⱼ) y obtén las tres probabilidades. 2. Verifica con una función NumPy softmax(z). 3. Aplica ahora softmax a los logits divididos por una temperatura T=0.5 y T=2.0. Compara las tres distribuciones. 4. Explica qué hace la temperatura y por qué los proveedores de LLMs la exponen como parámetro.

Ver solución **1. A mano:**
e^2.0 = 7.389
e^1.0 = 2.718
e^0.1 = 1.105
Suma  = 7.389 + 2.718 + 1.105 = 11.212

P(tejado) = 7.389 / 11.212 ≈ 0.659  (65.9%)
P(árbol)  = 2.718 / 11.212 ≈ 0.242  (24.2%)
P(coche)  = 1.105 / 11.212 ≈ 0.099  ( 9.9%)

Comprobación: 0.659 + 0.242 + 0.099 = 1.0 ✅ (softmax SIEMPRE suma 1)
**2. Verificación con NumPy:**
import numpy as np

def softmax(z: np.ndarray) -> np.ndarray:
    """Convierte logits en una distribución de probabilidad."""
    exp_z = np.exp(z - np.max(z))   # Restamos el máximo: truco de estabilidad numérica
                                    # (no cambia el resultado, evita overflow con logits grandes)
    return exp_z / exp_z.sum()      # Cada exponencial dividida por la suma total

logits = np.array([2.0, 1.0, 0.1])
probs = softmax(logits)

print(np.round(probs, 3))   # [0.659 0.242 0.099]
print(probs.sum())          # 1.0
**3. El efecto de la temperatura (softmax sobre `logits / T`):**
for T in [0.5, 1.0, 2.0]:
    probs_T = softmax(logits / T)               # La temperatura escala los logits ANTES del softmax
    print(f"T={T}: {np.round(probs_T, 3)}")
Salida:
T=0.5: [0.864 0.117 0.019]   ← más "afilada": el ganador arrasa
T=1.0: [0.659 0.242 0.099]   ← la distribución original
T=2.0: [0.502 0.304 0.194]   ← más "plana": las opciones se igualan
**4. Interpretación de la temperatura:** - **T < 1 (baja):** al dividir por un número pequeño, las diferencias entre logits se **amplifican**; el softmax concentra casi toda la probabilidad en la mejor opción. El modelo se vuelve **determinista y conservador**: ideal para tareas con respuesta correcta (extracción de datos, clasificación, código). - **T > 1 (alta):** las diferencias se **atenúan** y las opciones improbables ganan terreno. El modelo se vuelve **variado y creativo** (y más propenso a disparates): útil para brainstorming o escritura creativa. - **T = 1:** la distribución "cruda" del modelo, sin retocar. Los proveedores exponen la temperatura porque es el mando de control **creatividad ↔ fiabilidad** más barato que existe: no cambia el modelo, solo cómo se muestrea de él. **Consejo profesional:** para aplicaciones empresariales donde la respuesta se procesa automáticamente (JSON, SQL, decisiones), empieza siempre con temperatura 0 o cercana a 0. Sube la temperatura solo cuando la variedad sea un requisito, no un accidente.

Ejercicio 15 — Un paso de descenso de gradiente a mano para f(x) = x²

Dificultad: · Capítulo relacionado: Cap. 3 (Matemáticas para IA)

Contexto: El descenso de gradiente es el algoritmo con el que aprende prácticamente toda la IA moderna. Antes de aplicarlo a redes con miles de millones de parámetros, domínalo en el caso más simple posible: minimizar f(x) = x² (cuyo mínimo, obviamente, está en x = 0). La regla de actualización es:

x_nuevo = x_actual − η · f'(x_actual)      donde η (eta) es la tasa de aprendizaje

Tu tarea: 1. Deriva f(x) = x² (¿cuánto vale f'(x)?). 2. Partiendo de x₀ = 4 y con η = 0.1, calcula a mano 3 pasos de descenso de gradiente. 3. Implementa el bucle en Python (sin NumPy, no hace falta) y ejecuta 20 pasos. ¿A qué valor se acerca x? 4. Repite con η = 1.1 y observa qué ocurre. Explica el fenómeno.

Ver solución **1. La derivada:** `f'(x) = 2x`. La derivada nos dice la pendiente: si x = 4, la pendiente es 8 (cuesta arriba hacia la derecha), así que para bajar hay que moverse hacia la izquierda. El signo negativo de la regla hace exactamente eso: moverse **en contra** del gradiente. **2. Tres pasos a mano (x₀ = 4, η = 0.1):**
Paso 1: x₁ = 4    − 0.1 × (2 × 4)    = 4 − 0.8    = 3.2
Paso 2: x₂ = 3.2  − 0.1 × (2 × 3.2)  = 3.2 − 0.64 = 2.56
Paso 3: x₃ = 2.56 − 0.1 × (2 × 2.56) = 2.56 − 0.512 = 2.048
Cada paso acerca x al mínimo (0), y los pasos se hacen más pequeños a medida que la pendiente disminuye. Fíjate en el patrón: cada paso multiplica x por 0.8. **3. El bucle en Python:**
def gradiente_descendente(x_inicial: float, eta: float, pasos: int) -> float:
    x = x_inicial                        # Punto de partida
    for i in range(pasos):
        gradiente = 2 * x                # f'(x) = 2x: la pendiente en el punto actual
        x = x - eta * gradiente          # La regla: un paso en dirección contraria a la pendiente
        print(f"Paso {i+1:2d}: x = {x:.6f}, f(x) = {x**2:.6f}")
    return x

x_final = gradiente_descendente(x_inicial=4.0, eta=0.1, pasos=20)
# Paso  1: x = 3.200000, f(x) = 10.240000
# Paso  2: x = 2.560000, f(x) = 6.553600
# Paso  3: x = 2.048000, f(x) = 4.194304
# ...
# Paso 20: x = 0.046117, f(x) = 0.002127   ← convergiendo suavemente hacia x = 0
x se acerca a **0**, el mínimo verdadero, sin llegar nunca exactamente (se acerca asintóticamente, cada vez más despacio). **4. ¿Qué pasa con η = 1.1?**
gradiente_descendente(x_inicial=4.0, eta=1.1, pasos=10)
# Paso  1: x = -4.800000, f(x) = 23.04      ← ¡saltó al otro lado, y MÁS LEJOS!
# Paso  2: x = 5.760000,  f(x) = 33.18      ← rebota y empeora
# Paso  3: x = -6.912000, f(x) = 47.78
# ...cada paso multiplica x por -1.2: DIVERGE hacia infinito
**El fenómeno: divergencia por tasa de aprendizaje excesiva.** Con η demasiado grande, cada paso salta por encima del mínimo y aterriza más lejos de lo que estaba, en el lado contrario. El error crece en vez de disminuir. En este problema, la frontera exacta está en η = 1.0 (cada paso multiplica x por 1 − 2η: converge si ese factor está entre −1 y 1). **Nota:** la tasa de aprendizaje es el hiperparámetro más importante del deep learning. Demasiado baja → entrenamiento eterno; demasiado alta → el error explota (verás `loss: nan` en los registros). Cuando en el Módulo de redes neuronales veas curvas de pérdida que suben en vez de bajar, recuerda este ejercicio: probablemente alguien puso η demasiado grande.

Ejercicio 16 — Teorema de Bayes: el test médico que engaña

Dificultad: · Capítulo relacionado: Cap. 3 (Matemáticas para IA)

Contexto: Una aseguradora médica evalúa un test de cribado para una enfermedad. Datos:

  • Prevalencia: el 1% de la población tiene la enfermedad. → P(E) = 0.01
  • Sensibilidad: si tienes la enfermedad, el test da positivo el 99% de las veces. → P(+|E) = 0.99
  • Falsos positivos: si NO tienes la enfermedad, el test da positivo el 5% de las veces. → P(+|¬E) = 0.05

Un cliente recibe un positivo y, aterrado, pregunta: "¿Significa que tengo la enfermedad con un 99% de seguridad?"

Tu tarea: 1. Calcula a mano con el teorema de Bayes la probabilidad real de tener la enfermedad dado el positivo: P(E|+). 2. Verifica el cálculo con Python. 3. Explica el resultado con el "método de las 10 000 personas" (frecuencias naturales), que es como se le explica a un paciente. 4. ¿Qué tiene que ver esto con los sistemas de IA de detección (fraude, spam, diagnóstico)?

Ver solución **1. Teorema de Bayes a mano:**
           P(+|E) · P(E)                      0.99 × 0.01
P(E|+) = ————————————————— = ———————————————————————————————————————
              P(+)              0.99 × 0.01  +  0.05 × 0.99
                              (positivos reales) (falsos positivos)

       =   0.0099 / (0.0099 + 0.0495)
       =   0.0099 / 0.0594
       ≈   0.1667  →  ¡Solo un 16.7%!
El denominador `P(+)` es la probabilidad total de dar positivo, sumando los dos caminos posibles: estar enfermo y dar positivo (0.99 × 0.01) o estar sano y dar positivo por error (0.05 × 0.99). **2. Verificación con Python:**
p_enfermedad = 0.01        # P(E): prevalencia
p_pos_dado_e = 0.99        # P(+|E): sensibilidad
p_pos_dado_sano = 0.05     # P(+|¬E): tasa de falsos positivos

p_sano = 1 - p_enfermedad  # P(¬E) = 0.99

# Probabilidad total de positivo: los dos caminos que llevan a un "+"
p_positivo = (p_pos_dado_e * p_enfermedad) + (p_pos_dado_sano * p_sano)

# Bayes: de todos los positivos, ¿qué fracción está realmente enferma?
p_e_dado_positivo = (p_pos_dado_e * p_enfermedad) / p_positivo

print(f"P(E|+) = {p_e_dado_positivo:.4f}")   # 0.1667
print(f"Es decir: {p_e_dado_positivo:.1%}")  # 16.7%
**3. El método de las 10 000 personas (así se explica a humanos):** Imagina 10 000 personas haciéndose el test: | Grupo | Cuántos | Positivos que produce | |-------|---------|----------------------| | Enfermos (1%) | 100 | 99 positivos verdaderos (99%) | | Sanos (99%) | 9 900 | 495 falsos positivos (5%) | | **Total de positivos** | | **594** | De 594 personas con test positivo, solo 99 están enfermas: `99 / 594 = 16.7%`. La trampa intuitiva: aunque el test "acierta el 99%", los sanos son TANTOS (9 900) que incluso un pequeño 5% de error sobre ellos genera muchos más falsos positivos (495) que positivos verdaderos hay (99). **La prevalencia manda.** **4. Conexión con la IA:** Todo sistema de detección de eventos raros sufre exactamente esta trampa: - **Detección de fraude:** si el fraude es el 0.1% de las transacciones, un modelo "99% preciso" puede generar decenas de falsas alarmas por cada fraude real. El equipo de revisión muere ahogado en falsos positivos. - **Filtros de spam, alarmas de seguridad, diagnóstico asistido:** ídem. - **Moraleja profesional:** nunca evalúes un detector de eventos raros solo por su "accuracy". Pregunta siempre por la tasa base del evento (prevalencia) y por las métricas que separan los tipos de error (precisión y recall, que estudiarás en el módulo de ML). Un modelo que dice "todo es legítimo" acierta el 99.9% en un dataset con 0.1% de fraude — y es perfectamente inútil. **Advertencia:** confundir `P(+|E)` con `P(E|+)` (la "falacia del fiscal") es uno de los errores estadísticos más comunes y costosos en decisiones reales, desde medicina hasta juicios. Bayes existe precisamente para no caer en ella.

Ejercicio 17 — Media y desviación estándar de latencias con NumPy

Dificultad: · Capítulos relacionados: Caps. 3-4

Contexto: Monitorizas la latencia (en milisegundos) de las respuestas de tu API de IA durante 12 peticiones consecutivas:

latencias = [220, 235, 210, 240, 225, 1900, 230, 215, 245, 228, 232, 218]

(Sí, hay una petición que tardó 1.9 segundos: algo pasó ahí.)

Tu tarea: 1. Calcula la media a mano (suma y divide) de las primeras 4 latencias para calibrar tu intuición. 2. Con NumPy: calcula media, mediana y desviación estándar del array completo. 3. Compara media y mediana. ¿Por qué son tan distintas y cuál representa mejor la experiencia típica del usuario? 4. Detecta programáticamente los valores atípicos usando la regla "más de 2 desviaciones estándar de la media".

Ver solución **1. A mano (primeras 4):**
(220 + 235 + 210 + 240) / 4 = 905 / 4 = 226.25 ms
Esa es la zona "normal" del servicio: unos 225 ms. **2. Con NumPy:**
import numpy as np

latencias = np.array([220, 235, 210, 240, 225, 1900, 230, 215, 245, 228, 232, 218])

media = latencias.mean()             # Suma de todos / cantidad
mediana = np.median(latencias)       # El valor central al ordenarlos
desviacion = latencias.std()         # Cuánto se alejan los valores de la media, en promedio

print(f"Media:      {media:.1f} ms")      # 366.5 ms
print(f"Mediana:    {mediana:.1f} ms")    # 229.0 ms
print(f"Desv. est.: {desviacion:.1f} ms") # 462.5 ms
**3. Media vs mediana — la lección importante:** - La **media (366.5 ms)** está inflada: UN solo valor extremo (1900) arrastra la media hacia arriba, aunque 11 de 12 peticiones fueron rápidas. La media es sensible a outliers. - La **mediana (229 ms)** ni se inmuta: al ser el valor central de la lista ordenada, un outlier en la punta no la mueve. Representa mucho mejor "lo que vive el usuario típico". - Regla práctica de monitorización: para latencias se reportan **percentiles** (p50 = mediana, p95, p99), nunca solo la media. "Media de 366 ms" describe un servicio que no existe: ninguna petición tardó eso. **4. Detección de outliers:**
limite_superior = media + 2 * desviacion     # Umbral: media + 2 desviaciones
limite_inferior = media - 2 * desviacion     # (por abajo también, por completitud)

# Máscara booleana: True donde el valor es atípico
es_outlier = (latencias > limite_superior) | (latencias < limite_inferior)

print(latencias[es_outlier])    # [1900]  ← NumPy filtra el array con la máscara
print(np.where(es_outlier)[0])  # [5]     ← posición del outlier en el array
**Cómo funciona:** `latencias > limite_superior` no compara un número, compara TODO el array de golpe y devuelve un array de `True/False` (vectorización). Usar esa máscara como índice (`latencias[es_outlier]`) extrae solo los valores marcados. Este patrón máscara-filtrado es de los más usados en preparación de datos. **Nota:** la regla de "2 desviaciones" es un heurístico razonable para empezar, pero tiene una ironía: el propio outlier infla la desviación estándar que usas para detectarlo. Con métodos robustos (percentiles, rango intercuartílico) se evita ese problema; los verás en el módulo de análisis de datos.

Ejercicio 18 — Mini buscador semántico con similitud coseno

Dificultad: · Capítulos relacionados: Caps. 3-5

Contexto: Vas a construir el esqueleto de un buscador semántico — la pieza central de cualquier sistema RAG — en versión juguete. La "base de conocimiento" de una tienda online tiene 4 documentos, representados como vectores sobre el vocabulario ["envío", "devolución", "pago", "producto"] (conteo de menciones de cada tema):

documentos = {
    "politica_envios":       [5, 0, 0, 1],
    "politica_devoluciones": [1, 6, 0, 2],
    "metodos_de_pago":       [0, 0, 7, 1],
    "garantia_producto":     [0, 2, 0, 5],
}

Llega la consulta de un cliente, ya vectorizada igual: consulta = [2, 1, 0, 0] ("¿puedo cambiar la dirección de envío de mi pedido?": menciona 2 veces envío, 1 devolución).

Tu tarea: 1. Escribe una función buscar(consulta, documentos) que calcule la similitud coseno de la consulta contra cada documento y devuelva el ranking ordenado de mayor a menor. 2. Ejecuta la búsqueda e interpreta el resultado: ¿qué documento debería mostrarse al cliente? 3. Explica qué cambiaría en un sistema real (pista: ¿de dónde saldrían los vectores?).

Ver solución **1 y 2. Implementación completa:**
import numpy as np

def similitud_coseno(v: np.ndarray, w: np.ndarray) -> float:
    """Similitud coseno entre dos vectores (reutilizada del Ejercicio 12)."""
    return (v @ w) / (np.linalg.norm(v) * np.linalg.norm(w))

def buscar(consulta: list[float], documentos: dict[str, list[float]]) -> list[tuple[str, float]]:
    """Devuelve los documentos ordenados por similitud con la consulta (mayor primero)."""
    q = np.array(consulta, dtype=float)              # Convertimos la consulta a array NumPy
    resultados = []
    for nombre, vector in documentos.items():        # Recorremos cada documento de la base
        d = np.array(vector, dtype=float)            # Vector del documento
        score = similitud_coseno(q, d)               # Puntuación de parecido consulta-documento
        resultados.append((nombre, score))           # Guardamos (nombre, puntuación)
    # Ordenamos por puntuación descendente: el más parecido primero
    resultados.sort(key=lambda par: par[1], reverse=True)
    return resultados

documentos = {
    "politica_envios":       [5, 0, 0, 1],
    "politica_devoluciones": [1, 6, 0, 2],
    "metodos_de_pago":       [0, 0, 7, 1],
    "garantia_producto":     [0, 2, 0, 5],
}

consulta = [2, 1, 0, 0]

for nombre, score in buscar(consulta, documentos):
    print(f"{nombre:25s}{score:.3f}")
**Salida:**
politica_envios           → 0.877
politica_devoluciones     → 0.559
garantia_producto         → 0.166
metodos_de_pago           → 0.000
**Interpretación:** - **`politica_envios` gana (0.877):** la consulta habla sobre todo de envíos, y ese documento también. Es el que debe mostrarse al cliente (o pasarse como contexto al LLM, en un RAG). - `politica_devoluciones` queda segunda (0.559) porque la consulta menciona una vez "devolución"; razonable como resultado secundario. - `metodos_de_pago` puntúa **exactamente 0**: no comparte ninguna dimensión con la consulta (vectores perpendiculares). El coseno captura perfectamente que son temas ajenos. **3. Qué cambia en un sistema real:** 1. **Los vectores no vendrían de contar palabras sino de un modelo de embeddings** (p. ej. `text-embedding-3`, `voyage`, modelos de sentence-transformers): vectores de cientos o miles de dimensiones que capturan el *significado*, no la coincidencia literal. Con embeddings, "cambiar la dirección de mi pedido" encontraría la política de envíos aunque no compartiera NI UNA palabra con ella — esa es la magia "semántica" que el conteo de palabras no puede dar. 2. **La base no sería un diccionario sino una base de datos vectorial** (FAISS, Chroma, pgvector, Pinecone...) con índices aproximados para buscar entre millones de documentos en milisegundos, en lugar de comparar contra todos uno a uno como hace nuestro bucle. 3. **Habría un paso previo de troceado (chunking):** los documentos largos se parten en fragmentos antes de vectorizarlos. **Consejo profesional:** guarda este ejercicio. Cuando en módulos posteriores construyas tu primer RAG de verdad, compara el código: descubrirás que la arquitectura es idéntica — solo habrás sustituido `contar_palabras()` por `modelo_embeddings.encode()` y el bucle por una base vectorial. Entender esta versión de juguete es entender RAG.

SECCIÓN C — Python para IA

Dificultad creciente sobre el Capítulo 4. Todo el código usa contextos del mundo de la IA: no es Python "de juguete", es el Python que escribirás en tu trabajo.


Ejercicio 19 — FizzBuzz con giro: monitor de lotes de entrenamiento

Dificultad: · Capítulo relacionado: Cap. 4 (Python para IA)

Contexto: Estás monitorizando un entrenamiento que procesa 30 lotes (batches) de datos. Las reglas del sistema de logging son:

  • Si el número de lote es divisible por 3, imprime "checkpoint" (se guarda el modelo).
  • Si es divisible por 5, imprime "validacion" (se evalúa con el conjunto de validación).
  • Si es divisible por ambos, imprime "checkpoint+validacion".
  • En cualquier otro caso, imprime "lote N procesado".

Tu tarea: Escribe un bucle que recorra los lotes 1 a 30 aplicando las reglas. Pregunta extra: ¿por qué hay que comprobar el caso "ambos" PRIMERO?

Ver solución
for lote in range(1, 31):                      # range(1, 31) genera 1, 2, ..., 30 (el tope se excluye)
    if lote % 15 == 0:                         # Divisible por 3 Y por 5 equivale a divisible por 15
        print("checkpoint+validacion")         # El caso combinado SIEMPRE se comprueba primero
    elif lote % 3 == 0:                        # % es el módulo: resto de la división (resto 0 = divisible)
        print("checkpoint")
    elif lote % 5 == 0:
        print("validacion")
    else:
        print(f"lote {lote} procesado")        # f-string: interpola la variable dentro del texto
**Salida (fragmento):**
lote 1 procesado
lote 2 procesado
checkpoint
lote 4 procesado
validacion
checkpoint
...
checkpoint+validacion      ← lote 15
...
checkpoint+validacion      ← lote 30
**La pregunta extra (el corazón del ejercicio):** las ramas `if/elif` se evalúan **en orden y solo entra una**. Si comprobaras `% 3` primero, el lote 15 entraría en esa rama, imprimiría `"checkpoint"` y saltaría el resto: nunca llegaría a la rama combinada. Regla general de los condicionales en cascada: **del caso más específico al más general.** **Nota:** este patrón no es académico: los bucles de entrenamiento reales de PyTorch hacen exactamente esto — `if step % save_every == 0: guardar_checkpoint()`, `if step % eval_every == 0: evaluar()`. Acabas de escribir tu primer esqueleto de training loop.

Ejercicio 20 — Funciones con type hints: calculadora de coste de tokens

Dificultad: · Capítulo relacionado: Cap. 4 (Python para IA)

Contexto: Tu empresa usa un LLM por API que cobra 3 € por millón de tokens de entrada y 15 € por millón de tokens de salida. Necesitas una función para estimar costes de proyectos.

Tu tarea: Escribe una función coste_llamada con type hints completos que: 1. Reciba tokens_entrada: int, tokens_salida: int y, opcionalmente, los precios (con los valores por defecto de arriba). 2. Devuelva el coste en euros como float. 3. Lance ValueError si algún número de tokens es negativo. 4. Úsala para calcular: (a) una llamada de 1 200 tokens de entrada y 350 de salida; (b) el coste mensual de un chatbot con 10 000 llamadas/día como esa.

Ver solución
def coste_llamada(
    tokens_entrada: int,                     # Tokens que enviamos al modelo (prompt + contexto)
    tokens_salida: int,                      # Tokens que el modelo genera (la respuesta)
    precio_entrada: float = 3.0,             # € por MILLÓN de tokens de entrada (valor por defecto)
    precio_salida: float = 15.0,             # € por MILLÓN de tokens de salida
) -> float:                                  # La función devuelve un float: el coste en euros
    """Calcula el coste en euros de una llamada a la API del LLM."""
    if tokens_entrada < 0 or tokens_salida < 0:
        # Validar las entradas al principio ("fail fast"): un valor absurdo debe explotar
        # aquí con un mensaje claro, no propagarse como un coste negativo silencioso.
        raise ValueError("El número de tokens no puede ser negativo")

    coste_entrada = (tokens_entrada / 1_000_000) * precio_entrada   # Regla de tres sobre el millón
    coste_salida = (tokens_salida / 1_000_000) * precio_salida      # 1_000_000: los guiones bajos
    return coste_entrada + coste_salida                             # son solo legibilidad


# (a) Una llamada individual
coste_unitario = coste_llamada(tokens_entrada=1200, tokens_salida=350)
print(f"Coste por llamada: {coste_unitario:.6f} €")     # 0.008850 €

# (b) Proyección mensual: 10 000 llamadas/día × 30 días
llamadas_mes = 10_000 * 30
coste_mensual = coste_unitario * llamadas_mes
print(f"Coste mensual estimado: {coste_mensual:.2f} €")  # 2655.00 €
**Verificación del cálculo unitario a mano:**
Entrada: 1200 / 1 000 000 × 3 €  = 0.0036 €
Salida:   350 / 1 000 000 × 15 € = 0.00525 €
Total:                             0.00885 € por llamada
Mensual: 0.00885 × 300 000 llamadas = 2 655 €
**Puntos de estilo que se evalúan en código profesional:** 1. **Type hints en todo:** parámetros y retorno. No cambian la ejecución (Python no los impone), pero documentan la función, activan el autocompletado del editor y permiten detectar errores con herramientas como `mypy` antes de ejecutar. 2. **Valores por defecto para lo que casi nunca cambia** (los precios), obligatorios para lo que siempre cambia (los tokens). 3. **Validación con excepción, no con `print`:** una función de biblioteca nunca imprime errores; los lanza, y quien la llama decide qué hacer. 4. **Docstring de una línea:** qué hace, no cómo lo hace. **Consejo profesional:** fíjate en la asimetría de precios: los tokens de salida cuestan 5 veces más que los de entrada (patrón habitual en las APIs reales). Consecuencia de diseño: pedir al modelo respuestas concisas no es solo estética — es dinero. Un "responde en una sola frase" en el prompt puede recortar la factura a la mitad.

Ejercicio 21 — Comprehensions sobre datos de clientes

Dificultad: · Capítulo relacionado: Cap. 4 (Python para IA)

Contexto: Recibes datos de clientes como lista de diccionarios (el formato en que casi todas las APIs devuelven registros):

clientes = [
    {"nombre": "Ana",    "gasto_anual": 4200, "pais": "ES", "activo": True},
    {"nombre": "Luis",   "gasto_anual": 1500, "pais": "MX", "activo": False},
    {"nombre": "Carla",  "gasto_anual": 8800, "pais": "ES", "activo": True},
    {"nombre": "Pedro",  "gasto_anual": 300,  "pais": "AR", "activo": True},
    {"nombre": "Sofía",  "gasto_anual": 6100, "pais": "MX", "activo": True},
]

Tu tarea: Resuelve cada punto con UNA comprehension (sin bucles for tradicionales): 1. Lista con los nombres de todos los clientes. 2. Lista con los nombres de los clientes activos con gasto > 4000 (los VIP). 3. Diccionario {nombre: gasto_anual} solo de clientes activos. 4. Lista de los gastos con un descuento del 10% aplicado, solo para clientes de "ES". 5. (Extra) ¿Cuándo NO deberías usar una comprehension?

Ver solución
# 1. Extraer un campo de cada registro: la comprehension más básica
nombres = [c["nombre"] for c in clientes]
# ["Ana", "Luis", "Carla", "Pedro", "Sofía"]

# 2. Extraer con filtro: el "if" al final selecciona qué elementos entran
vips = [c["nombre"] for c in clientes if c["activo"] and c["gasto_anual"] > 4000]
# ["Ana", "Carla", "Sofía"]  ← Ana entra por poco: 4200 > 4000 y está activa

# 3. Dict comprehension: pares clave: valor entre llaves
gastos_activos = {c["nombre"]: c["gasto_anual"] for c in clientes if c["activo"]}
# {"Ana": 4200, "Carla": 8800, "Pedro": 300, "Sofía": 6100}

# 4. Transformación + filtro combinados: la expresión de la izquierda transforma,
#    el if de la derecha filtra
gastos_es_descuento = [c["gasto_anual"] * 0.9 for c in clientes if c["pais"] == "ES"]
# [3780.0, 7920.0]  ← solo Ana y Carla son de "ES"; Luis (MX) no entra aunque esté inactivo... ni por eso
**Detalles que suelen fallar en este ejercicio:** - En el punto 2, olvidar una de las dos condiciones (o unirlas con `or` en vez de `and`). Léelo como frase: "clientes que estén activos **y** gasten más de 4000". - En el punto 3, confundir la sintaxis: dict comprehension usa `{clave: valor for ...}` con **llaves y dos puntos**; si pones corchetes obtienes una lista de tuplas. - En el punto 4, aplicar el descuento dentro del `if` (el `if` solo filtra; la transformación va en la expresión de la izquierda). **Advertencia:** no valides tu resultado leyendo el código: **ejecútalo**. Los errores "obvios que no pueden fallar" son exactamente los que llegan a producción. (Y si usas un LLM para generar código, mismo protocolo: ejecutar antes de confiar.) **Anatomía de una comprehension (para fijarla de una vez):**
[ expresión      for elemento in iterable   if condición ]
  ^ qué guardo   ^ de dónde saco elementos  ^ cuáles pasan (opcional)
**5. ¿Cuándo NO usar comprehensions?** - Cuando la lógica tiene **más de un nivel de anidamiento o varios pasos**: una comprehension de tres líneas con dos `if` y un `else` es más ilegible que un bucle claro. La comprehension optimiza legibilidad; si la empeora, ha perdido su razón de ser. - Cuando hay **efectos secundarios** (escribir a un archivo, llamar a una API por elemento): las comprehensions son para *construir colecciones*, no para *hacer cosas*. - Regla práctica: si no cabe cómodamente en una línea (dos como máximo), usa un bucle.

Ejercicio 22 — Parsear el JSON de una API de IA ficticia

Dificultad: · Capítulo relacionado: Cap. 4 (Python para IA)

Contexto: Tu aplicación llama a la API de un LLM ficticio ("MegaBrain API") y recibe esta respuesta JSON como texto. Tu trabajo: extraer los datos de forma robusta, porque las APIs a veces devuelven estructuras incompletas.

respuesta_api = '''
{
  "id": "resp_8f4a2",
  "modelo": "megabrain-4",
  "opciones": [
    {
      "mensaje": {"rol": "asistente", "contenido": "La capital de Australia es Canberra."},
      "motivo_fin": "stop"
    }
  ],
  "uso": {"tokens_entrada": 18, "tokens_salida": 9}
}
'''

Tu tarea: 1. Parsea el texto JSON a un diccionario Python. 2. Extrae e imprime: el texto de la respuesta, el modelo y el total de tokens usados. 3. Hazlo a prueba de fallos: si falta cualquier campo (por ejemplo "uso"), el programa no debe romperse, sino usar valores por defecto sensatos. 4. Empaqueta la lógica en una función extraer_respuesta(texto_json) -> dict que devuelva {"texto": ..., "modelo": ..., "tokens_totales": ...}.

Ver solución
import json                                       # Módulo estándar para JSON: viene con Python

def extraer_respuesta(texto_json: str) -> dict:
    """Extrae los campos útiles de una respuesta de la MegaBrain API.

    Devuelve siempre un dict con las claves texto/modelo/tokens_totales,
    aunque la respuesta venga incompleta o malformada.
    """
    # --- Paso 1: parsear el texto a estructuras Python ---
    try:
        datos = json.loads(texto_json)            # loads = "load string": texto JSON → dict/list
    except json.JSONDecodeError:                  # Si el texto no es JSON válido...
        # ...devolvemos la estructura por defecto: quien nos llama no tiene que romperse
        return {"texto": "", "modelo": "desconocido", "tokens_totales": 0}

    # --- Paso 2: extraer campos con .get() y valores por defecto ---
    # dict.get(clave, defecto) devuelve el defecto si la clave no existe,
    # en lugar de lanzar KeyError como haría datos["clave"]
    modelo = datos.get("modelo", "desconocido")

    # Para rutas anidadas, encadenamos .get() con defectos intermedios:
    # si "opciones" no existe → lista vacía → el if la detecta
    opciones = datos.get("opciones", [])
    if opciones:                                  # Lista no vacía: hay al menos una respuesta
        mensaje = opciones[0].get("mensaje", {})  # Primer candidato; {} como defecto anidado
        texto = mensaje.get("contenido", "")
    else:
        texto = ""                                # Sin opciones: respuesta vacía pero válida

    # "uso" puede faltar entero: {} como defecto permite encadenar los .get() interiores
    uso = datos.get("uso", {})
    tokens_totales = uso.get("tokens_entrada", 0) + uso.get("tokens_salida", 0)

    # --- Paso 3: devolver SIEMPRE la misma estructura ---
    return {"texto": texto, "modelo": modelo, "tokens_totales": tokens_totales}


# Uso normal
resultado = extraer_respuesta(respuesta_api)
print(resultado["texto"])            # La capital de Australia es Canberra.
print(resultado["modelo"])           # megabrain-4
print(resultado["tokens_totales"])   # 27

# Pruebas de robustez: nada de esto debe romper el programa
print(extraer_respuesta('{"modelo": "megabrain-4"}'))
# {"texto": "", "modelo": "megabrain-4", "tokens_totales": 0}
print(extraer_respuesta("esto no es json"))
# {"texto": "", "modelo": "desconocido", "tokens_totales": 0}
**Las tres técnicas defensivas del ejercicio (memorízalas, las usarás a diario):** 1. **`json.loads` siempre dentro de `try/except JSONDecodeError`:** el texto que llega por red NUNCA es de confianza — respuestas truncadas, errores HTML en lugar de JSON, encoding roto. 2. **`.get(clave, defecto)` en lugar de `[clave]`:** convierte "campo ausente" de excepción fatal a caso manejado. Para estructuras anidadas, usa defectos encadenables (`{}` para dicts, `[]` para listas). 3. **Contrato de salida estable:** la función devuelve SIEMPRE las mismas claves con tipos predecibles. El código que la usa no necesita defenderse otra vez. **Consejo profesional:** este ejercicio es literalmente tu futuro trabajo como AI Engineer: las respuestas de OpenAI/Anthropic tienen exactamente esta forma (lista de candidatos, mensaje anidado, bloque de uso de tokens). La única diferencia real será el nombre de los campos.

Ejercicio 23 — Clase Documento con métodos

Dificultad: · Capítulo relacionado: Cap. 4 (Python para IA)

Contexto: Estás construyendo la base de un futuro sistema RAG y necesitas una estructura para representar documentos con comportamiento propio, no solo datos sueltos.

Tu tarea: Implementa una clase Documento con: 1. Constructor que reciba titulo: str, contenido: str y opcionalmente fuente: str = "desconocida". 2. Método num_palabras() -> int. 3. Método resumen(n: int = 10) -> str que devuelva las primeras n palabras seguidas de "...". 4. Método contiene(termino: str) -> bool que busque sin distinguir mayúsculas/minúsculas. 5. Método especial __repr__ para que al imprimir el objeto se vea algo útil. 6. Crea dos documentos y demuestra todos los métodos.

Ver solución
class Documento:
    """Representa un documento de texto para un pipeline de IA."""

    def __init__(self, titulo: str, contenido: str, fuente: str = "desconocida"):
        # __init__ es el constructor: se ejecuta al crear cada objeto.
        # self es el objeto concreto que se está creando; todo lo que
        # guardemos en self.* será un atributo de ESE objeto.
        self.titulo = titulo
        self.contenido = contenido
        self.fuente = fuente

    def num_palabras(self) -> int:
        """Número de palabras del contenido."""
        # .split() sin argumentos parte por CUALQUIER espacio en blanco
        # (espacios, saltos de línea, tabuladores) y descarta los vacíos
        return len(self.contenido.split())

    def resumen(self, n: int = 10) -> str:
        """Las primeras n palabras, con puntos suspensivos si hay más."""
        palabras = self.contenido.split()
        if len(palabras) <= n:               # Documento corto: no hace falta truncar
            return self.contenido
        return " ".join(palabras[:n]) + "..."  # Rebanada [:n] + reconstrucción con join

    def contiene(self, termino: str) -> bool:
        """¿Aparece el término en el contenido? (ignorando mayúsculas)."""
        # Bajamos AMBOS lados a minúsculas: así "Python" encuentra "python" y viceversa
        return termino.lower() in self.contenido.lower()

    def __repr__(self) -> str:
        # __repr__ define qué se muestra al hacer print(objeto) o verlo en consola.
        # Sin él, verías algo inútil como <__main__.Documento object at 0x7f3a...>
        return f"Documento(titulo='{self.titulo}', palabras={self.num_palabras()}, fuente='{self.fuente}')"


# --- Demostración ---
doc1 = Documento(
    titulo="Política de devoluciones",
    contenido="Los clientes disponen de treinta dias naturales para devolver cualquier producto sin justificacion y con reembolso completo del importe abonado",
    fuente="intranet",
)
doc2 = Documento("Nota rápida", "Revisar el contrato con el proveedor de GPUs")

print(doc1)                       # Documento(titulo='Política de devoluciones', palabras=21, fuente='intranet')
print(doc2)                       # Documento(titulo='Nota rápida', palabras=8, fuente='desconocida')
print(doc1.num_palabras())        # 21
print(doc1.resumen(5))            # Los clientes disponen de treinta...
print(doc1.contiene("REEMBOLSO")) # True  ← insensible a mayúsculas
print(doc1.contiene("garantía"))  # False
print(doc2.resumen())             # Revisar el contrato con el proveedor de GPUs (corto: sin truncar)
**Razonamiento de diseño (qué hace "buena" a esta clase):** 1. **Datos + comportamiento juntos:** la alternativa (diccionarios sueltos y funciones que los reciben) funciona, pero la clase agrupa todo lo que un documento "sabe hacer" y el autocompletado del editor te muestra sus métodos. Cuando en el módulo de RAG añadas `doc.trocear(tamaño=500)` o `doc.a_embedding(modelo)`, la estructura ya estará lista. 2. **Valores por defecto en el constructor** (`fuente="desconocida"`): mismo principio que en las funciones — obligatorio lo esencial, opcional lo secundario. 3. **`__repr__` no es decorativo:** cuando tengas una lista de 500 documentos y algo falle, `print(documentos[criticos])` con un buen `__repr__` es la diferencia entre depurar en 1 minuto o en 1 hora. 4. **Casos límite cubiertos:** `resumen()` no añade "..." a documentos cortos. Los métodos pequeños también merecen pensar sus bordes. **Nota:** las bibliotecas reales de RAG (LangChain, LlamaIndex) tienen una clase `Document` casi idéntica a esta: texto + metadatos + métodos de utilidad. No es casualidad: es el patrón natural del dominio.

Ejercicio 24 — Manejo de excepciones con datos sucios

Dificultad: · Capítulo relacionado: Cap. 4 (Python para IA)

Contexto: Recibes lecturas de gasto mensual de clientes exportadas de un sistema antiguo. Vienen como strings y están sucias: números válidos mezclados con basura. En datos reales, esto es la norma, no la excepción.

lecturas = ["1250.50", "890", "N/A", "2100.75", "", "error", "3400", "-50", "1.200,00", None]

Tu tarea: Escribe una función limpiar_lecturas(lecturas) -> tuple[list[float], list] que: 1. Convierta a float todo lo convertible. 2. Descarte lo no convertible SIN romper el programa, acumulándolo en una lista de rechazos. 3. Rechace también los números negativos (un gasto negativo es un error de origen). 4. Devuelva (valores_validos, rechazados) e imprima un informe: cuántos entraron, cuántos se rechazaron y el total.

Ver solución
def limpiar_lecturas(lecturas: list) -> tuple[list[float], list]:
    """Convierte lecturas sucias a floats válidos; devuelve (válidos, rechazados)."""
    validos: list[float] = []
    rechazados: list = []

    for lectura in lecturas:
        try:
            # float() es quien "sabe" qué es convertible; no intentamos adivinarlo
            # nosotros con ifs. Filosofía Python: EAFP — "es más fácil pedir perdón
            # que pedir permiso": intenta la conversión y captura el fallo.
            valor = float(lectura)
        except (ValueError, TypeError):
            # ValueError: strings no numéricos ("N/A", "", "error", "1.200,00")
            # TypeError:  tipos no convertibles (None)
            # Capturamos SOLO estas dos: cualquier otro error (un bug real)
            # debe seguir explotando para que lo veamos.
            rechazados.append(lectura)
            continue                        # Pasamos a la siguiente lectura

        if valor < 0:
            # Convertible pero inválido por regla de negocio: también se rechaza.
            # Nótese que esto NO es una excepción: es una validación normal con if.
            rechazados.append(lectura)
            continue

        validos.append(valor)

    # Informe final
    print(f"Lecturas recibidas:  {len(lecturas)}")
    print(f"Válidas:             {len(validos)}")
    print(f"Rechazadas:          {len(rechazados)}{rechazados}")
    print(f"Gasto total:         {sum(validos):.2f} €")
    return validos, rechazados


lecturas = ["1250.50", "890", "N/A", "2100.75", "", "error", "3400", "-50", "1.200,00", None]
validos, rechazados = limpiar_lecturas(lecturas)
**Salida:**
Lecturas recibidas:  10
Válidas:             4
Rechazadas:          6 → ['N/A', '', 'error', '-50', '1.200,00', None]
Gasto total:         7641.25 €
**Decisiones de diseño que debes saber defender:** 1. **`except (ValueError, TypeError)` y NADA más.** Un `except Exception` (o el pecado capital: `except:` desnudo) se tragaría también bugs auténticos — un `KeyboardInterrupt`, un error de programación tuyo — y los convertiría en "datos rechazados" silenciosos. Captura exactamente lo que esperas; deja pasar lo que no. 2. **Los rechazados se GUARDAN, no se tiran.** En un proyecto real, la lista de rechazos se registra o se escribe a un archivo: es la evidencia para ir al dueño de los datos y decir "el 60% de vuestro export está roto, arregladlo en origen". Descartar silenciosamente es destruir información de calidad de datos. 3. **El caso `"1.200,00"`:** es un número en formato europeo que `float()` no entiende (esperaría `"1200.00"`). Se rechaza correctamente... pero fíjate: son 1 200 € reales que estamos perdiendo. Decidir si añadir una normalización de formato (`.replace(".", "").replace(",", ".")`) es una conversación con negocio, no una decisión técnica silenciosa. Los datos sucios casi siempre esconden decisiones de negocio. 4. **El `-50` NO usa try/except:** es un valor perfectamente convertible que viola una regla de negocio. Excepciones para lo *excepcional* (fallos de conversión); `if` para las *reglas* (validaciones). Mezclar ambos conceptos produce código confuso. **Advertencia:** en datasets reales, la limpieza de datos consume el 60-80% del tiempo de un proyecto de IA. Este ejercicio en miniatura es el día a día del oficio: quien domina el manejo de errores domina los pipelines.

Ejercicio 25 — NumPy: matrices y vectorización vs bucle (benchmark)

Dificultad: · Capítulo relacionado: Cap. 4 (Python para IA)

Contexto: Te han dicho mil veces que "NumPy es más rápido que los bucles de Python", pero un profesional no repite eslóganes: los mide.

Tu tarea: 1. Crea una matriz 3×4 con los números del 1 al 12 (reshape) y muestra: su forma, su transpuesta, la suma por filas y por columnas. 2. Crea dos vectores aleatorios de 1 000 000 de elementos y calcula su producto punto de dos maneras: (a) bucle Python puro con zip, (b) np.dot. 3. Cronometra ambas con time.perf_counter() y calcula cuántas veces más rápida es la versión vectorizada. 4. Explica POR QUÉ NumPy es más rápido (la explicación importa más que el número).

Ver solución **1. La matriz y sus operaciones básicas:**
import numpy as np

M = np.arange(1, 13).reshape(3, 4)   # arange genera [1..12]; reshape lo reorganiza en 3 filas × 4 columnas

print(M)
# [[ 1  2  3  4]
#  [ 5  6  7  8]
#  [ 9 10 11 12]]

print(M.shape)          # (3, 4)  ← (filas, columnas): SIEMPRE en ese orden
print(M.T)              # Transpuesta: filas ↔ columnas, ahora es 4×3
print(M.sum(axis=1))    # [10 26 42]     ← axis=1: colapsa las columnas → suma de cada FILA
print(M.sum(axis=0))    # [15 18 21 24]  ← axis=0: colapsa las filas → suma de cada COLUMNA
**Nota:** el parámetro `axis` confunde a todo el mundo al principio. Truco mental: `axis` indica **la dimensión que desaparece**. `axis=0` elimina las filas (queda un valor por columna); `axis=1` elimina las columnas (queda un valor por fila). **2 y 3. El benchmark:**
import time

n = 1_000_000
rng = np.random.default_rng(seed=42)   # Generador aleatorio con semilla: resultados reproducibles
a = rng.random(n)                      # 1 millón de floats en [0, 1)
b = rng.random(n)

# --- Versión A: bucle Python puro ---
inicio = time.perf_counter()           # perf_counter: el reloj de mayor precisión para medir código
total = 0.0
for x, y in zip(a, b):                 # zip empareja elemento a elemento
    total += x * y                     # multiplicar y acumular... un millón de veces
tiempo_bucle = time.perf_counter() - inicio

# --- Versión B: NumPy vectorizado ---
inicio = time.perf_counter()
total_np = np.dot(a, b)                # UNA línea: toda la operación baja a código C compilado
tiempo_numpy = time.perf_counter() - inicio

print(f"Bucle Python: {tiempo_bucle:.4f} s")
print(f"NumPy dot:    {tiempo_numpy:.4f} s")
print(f"Aceleración:  {tiempo_bucle / tiempo_numpy:.0f}x")
print(f"¿Mismo resultado? {np.isclose(total, total_np)}")   # True
**Salida típica (variará según tu máquina):**
Bucle Python: 0.1842 s
NumPy dot:    0.0009 s
Aceleración:  205x
¿Mismo resultado? True
Aceleraciones de **100x a 400x** son normales en este experimento. **4. Por qué NumPy es tan rápido (la parte que importa):** 1. **El bucle de Python paga peaje en cada vuelta:** cada `x * y` obliga al intérprete a averiguar el tipo de los objetos, desempaquetar los floats de sus "cajas" de objeto Python, operar y volver a empaquetar. Un millón de iteraciones = un millón de peajes. 2. **NumPy ejecuta UN solo bucle en C compilado:** `np.dot` recibe los dos arrays completos y recorre la memoria en código máquina optimizado, sin intérprete de por medio. 3. **Memoria contigua:** los arrays de NumPy guardan los números en un bloque continuo de memoria (no como las listas de Python, que guardan punteros dispersos). Eso permite al procesador usar la caché con eficacia e instrucciones SIMD que operan sobre varios números a la vez. **Consejo profesional:** la regla de oro del código numérico en Python: **si estás escribiendo un `for` sobre números, párate y busca la operación NumPy equivalente.** Todo el deep learning (PyTorch, TensorFlow) hereda esta filosofía: tensores y operaciones vectorizadas, jamás bucles sobre elementos. Acostumbrarte ahora te prepara para los módulos de redes neuronales.

Ejercicio 26 — Pandas: análisis de ventas con CSV inline

Dificultad: · Capítulo relacionado: Cap. 4 (Python para IA)

Contexto: El equipo de dirección te pasa las ventas del último trimestre y tres preguntas de negocio. Para que el ejercicio sea autocontenido, el CSV va incrustado en el código con io.StringIO (que hace que un string se comporte como un archivo).

Preguntas de negocio: 1. ¿Qué categoría genera más ingresos totales? 2. ¿Cuál es el ticket medio (importe medio por venta) de cada canal (web/tienda)? 3. ¿Cuántas ventas superiores a 500 € hubo, y qué porcentaje del total de ingresos representan?

Tu tarea: Carga los datos con Pandas y responde las tres preguntas con código, mostrando los resultados con print.

Ver solución
import io
import pandas as pd

# io.StringIO envuelve el string para que pd.read_csv lo lea como si fuera un archivo.
# En un proyecto real harías: pd.read_csv("ventas_q4.csv")
datos_csv = io.StringIO("""fecha,categoria,canal,importe
2026-04-03,electronica,web,899.99
2026-04-05,hogar,tienda,120.50
2026-04-08,electronica,tienda,1450.00
2026-04-12,ropa,web,89.90
2026-04-15,hogar,web,340.00
2026-04-20,electronica,web,650.75
2026-05-02,ropa,tienda,210.00
2026-05-06,hogar,web,95.25
2026-05-11,electronica,tienda,2100.00
2026-05-18,ropa,web,150.60
2026-05-23,hogar,tienda,480.00
2026-06-01,electronica,web,720.30
2026-06-07,ropa,tienda,65.99
2026-06-14,hogar,web,890.00
2026-06-21,electronica,tienda,330.45
2026-06-28,ropa,web,540.20
""")

df = pd.read_csv(datos_csv)              # Cargamos el CSV en un DataFrame

# Exploración mínima obligatoria antes de responder nada:
print(df.shape)        # (16, 4): 16 ventas, 4 columnas
print(df.dtypes)       # importe es float64: correcto; fecha es object (string): nos vale hoy
print(df.head(3))      # Primeras filas: verificar visualmente que cargó bien

# --- Pregunta 1: categoría con más ingresos totales ---
ingresos_por_categoria = df.groupby("categoria")["importe"].sum()
# groupby("categoria"): agrupa las filas por valor de esa columna
# ["importe"].sum():    dentro de cada grupo, suma los importes
print(ingresos_por_categoria.sort_values(ascending=False))
# electronica    6151.49
# hogar          1925.75
# ropa           1056.69
print(f"Líder: {ingresos_por_categoria.idxmax()}")   # idxmax: el ÍNDICE del valor máximo → 'electronica'

# --- Pregunta 2: ticket medio por canal ---
ticket_medio = df.groupby("canal")["importe"].mean()
print(ticket_medio.round(2))
# tienda    679.56
# web       486.33

# --- Pregunta 3: ventas > 500 € y su peso en los ingresos ---
grandes = df[df["importe"] > 500]        # Filtrado por máscara booleana (igual que en NumPy)
num_grandes = len(grandes)
peso = grandes["importe"].sum() / df["importe"].sum()
print(f"Ventas > 500 €: {num_grandes} de {len(df)}")
print(f"Representan el {peso:.1%} de los ingresos")
# Ventas > 500 €: 7 de 16
# Representan el 79.4% de los ingresos
**Respuestas de negocio (lo que dirías en la reunión, no el código):** 1. **Electrónica domina:** 6 151 € de 9 134 € totales (dos de cada tres euros). Hogar y ropa quedan muy atrás. 2. **El ticket medio de tienda (680 €) supera al de web (486 €):** las compras grandes de electrónica ocurren sobre todo en tienda física. Insight accionable: quizá la web necesita mejores fichas/financiación para productos caros. 3. **Concentración fuerte:** solo 7 de 16 ventas superan los 500 €, pero concentran el **79.4% de los ingresos**. El negocio depende de pocas ventas grandes: proteger esa experiencia de compra es prioritario. **Patrones de Pandas que acabas de usar (el kit de supervivencia):** | Patrón | Qué resuelve | |--------|--------------| | `pd.read_csv(...)` | Cargar datos tabulares (el 90% de los datos empresariales) | | `df.shape`, `df.dtypes`, `df.head()` | Reconocimiento del terreno: SIEMPRE antes de calcular nada | | `df.groupby(col)[col2].agg()` | "Ingresos POR categoría", "media POR canal": la pregunta de negocio más frecuente del mundo | | `df[condición]` | Filtrar filas por máscara booleana | | `.idxmax()`, `.sort_values()` | Encontrar y ordenar ganadores | **Consejo profesional:** fíjate en que las tres respuestas terminan en una frase de negocio, no en un número suelto. Un data scientist que responde "82.5%" es un técnico; uno que responde "el negocio depende de 7 ventas grandes, protejamos ese segmento" es alguien a quien invitan a las reuniones de decisión.

SECCIÓN D — Ejercicios integradores

Cuatro ejercicios finales que combinan varios capítulos. Son el examen real del módulo: si los superas, estás listo para el Módulo 02.


Ejercicio 27 — Pipeline completo: limpiar datos y calcular estadísticas

Dificultad: · Capítulos relacionados: Caps. 3-5

Contexto: Una fintech te pasa las puntuaciones de riesgo (0-100) que su sistema antiguo asignó a solicitudes de crédito. El export está sucio y te piden un pipeline reutilizable: una función que reciba los datos crudos y devuelva datos limpios más un informe estadístico. Este patrón (ingesta → limpieza → validación → estadísticas) es el esqueleto de cualquier preparación de datos para ML.

datos_crudos = ["72", "85", "N/A", "91", "45", "", "105", "63", None, "78", "-12", "88", "56", "error", "94", "70"]

Reglas de negocio: las puntuaciones válidas son números entre 0 y 100 (inclusive). Todo lo demás se descarta con registro.

Tu tarea: Escribe un pipeline con tres funciones puras y una orquestadora: 1. convertir(datos) -> tuple[list[float], list]: convierte a float lo convertible, separa lo demás. 2. validar_rango(valores, minimo, maximo) -> tuple[list[float], list]: aplica la regla 0-100. 3. calcular_estadisticas(valores) -> dict: media, mediana, desviación estándar, mínimo, máximo y los valores normalizados min-max (escala 0-1) usando NumPy. 4. pipeline(datos_crudos) -> dict: encadena las tres y devuelve un informe completo.

Ver solución
import numpy as np

def convertir(datos: list) -> tuple[list[float], list]:
    """Etapa 1: conversión de tipos. Separa lo convertible de la basura."""
    convertidos, rechazados = [], []
    for dato in datos:
        try:
            convertidos.append(float(dato))       # Intentamos la conversión...
        except (ValueError, TypeError):           # ...y capturamos SOLO los fallos esperables
            rechazados.append(dato)               # La basura se guarda como evidencia, no se tira
    return convertidos, rechazados


def validar_rango(valores: list[float], minimo: float, maximo: float) -> tuple[list[float], list]:
    """Etapa 2: reglas de negocio. Un número puede ser convertible pero inválido."""
    validos = [v for v in valores if minimo <= v <= maximo]      # Los que cumplen la regla
    fuera_rango = [v for v in valores if not (minimo <= v <= maximo)]  # Los que no (105, -12)
    return validos, fuera_rango


def calcular_estadisticas(valores: list[float]) -> dict:
    """Etapa 3: estadísticas descriptivas + normalización min-max con NumPy."""
    arr = np.array(valores)                        # A partir de aquí, todo vectorizado
    minimo, maximo = arr.min(), arr.max()
    # Normalización min-max: (x - min) / (max - min) → todos los valores en [0, 1].
    # Es la misma transformación que aplicarás a features antes de entrenar modelos (Cap. 5).
    rango = maximo - minimo
    normalizados = (arr - minimo) / rango if rango > 0 else np.zeros_like(arr)
    return {
        "n": len(arr),
        "media": round(arr.mean(), 2),
        "mediana": round(float(np.median(arr)), 2),
        "desviacion": round(arr.std(), 2),
        "minimo": minimo,
        "maximo": maximo,
        "normalizados": np.round(normalizados, 3).tolist(),
    }


def pipeline(datos_crudos: list) -> dict:
    """Orquestadora: encadena las etapas y produce el informe final."""
    convertidos, no_convertibles = convertir(datos_crudos)             # Etapa 1
    validos, fuera_rango = validar_rango(convertidos, 0, 100)          # Etapa 2
    estadisticas = calcular_estadisticas(validos)                      # Etapa 3
    return {
        "recibidos": len(datos_crudos),
        "no_convertibles": no_convertibles,        # Trazabilidad: QUÉ se rechazó y POR QUÉ etapa
        "fuera_de_rango": fuera_rango,
        "validos": len(validos),
        "estadisticas": estadisticas,
    }


datos_crudos = ["72", "85", "N/A", "91", "45", "", "105", "63", None, "78", "-12", "88", "56", "error", "94", "70"]
informe = pipeline(datos_crudos)

for clave, valor in informe.items():
    print(f"{clave}: {valor}")
**Salida:**
recibidos: 16
no_convertibles: ['N/A', '', None, 'error']
fuera_de_rango: [105.0, -12.0]
validos: 10
estadisticas: {'n': 10, 'media': 74.2, 'mediana': 75.0, 'desviacion': 15.09,
               'minimo': 45.0, 'maximo': 94.0,
               'normalizados': [0.551, 0.816, 0.939, 0.0, 0.367, 0.673, 0.878, 0.224, 1.0, 0.51]}
**Por qué esta arquitectura y no un solo bucle gigante:** 1. **Cada función hace UNA cosa** y se puede probar por separado: `convertir` no sabe nada de rangos; `validar_rango` no sabe nada de strings. Si mañana el rango cambia a 300-850 (puntuación crediticia americana), tocas UNA línea. 2. **Los rechazos se separan por causa:** "no convertible" (problema de formato en origen) y "fuera de rango" (problema de lógica en origen) son enfermedades distintas con médicos distintos. Un informe que los mezcla no sirve para reclamar al proveedor de los datos. 3. **La normalización min-max es la conexión con el Cap. 5:** los modelos de ML necesitan features en escalas comparables; acabas de implementar a mano lo que `MinMaxScaler` de scikit-learn hará por ti en el próximo módulo. Verifica a mano el primer valor: (72 − 45) / (94 − 45) = 27/49 ≈ 0.551 . 4. **Caso límite cubierto:** si todos los valores fueran iguales, `max - min = 0` y la normalización dividiría por cero. El `if rango > 0` lo previene. Pregúntate siempre: "¿qué entrada rompería esta línea?" **Consejo profesional:** guarda esta estructura de pipeline como plantilla personal. En proyectos reales solo crecerá: una etapa de deduplicación aquí, un logging allá... pero el esqueleto convertir → validar → transformar → informar te acompañará toda la carrera.

Ejercicio 28 — Regresión lineal con descenso de gradiente desde cero

Dificultad: · Capítulos relacionados: Caps. 3-6

Contexto: Este es EL ejercicio del módulo: vas a implementar, sin ninguna biblioteca de ML, el ciclo completo de entrenamiento del Capítulo 6 — el mismo que, a escala gigante, entrena a GPT. Una empresa de reparto quiere predecir el tiempo de entrega (minutos) a partir de la distancia (km):

distancias = [1.0, 2.0, 3.0, 4.0, 5.0]      # km
tiempos    = [12.0, 19.0, 25.0, 33.0, 39.0] # minutos observados

El modelo es una recta: predicción = w · distancia + b, donde w y b son los dos parámetros a aprender. La función de pérdida es el error cuadrático medio (MSE).

Tu tarea: 1. Implementa el entrenamiento con descenso de gradiente: inicializa w = 0, b = 0, tasa de aprendizaje 0.05, 1 000 épocas. Las derivadas del MSE son: - dw = -2/n · Σ xᵢ·(yᵢ − pred_i) - db = -2/n · Σ (yᵢ − pred_i) 2. Imprime la pérdida cada 100 épocas para VER cómo aprende. 3. Reporta los w y b finales e interpreta su significado de negocio. 4. Predice el tiempo para una entrega de 7 km.

Ver solución
import numpy as np

# --- Los datos (convertidos a arrays para operar vectorizado) ---
X = np.array([1.0, 2.0, 3.0, 4.0, 5.0])       # Entrada: distancia en km
y = np.array([12.0, 19.0, 25.0, 33.0, 39.0])  # Objetivo: minutos reales observados
n = len(X)

# --- Paso 0 del ciclo: inicializar los parámetros (el modelo "no sabe nada") ---
w = 0.0      # Pendiente: minutos extra por cada km adicional
b = 0.0      # Intercepto: minutos fijos independientes de la distancia
eta = 0.05   # Tasa de aprendizaje: el tamaño del paso (recuerda el Ejercicio 15)

for epoca in range(1, 1001):
    # --- 1. FORWARD: el modelo hace sus predicciones con los parámetros actuales ---
    pred = w * X + b                    # Vectorizado: predice los 5 ejemplos de golpe

    # --- 2. PÉRDIDA: ¿cómo de mal lo está haciendo? ---
    error = y - pred                    # Diferencia entre realidad y predicción
    mse = (error ** 2).mean()           # Error cuadrático medio: castiga más los errores grandes

    # --- 3. GRADIENTES: ¿hacia dónde ajustar cada parámetro para reducir la pérdida? ---
    dw = (-2 / n) * (X * error).sum()   # Derivada del MSE respecto a w
    db = (-2 / n) * error.sum()         # Derivada del MSE respecto a b

    # --- 4. ACTUALIZACIÓN: un paso en contra del gradiente (descenso) ---
    w = w - eta * dw
    b = b - eta * db

    # --- 5. REPETIR (y observar el aprendizaje cada 100 épocas) ---
    if epoca % 100 == 0:
        print(f"Época {epoca:4d} | pérdida (MSE) = {mse:8.4f} | w = {w:.3f} | b = {b:.3f}")

print(f"\nModelo aprendido: tiempo ≈ {w:.2f} × distancia + {b:.2f}")

# --- Predicción sobre un caso nuevo (inferencia) ---
distancia_nueva = 7.0
prediccion = w * distancia_nueva + b
print(f"Entrega de {distancia_nueva} km → {prediccion:.1f} minutos estimados")
**Salida (verificada ejecutando el código):**
Época  100 | pérdida (MSE) =   0.2190 | w = 6.955 | b = 4.640
Época  200 | pérdida (MSE) =   0.1620 | w = 6.828 | b = 5.098
Época  300 | pérdida (MSE) =   0.1601 | w = 6.805 | b = 5.181
Época  400 | pérdida (MSE) =   0.1600 | w = 6.801 | b = 5.197
Época  500 | pérdida (MSE) =   0.1600 | w = 6.800 | b = 5.199
Época  600 | pérdida (MSE) =   0.1600 | w = 6.800 | b = 5.200
Época  700 | pérdida (MSE) =   0.1600 | w = 6.800 | b = 5.200
Época  800 | pérdida (MSE) =   0.1600 | w = 6.800 | b = 5.200
Época  900 | pérdida (MSE) =   0.1600 | w = 6.800 | b = 5.200
Época 1000 | pérdida (MSE) =   0.1600 | w = 6.800 | b = 5.200

Modelo aprendido: tiempo ≈ 6.80 × distancia + 5.20
Entrega de 7.0 km → 52.8 minutos estimados
**Lectura del entrenamiento (esto es lo que debes saber "ver"):** - **La pérdida baja rápido al principio y luego se aplana:** la curva típica de todo entrenamiento sano. Cuando deja de bajar (época ~400), el modelo ha convergido; seguir entrenando ya no aporta. - **Interpretación de negocio de los parámetros:** `w ≈ 6.8` significa "cada km añade unos 6.8 minutos de reparto" (velocidad efectiva en ciudad); `b ≈ 5.2` son los "minutos fijos" de toda entrega (aparcar, subir, entregar) — existen aunque la distancia fuera 0. Un modelo interpretable te regala conocimiento del negocio. - **La pérdida final NO es 0 (0.16):** los datos reales tienen ruido y una recta no puede pasar por todos los puntos. Perseguir pérdida 0 aquí sería... exacto, overfitting (Ejercicio 8). **La conexión con TODO el deep learning:** este bucle de 5 pasos — forward, pérdida, gradientes, actualización, repetir — es literalmente el mismo con el que se entrena un LLM. Las diferencias son de escala, no de naturaleza: 2 parámetros → miles de millones; recta → red de Transformers; derivadas a mano → diferenciación automática (autograd); un CPU → miles de GPUs. Si entiendes este ejercicio, entiendes *cómo aprende la IA moderna*. Punto. **Advertencia:** prueba a subir `eta` a 0.2 y observa cómo la pérdida oscila o explota (`nan`). Es el mismo fenómeno de divergencia del Ejercicio 15, ahora en 2 dimensiones. Reconocer una pérdida que explota te ahorrará horas de depuración en el futuro.

Ejercicio 29 — Clasificador de sentimiento por reglas y sus límites vs ML

Dificultad: · Capítulos relacionados: Caps. 1, 4 y 6

Contexto: Una cadena hotelera quiere clasificar reseñas como positivas o negativas. Antes de invertir en ML, el CTO propone algo razonable: "Probemos primero lo simple: un clasificador por reglas con listas de palabras. Si funciona, nos ahorramos el proyecto de ML." Tu trabajo: construirlo BIEN, medirlo con honestidad y emitir un veredicto informado.

Datos de prueba (con etiqueta real anotada):

resenas = [
    ("La habitación era excelente y el personal muy amable", "positivo"),
    ("Servicio horrible, la habitación estaba sucia", "negativo"),
    ("Una estancia maravillosa, volveremos seguro", "positivo"),
    ("Ruido terrible toda la noche, experiencia pésima", "negativo"),
    ("El desayuno estaba bueno y las vistas eran bonitas", "positivo"),
    ("No me gustó nada, todo mal desde el principio", "negativo"),
    ("No está mal, aunque esperaba algo mejor", "negativo"),
    ("El hotel no es malo, de hecho es bastante bueno", "positivo"),
]

Tu tarea: 1. Implementa clasificar(texto) -> str con dos listas de palabras (positivas/negativas): cuenta apariciones y decide por mayoría. 2. Evalúa el clasificador sobre las 8 reseñas y calcula su acierto (accuracy). 3. Analiza los fallos: ¿qué tienen en común las reseñas que falla? 4. Escribe la recomendación final al CTO: ¿reglas o ML? ¿Y qué haría falta para la opción ML?

Ver solución **1 y 2. Implementación y evaluación:**
PALABRAS_POSITIVAS = ["excelente", "amable", "maravillosa", "bueno", "bonitas", "mejor", "gustó"]
PALABRAS_NEGATIVAS = ["horrible", "sucia", "terrible", "pésima", "mal", "malo", "ruido", "nada"]

def clasificar(texto: str) -> str:
    """Clasifica el sentimiento contando palabras positivas y negativas."""
    texto = texto.lower()                                          # Normalizamos a minúsculas
    puntos_pos = sum(1 for p in PALABRAS_POSITIVAS if p in texto)  # Cuántas positivas aparecen
    puntos_neg = sum(1 for p in PALABRAS_NEGATIVAS if p in texto)  # Cuántas negativas aparecen
    # Decisión por mayoría; en caso de empate, elegimos negativo
    # (política conservadora: mejor revisar de más una reseña dudosa)
    return "positivo" if puntos_pos > puntos_neg else "negativo"

aciertos = 0
for texto, etiqueta_real in resenas:
    prediccion = clasificar(texto)
    correcto = prediccion == etiqueta_real
    aciertos += correcto
    marca = "✅" if correcto else "❌"
    print(f"{marca} real={etiqueta_real:8s} pred={prediccion:8s} | {texto[:50]}")

print(f"\nAccuracy: {aciertos}/{len(resenas)} = {aciertos/len(resenas):.0%}")
**Salida (verificada ejecutando el código):**
✅ real=positivo pred=positivo | La habitación era excelente y el personal muy amab
✅ real=negativo pred=negativo | Servicio horrible, la habitación estaba sucia
✅ real=positivo pred=positivo | Una estancia maravillosa, volveremos seguro
✅ real=negativo pred=negativo | Ruido terrible toda la noche, experiencia pésima
✅ real=positivo pred=positivo | El desayuno estaba bueno y las vistas eran bonita
✅ real=negativo pred=negativo | No me gustó nada, todo mal desde el principio
✅ real=negativo pred=negativo | No está mal, aunque esperaba algo mejor
❌ real=positivo pred=negativo | El hotel no es malo, de hecho es bastante bueno

Accuracy: 7/8 = 88%
**3. Análisis de los fallos — el corazón del ejercicio:** Un 88% parece un buen resultado... hasta que miras CÓMO se produjo. Analiza los tres últimos casos: - *"El hotel no es **malo**, de hecho es bastante **bueno**"* (el fallo) → "malo" y "mal" (que es subcadena de "malo", ¡doble conteo!) puntúan negativo aunque el "no" los invierte. La regla cuenta palabras; no lee. - *"No me **gustó** nada, todo mal"* (acierto... con trampa) → acertó por casualidad aritmética: "gustó" sumó un punto positivo ESPURIO (está negado), pero "nada" y "mal" lo superaron 2 a 1. Con una redacción apenas distinta ("no me gustó, esperaba más"), el mismo mecanismo fallaría. - *"No está mal, aunque esperaba algo mejor"* (acierto... por el desempate) → quedó en empate 1-1 y lo salvó la política arbitraria de "empate → negativo". No hay comprensión, hay una moneda cargada. Es decir: **de 3 casos con negación, la regla solo "entiende" cero.** Y esto es solo el principio: espera en producción sarcasmo ("*genial, otra noche sin dormir*"), erratas ("*orrible*"), intensidades ("*no está mal*" ≈ tibio positivo en español), comparaciones, ironía... Podrías parchear: regla para "no" antes de adjetivo, bigramas, coincidencia por palabra completa en vez de subcadena, listas de excepciones... Cada parche añade fragilidad y las excepciones crecen sin fin. **Es exactamente la historia de los sistemas expertos de los años 80 (Cap. 2) repetida en miniatura: las reglas explícitas se ahogan en la complejidad del lenguaje real.** **4. Recomendación al CTO (respuesta modelo):** > "Construí y medí el clasificador por reglas: 88% en la prueba, PERO el análisis de errores muestra que todos los casos con negación se resuelven por azar o fallan — el acierto está inflado por la muestra pequeña y no generalizará. Recomiendo: (1) usar las reglas HOY como filtro provisional y como línea base que cualquier modelo deberá superar; (2) en paralelo, etiquetar 2 000-5 000 reseñas reales y entrenar un clasificador de ML supervisado (o directamente evaluar un LLM vía API, que para sentimiento en español funciona muy bien sin entrenamiento). El criterio del Cap. 1 aplica de manual: el lenguaje natural es el territorio donde las reglas explícitas pierden y los ejemplos ganan." **Lo que necesita la opción ML:** datos etiquetados (las 8 reseñas de prueba son anécdota; se necesitan miles), un train/test split honesto (Cap. 5), una métrica acordada con negocio, y la línea base por reglas como listón mínimo a batir. Empezar por reglas NO fue una pérdida de tiempo: fue la forma más barata de descubrir dónde está la dificultad real del problema. **Consejo profesional:** "primero la línea base simple, después el modelo complejo" es una de las mejores prácticas del ML profesional. Un modelo sofisticado que no supera a una regla tonta es un proyecto que no debió existir. Mide siempre contra la alternativa simple.

Ejercicio 30 — Caso de empresa: ¿reglas, ML clásico o LLM?

Dificultad: · Capítulos relacionados: Todos los del módulo

Contexto: Cierre del módulo: un caso completo de arquitectura de solución. "SegurPlus", aseguradora mediana (600 empleados), quiere automatizar su gestión de partes de accidente de coche. El proceso actual, 100% manual, por cada parte:

  1. Comprobar que el formulario tiene todos los campos obligatorios y que la póliza está vigente.
  2. Estimar el coste de la reparación a partir de datos estructurados del parte (modelo del coche, antigüedad, tipo de daño, taller de la zona). Tienen 8 años de histórico con costes finales reales.
  3. Redactar la carta personalizada al cliente explicando la resolución, cobertura y próximos pasos, en tono cercano pero formal.

Restricciones: presupuesto moderado, equipo de datos de 3 personas, los datos de clientes son sensibles (sector regulado), y dirección quiere resultados visibles en 6 meses.

Tu tarea: Para cada uno de los tres subprocesos, elige la tecnología adecuada — reglas programadas, ML clásico o LLM — y justifica. Después responde: ¿en qué orden implementarías las tres piezas y por qué? ¿Qué supervisión humana mantendrías?

Ver solución **La decisión, pieza a pieza:** | Subproceso | Tecnología | Justificación | |------------|------------|---------------| | 1. Validación de formularios y pólizas | **Reglas programadas** | Lógica 100% especificable: campos obligatorios presentes, formato correcto, consulta del estado de la póliza en la base de datos. Determinista, auditable, exacta — y en un sector regulado, la auditabilidad no es negociable. Usar ML aquí sería introducir probabilidad donde se exige certeza (criterio del Cap. 1 / Ejercicio 3). | | 2. Estimación del coste de reparación | **ML clásico (regresión supervisada)** | Es una predicción numérica a partir de **datos tabulares** con un histórico etiquetado de lujo: 8 años de partes con coste final real (la etiqueta perfecta). Terreno ideal de regresión — la versión industrial del Ejercicio 28. Un LLM sería peor opción: los LLMs no destacan prediciendo números a partir de tablas, no se pueden calibrar estadísticamente igual de bien, y costarían más por predicción. Además, un modelo clásico es interpretable ("el coste sube por la antigüedad del vehículo"), lo que el regulador y los peritos agradecerán. | | 3. Redacción de cartas personalizadas | **LLM** | Generación de lenguaje natural con tono controlado: EXACTAMENTE la tarea para la que los LLMs son la tecnología correcta. Con reglas serían plantillas rígidas y robóticas; con ML clásico, imposible. La carta se genera a partir de los datos ya validados (pieza 1) y la estimación (pieza 2), con una plantilla de prompt corporativa. Por la sensibilidad de los datos: API con contrato empresarial de no-retención de datos o, si la política interna lo exige, un modelo open source desplegado en su infraestructura (el análisis del Ejercicio 4 aplica directo). | **Orden de implementación recomendado (y su porqué):** 1. **Primero las reglas (mes 1-2):** valor inmediato, riesgo casi nulo, y de paso fuerza a limpiar y estructurar el flujo de datos — cimiento para todo lo demás. Dirección ve resultados en semanas, lo que compra confianza (y presupuesto) para las fases siguientes. 2. **Después el modelo de costes (mes 2-5):** es el proyecto con más valor económico (estimaciones consistentes, detección de desviaciones) y requiere el trabajo serio de datos: limpieza del histórico (Ejercicio 27), train/test split (Cap. 5), línea base simple primero (media por tipo de daño — ¡Ejercicio 29, la lección de la línea base!), luego el modelo. 3. **Por último la carta con LLM (mes 4-6, en paralelo parcial):** depende de las salidas de 1 y 2, y es la pieza con más matices de riesgo reputacional (una carta con un error factual llega directamente a un cliente). Se beneficia de que las otras dos piezas ya le den datos fiables. **Supervisión humana que se mantiene (imprescindible en el diseño):** - **Pieza 1:** los rechazos de validación los revisa una persona antes de comunicar nada al cliente (un falso rechazo de póliza vigente es un problema serio). - **Pieza 2:** el perito revisa las estimaciones fuera de rango típico (o de baja confianza) — el modelo asiste, el perito decide en los casos raros. - **Pieza 3:** al principio, TODAS las cartas pasan revisión humana antes de enviarse; cuando la tasa de corrección baje de forma sostenida, se muestrea (p. ej. revisar el 10%). Nunca envío 100% automático de comunicaciones legales sin período de rodaje medido. **La lección final del módulo:** la pregunta profesional nunca es *"¿qué puede hacer la IA?"* sino *"¿qué necesita este problema?"*. La solución real de SegurPlus no es "un sistema de IA": son **tres tecnologías distintas bien elegidas** — reglas donde hay certeza, ML clásico donde hay historia y números, LLM donde hay lenguaje — cosidas con supervisión humana proporcional al riesgo. Si este razonamiento te resulta natural, el Módulo 01 ha cumplido su misión. **Consejo profesional:** en entrevistas de AI Engineer, los casos como este (descomponer un proceso y asignar tecnología a cada pieza) son cada vez más habituales que las preguntas de teoría. Practica el formato: descomponer → clasificar cada pieza con criterio → ordenar por valor/riesgo → definir la supervisión humana.

Cierre del módulo

Si has llegado hasta aquí con 25 o más ejercicios superados: enhorabuena, has aprobado el Módulo 01. Ya no eres alguien "que ha leído sobre IA": sabes distinguir cuándo usarla, entiendes la matemática que la mueve, escribes el Python que la implementa y razonas como profesional ante casos reales.

Nota

antes de pasar al siguiente módulo, haz los Laboratorios del módulo si aún no los hiciste — son la práctica guiada que complementa estos ejercicios.

Advertencia

si no llegaste a 25, no avances todavía. Los módulos siguientes construyen sobre estos cimientos, y las grietas de hoy son derrumbes mañana. Repasa el capítulo de la sección donde más fallaste y reintenta en unos días: la segunda pasada siempre es más rápida.


Anterior Módulo Siguiente
Capítulo 7: Ecosistema 01-FUNDAMENTOS Laboratorios