Saltar a contenido

Capítulo 5: Datos y representación — cómo el mundo real se convierte en números

Módulo 01-FUNDAMENTOS · AI Master Academy Tiempo estimado de estudio: 4-6 horas · Nivel: Fundamentos · Requisitos: Capítulo 3 (matemáticas) y Capítulo 4 (Python, NumPy, Pandas)


"In God we trust. All others must bring data." — W. Edwards Deming

Si un modelo de IA fuera un chef, los datos serían sus ingredientes. Puedes tener al mejor chef del mundo (la arquitectura más sofisticada, las GPUs más potentes), pero si le das ingredientes podridos, el plato será incomible. Este capítulo trata de los ingredientes: qué son los datos, cómo se convierten en números que una máquina puede procesar, cómo detectar cuándo están "podridos" y cómo prepararlos profesionalmente.

Este es, probablemente, el capítulo más importante del módulo de fundamentos desde el punto de vista laboral: en la industria, un ingeniero de IA pasa entre el 60% y el 80% de su tiempo trabajando con datos, no con modelos.


Índice

  1. Los datos como materia prima de la IA
  2. 1.1. Garbage in, garbage out
  3. 1.2. Casos reales de proyectos fallidos por datos malos
  4. Tipos de datos: estructurados, semiestructurados y no estructurados
  5. Representación: convertir cada modalidad en números
  6. 3.1. Tablas: features numéricas y categóricas
  7. 3.2. Texto: del carácter al embedding
  8. 3.3. Imágenes: píxeles, canales y matrices 3D
  9. 3.4. Audio: ondas, sampling rate y espectrogramas
  10. Calidad de datos
  11. 4.1. Valores faltantes
  12. 4.2. Outliers
  13. 4.3. Duplicados
  14. 4.4. Desbalanceo de clases
  15. 4.5. Sesgos en los datos
  16. Datasets: train, validation y test
  17. El pipeline de datos profesional
  18. Privacidad y aspectos legales básicos
  19. Ejemplo integrador: limpiar un CSV de clientes de principio a fin
  20. Buenas prácticas
  21. Malas prácticas
  22. Errores comunes
  23. FAQ — Preguntas frecuentes
  24. Resumen del capítulo
  25. Bibliografía y recursos

1. Los datos como materia prima de la IA

1.1. Garbage in, garbage out

Un modelo de machine learning no inventa conocimiento: extrae patrones de los datos que le das. Esto tiene una consecuencia brutal y liberadora a la vez:

Un modelo es exactamente tan bueno como los datos con los que fue entrenado. Ni un gramo más.

El principio garbage in, garbage out (GIGO, "basura entra, basura sale") es anterior a la IA moderna —se atribuye a los primeros programadores de IBM en los años 50— pero nunca ha sido tan relevante como hoy:

  • Qué significa: si los datos de entrada contienen errores, sesgos, ruido o información irrelevante, el modelo aprenderá esos errores, sesgos y ruido, y los reproducirá (a veces amplificados) en producción.
  • Por qué existe: los algoritmos de aprendizaje son optimizadores estadísticos. Minimizan una función de error sobre los datos que ven. No tienen forma de saber que un dato es "incorrecto" respecto al mundo real; para ellos, los datos SON el mundo real.
  • Consecuencia práctica: mejorar los datos suele dar más retorno que mejorar el modelo. Esta idea, popularizada por Andrew Ng como Data-Centric AI, dice: fija el modelo y itera sobre los datos, en lugar de fijar los datos e iterar sobre el modelo.

Piénsalo con una analogía académica: un estudiante que estudia con apuntes llenos de erratas aprobará el examen... reproduciendo las erratas. No importa lo inteligente que sea el estudiante: nunca vio la versión correcta.

Nota

en la era de los LLMs (modelos de lenguaje grandes) este principio se extiende: no solo importan los datos de entrenamiento, sino también los documentos que alimentas a un sistema RAG, los ejemplos de tus prompts (few-shot) y los casos de tus evaluaciones. Todo eso son "datos" y todo eso puede ser basura.

1.2. Casos reales de proyectos fallidos por datos malos

Caso empresarial: Amazon y el reclutador sesgado (2014-2018).

Amazon desarrolló un sistema interno de IA para puntuar currículums. Lo entrenó con los CVs recibidos durante los 10 años anteriores, en los que la mayoría de contratados en puestos técnicos eran hombres. El modelo aprendió que "ser hombre" correlacionaba con "ser contratado" y penalizaba CVs que contenían la palabra "women's" (como en "women's chess club captain") o que mencionaban universidades femeninas. Amazon intentó corregirlo, no pudo garantizar que el sesgo desapareciera, y canceló el proyecto. Los datos históricos codificaban una discriminación histórica; el modelo solo la hizo explícita y automática.

Caso empresarial: IBM Watson for Oncology (2013-2022).

El sistema de recomendación de tratamientos oncológicos de IBM fue entrenado en gran parte con casos sintéticos e hipotéticos creados por médicos de un único hospital (Memorial Sloan Kettering), no con historiales reales de pacientes diversos. Documentos internos filtrados en 2018 revelaron que el sistema llegó a recomendar tratamientos "inseguros e incorrectos". Tras miles de millones de dólares invertidos, IBM vendió Watson Health en 2022. Datos poco representativos → recomendaciones poco fiables.

Caso empresarial: Zillow Offers (2021).

Zillow, el portal inmobiliario estadounidense, usaba un modelo (la "Zestimate") para comprar casas automáticamente y revenderlas. El modelo, entrenado con datos históricos, no capturó el cambio brusco del mercado post-pandemia (un caso de data drift: los datos de producción dejaron de parecerse a los de entrenamiento). Resultado: Zillow compró miles de casas por encima de su valor real, perdió más de 500 millones de dólares, cerró la división y despidió al 25% de su plantilla.

Caso empresarial: Google Flu Trends (2008-2015).

Google intentó predecir brotes de gripe a partir de búsquedas. Funcionó un tiempo, pero llegó a sobreestimar los casos en más de un 140% porque las búsquedas capturaban miedo mediático a la gripe, no gripe real. Los datos medían otra cosa distinta a la que se creía. El proyecto se cerró.

La moraleja común de los cuatro casos:

Caso Problema de datos Lección
Amazon reclutador Datos históricos sesgados Los datos codifican los prejuicios del pasado
IBM Watson Oncology Datos sintéticos y poco representativos Sin datos reales y diversos, no hay generalización
Zillow Offers Data drift (el mundo cambió, los datos no) Los datos caducan; hay que monitorizarlos
Google Flu Trends Los datos medían un proxy, no el fenómeno Pregúntate SIEMPRE qué miden realmente tus datos

Consejo profesional

antes de escribir una sola línea de código de modelado, dedica una sesión completa a responder: ¿de dónde vienen estos datos?, ¿quién los generó y con qué incentivos?, ¿qué población representan y cuál excluyen?, ¿siguen siendo válidos hoy? Esta "auditoría de origen" de 2 horas evita meses de trabajo desperdiciado.

Ejercicio rápido 1

Tu empresa quiere predecir qué empleados renunciarán, usando datos de RRHH de los últimos 15 años. Nombra dos problemas de datos potenciales inspirados en los casos anteriores.

Ver solución Entre otros posibles: 1. **Sesgo histórico (caso Amazon):** si en el pasado la empresa trató peor a cierto colectivo y ese colectivo renunció más, el modelo puede aprender a "predecir renuncia" basándose en pertenecer a ese colectivo, perpetuando la discriminación en decisiones de retención/promoción. 2. **Data drift (caso Zillow):** el mercado laboral de hace 15 años (o incluso pre-pandemia, con menos teletrabajo) no se parece al actual. Los patrones de renuncia antiguos pueden no ser válidos hoy. 3. **Proxy incorrecto (caso Google Flu):** ¿"renuncia registrada en RRHH" mide lo que crees? Quizá muchos empleados descontentos no renuncian pero bajan su rendimiento, y ese fenómeno no está en los datos.

2. Tipos de datos: estructurados, semiestructurados y no estructurados

No todos los datos son iguales. La primera clasificación que debe dominar un ingeniero de IA es por grado de estructura, porque determina qué herramientas y técnicas usar.

2.1. Datos estructurados

  • Qué son: datos organizados en un esquema fijo de filas y columnas, donde cada columna tiene un tipo definido (número, fecha, texto corto, booleano). El ejemplo canónico: una tabla de base de datos relacional o un fichero CSV/Excel.
  • Para qué sirven: son el alimento clásico del machine learning "tabular" (regresiones, árboles de decisión, gradient boosting).
  • Ventajas: fáciles de consultar (SQL), validar, agregar y versionar. Ocupan poco. Las herramientas están ultra maduras.
  • Limitaciones: el mundo real rara vez nace estructurado; estructurar datos cuesta esfuerzo y siempre pierde matices (un campo "motivo_queja: facturación" pierde toda la riqueza del email original del cliente).

2.2. Datos semiestructurados

  • Qué son: datos con estructura parcial y flexible: tienen etiquetas o jerarquía, pero no un esquema rígido. Ejemplos: JSON, XML, YAML, logs de servidores, emails (cabecera estructurada + cuerpo libre).
  • Para qué sirven: son el formato natural de las APIs, los eventos de aplicaciones y la telemetría. En IA moderna, los JSON de interacciones usuario-producto son oro para sistemas de recomendación.
  • Ventajas: flexibles (cada registro puede tener campos distintos), legibles por humanos y máquinas.
  • Limitaciones: la flexibilidad es un arma de doble filo: campos que a veces existen y a veces no, tipos inconsistentes ("edad": 25 vs "edad": "veinticinco"), anidamiento profundo que hay que "aplanar" para modelar.

2.3. Datos no estructurados

  • Qué son: datos sin esquema interno explotable directamente: texto libre, imágenes, audio, vídeo, PDFs escaneados. Se estima que representan más del 80% de los datos de una empresa típica.
  • Para qué sirven: son el territorio del deep learning y de los LLMs. Contratos, emails, llamadas de soporte, fotos de productos: ahí vive el valor que el ML clásico no podía tocar.
  • Ventajas: riqueza máxima de información; abundancia (se generan solos).
  • Limitaciones: requieren modelos grandes para procesarse, ocupan mucho, son difíciles de auditar y gobernar, y su calidad es difícil de medir.

2.4. Tabla comparativa: dónde viven y qué IA los consume

Tipo Ejemplos concretos Dónde viven en una empresa Técnica de IA que los consume
Estructurados Tabla de ventas, ficha de cliente, histórico de precios, sensores IoT tabulados Bases de datos SQL (PostgreSQL, MySQL), data warehouse (BigQuery, Snowflake, Redshift), Excel/CSV ML tabular: regresión logística, Random Forest, XGBoost/LightGBM
Semiestructurados JSON de eventos de la app, logs de Nginx, respuestas de APIs, XML de facturación electrónica Data lakes (S3, Azure Data Lake), MongoDB, Elasticsearch, colas Kafka Detección de anomalías en logs, sistemas de recomendación, feature engineering hacia tablas
Texto (no estruct.) Emails, contratos, tickets de soporte, reseñas, documentación interna Gestores documentales (SharePoint, Drive), CRMs (Salesforce, HubSpot), help desks (Zendesk) NLP: clasificación, NER, LLMs, RAG, embeddings + búsqueda semántica
Imágenes Fotos de producto, radiografías, documentos escaneados, cámaras de calidad en fábrica Object storage (S3, GCS), DAMs, PACS (hospitales) Visión por computador: CNNs, ViT, OCR, modelos multimodales
Audio Llamadas de call center, notas de voz, podcasts internos Grabadoras de centralita, S3, plataformas de telefonía (Twilio) ASR (speech-to-text, ej. Whisper), análisis de sentimiento en voz, diarización
Vídeo Cámaras de seguridad, vídeos de formación, inspección de líneas de producción NVRs, object storage, plataformas de streaming interno Detección de objetos/acciones, resumen multimodal, análisis frame a frame

Nota

en la práctica, casi todo proyecto serio mezcla tipos. Un sistema antifraude usa la tabla de transacciones (estructurado), los logs de sesión (semiestructurado) y las notas del gestor (texto). Saber unir modalidades es una habilidad diferencial.


3. Representación: convertir cada modalidad en números

Aquí llegamos al núcleo del capítulo. Los modelos de IA —todos, desde una regresión lineal hasta GPT— solo operan sobre números (tensores de números en coma flotante, para ser exactos). Por tanto, el trabajo previo a cualquier modelo es siempre el mismo:

Mundo real ──► Representación numérica ──► Modelo ──► Números ──► Decisión/Texto/Acción
 (texto,          (vectores, matrices,
  imagen,          tensores)
  tabla...)

La calidad de esa representación determina qué puede aprender el modelo. Una mala representación destruye información antes de que el modelo la vea; una buena representación le pone el patrón "en bandeja".

3.1. Tablas: features numéricas y categóricas

En datos tabulares, cada fila es un ejemplo y cada columna una feature (característica o variable). Hay dos grandes familias:

  • Features numéricas: cantidades con orden y magnitud (edad, ingresos, temperatura). Se pueden usar casi directamente... con un matiz de escala que veremos.
  • Features categóricas: etiquetas sin magnitud intrínseca (ciudad, color, tipo de contrato). NO se pueden meter tal cual: "Madrid" no es un número.

3.1.1. One-hot encoding

  • Qué es: convertir una feature categórica de K categorías en K columnas binarias (0/1), donde solo una está "encendida" (de ahí one-hot, "una caliente").
  • Por qué existe: porque asignar números arbitrarios a categorías (Madrid=1, Barcelona=2, Sevilla=3) inventaría un orden y unas distancias falsas: el modelo creería que Sevilla es "el triple" que Madrid, y que Barcelona está "entre" ambas. One-hot elimina ese orden ficticio: todas las categorías quedan equidistantes.
  • Cómo funciona:
ciudad          ciudad_Madrid  ciudad_Barcelona  ciudad_Sevilla
Madrid     ──►        1               0                 0
Barcelona  ──►        0               1                 0
Sevilla    ──►        0               0                 1

Código con Pandas, línea por línea:

import pandas as pd                                   # Importamos Pandas para manipular tablas

# Creamos un DataFrame de ejemplo con una columna categórica
df = pd.DataFrame({
    "cliente": ["Ana", "Luis", "Marta", "Pau"],       # Identificador (no es una feature)
    "ciudad": ["Madrid", "Barcelona", "Madrid", "Sevilla"],  # Feature categórica
    "gasto": [120.5, 89.0, 300.2, 45.9]               # Feature numérica
})

# get_dummies crea una columna binaria por cada categoría encontrada en "ciudad"
df_encoded = pd.get_dummies(
    df,                       # DataFrame de entrada
    columns=["ciudad"],       # Lista de columnas categóricas a codificar
    prefix="ciudad",          # Prefijo para nombrar las nuevas columnas
    dtype=int                 # Queremos 0/1 enteros, no True/False
)

print(df_encoded)
#   cliente  gasto  ciudad_Barcelona  ciudad_Madrid  ciudad_Sevilla
# 0     Ana  120.5                 0              1               0
# 1    Luis   89.0                 1              0               0
# 2   Marta  300.2                 0              1               0
# 3     Pau   45.9                 0              0               1
  • Ventajas: no inventa orden; funciona con cualquier modelo; interpretable (cada columna tiene significado claro).
  • Limitaciones — cuándo explota: con alta cardinalidad. Si la columna es "código postal" (≈53.000 códigos en España) o "ID de producto" (millones), one-hot genera decenas de miles de columnas casi todas a cero: matrices gigantes, dispersas, con muy pocos ejemplos por categoría → el modelo no aprende nada útil de la mayoría y el coste computacional se dispara.
  • Cuándo NO usarlo: cardinalidad > ~50-100 categorías (umbral orientativo). Alternativas: agrupar categorías raras en "otros", target encoding (sustituir cada categoría por la media del objetivo en esa categoría, con cuidado extremo del leakage), hashing trick, o —la solución moderna— embeddings de categorías aprendidos por una red neuronal (la misma idea que veremos en texto).

3.1.2. Label encoding

  • Qué es: asignar un entero a cada categoría (Madrid→0, Barcelona→1, Sevilla→2).
  • Cuándo SÍ usarlo: (a) cuando la categoría tiene orden natural (talla S<M<L<XL, nivel educativo); (b) con modelos de árboles (Random Forest, XGBoost), que solo hacen preguntas de tipo "¿x ≤ umbral?" y no interpretan magnitudes de forma lineal, por lo que toleran bien enteros arbitrarios.
  • Cuándo NO: con modelos lineales, redes neuronales o cualquier modelo basado en distancias (k-NN, k-means), porque el orden y las distancias inventadas los confunden.
# Label encoding manual con Pandas (sin sklearn, para ver la mecánica)
tallas = pd.Series(["M", "S", "XL", "M", "L"])          # Feature ordinal

orden = {"S": 0, "M": 1, "L": 2, "XL": 3}               # Definimos el orden EXPLÍCITAMENTE
tallas_encoded = tallas.map(orden)                       # map sustituye cada valor por su entero

print(tallas_encoded.tolist())    # [1, 0, 3, 1, 2] — el orden S<M<L<XL se conserva

Advertencia

si dejas que la librería asigne los enteros alfabéticamente (L=0, M=1, S=2, XL=3), destruyes el orden real. Para ordinales, define SIEMPRE el mapeo a mano.

3.1.3. Comparación de codificaciones categóricas

Técnica Idea Mejor para Explota cuando Modelos compatibles
One-hot K columnas binarias Baja cardinalidad, categorías sin orden Alta cardinalidad (miles de categorías) Todos
Label/Ordinal Un entero por categoría Categorías CON orden natural Se usa con categorías sin orden en modelos lineales/distancia Árboles; lineales solo si hay orden real
Target encoding Media del target por categoría Alta cardinalidad en ML tabular competitivo Data leakage si se calcula sobre todo el dataset Todos (con validación cuidadosa)
Embeddings Vector denso aprendido Cardinalidad enorme (IDs de usuario/producto) Pocos datos para aprender los vectores Redes neuronales

3.1.4. Normalización y estandarización

  • Qué son: transformaciones que ponen todas las features numéricas en escalas comparables.
  • Normalización (min-max): reescala al rango [0, 1]: x' = (x - min) / (max - min).
  • Estandarización (z-score): centra en media 0 y desviación estándar 1: x' = (x - μ) / σ.
  • Por qué los modelos las necesitan: imagina dos features: edad (18-90) e ingresos_anuales (12.000-200.000). Para un modelo basado en distancias o en gradientes, los ingresos dominan totalmente: una diferencia de 1.000 € pesa 1.000 veces más que un año de edad, no porque importe más, sino por pura escala. Consecuencias concretas:
  • k-NN, k-means, SVM: las distancias quedan secuestradas por la feature de mayor rango.
  • Redes neuronales y regresión con descenso de gradiente: la superficie de error se vuelve un "valle alargado" y el gradiente zigzaguea; el entrenamiento converge lento o diverge.
  • Regularización (L1/L2): penaliza coeficientes por igual, pero los coeficientes de features grandes son artificialmente pequeños → la penalización queda desequilibrada.
  • Quién NO las necesita: los árboles de decisión y sus ensembles (Random Forest, XGBoost) son invariantes a transformaciones monótonas de cada feature: preguntar "¿edad ≤ 30?" da igual en años que en años normalizados. Por eso el ML tabular con árboles es tan cómodo.

Código NumPy desde cero, línea por línea:

import numpy as np                                    # NumPy para cálculo numérico

# Matriz de datos: 5 personas x 2 features (edad, ingresos anuales en €)
X = np.array([
    [25,  22_000],
    [47,  61_000],
    [31,  35_000],
    [58, 120_000],
    [22,  19_000],
], dtype=float)                                       # float: vamos a dividir, evitamos enteros

# ----- NORMALIZACIÓN MIN-MAX: lleva cada columna al rango [0, 1] -----
x_min = X.min(axis=0)                                 # Mínimo POR COLUMNA (axis=0): [22, 19000]
x_max = X.max(axis=0)                                 # Máximo por columna: [58, 120000]
X_norm = (X - x_min) / (x_max - x_min)                # Broadcasting: opera columna a columna

print(X_norm.round(3))
# [[0.083 0.03 ]      ← 25 años y 22k€ quedan cerca del mínimo en ambas escalas
#  [0.694 0.416]
#  [0.25  0.158]
#  [1.    1.   ]      ← la fila del máximo vale exactamente 1 en ambas columnas
#  [0.    0.   ]]     ← la fila del mínimo vale exactamente 0

# ----- ESTANDARIZACIÓN Z-SCORE: media 0, desviación 1 por columna -----
mu = X.mean(axis=0)                                   # Media por columna
sigma = X.std(axis=0)                                 # Desviación estándar por columna
X_std = (X - mu) / sigma                              # Resta la media y divide por la desviación

print(X_std.round(3))
print("Medias tras estandarizar:", X_std.mean(axis=0).round(6))   # ≈ [0, 0]
print("Desv. tras estandarizar:", X_std.std(axis=0).round(6))     # ≈ [1, 1]

¿Cuál elegir?

Criterio Min-max [0,1] Z-score (μ=0, σ=1)
Sensibilidad a outliers Alta (un outlier aplasta el resto del rango) Moderada (el outlier estira σ, pero menos)
Rango de salida Acotado y conocido [0,1] No acotado (típicamente -3 a +3)
Uso típico Imágenes (píxeles/255), redes que exigen entradas acotadas Caso general en ML tabular, regresiones, SVM
Si hay outliers fuertes Evitar, o usar RobustScaler (mediana e IQR) Preferible, o RobustScaler

Advertencia — data leakage al normalizar (léelo dos veces)

el error más común del principiante es calcular min/max o μ/σ sobre todo el dataset y luego dividir en train/test. Al hacerlo, las estadísticas del test "se filtran" al train: tu preprocesado ya sabe cosas del futuro. En evaluación obtendrás resultados optimistas que no se cumplirán en producción. El orden correcto es inmutable:

# ❌ MAL: estadísticas calculadas con TODO (train + test mezclados)
# mu, sigma = X.mean(axis=0), X.std(axis=0)
# X_escalado = (X - mu) / sigma
# ... y luego dividir en train/test  ← LEAKAGE

# ✅ BIEN: dividir PRIMERO, calcular estadísticas SOLO con train
n = len(X)                                            # Número total de ejemplos
idx = np.random.permutation(n)                        # Índices barajados aleatoriamente
corte = int(n * 0.8)                                  # 80% para entrenar
X_train, X_test = X[idx[:corte]], X[idx[corte:]]      # División física de los datos

mu = X_train.mean(axis=0)                             # Estadísticas SOLO del train
sigma = X_train.std(axis=0)

X_train_std = (X_train - mu) / sigma                  # Train: transformado con SUS estadísticas
X_test_std = (X_test - mu) / sigma                    # Test: transformado con las MISMAS
                                                      # estadísticas del train (¡nunca las suyas!)

¿Por qué el test se transforma con las estadísticas del train? Porque en producción llegarán datos nuevos de uno en uno y no podrás calcular "su media": usarás la media que aprendiste al entrenar. El test debe simular exactamente esa situación.

Ejercicio rápido 2

Tienes una columna pais con 195 valores posibles y una columna nivel_estudios con valores {primaria, secundaria, grado, máster, doctorado}. ¿Qué codificación usarías para cada una y por qué?

Ver solución - **`pais` (195 categorías, sin orden):** one-hot es viable pero está en el límite (195 columnas). Mejor: agrupar países con pocos ejemplos en "otros" y aplicar one-hot al resto; o usar target encoding con validación cruzada; o embeddings si usas una red. Label encoding directo sería un error con modelos lineales/distancia porque inventaría que Zimbabue > Alemania. - **`nivel_estudios` (5 categorías CON orden natural):** label/ordinal encoding con mapeo manual (primaria=0 ... doctorado=4). El orden es información real que queremos conservar. One-hot también funcionaría, pero descartaría la ordinalidad y usaría 5 columnas en vez de 1.

3.2. Texto: del carácter al embedding

El texto es el caso más fascinante: ¿cómo conviertes "el gato duerme en el sofá" en números sin perder el significado? La historia de esta pregunta es la historia del NLP, y la recorreremos completa porque cada etapa explica la siguiente.

3.2.1. Etapa 1 — Bag of Words (bolsa de palabras)

  • Qué es: representar un documento como un vector de conteos: una posición por cada palabra del vocabulario, con el número de veces que aparece.
  • Por qué existe: es la forma más simple de vectorizar texto: solo requiere contar.
  • Cómo funciona:
Vocabulario: [el, gato, duerme, en, sofá, perro, ladra]

"el gato duerme en el sofá"  ──►  [2, 1, 1, 1, 1, 0, 0]
"el perro ladra"             ──►  [1, 0, 0, 0, 0, 1, 1]
# Bag of Words desde cero, sin librerías de NLP
docs = [
    "el gato duerme en el sofá",
    "el perro ladra al gato",
    "el sofá es cómodo",
]

# 1) Construir el vocabulario: todas las palabras únicas, ordenadas para reproducibilidad
vocab = sorted({palabra for doc in docs for palabra in doc.split()})
print(vocab)  # ['al', 'cómodo', 'duerme', 'el', 'en', 'es', 'gato', 'ladra', 'perro', 'sofá']

# 2) Mapear cada palabra a su índice en el vector
idx = {palabra: i for i, palabra in enumerate(vocab)}   # {'al': 0, 'cómodo': 1, ...}

# 3) Vectorizar: para cada documento, contar apariciones de cada palabra
import numpy as np
X = np.zeros((len(docs), len(vocab)), dtype=int)        # Matriz documentos x vocabulario
for i, doc in enumerate(docs):                          # Recorremos cada documento
    for palabra in doc.split():                         # Recorremos sus palabras
        X[i, idx[palabra]] += 1                         # Sumamos 1 en la celda correspondiente

print(X)
# [[0 0 1 2 1 0 1 0 0 1]     ← "el" aparece 2 veces en el primer documento
#  [1 0 0 1 0 0 1 1 1 0]
#  [0 1 0 1 0 1 0 0 0 1]]
  • Ventajas: trivial de implementar, sorprendentemente eficaz para clasificación temática y filtros de spam clásicos.
  • Limitaciones: (1) ignora el orden: "el perro muerde al hombre" y "el hombre muerde al perro" tienen el mismo vector; (2) vocabularios reales generan vectores de decenas de miles de dimensiones casi todos a cero; (3) todas las palabras pesan igual: "el" cuenta tanto como "hipoteca".

3.2.2. Etapa 2 — TF-IDF: pesar las palabras por lo informativas que son

  • Qué es: una mejora de BoW que da más peso a las palabras frecuentes en este documento pero raras en el resto de la colección.
  • Fórmula intuitiva:
TF-IDF(palabra, doc) = TF · IDF

TF  (Term Frequency)              = ¿cuánto aparece la palabra EN ESTE documento?
IDF (Inverse Document Frequency)  = log( N_documentos / N_documentos_que_la_contienen )
                                    → palabra que está en TODOS los docs: IDF ≈ 0 (no informa)
                                    → palabra que está en POCOS docs: IDF alto (informa mucho)

Intuición pura: si buscas "hipoteca variable" en 1.000 contratos, la palabra "el" aparece en los 1.000 (IDF≈0, inútil para distinguir), pero "hipoteca" aparece en 40 (IDF alto: el documento que la repite mucho probablemente TRATA de hipotecas).

# TF-IDF sencillo desde cero (versión didáctica, sin suavizados de sklearn)
import numpy as np

N = len(docs)                                           # Número de documentos (3)

# df[j] = en cuántos documentos aparece la palabra j (Document Frequency)
df = (X > 0).sum(axis=0)                                # X>0 → booleano; sum por columnas

# IDF: logaritmo del ratio N/df. +1e-9 evitaría división por cero en colecciones raras
idf = np.log(N / df)                                    # Palabra en los 3 docs → log(1)=0

# TF: frecuencia relativa dentro de cada documento (conteo / longitud del doc)
tf = X / X.sum(axis=1, keepdims=True)                   # keepdims para que el broadcasting cuadre

tfidf = tf * idf                                        # Producto elemento a elemento

print(tfidf.round(3))
# Observa: la columna de "el" (presente en los 3 docs) vale 0.0 en todos:
# TF-IDF ha decidido automáticamente que "el" no aporta información distintiva.
  • Ventajas: mejora enorme sobre BoW con coste mínimo; sigue siendo el baseline honesto para clasificación de texto y motores de búsqueda léxicos (BM25, su primo evolucionado, mueve Elasticsearch).
  • Limitaciones — el problema del significado: TF-IDF sigue tratando las palabras como símbolos atómicos sin relación entre sí. Para TF-IDF, "coche" y "automóvil" son tan diferentes entre sí como "coche" y "lechuga": vectores ortogonales, similitud cero. Un buscador TF-IDF no encuentra "reparación de automóviles" cuando buscas "arreglar mi coche". El significado —la sinonimia, la analogía, el contexto— es invisible.

La evolución completa de la representación del texto, de un vistazo:

flowchart TD
    A["Bag of Words (años 50-90)<br/>vector de CONTEOS de palabras"] -->|Problema: 'el' pesa<br/>tanto como 'hipoteca'|B["TF-IDF (1972→hoy)<br/>conteos PONDERADOS por rareza"]
    B -->|Problema: 'coche' y 'automóvil'<br/>son ortogonales — similitud 0|C["Embeddings estáticos<br/>Word2Vec/GloVe (2013)<br/>vectores DENSOS de significado"]
    C -->|Problema: 'banco' (asiento) y<br/>'banco' (dinero) = mismo vector|D["Embeddings contextuales<br/>Transformers (2018→hoy)<br/>el vector depende de la FRASE"]
    D --> E["LLMs y RAG (2020→hoy)<br/>tokens → embeddings → atención"]

    style C fill:#f9e79f,stroke:#b7950b
    style D fill:#aed6f1,stroke:#2471a3

Cada salto de la cadena resuelve la limitación concreta del anterior. Recorrámoslo con calma, porque entender POR QUÉ falló cada etapa es entender por qué la IA moderna es como es.

3.2.3. Etapa 3 — EMBEDDINGS: el mapa de significados

Esta es la idea más importante del capítulo y una de las tres o cuatro ideas fundacionales de la IA moderna.

  • Qué son: un embedding es un vector denso (todas sus posiciones tienen valores, no ceros dispersos) y de dimensión moderada (típicamente 100-3072) que representa el significado de una palabra, frase, documento, imagen o cualquier objeto, de forma que objetos de significado parecido tienen vectores cercanos.
  • Por qué existen: para resolver exactamente el problema anterior: necesitamos una representación donde "coche" y "automóvil" estén cerca, y "coche" y "lechuga" lejos.
  • La analogía del mapa de significados: imagina un mapa gigante donde cada palabra es una ciudad. Las palabras se colocan de modo que las relacionadas queden cerca: hay un "barrio de los animales" donde gato, perro y caballo son vecinos; un "distrito financiero" con banco, préstamo e hipoteca; y "gato" está más cerca de "perro" que de "hipoteca". Solo que el mapa no tiene 2 dimensiones (norte-sur, este-oeste) sino cientos: una dirección podría capturar género, otra tamaño, otra formalidad, otra tiempo verbal... El modelo no etiqueta las dimensiones; emergen del entrenamiento.
  • ¿Cómo se aprenden? La intuición (Word2Vec, 2013, y sucesores): "una palabra se conoce por las compañías que frecuenta" (hipótesis distribucional). Se entrena una red pequeña a predecir una palabra a partir de sus vecinas en millones de frases. Palabras que aparecen en contextos parecidos ("el ___ maúlla", "el ___ ladra") acaban con vectores parecidos. El embedding es un subproducto de esa tarea de predicción.
  • La aritmética del significado — rey - hombre + mujer ≈ reina: el hallazgo que asombró al mundo en 2013: las direcciones del espacio capturan relaciones. El vector que va de "hombre" a "rey" (≈ "realeza masculina") es casi el mismo que va de "mujer" a "reina". Por eso:
vector("rey") - vector("hombre") + vector("mujer") ≈ vector("reina")
vector("París") - vector("Francia") + vector("Italia") ≈ vector("Roma")
vector("caminando") - vector("caminar") + vector("nadar") ≈ vector("nadando")

El significado se ha vuelto geometría: las analogías son paralelogramos en el espacio.

  • Dimensionalidad: ¿por qué 300 o 1536 dimensiones y no 2 o 3? Porque el significado tiene muchísimos matices independientes y hacen falta muchas direcciones para codificarlos sin que se pisen. Órdenes de magnitud reales: Word2Vec/GloVe clásicos, 100-300 dims; embeddings de frases modernos (sentence-transformers), 384-1024; text-embedding-3-large de OpenAI, hasta 3072; los embeddings internos de un LLM grande, varios miles. Más dimensiones = más capacidad de matiz, pero más coste de almacenamiento y cómputo; muchos proveedores permiten truncar dimensiones sacrificando algo de calidad.

  • Similitud coseno: ¿cómo medimos "cercanía" entre dos embeddings? No con la distancia euclídea (sensible a la longitud del vector) sino con el coseno del ángulo entre ellos: 1 = misma dirección (significados casi idénticos), 0 = ortogonales (sin relación), -1 = opuestos.

                cos(θ) = (A · B) / (‖A‖ · ‖B‖)

     producto escalar de A y B, dividido por el producto de sus longitudes

Código completo con vectores de juguete, línea por línea:

import numpy as np

# Embeddings DE JUGUETE en 4 dimensiones inventadas para que se vea la intuición.
# Dimensiones imaginarias: [realeza, masculinidad, feminidad, felinidad]
# (en la realidad las dimensiones no son interpretables así, pero la geometría es la misma)
emb = {
    "rey":    np.array([0.95, 0.90, 0.05, 0.02]),   # Muy realeza, muy masculino
    "reina":  np.array([0.93, 0.06, 0.91, 0.01]),   # Muy realeza, muy femenina
    "hombre": np.array([0.10, 0.92, 0.04, 0.03]),   # Poca realeza, muy masculino
    "mujer":  np.array([0.08, 0.05, 0.93, 0.02]),   # Poca realeza, muy femenina
    "gato":   np.array([0.02, 0.30, 0.28, 0.95]),   # Nada realeza, muy felino
}

def similitud_coseno(a, b):
    """Coseno del ángulo entre dos vectores: 1=iguales, 0=sin relación."""
    producto_escalar = np.dot(a, b)                  # Suma de productos elemento a elemento
    norma_a = np.linalg.norm(a)                      # Longitud (módulo) del vector a
    norma_b = np.linalg.norm(b)                      # Longitud del vector b
    return producto_escalar / (norma_a * norma_b)    # Normalizamos: solo importa la DIRECCIÓN

# --- 1) Palabras relacionadas están cerca; no relacionadas, lejos ---
print(similitud_coseno(emb["rey"], emb["reina"]))    # ≈ 0.67  (comparten "realeza")
print(similitud_coseno(emb["rey"], emb["gato"]))     # ≈ 0.24  (poco en común)
print(similitud_coseno(emb["hombre"], emb["mujer"])) # ≈ 0.08  (solo comparten "persona no real")

# --- 2) La famosa aritmética: rey - hombre + mujer ≈ ¿? ---
resultado = emb["rey"] - emb["hombre"] + emb["mujer"]  # Operación vectorial pura

# Buscamos qué palabra del vocabulario está más cerca del vector resultado
for palabra, vector in emb.items():
    print(f"{palabra:8s} → similitud {similitud_coseno(resultado, vector):.3f}")
# rey      → 0.797
# reina    → 0.978   ← ¡GANADORA! rey - hombre + mujer apunta casi exactamente a "reina"
# hombre   → 0.086
# mujer    → 0.583
# gato     → 0.171

La diferencia física entre un vector disperso (BoW/TF-IDF) y uno denso (embedding), en ASCII:

Vector DISPERSO (TF-IDF, dim = tamaño del vocabulario, ej. 50.000):
"coche"     → [0, 0, 0, ..., 0, 0.83, 0, 0, ..., 0, 0, 0]   ← un solo valor ≠ 0
"automóvil" → [0, 0.79, 0, ..., 0, 0,    0, 0, ..., 0, 0, 0]  ← OTRO valor, OTRA posición
                                                    similitud("coche","automóvil") = 0 

Vector DENSO (embedding, dim = 300):
"coche"     → [0.21, -0.83, 0.45, 0.02, -0.11, ..., 0.37]   ← TODAS las posiciones informan
"automóvil" → [0.19, -0.81, 0.47, 0.05, -0.09, ..., 0.35]   ← casi idéntico, punto a punto
                                                    similitud("coche","automóvil") ≈ 0.97 ✅
  • Ventajas de los embeddings: capturan sinonimia y analogía; son densos y compactos; son transferibles (se entrenan una vez sobre corpus gigantes y se reutilizan); funcionan para CUALQUIER objeto (texto, imagen, audio, usuarios, productos) → son la base de la búsqueda semántica, RAG, recomendadores y clustering moderno.
  • Limitaciones: (1) los estáticos (Word2Vec) dan UN vector por palabra: "banco" (asiento) y "banco" (entidad financiera) colapsan en el mismo punto — los embeddings contextuales de los transformers (cap. módulo 04) lo resuelven; (2) heredan los sesgos del corpus (célebre: "médico - hombre + mujer ≈ enfermera" en algunos modelos antiguos); (3) no son interpretables dimensión a dimensión; (4) comparar embeddings de modelos distintos no tiene sentido (cada modelo define su propio espacio).
  • Cuándo NO usarlos: si necesitas coincidencia exacta (buscar un número de factura, un DNI, un código de error), la búsqueda léxica (TF-IDF/BM25) es superior. Los sistemas profesionales de búsqueda suelen ser híbridos: léxico + semántico.

3.2.4. Tokens y tokenización: cómo "ven" el texto los LLMs

Antes de que un LLM calcule embeddings, tiene que trocear el texto. Y no lo trocea ni en letras ni en palabras, sino en tokens.

  • Qué es un token: una unidad subléxica (trozo de palabra) de un vocabulario fijo (típicamente 32.000-200.000 tokens) aprendido por algoritmos como BPE (Byte Pair Encoding): los pares de caracteres/trozos más frecuentes del corpus se van fusionando hasta formar el vocabulario.
  • Por qué tokens y no palabras: un vocabulario de palabras completas sería infinito (nombres propios, erratas, palabras nuevas, otros idiomas). Con caracteres, las secuencias serían larguísimas y costosas. Los tokens son el punto medio: palabras frecuentes = 1 token ("the", "casa"); palabras raras = varios tokens ("desoxirribonucleico" → des|oxi|rribo|nucle|ico).
"El ingeniero entrena transformers"
   ▼ tokenizador (ejemplo ilustrativo)
["El", " ingenier", "o", " entren", "a", " transform", "ers"]
   ▼ cada token se mapea a un ID entero del vocabulario
[1725, 48210, 78, 31502, 64, 5276, 388]
   ▼ cada ID se convierte en su embedding (vector denso)
7 vectores de ~4096 dimensiones → esto es lo que realmente entra al modelo
  • La consecuencia famosa — ¿cuántas "r" tiene "fresa"? Los LLMs históricamente fallaban preguntas como "¿cuántas letras R hay en strawberry?" (o "fresa"). La razón es estructural: el modelo nunca ve letras. Ve algo como ["straw", "berry"][45123, 19772]. Preguntarle cuántas "r" contiene es como preguntarte a ti cuántos trazos tiene un carácter chino que solo has oído pronunciar: la información no está en su formato de entrada; solo puede "recordar" respuestas sobre deletreo que viera en el corpus. Los modelos recientes lo compensan razonando paso a paso o usando herramientas, pero la limitación de fondo ilustra perfectamente qué es una representación: el modelo solo conoce el mundo a través de ella.

Nota

los tokens también son la unidad de facturación y de límite de contexto de las APIs de LLMs (en español, muy aproximadamente 1 token ≈ 3-4 caracteres). Profundizaremos en tokenización, BPE y sus efectos en el módulo 04-LLM.

Ejercicio rápido 3

Sin ejecutar código: la similitud coseno entre los vectores BoW de "coche rápido" y "automóvil veloz" ¿será alta o cero? ¿Y entre sus embeddings? Justifica.

Ver solución - **BoW/TF-IDF: exactamente 0.** No comparten ninguna palabra, así que sus vectores dispersos no tienen ninguna posición común distinta de cero → producto escalar 0 → coseno 0. Para BoW son frases totalmente ajenas. - **Embeddings: alta (típicamente >0.8).** "coche"≈"automóvil" y "rápido"≈"veloz" aparecen en contextos casi intercambiables en el corpus, así que sus vectores son casi paralelos y el embedding de las frases completas también. Esta es precisamente la razón de ser de los embeddings: capturan que significan lo mismo aunque no compartan ni una letra.

3.3. Imágenes: píxeles, canales y matrices 3D

  • Qué es una imagen digital: una rejilla de píxeles, donde cada píxel es un número (o varios) que indica intensidad de luz. Para una máquina, una foto ES una matriz de números. No hay nada más.
  • Escala de grises: una matriz 2D de forma (alto, ancho). Cada valor va de 0 (negro) a 255 (blanco) en el formato habitual de 8 bits.
  • Color — canales RGB: tres matrices superpuestas (Roja, Verde, Azul). La imagen es un tensor 3D de forma (alto, ancho, 3). Cada píxel es una tripleta: (255, 0, 0) = rojo puro, (255, 255, 0) = amarillo, (128, 128, 128) = gris medio.

ASCII art — una imagen 5x5 en escala de grises y lo que "ve" la máquina:

   Lo que ves tú:                Lo que ve la máquina:

   ░░░░░ █ ░░░░░                 [[  0,   0, 255,   0,   0],
   ░░░░░ █ ░░░░░                  [  0,   0, 255,   0,   0],
   ░░░░░ █ ░░░░░      ◄────►      [  0,   0, 255,   0,   0],
   ░░░░░ █ ░░░░░                  [  0,   0, 255,   0,   0],
   ░░░░░ █ ░░░░░                  [  0,   0, 255,   0,   0]]

   "una línea vertical"           una matriz (5, 5) con una
                                  columna de valores altos

Ejemplo NumPy completo, línea por línea:

import numpy as np

# ----- Imagen 5x5 en ESCALA DE GRISES: una línea vertical blanca sobre fondo negro -----
img_gris = np.zeros((5, 5), dtype=np.uint8)    # Matriz 5x5 de ceros (todo negro). uint8: 0-255
img_gris[:, 2] = 255                           # Todas las filas (:), columna 2 → blanco (255)

print(img_gris)
# [[  0   0 255   0   0]
#  [  0   0 255   0   0]
#  [  0   0 255   0   0]
#  [  0   0 255   0   0]
#  [  0   0 255   0   0]]
print(img_gris.shape)                          # (5, 5) → 25 números en total

# ----- Imagen 5x5 en COLOR (RGB): tensor 3D -----
img_rgb = np.zeros((5, 5, 3), dtype=np.uint8)  # (alto=5, ancho=5, canales=3): 75 números
img_rgb[:, 2, 0] = 255                         # Columna 2, canal 0 (Rojo) → línea ROJA vertical
img_rgb[0, :, 1] = 255                         # Fila 0, canal 1 (Verde) → línea VERDE horizontal

print(img_rgb.shape)                           # (5, 5, 3)
print(img_rgb[0, 2])                           # [255 255 0] → ese píxel es rojo+verde = AMARILLO

# ----- Operaciones típicas de preprocesado -----
img_float = img_rgb.astype(np.float32) / 255.0 # Normalizar a [0,1]: min-max con min=0, max=255
gris = img_rgb.mean(axis=2)                    # Pasar a grises: promediar los 3 canales
print(img_float.max(), gris.shape)             # 1.0 (5, 5)

Órdenes de magnitud que conviene interiorizar: una foto de móvil de 12 Mpx en color son 4000 × 3000 × 3 = 36.000.000 números. Por eso las redes de visión no conectan cada píxel con todo (sería inabordable) sino que usan convoluciones: filtros pequeños que se deslizan por la imagen detectando patrones locales.

¿Qué "ve" una red en esa matriz? Una jerarquía creciente de patrones: las primeras capas detectan bordes y esquinas (cambios bruscos de valores vecinos); las intermedias combinan bordes en texturas y formas (un círculo, un tejido); las profundas combinan formas en conceptos (un ojo, una rueda, una cara). Nadie programa esa jerarquía: emerge del entrenamiento. Lo veremos en detalle en el módulo 03-DEEP-LEARNING.

Nota

cuidado con las convenciones de forma: NumPy/matplotlib usan (alto, ancho, canales) ("channels last"), PyTorch usa (canales, alto, ancho) ("channels first"), y OpenCV carga en orden BGR en lugar de RGB. Mezclarlas produce imágenes con colores alienígenas y modelos que no aprenden: es uno de los bugs más clásicos en visión.

3.4. Audio: ondas, sampling rate y espectrogramas

  • Qué es el sonido digital: el sonido físico es una onda de presión continua. Para digitalizarla se muestrea: se mide la amplitud de la onda muchas veces por segundo. El audio digital es, por tanto, un simple vector 1D de números.
  • Sampling rate (frecuencia de muestreo): cuántas mediciones por segundo. Valores típicos: 8.000 Hz (telefonía), 16.000 Hz (estándar en reconocimiento de voz, ej. Whisper), 44.100 Hz (CD, música). El teorema de Nyquist dice que necesitas muestrear al doble de la frecuencia máxima que quieras capturar (el oído llega a ~20 kHz → CD a 44,1 kHz).
Onda continua:      ∿∿∿∿∿∿∿∿∿∿∿∿
                    │ │ │ │ │ │ │   ← medimos la altura 16.000 veces/segundo
Vector resultante:  [0.02, 0.13, 0.31, 0.28, 0.11, -0.05, -0.19, ...]

3 segundos de voz a 16 kHz = vector de 48.000 números
  • El problema de la onda cruda: la forma de onda mezcla todas las frecuencias en una sola señal temporal; los patrones relevantes (fonemas, notas) son difíciles de ver ahí directamente.
  • Espectrograma — la intuición clave: aplicando la transformada de Fourier por ventanas (recuerda el cap. 3), el audio se convierte en una imagen: eje X = tiempo, eje Y = frecuencia (graves abajo, agudos arriba), color/intensidad = energía de esa frecuencia en ese instante. Es literalmente "la partitura" del sonido. Y aquí ocurre algo hermoso: como el espectrograma es una imagen, los modelos de audio reutilizan las técnicas de visión (convoluciones, transformers de parches). Whisper, por ejemplo, no procesa ondas: procesa espectrogramas log-Mel de 80 bandas.
Espectrograma de alguien diciendo "hola":

frecuencia ▲
 (agudos)  │  ░░░░▒▒░░░░░░░░░░
           │  ░░▒▓▓▓▒░░░▒▒░░░░       cada columna = un instante
           │  ░▒▓███▓▒░▒▓▓▒░░░       cada fila    = una frecuencia
 (graves)  │  ▒▓█████▓▓▓██▓▒░░       intensidad   = energía
           └──────────────────►
              "h  o   l  a"    tiempo
  • Ventajas de esta representación: hace visibles los patrones fonéticos y musicales; conecta el audio con todo el arsenal de visión.
  • Limitaciones: el espectrograma estándar descarta la fase de la señal (reconstruir audio desde él requiere técnicas extra); la resolución tiempo-frecuencia es un compromiso (ventanas largas = buena resolución en frecuencia, mala en tiempo, y viceversa).

Nota

fíjate en el patrón general del capítulo: todas las modalidades acaban en tensores. Tabla → matriz 2D. Texto → secuencia de vectores (tokens→embeddings). Imagen → tensor 3D. Audio → vector 1D o espectrograma 2D. La IA moderna es, en el fondo, una fábrica de convertir mundo en tensores y tensores en decisiones.


4. Calidad de datos

Ya sabemos representar. Ahora, la pregunta incómoda: ¿y si los datos están mal? Spoiler: siempre están mal en algún grado. Veamos los cinco problemas universales.

4.1. Valores faltantes

  • Qué son: celdas sin valor (NaN, None, NULL, o peor: valores centinela camuflados como -999, "N/A", 0 o cadenas vacías).
  • Por qué existen: fallos de sensores, campos opcionales de formularios, uniones de tablas sin correspondencia, errores de integración, gente que no quiere dar su teléfono.
  • Lo primero es preguntarse POR QUÉ faltan, porque el mecanismo determina la estrategia. La estadística distingue tres casos: falta al azar puro (MCAR: un sensor se desconectó aleatoriamente), falta según otras variables observadas (MAR: los jóvenes rellenan menos el campo "teléfono fijo") y falta según el propio valor oculto (MNAR: los que más ganan ocultan más sus ingresos — ¡el hueco ES información!).

Estrategias, con sus pros y contras:

Estrategia Qué hace Ventajas Inconvenientes Cuándo usarla
Eliminar filas Descarta ejemplos con nulos Simple, no inventa datos Pierdes datos (caro); si faltan de forma no aleatoria, SESGAS el dataset <5% de filas afectadas y mecanismo aleatorio
Eliminar columna Descarta la feature entera Simple Pierdes una señal potencialmente útil Columna con >50-60% de nulos y poca importancia
Imputar media/mediana Rellena con el promedio (numéricas) Conserva todas las filas; rápido Reduce la varianza real; distorsiona correlaciones; mediana mejor si hay outliers Numéricas con pocos nulos; baseline razonable
Imputar moda / "desconocido" Rellena categóricas con la más frecuente o una categoría nueva Simple; "desconocido" preserva la señal de ausencia La moda puede sobre-representarse Categóricas; "desconocido" si sospechas MNAR
Imputación por modelo (k-NN, iterativa) Predice el valor faltante desde las otras columnas Más preciso; respeta relaciones Complejo; riesgo de leakage si se ajusta con todo el dataset; más caro Datasets valiosos y pequeños donde cada fila cuenta
Indicador de nulo + imputación Añade columna binaria "faltaba_X" además de imputar Conserva la información de QUE faltaba Duplica columnas Casi siempre recomendable si el nulo puede ser informativo

Advertencia

las estadísticas de imputación (media, mediana, moda) se calculan solo con el train, igual que la normalización. Imputar con la media global antes de dividir es data leakage, sutil pero real.

Consejo profesional

antes de imputar nada, ejecuta df.isna().sum() y también busca nulos disfrazados: (df["edad"] <= 0).sum(), (df["email"] == "").sum(), df["pais"].value_counts() (buscando "N/A", "-", "?"). Los nulos honestos son fáciles; los camuflados hunden proyectos.

4.2. Outliers

  • Qué son: valores anormalmente alejados del resto: una edad de 190 años, una compra de 9.999.999 €.
  • La distinción crítica: ¿es un error (dedo que baila, unidad equivocada: metros vs centímetros) o un valor real extremo (un cliente VIP que de verdad gastó 2 millones)? Los errores se corrigen o eliminan; los extremos reales suelen ser la parte más valiosa del dataset (¡en detección de fraude, el outlier ES el objetivo!).
  • Cómo detectarlos: regla del rango intercuartílico (IQR): sospechoso todo lo que quede fuera de [Q1 - 1.5·IQR, Q3 + 1.5·IQR]; o z-score (>3 desviaciones de la media); o simplemente mirar histogramas y df.describe().
import numpy as np

gastos = np.array([120, 89, 300, 95, 110, 150_000, 87, 132])  # Un valor sospechoso...

q1, q3 = np.percentile(gastos, [25, 75])      # Cuartiles 1 y 3 (percentiles 25 y 75)
iqr = q3 - q1                                 # Rango intercuartílico: "anchura del 50% central"
limite_sup = q3 + 1.5 * iqr                   # Umbral clásico de Tukey
limite_inf = q1 - 1.5 * iqr

outliers = gastos[(gastos > limite_sup) | (gastos < limite_inf)]  # Máscara booleana
print(outliers)                               # [150000] → detectado; ahora INVESTIGA antes de borrar
  • Qué hacer: corregir si es error demostrable; eliminar solo con justificación documentada; o mitigar su influencia sin borrarlos (transformación logarítmica, winsorización —recortar al percentil 1/99—, usar modelos robustos como árboles o RobustScaler).
  • Cuándo NO eliminarlos: cuando el fenómeno de interés vive en las colas (fraude, fallos industriales, clientes premium, picos de demanda). Borrar outliers "para que el modelo vaya mejor" en esos dominios es borrar el problema que te pagan por resolver.

4.3. Duplicados

  • Qué son: filas repetidas, exactas o "borrosas" ("Juan Pérez" / "juan perez " / "J. Pérez" con el mismo email).
  • Por qué importan (más de lo que parece): (1) sobre-representan ciertos ejemplos → sesgan el modelo hacia ellos; (2) el peligro grave: si el mismo registro cae en train Y en test, el modelo "aprueba el examen" porque ya vio la respuesta → métricas infladas artificialmente (otra forma de leakage); (3) en la era LLM, documentos duplicados en un índice RAG desperdician contexto y distorsionan la recuperación.
  • Cómo tratarlos: df.duplicated() para exactos; para borrosos, normalizar antes (minúsculas, sin espacios extra, sin acentos) y deduplicar por clave de negocio (email, DNI) quedándose con el registro más reciente o más completo. La deduplicación borrosa a gran escala (record linkage) es un campo en sí mismo.

4.4. Desbalanceo de clases

  • Qué es: cuando las categorías a predecir tienen frecuencias muy dispares. El ejemplo canónico: fraude con tarjetas, 99% transacciones legítimas / 1% fraudulentas (en la realidad, a menudo 99,9/0,1).
  • La trampa del "accuracy 99%": un "modelo" que responda SIEMPRE "legítima" acierta el 99% de las veces. Accuracy 99%... y cero fraudes detectados. Utilidad real: nula. La métrica global esconde el fracaso total en la clase que importa.
Matriz de confusión del modelo "todo es legítimo" (10.000 transacciones, 1% fraude):

                    Predicho: legítima   Predicho: fraude
Real: legítima            9.900                 0          ← todo perfecto aquí
Real: fraude                100                 0          ← ¡100 fraudes sin detectar!

Accuracy = 9.900/10.000 = 99%     Recall de fraude = 0/100 = 0%  
  • Qué mirar en su lugar: precisión y recall por clase, F1, matriz de confusión completa, AUC-PR. En fraude, el recall de la clase minoritaria (¿qué fracción de fraudes cazo?) y el coste de cada tipo de error son lo que importa. (Métricas en detalle: capítulo 6.)
  • Qué hacer con el desbalanceo: submuestrear la clase mayoritaria (rápido, pierde datos), sobremuestrear la minoritaria o generar sintéticos tipo SMOTE ( solo sobre el train, nunca antes de dividir: leakage), ponderar las clases en la función de pérdida (class_weight — suele ser la opción más limpia), o replantear el problema como detección de anomalías.
  • Cuándo NO tocar nada: si tu modelo produce probabilidades bien calibradas y eliges bien el umbral de decisión según costes, a veces no necesitas remuestrear. Remuestrear por inercia distorsiona las probabilidades.

4.5. Sesgos en los datos

  • Qué son: distorsiones sistemáticas que hacen que los datos no representen fielmente a la población o codifiquen discriminaciones históricas.
  • Tipos frecuentes: sesgo de selección (tus datos solo cubren a quien ya era cliente), sesgo histórico (el pasado discriminó y los datos lo registran), sesgo de medición (el proxy mide otra cosa: "arrestos" no es "delitos"), sesgo de supervivencia (solo ves los casos que "sobrevivieron" al proceso).
  • Caso empresarial — contratación: el ya citado sistema de Amazon penalizando CVs con la palabra "women's". Sesgo histórico puro: 10 años de decisiones humanas sesgadas convertidas en etiquetas de entrenamiento.
  • Caso empresarial — crédito: en 2019, el algoritmo de la Apple Card (gestionada por Goldman Sachs) fue investigado por el regulador de Nueva York tras casos virales de matrimonios con finanzas compartidas donde el marido recibía hasta 20 veces más límite de crédito que la esposa (le ocurrió al propio cofundador de Apple, Steve Wozniak). Aunque la investigación no halló infracción legal probada, el caso se volvió el ejemplo mundial de un problema real: aun sin usar "género" como variable, un modelo puede discriminar a través de variables proxy correlacionadas (historial laboral con interrupciones, patrones de gasto, tipo de comercio). Quitar la columna sensible NO elimina el sesgo.
  • Caso empresarial — justicia: COMPAS, sistema usado en EE.UU. para estimar riesgo de reincidencia. La investigación de ProPublica (2016) mostró que la tasa de falsos positivos (etiquetar como "alto riesgo" a quien no reincidió) era aproximadamente el doble para personas negras que blancas. Origen: entrenar sobre datos de arrestos (proxy sesgado de criminalidad) en un sistema con sesgo policial histórico.
  • Qué hacer: auditar la composición del dataset frente a la población objetivo; medir métricas de rendimiento desglosadas por grupos (no solo la media global); documentar el origen y las limitaciones de los datos (datasheets for datasets); implicar a expertos del dominio y, en casos de alto riesgo, cumplir las obligaciones del Reglamento Europeo de IA (AI Act), que desde 2026 exige gobernanza de datos explícita para sistemas de alto riesgo como crédito y contratación.

Advertencia

el sesgo no es un bug técnico que se arregla con una línea de código; es una propiedad del proceso que generó los datos. Detectarlo exige mirar los datos con preguntas sociales, no solo estadísticas.

Ejercicio rápido 4

Un modelo de detección de spam alcanza 98% de accuracy. El 97% de los correos del dataset son legítimos. ¿Es un buen modelo? ¿Qué pedirías para saberlo?

Ver solución No se puede saber con la accuracy sola: el modelo trivial "nada es spam" ya tendría 97%. Un 98% podría significar que detecta solo un tercio del spam. Pedirías la **matriz de confusión** y las métricas por clase: recall de spam (¿qué fracción del spam real caza?) y precisión de spam (¿cuántos de los marcados como spam lo eran de verdad?, porque los falsos positivos —correos legítimos a la carpeta de spam— son carísimos para el usuario). Con 97/3 de desbalanceo, la accuracy es casi ruido.

5. Datasets: train, validation y test

5.1. Por qué tres conjuntos y no uno

El objetivo de un modelo NO es acertar en los datos que ya vio, sino generalizar a datos nuevos. Evaluar con los mismos datos de entrenamiento es como puntuar a un estudiante con el mismo examen que usó para estudiar, con las respuestas incluidas: nota perfecta, aprendizaje desconocido. De ahí la división ritual:

Conjunto Proporción típica Para qué sirve Quién lo "ve"
Train (entrenamiento) 60-80% Ajustar los parámetros del modelo (pesos) El modelo, en cada paso de entrenamiento
Validation (validación) 10-20% Comparar alternativas: hiperparámetros, arquitecturas, cuándo parar de entrenar Tú, muchas veces, durante el desarrollo
Test 10-20% Estimación FINAL y honesta del rendimiento real Idealmente, UNA sola vez, al final

¿Por qué no bastan dos? Porque si eliges hiperparámetros mirando el test, estás ajustándote al test indirectamente: tras 50 experimentos eligiendo "el que mejor va en test", el test ya no es una medida honesta — te has sobreajustado a él a través de tus decisiones. La validación existe para absorber ese desgaste; el test queda virgen para el veredicto final.

  • Proporciones habituales: 70/15/15 u 80/10/10 en datasets medianos. Con millones de ejemplos, el test puede ser el 1% y sobrar. Con pocos datos (<10.000), mejor validación cruzada (k-fold): rotar qué porción valida, promediar resultados.
  • Casos con estructura especial: series temporales → dividir por TIEMPO (entrenar con el pasado, testear con el futuro; barajar aleatoriamente sería predecir el lunes usando el martes); datos agrupados (varias filas del mismo paciente/cliente) → dividir por GRUPO, de modo que un mismo paciente nunca esté en train y test a la vez.

5.2. Data leakage: el enemigo silencioso

  • Qué es: cualquier vía por la que información del test (o del futuro, o de la respuesta) se cuela en el entrenamiento. El síntoma: métricas maravillosas en desarrollo, fracaso en producción.
  • Los cuatro leakages clásicos, con ejemplo:
  • Preprocesar antes de dividir: normalizar/imputar/hacer SMOTE con estadísticas de TODO el dataset (ya visto en §3.1.4). La regla: dividir primero, ajustar el preprocesado solo con train.
  • Duplicados a ambos lados: el mismo cliente (o la misma foto con nombre distinto) en train y test. El modelo memoriza, no generaliza.
  • Features del futuro: predecir si un cliente abandonará usando la columna fecha_de_baja o numero_de_llamadas_de_cancelacion... que solo existen DESPUÉS del abandono. En el histórico están disponibles; el día que quieras predecir de verdad, no. Ejemplo médico real y frecuente: predecir una enfermedad usando la variable "medicación prescrita" — que el médico prescribió porque ya la diagnosticó.
  • Leakage del target: una feature que es casi un sinónimo de la etiqueta (predecir "importe del fraude" con la feature "importe reembolsado al cliente").
  • Cómo detectarlo: desconfía de resultados demasiado buenos (accuracy 99,8% al primer intento = alarma, no celebración); revisa las features más importantes del modelo preguntando "¿esta información existiría EN EL MOMENTO de predecir?"; comprueba solapamiento de IDs entre conjuntos.

Consejo profesional

la pregunta-talismán contra el leakage es: "¿tendré este dato disponible, con este valor exacto, en el instante en que el modelo tenga que predecir en producción?". Hazla columna por columna. Es aburrido. Funciona.

5.3. En la era LLM: evals

Cuando trabajas con LLMs no siempre "entrenas", pero la lógica train/test sobrevive con otro nombre: evals. Tus prompts y ejemplos few-shot son "train"; tu conjunto de casos de prueba con respuestas esperadas es "test". Y el leakage también existe: si iteras el prompt mirando los mismos 20 casos de eval, acabarás con un prompt sobreajustado a esos 20 casos. Mantén un conjunto de evaluación reservado también para prompts.

5.4. Dónde conseguir datasets

Fuente URL Qué encuentras
Hugging Face Datasets https://huggingface.co/datasets +300.000 datasets de texto, imagen, audio; carga en una línea con datasets
Kaggle https://www.kaggle.com/datasets Datasets de competición y comunidad, muy variados, con notebooks de ejemplo
datos.gob.es https://datos.gob.es Datos abiertos de las administraciones públicas españolas
UCI ML Repository https://archive.ics.uci.edu El repositorio académico clásico (Iris, Adult, Wine...)
Google Dataset Search https://datasetsearch.research.google.com Buscador de datasets de toda la web
Eurostat / INE https://ec.europa.eu/eurostat · https://www.ine.es Estadística oficial europea y española

Advertencia

un dataset público también trae licencia (¿permite uso comercial?), sesgos documentados o no, y a veces errores célebres (MNIST y CIFAR tienen etiquetas mal puestas conocidas). Lee siempre la ficha del dataset (dataset card) antes de usarlo.


6. El pipeline de datos profesional

En una empresa, los datos no aparecen limpios en un CSV: fluyen por un pipeline — una cadena reproducible de etapas. Este es el mapa general:

flowchart LR
    subgraph FUENTES["Recolección"]
        A1[Bases de datos<br/>transaccionales]
        A2[APIs y eventos]
        A3[Documentos, emails,<br/>imágenes, audio]
    end

    subgraph LIMPIEZA["Limpieza"]
        B[Nulos, duplicados,<br/>outliers, validación<br/>de esquemas]
    end

    subgraph TRANSF["Transformación"]
        C[Encoding, escalado,<br/>feature engineering,<br/>tokenización, embeddings]
    end

    subgraph ALMACEN["Almacenamiento"]
        D1[Data warehouse /<br/>feature store]
        D2[Vector database<br/>para embeddings]
    end

    subgraph CONSUMO["Consumo"]
        E1[Entrenamiento de<br/>modelos ML]
        E2[RAG: recuperación<br/>para LLMs]
        E3[Evals y<br/>monitorización]
    end

    A1 --> B
    A2 --> B
    A3 --> B
    B --> C
    C --> D1
    C --> D2
    D1 --> E1
    D2 --> E2
    D1 --> E3
    E3 -.feedback: nuevos datos,<br/>errores detectados.-> B

Puntos que diferencian un pipeline profesional de un notebook casero:

  • Reproducibilidad: cada etapa es código versionado (Git) que puede re-ejecutarse; nada de "abrí el Excel y borré filas a mano" (imposible de auditar ni repetir).
  • Validación automática: esquemas y expectativas comprobadas en cada ejecución ("la columna edad está entre 0 y 120", "no hay más de 2% de nulos"); herramientas tipo Great Expectations o Pandera.
  • Idempotencia: ejecutar el pipeline dos veces produce el mismo resultado, no duplicados.
  • Linaje (lineage): poder responder "¿de qué fuente y qué transformaciones salió este número?"; crítico en auditorías.
  • Versionado de datos: igual que versionas código, versionas datasets (DVC, lakeFS, o simples snapshots fechados): "el modelo v3 se entrenó con el dataset v12".

La novedad de la era LLM: el pipeline ya no alimenta solo entrenamientos. Ahora los "datos" también son:

  • Documentos para RAG: manuales, contratos y wikis que se trocean (chunking), se convierten en embeddings y se indexan en una base vectorial para que el LLM los consulte. Toda la disciplina de calidad aplica igual: un documento obsoleto en el índice = respuestas obsoletas del chatbot con total confianza.
  • Ejemplos para evals: casos de prueba curados con su respuesta esperada, que hacen de "test set" de tus prompts y agentes.
  • Datos para fine-tuning: pares instrucción-respuesta de alta calidad; aquí GIGO es despiadado: 1.000 ejemplos impecables superan a 100.000 mediocres.

El flujo de decisión sobre qué representación aplicar, resumido:

flowchart TD
    A{¿Qué tipo de dato es?} -->|Tabla|B{¿Columna numérica<br/>o categórica?}
    B -->|Numérica|C[Escalar: z-score o min-max<br/>ajustado SOLO con train]
    B -->|Categórica|D{¿Cardinalidad?}
    D -->|Baja y sin orden|E[One-hot encoding]
    D -->|Con orden natural|F[Ordinal encoding manual]
    D -->|Alta|G[Agrupar + target encoding<br/>o embeddings]
    A -->|Texto|H{¿Necesitas semántica?}
    H -->|No: coincidencia léxica|I[TF-IDF / BM25]
    H -->|Sí: significado|J[Embeddings + similitud coseno<br/>o híbrido léxico+semántico]
    A -->|Imagen|K[Tensor alto x ancho x canales<br/>normalizado a 0-1]
    A -->|Audio|L[Onda 1D → espectrograma<br/>log-Mel 2D]

7. Privacidad y aspectos legales básicos

Trabajar con datos es trabajar, muy a menudo, con datos de personas. Y eso tiene reglas serias, especialmente en Europa.

  • Dato personal: cualquier información sobre una persona identificada o identificable: nombre, email, DNI, pero también IP, ID de dispositivo, geolocalización o combinaciones que permitan reidentificar ("varón, código postal 28004, nacido el 3/2/1987" identifica casi seguro a una sola persona).
  • GDPR / RGPD (Reglamento General de Protección de Datos, UE) y LOPDGDD (España): las ideas de fondo que un ingeniero de IA debe conocer:
  • Necesitas una base legal para tratar datos personales (consentimiento, contrato, interés legítimo...): "los teníamos en la base de datos" no es una base legal.
  • Limitación de finalidad: datos recogidos para facturar no pueden reutilizarse alegremente para entrenar un modelo de scoring sin cobertura legal.
  • Minimización: trata solo los datos necesarios. Si el modelo no necesita el DNI, el DNI no entra al pipeline.
  • Derechos de las personas: acceso, rectificación, supresión (¿puedes borrar a un cliente de un modelo ya entrenado? — problema abierto y muy real), oposición a decisiones automatizadas (art. 22).
  • Sanciones de hasta el 4% de la facturación global anual. No es teórico: hay multas multimillonarias cada año.
  • Anonimización vs. pseudonimización: pseudonimizar (sustituir el nombre por un hash o ID) NO es anonimizar: si existe una tabla que revierte el mapeo, o si el resto de columnas permite reidentificar, sigue siendo dato personal a efectos legales. La anonimización real (agregación, k-anonimato, ruido diferencial) es difícil y a menudo degrada la utilidad; el famoso caso Netflix Prize (2007) demostró que "ratings anónimos" de películas eran reidentificables cruzándolos con IMDb.
  • Advertencia — la regla de oro con APIs de IA: nunca envíes datos de clientes (ni de empleados) a una API externa de LLM/embeddings sin que exista un acuerdo de procesamiento de datos (DPA) firmado con el proveedor y sin el visto bueno de legal/privacidad de tu empresa. Pegar en un chatbot público un extracto de la base de datos de clientes "para que me ayude a limpiarlo" puede ser una brecha de datos con obligación legal de notificación. Los proveedores serios ofrecen DPAs, opciones de no-retención y residencia de datos europea; úsalas. Y aun con DPA: minimiza (¿de verdad necesita la API los nombres y emails, o puedes enmascararlos antes de enviar?).
  • AI Act europeo: además del RGPD, desde 2025-2026 aplica el Reglamento de IA de la UE, que exige gobernanza y calidad de datos documentada para sistemas de "alto riesgo" (crédito, empleo, educación, sanidad...). La calidad de datos deja de ser buena práctica para ser obligación legal.

Nota

este apartado es solo el mapa básico; el módulo 19-SEGURIDAD profundiza en privacidad, DPAs, anonimización técnica y seguridad de sistemas de IA.


8. Ejemplo integrador: limpiar un CSV de clientes de principio a fin

Cerremos el capítulo uniendo TODO: un CSV sucio de clientes → un dataset listo para modelar. Lo haremos en cuatro versiones incrementales, como se progresa en la vida real.

El fichero clientes.csv (así de feo llegan de verdad):

id,nombre,edad,ciudad,plan,gasto_mensual,fecha_alta,baja
1,Ana Ruiz,34,Madrid,premium,89.9,2023-01-15,0
2,Luis Gil,,Barcelona,basico,19.9,2023-02-01,0
3,MARTA VEGA,29,madrid,premium,89.9,2023-02-10,1
4,Pau Soler,41,Sevilla,pro,49.9,2023-03-05,0
2,Luis Gil,,Barcelona,basico,19.9,2023-02-01,0
5,Iris Mora,-999,Valencia,basico,,2023-04-20,1
6,Leo Duarte,38,Bilbao,pro,49.9,2023-05-12,0
7,Sara Pons,190,Madrid,premium,89.9,2023-06-01,0
8,Noa Prat,27,barcelona ,basico,19.9,2023-06-15,1

Problemas a la vista: fila duplicada (id 2), edad faltante, edad centinela (-999), edad imposible (190), ciudades con mayúsculas/espacios inconsistentes, gasto faltante. Objetivo: predecir baja (churn).

Versión 1 — Sencilla: lo mínimo viable

import pandas as pd

df = pd.read_csv("clientes.csv")                  # 1. Cargar el CSV en un DataFrame

df = df.drop_duplicates()                         # 2. Eliminar filas EXACTAMENTE duplicadas
                                                  #    (el id 2 repetido desaparece)

df["edad"] = df["edad"].replace(-999, pd.NA)      # 3. El -999 es un nulo disfrazado:
                                                  #    lo convertimos en nulo honesto

df = df.dropna()                                  # 4. Eliminar todas las filas con algún nulo
                                                  #    (simple pero perdemos a Luis e Iris)

print(df.shape)                                   # Vemos cuántas filas sobreviven

Funciona, pero: hemos tirado el 25% de los clientes, la edad 190 sigue dentro, y "Madrid"/"madrid" siguen siendo ciudades distintas. Suficiente para un prototipo de una tarde; inaceptable para producción.

Versión 2 — Intermedia: imputar, validar y normalizar texto

import pandas as pd
import numpy as np

df = pd.read_csv("clientes.csv")

# --- Duplicados: por clave de negocio, no solo exactos ---
df = df.drop_duplicates(subset=["id"], keep="first")   # Un cliente = una fila (por id)

# --- Nulos disfrazados y valores imposibles en edad ---
df["edad"] = df["edad"].replace(-999, np.nan)          # Centinela → NaN
df.loc[(df["edad"] < 16) | (df["edad"] > 100), "edad"] = np.nan
                                                       # Edades fuera de [16,100] no son creíbles
                                                       # para clientes: las tratamos como faltantes
                                                       # (la 190 de Sara cae aquí)

# --- Normalizar texto ANTES de tratar categorías ---
df["ciudad"] = (df["ciudad"]
                .str.strip()                           # Quitar espacios: "barcelona " → "barcelona"
                .str.lower()                           # Unificar caja: "MADRID"/"Madrid" → "madrid"
                .str.capitalize())                     # Estética final: "madrid" → "Madrid"

# --- Imputación en lugar de borrado ---
df["edad"] = df["edad"].fillna(df["edad"].median())    # Mediana: robusta a outliers residuales
df["gasto_mensual"] = df["gasto_mensual"].fillna(      # El gasto depende del plan: imputamos
    df.groupby("plan")["gasto_mensual"].transform("median")
)                                                      # con la mediana DE SU PLAN (más fino que
                                                       # la mediana global)

print(df.isna().sum())                                 # Verificación: cero nulos restantes

Mejoras: conservamos todas las filas, la edad imposible se trata, las ciudades están unificadas, y la imputación de gasto usa estructura del negocio (el plan). Falta: features aún no son numéricas y no hay división train/test.

Versión 3 — Avanzada: encoding, escalado y división SIN leakage

import pandas as pd
import numpy as np

# ========== 1. CARGA Y LIMPIEZA (como versión 2, resumida) ==========
df = pd.read_csv("clientes.csv")
df = df.drop_duplicates(subset=["id"], keep="first")
df["edad"] = df["edad"].replace(-999, np.nan)
df.loc[(df["edad"] < 16) | (df["edad"] > 100), "edad"] = np.nan
df["ciudad"] = df["ciudad"].str.strip().str.lower().str.capitalize()

# ========== 2. FEATURE ENGINEERING ==========
df["fecha_alta"] = pd.to_datetime(df["fecha_alta"])         # Texto → tipo fecha real
df["antiguedad_dias"] = (pd.Timestamp("2026-07-01")         # Feature derivada: días como cliente
                         - df["fecha_alta"]).dt.days        # a una fecha de corte FIJA (reproducible)
df["edad_faltaba"] = df["edad"].isna().astype(int)          # Indicador: ¿faltaba la edad?
                                                            # (conservamos la señal de ausencia)

# ========== 3. DIVISIÓN TRAIN/TEST — ¡ANTES de imputar y escalar! ==========
rng = np.random.default_rng(42)                             # Generador con semilla: reproducible
idx = rng.permutation(len(df))                              # Barajar índices
n_train = int(len(df) * 0.8)                                # 80% train / 20% test
train = df.iloc[idx[:n_train]].copy()                       # .copy() evita avisos de vista/copia
test = df.iloc[idx[n_train:]].copy()

# ========== 4. IMPUTACIÓN — estadísticas calculadas SOLO con train ==========
mediana_edad = train["edad"].median()                       # ← del train, no del dataset entero
mediana_gasto = train.groupby("plan")["gasto_mensual"].median()  # Medianas por plan, del train

for parte in (train, test):                                 # Aplicamos LAS MISMAS a ambos
    parte["edad"] = parte["edad"].fillna(mediana_edad)
    parte["gasto_mensual"] = parte["gasto_mensual"].fillna(
        parte["plan"].map(mediana_gasto)                    # map: cada plan → su mediana de train
    )

# ========== 5. ENCODING ==========
orden_plan = {"basico": 0, "pro": 1, "premium": 2}          # 'plan' tiene ORDEN natural (precio):
for parte in (train, test):                                 # ordinal manual, no alfabético
    parte["plan"] = parte["plan"].map(orden_plan)

train = pd.get_dummies(train, columns=["ciudad"], dtype=int)  # 'ciudad' NO tiene orden: one-hot
test = pd.get_dummies(test, columns=["ciudad"], dtype=int)
test = test.reindex(columns=train.columns, fill_value=0)    # CRÍTICO: alinear columnas.
                                                            # Si en test aparece una ciudad no vista
                                                            # en train (o falta una), reindex garantiza
                                                            # exactamente las mismas columnas, con 0s.

# ========== 6. ESCALADO — parámetros del train, aplicados a ambos ==========
num_cols = ["edad", "gasto_mensual", "antiguedad_dias"]     # Solo las numéricas continuas
mu = train[num_cols].mean()                                 # Media por columna (train)
sigma = train[num_cols].std()                               # Desviación por columna (train)
train[num_cols] = (train[num_cols] - mu) / sigma            # Z-score al train
test[num_cols] = (test[num_cols] - mu) / sigma              # MISMOS mu/sigma al test (no los suyos)

# ========== 7. SEPARAR FEATURES (X) Y OBJETIVO (y) ==========
descartar = ["id", "nombre", "fecha_alta", "baja"]          # id/nombre: identificadores, no señal
X_train = train.drop(columns=descartar)                     # (además, 'nombre' es dato personal:
y_train = train["baja"]                                     #  minimización — fuera del modelo)
X_test = test.drop(columns=descartar)
y_test = test["baja"]

print(X_train.shape, X_test.shape)                          # Listo para el capítulo 6 

Fíjate en los tres detalles que separan a un junior de un mid: (1) división ANTES de imputar/escalar; (2) reindex para alinear columnas de one-hot entre train y test; (3) semilla fija para reproducibilidad.

Versión 4 — Empresarial: funciones, validaciones y trazabilidad

En producción, el mismo proceso se organiza como funciones puras y testeables, con validación de entrada, logging y separación entre ajustar el preprocesado (fit, solo con train) y aplicarlo (transform, a cualquier dato futuro). Este patrón fit/transform es exactamente el que formaliza scikit-learn con sus Pipelines; aquí lo escribimos a mano para entender qué hay dentro:

import pandas as pd
import numpy as np
import logging

logging.basicConfig(level=logging.INFO)                     # Logging en vez de prints sueltos:
log = logging.getLogger("pipeline_clientes")                # en producción, esto va a un sistema
                                                            # centralizado y queda auditado

ESQUEMA = {                                                 # Contrato de datos explícito:
    "id": "int64", "nombre": "object", "edad": "float64",   # qué columnas y tipos esperamos.
    "ciudad": "object", "plan": "object",                   # Si mañana el CSV llega distinto,
    "gasto_mensual": "float64", "fecha_alta": "object",     # fallamos PRONTO y con mensaje claro,
    "baja": "int64",                                        # no con un modelo silenciosamente roto
}
PLANES_VALIDOS = {"basico", "pro", "premium"}

def validar_entrada(df: pd.DataFrame) -> pd.DataFrame:
    """Falla ruidosamente si el fichero no cumple el contrato."""
    faltan = set(ESQUEMA) - set(df.columns)                 # ¿Falta alguna columna esperada?
    if faltan:
        raise ValueError(f"Columnas ausentes: {faltan}")    # Error explícito, no un KeyError críptico
    planes_raros = set(df["plan"].dropna().str.lower()) - PLANES_VALIDOS
    if planes_raros:                                        # ¿Apareció un plan desconocido?
        raise ValueError(f"Planes no reconocidos: {planes_raros}")
    if df["id"].duplicated().any():                         # Duplicados: los registramos ANTES
        log.warning("IDs duplicados: %d filas se deduplicarán",
                    df["id"].duplicated().sum())            # de tocarlos (trazabilidad)
    return df

def limpiar(df: pd.DataFrame) -> pd.DataFrame:
    """Limpieza sin estado: mismas entradas → mismas salidas (idempotente)."""
    df = df.drop_duplicates(subset=["id"], keep="first").copy()
    df["edad"] = df["edad"].replace(-999, np.nan)           # Centinelas conocidos → NaN
    fuera_rango = (df["edad"] < 16) | (df["edad"] > 100)
    log.info("Edades fuera de rango anuladas: %d", fuera_rango.sum())
    df.loc[fuera_rango, "edad"] = np.nan
    df["ciudad"] = df["ciudad"].str.strip().str.lower().str.capitalize()
    df["fecha_alta"] = pd.to_datetime(df["fecha_alta"], errors="raise")
    return df

def ajustar_preprocesador(train: pd.DataFrame) -> dict:
    """FIT: aprende parámetros SOLO del train y los devuelve como artefacto.
    Este diccionario se serializa (json/pickle) junto al modelo:
    en producción se reutiliza tal cual — jamás se recalcula con datos nuevos."""
    return {
        "mediana_edad": train["edad"].median(),
        "mediana_gasto_por_plan": train.groupby("plan")["gasto_mensual"].median().to_dict(),
        "mu": train[["edad", "gasto_mensual", "antiguedad_dias"]].mean().to_dict(),
        "sigma": train[["edad", "gasto_mensual", "antiguedad_dias"]].std().to_dict(),
        "columnas_finales": None,                           # Se fija tras el primer transform
    }

def transformar(df: pd.DataFrame, prep: dict, fecha_corte: str = "2026-07-01") -> pd.DataFrame:
    """TRANSFORM: aplica los parámetros aprendidos a CUALQUIER dato (train, test o producción)."""
    df = df.copy()
    df["antiguedad_dias"] = (pd.Timestamp(fecha_corte) - df["fecha_alta"]).dt.days
    df["edad_faltaba"] = df["edad"].isna().astype(int)      # Señal de ausencia como feature
    df["edad"] = df["edad"].fillna(prep["mediana_edad"])    # Imputación con parámetros del train
    df["gasto_mensual"] = df["gasto_mensual"].fillna(
        df["plan"].map(prep["mediana_gasto_por_plan"]))
    df["plan"] = df["plan"].map({"basico": 0, "pro": 1, "premium": 2})  # Ordinal explícito
    df = pd.get_dummies(df, columns=["ciudad"], dtype=int)  # One-hot de ciudad
    for col in ["edad", "gasto_mensual", "antiguedad_dias"]:
        df[col] = (df[col] - prep["mu"][col]) / prep["sigma"][col]      # Z-score con mu/sigma de train
    df = df.drop(columns=["id", "nombre", "fecha_alta"])    # Identificadores y PII fuera
    if prep["columnas_finales"] is None:                    # Primera llamada (train): fijamos
        prep["columnas_finales"] = df.columns.tolist()      # el contrato de columnas de salida
    return df.reindex(columns=prep["columnas_finales"], fill_value=0)   # Alineación garantizada

# ---------- Orquestación ----------
df = validar_entrada(pd.read_csv("clientes.csv"))           # 1. Validar el contrato de entrada
df = limpiar(df)                                            # 2. Limpiar (sin estado)

rng = np.random.default_rng(42)                             # 3. Dividir con semilla fija
idx = rng.permutation(len(df))
n_train = int(len(df) * 0.8)
train, test = df.iloc[idx[:n_train]].copy(), df.iloc[idx[n_train:]].copy()

prep = ajustar_preprocesador(                               # 4. FIT solo con train
    transformar_previo := train.assign(                     #    (antigüedad se necesita para mu/sigma,
        antiguedad_dias=(pd.Timestamp("2026-07-01") - train["fecha_alta"]).dt.days)
)
X_train = transformar(train, prep)                          # 5. TRANSFORM al train...
X_test = transformar(test, prep)                            # 6. ...y al test con el MISMO artefacto
y_train, y_test = train["baja"], test["baja"]

log.info("Pipeline OK: train %s, test %s", X_train.shape, X_test.shape)

Qué añade la versión empresarial respecto a la avanzada:

Aspecto V3 avanzada V4 empresarial
Estructura Script lineal Funciones puras, testeables unitariamente
Errores de datos Explotan donde pillen Validación de contrato al inicio, mensajes claros
Trazabilidad prints sueltos Logging estructurado y contadores de cada corrección
Preprocesado Variables sueltas (mu, sigma...) Artefacto prep serializable que viaja CON el modelo
Producción Habría que copiar/pegar código transformar(datos_nuevos, prep) y listo
Datos personales Se descartan al final Política explícita: PII fuera del feature set

Consejo profesional

en un proyecto real usarías sklearn.pipeline.Pipeline + ColumnTransformer, que empaquetan fit/transform, evitan leakage por construcción y se serializan junto al modelo. Lo veremos al usar scikit-learn en el módulo 02. Pero haber escrito el patrón a mano una vez —como aquí— es lo que te permitirá entender qué hace sklearn por dentro y depurarlo cuando falle.

Ejercicio rápido 5

En la versión 3, ¿por qué test = test.reindex(columns=train.columns, fill_value=0) es imprescindible? Describe el bug exacto que ocurre sin esa línea.

Ver solución `get_dummies` crea una columna por cada categoría **presente en ese DataFrame concreto**. Si en train hay clientes de Madrid, Barcelona, Sevilla y Bilbao pero en test solo de Madrid y Valencia, entonces: train tendrá columnas `ciudad_Madrid, ciudad_Barcelona, ciudad_Sevilla, ciudad_Bilbao` y test tendrá `ciudad_Madrid, ciudad_Valencia`. Los dos DataFrames tienen columnas distintas en número y orden: el modelo entrenado con las columnas de train recibirá en test una matriz incompatible (error inmediato en el mejor caso; en el peor —si los tamaños coinciden por casualidad— predicciones sin sentido porque cada columna significa otra cosa). `reindex(columns=train.columns, fill_value=0)` fuerza al test a tener exactamente las columnas del train, en el mismo orden, rellenando con 0 las que no existan y descartando categorías nunca vistas. En producción esto mismo se garantiza guardando la lista de columnas como parte del artefacto de preprocesado.

9. Buenas prácticas

  1. Mira los datos antes de modelar. Siempre. head(), describe(), isna().sum(), histogramas, value_counts(). Treinta minutos de exploración evitan semanas de depuración.
  2. Divide train/test ANTES de cualquier ajuste (imputación, escalado, remuestreo, selección de features). Grábatelo a fuego.
  3. Ajusta el preprocesado solo con train y guárdalo como artefacto que viaja con el modelo (patrón fit/transform).
  4. Fija semillas aleatorias y versiona datos y código: reproducibilidad o caos.
  5. Busca nulos disfrazados (-999, 0 imposibles, "N/A", cadenas vacías) además de los NaN honestos.
  6. Añade indicadores de ausencia (columna_faltaba) cuando imputes: la ausencia suele ser señal.
  7. Investiga los outliers antes de tocarlos: primero entender, luego decidir, siempre documentar.
  8. Con clases desbalanceadas, destierra la accuracy: matriz de confusión y métricas por clase desde el minuto uno.
  9. Pregunta por cada feature: "¿existirá este dato en el momento de predecir?" — el antídoto del leakage.
  10. Desglosa las métricas por subgrupos (edad, género, región) para detectar sesgos antes de que lo haga un periódico.
  11. Trata los documentos de RAG y los casos de evals como datasets de primera clase: versionados, limpios, auditados.
  12. Minimiza datos personales: lo que no entra al pipeline no puede filtrarse; y ningún dato de cliente sale a APIs externas sin DPA.

10. Malas prácticas

  1. Limpiar "a mano" en Excel sin registro de qué se cambió: irreproducible e inauditable.
  2. Normalizar/imputar sobre el dataset completo y dividir después: leakage de manual.
  3. dropna() indiscriminado sin preguntarse por qué faltan los datos: puede sesgar el dataset silenciosamente.
  4. Label encoding alfabético para categorías sin orden en modelos lineales o de distancia.
  5. One-hot sobre columnas de miles de categorías sin agrupar: matrices monstruosas e inútiles.
  6. Borrar outliers para "mejorar métricas" en dominios donde el outlier es el fenómeno (fraude, fallos).
  7. Celebrar un 99,8% de accuracy al primer intento en vez de sospechar leakage o desbalanceo.
  8. Eliminar la columna "género" y declarar el modelo libre de sesgo: los proxies siguen ahí.
  9. Iterar el prompt/modelo mirando siempre el mismo test: sobreajuste humano al conjunto de evaluación.
  10. Subir extractos de la base de datos de clientes a un chatbot público "solo para probar una cosa".

11. Errores comunes

# Error Síntoma Causa raíz Solución
1 Escalar antes de dividir Test demasiado bueno; producción decepciona Estadísticas del test filtradas al preprocesado Dividir primero; fit solo con train
2 Columnas de one-hot desalineadas train/test Error de dimensiones o predicciones absurdas get_dummies genera columnas según categorías presentes reindex(columns=train.columns, fill_value=0) o encoder con vocabulario fijo
3 Nulos centinela sin detectar (-999, 0) Medias imposibles, modelos raros Convención heredada del sistema origen Auditar rangos y value_counts antes de imputar
4 Accuracy alta con clases 99/1 "Funciona genial" pero no detecta nada La métrica global ignora la clase minoritaria Matriz de confusión, recall/precision por clase
5 Mismo cliente en train y test Métricas infladas División aleatoria por fila con datos agrupados Dividir por grupo (cliente/paciente)
6 Serie temporal barajada aleatoriamente Test excelente, producción desastrosa Entrenaste con el futuro División temporal: pasado→train, futuro→test
7 Feature que solo existe tras el evento Modelo "perfecto" en histórico, inútil en real Leakage del futuro (ej. fecha_de_baja para predecir baja) Pregunta-talismán: ¿disponible al predecir?
8 Comparar embeddings de modelos distintos Similitudes sin sentido Cada modelo define su propio espacio vectorial Un único modelo de embeddings por índice/aplicación
9 RGB/BGR o channels first/last mezclados Colores alienígenas, red que no aprende Convenciones distintas entre OpenCV/NumPy/PyTorch Verificar shape y orden de canales al cargar
10 Imputar con la media habiendo outliers Valores imputados irreales La media es sensible a extremos Usar la mediana (robusta)

12. FAQ — Preguntas frecuentes

1. ¿Cuántos datos necesito para entrenar un modelo? Depende del problema y del modelo: reglas orientativas dicen ~10 ejemplos por feature para modelos lineales, miles-decenas de miles para gradient boosting serio, y millones para deep learning desde cero. Pero hoy la pregunta cambia: con transfer learning y LLMs preentrenados, a menudo bastan cientos de ejemplos de calidad para fine-tuning, o cero (few-shot en el prompt). La calidad manda sobre la cantidad: 1.000 ejemplos limpios y representativos > 100.000 sucios.

2. ¿Normalización o estandarización? ¿Cuál uso por defecto? Por defecto, estandarización (z-score): funciona bien en la mayoría de casos y aguanta mejor los outliers que min-max. Min-max cuando necesitas rango acotado [0,1] (imágenes, ciertas redes). Con outliers fuertes, RobustScaler (mediana/IQR). Y con árboles (Random Forest, XGBoost): ninguna, no la necesitan.

3. ¿One-hot o label encoding? Resumen ejecutivo. ¿La categoría tiene orden natural (S<M<L)? → ordinal con mapeo manual. ¿Sin orden y pocas categorías (<50)? → one-hot. ¿Sin orden y muchísimas? → agrupar raras + one-hot, target encoding con cuidado, o embeddings. ¿Modelo de árboles? → casi cualquier cosa le vale, incluso label encoding arbitrario.

4. ¿Qué diferencia hay entre un embedding y un vector TF-IDF, si ambos son vectores? TF-IDF es disperso (miles de dimensiones, casi todas cero), cada dimensión ES una palabra concreta, y no captura significado: sinónimos = similitud cero. Un embedding es denso (cientos de dimensiones, todas con valor), las dimensiones no son interpretables individualmente, y captura semántica: sinónimos = vectores cercanos. TF-IDF se calcula contando; los embeddings se aprenden con redes neuronales sobre corpus enormes.

5. ¿Por qué mi LLM no sabe contar las letras de una palabra? Porque no ve letras: ve tokens (trozos de palabra convertidos en IDs y luego en vectores). "fresa" puede ser un único token opaco; contar sus letras exige información que no está en su representación de entrada. Es el ejemplo perfecto de que un modelo solo conoce el mundo a través de su representación. Los modelos recientes lo sortean razonando o usando herramientas (ej. ejecutar código que cuente).

6. ¿El data leakage solo pasa al normalizar? No; normalizar antes de dividir es solo el caso más famoso. También hay leakage por imputar/seleccionar features/hacer SMOTE con todo el dataset, por duplicados repartidos entre train y test, por features que solo existen después del evento a predecir, y por dividir series temporales al azar. Regla única: nada que se "aprenda" de los datos puede aprenderse fuera del train, y ninguna información posterior al momento de predicción puede entrar como feature.

7. ¿Puedo usar datos de clientes reales para practicar o probar una API de IA? Como norma: no, salvo que tu empresa lo haya autorizado y exista un acuerdo de procesamiento (DPA) con el proveedor. Para practicar usa datasets públicos (Kaggle, Hugging Face, datos.gob.es) o datos sintéticos generados con Faker/LLMs. Enviar PII real a servicios externos sin cobertura legal puede constituir una brecha del RGPD con sanciones serias.

8. ¿Qué es exactamente un "feature store" y un "vector database"? El feature store es un almacén centralizado de features ya calculadas y versionadas, para que todos los modelos de la empresa reutilicen las mismas definiciones ("gasto_medio_90d") sin recalcularlas inconsistentemente. La vector database (Pinecone, Weaviate, pgvector, Qdrant...) almacena embeddings e implementa búsqueda eficiente por similitud (vecinos más cercanos aproximados) sobre millones de vectores: es la pieza central de la búsqueda semántica y del RAG.

9. ¿Los embeddings sirven para cosas que no son texto? Sí, y esa es su superpotencia: cualquier objeto puede embeberse. Imágenes (CLIP embebe imagen y texto EN EL MISMO espacio: puedes buscar fotos escribiendo frases), audio, productos, usuarios, moléculas, grafos. La receta general: entrena una red en una tarea que obligue a entender el objeto, y usa su representación interna como embedding.

10. Mi dataset tiene un 40% de nulos en una columna importante. ¿La elimino? Con 40% aún hay señal aprovechable. Antes de eliminar: (1) investiga POR QUÉ falta (¿es MNAR? entonces la ausencia es información); (2) prueba imputación + columna indicadora faltaba_X; (3) si usas gradient boosting moderno (XGBoost, LightGBM), estos manejan nulos de forma nativa y a menudo es la mejor opción: no imputes, deja que el árbol decida hacia dónde mandar los nulos. Elimina solo si demuestra no aportar nada.

13. Resumen del capítulo

  • Los datos son la materia prima: GIGO gobierna todo; los fracasos de Amazon (sesgo), IBM Watson (datos no representativos), Zillow (drift) y Google Flu (proxy equivocado) son fracasos de datos, no de algoritmos. La era Data-Centric AI dice: itera sobre los datos, no solo sobre el modelo.
  • Tres grados de estructura: estructurados (tablas → ML clásico), semiestructurados (JSON/logs → hay que aplanar), no estructurados (texto/imagen/audio/vídeo → deep learning y LLMs; son el 80% del dato empresarial).
  • Todo acaba en tensores. Tablas: categóricas → one-hot (ojo a la cardinalidad) u ordinal (si hay orden); numéricas → estandarizar o normalizar (ajustando SOLO con train). Texto: BoW → TF-IDF (cuenta y pesa, pero no entiende) → embeddings (el mapa de significados: vectores densos donde cercanía = similitud semántica, con aritmética tipo rey−hombre+mujer≈reina y similitud coseno como regla de medir); los LLMs ven tokens, no letras. Imágenes: tensores (alto, ancho, canales) de píxeles 0-255. Audio: vector 1D muestreado → espectrograma (que es una imagen).
  • Calidad: nulos (eliminar/imputar/indicador — según por qué faltan), outliers (¿error o señal valiosa?), duplicados (riesgo de leakage), desbalanceo (accuracy 99% puede ser un fracaso total: mira la matriz de confusión) y sesgos (los proxies discriminan aunque borres la columna sensible).
  • Train/validation/test: tres conjuntos porque elegir hiperparámetros mirando el test lo quema; leakage = información del futuro/test filtrada al entrenamiento, y es el bug más caro del ML. La pregunta-talismán: "¿tendré este dato en el momento de predecir?".
  • Pipeline profesional: recolección → limpieza → transformación → almacenamiento (warehouse + vector DB) → entrenamiento/RAG/evals, todo versionado, validado y reproducible. En la era LLM, los documentos de RAG y los casos de evals son datasets de primera clase.
  • Legal: RGPD manda; pseudonimizar no es anonimizar; y nunca salen datos de clientes hacia APIs externas sin DPA.
  • El patrón de código que te llevas: validar → limpiar → dividir → fit (solo train) → transform (train, test y producción con el mismo artefacto).

En el próximo capítulo por fin entrenaremos: veremos cómo aprende una máquina a partir de estos datos ya representados — funciones de pérdida, descenso de gradiente y el ciclo de entrenamiento completo.

14. Bibliografía y recursos

Libros:

  • Aurélien Géron, Hands-On Machine Learning with Scikit-Learn, Keras & TensorFlow (3ª ed., O'Reilly, 2022) — capítulo 2 es el mejor tutorial de pipeline de datos end-to-end que existe. https://www.oreilly.com/library/view/hands-on-machine-learning/9781098125967/
  • Wes McKinney, Python for Data Analysis (3ª ed., O'Reilly, 2022) — el manual de Pandas por su creador; gratis online: https://wesmckinney.com/book/
  • Chip Huyen, Designing Machine Learning Systems (O'Reilly, 2022) — datos en producción: pipelines, drift, feature stores. https://www.oreilly.com/library/view/designing-machine-learning/9781098107956/
  • Cathy O'Neil, Weapons of Math Destruction (2016) — sesgos y daños de modelos alimentados con malos datos, para lectores no técnicos. https://en.wikipedia.org/wiki/Weapons_of_Math_Destruction

Artículos y papers:

  • Mikolov et al., Efficient Estimation of Word Representations in Vector Space (Word2Vec, 2013): https://arxiv.org/abs/1301.3781
  • Gebru et al., Datasheets for Datasets (2018) — cómo documentar un dataset: https://arxiv.org/abs/1803.09010
  • ProPublica, Machine Bias (2016) — la investigación sobre COMPAS: https://www.propublica.org/article/machine-bias-risk-assessments-in-criminal-sentencing
  • Reuters, Amazon scraps secret AI recruiting tool that showed bias against women (2018): https://www.reuters.com/article/us-amazon-com-jobs-automation-insight-idUSKCN1MK08G
  • Andrew Ng, Data-Centric AI — recursos del movimiento: https://datacentricai.org/

Herramientas y datasets:

  • Documentación de Pandas: https://pandas.pydata.org/docs/ · NumPy: https://numpy.org/doc/
  • Preprocesado en scikit-learn (encoding, escalado, pipelines): https://scikit-learn.org/stable/modules/preprocessing.html
  • Hugging Face Datasets: https://huggingface.co/datasets · Kaggle: https://www.kaggle.com/datasets
  • Datos abiertos de España: https://datos.gob.es · UCI ML Repository: https://archive.ics.uci.edu
  • Great Expectations (validación de datos): https://greatexpectations.io/ · DVC (versionado de datos): https://dvc.org/

Legal:

  • Texto del RGPD (español): https://eur-lex.europa.eu/legal-content/ES/TXT/?uri=CELEX%3A32016R0679
  • Agencia Española de Protección de Datos (guías sobre IA y datos): https://www.aepd.es
  • Reglamento Europeo de IA (AI Act): https://eur-lex.europa.eu/legal-content/ES/TXT/?uri=CELEX%3A32024R1689

Anterior Módulo Siguiente
Capítulo 4: Python para IA 01-FUNDAMENTOS Capítulo 6: ¿Cómo aprende una máquina?