Saltar a contenido

Capítulo 4: Aprendizaje no supervisado — encontrar estructura sin etiquetas

Módulo 02-MACHINE-LEARNING · AI Master Academy Nivel: Intermedio · Requisitos: Capítulos 2 (Regresión) y 3 (Clasificación), Pandas, NumPy, scikit-learn Tiempo estimado de estudio: 4–6 horas (con ejercicios)


Hasta ahora has entrenado modelos con una ventaja enorme: siempre tenías la respuesta correcta. En regresión conocías el precio real de cada casa; en clasificación sabías qué correos eran spam. El modelo aprendía comparando sus predicciones con esas etiquetas.

En este capítulo te quitamos la red de seguridad. No hay columna y. No hay etiquetas. Solo datos crudos y una pregunta incómoda: ¿qué estructura se esconde aquí dentro?

Bienvenido al aprendizaje no supervisado: el arte de encontrar grupos, patrones, direcciones de variación y rarezas en datos que nadie ha etiquetado. Es, probablemente, la disciplina más "detectivesca" del machine learning clásico, y —como veremos al final— la que mejor ha envejecido en la era de los LLMs y los embeddings.


Índice

  1. ¿Qué es el aprendizaje no supervisado?
  2. K-Means a fondo
  3. Clustering jerárquico
  4. DBSCAN: clustering por densidad
  5. Reducción de dimensionalidad: PCA
  6. Detección de anomalías
  7. Conexión con la era LLM: clustering de embeddings
  8. Ejemplo integrador: segmentación RFM de clientes
  9. Caso empresarial: banco detectando transacciones anómalas
  10. Buenas prácticas
  11. Malas prácticas
  12. Errores comunes
  13. FAQ — Preguntas frecuentes
  14. Resumen
  15. Bibliografía y recursos

1. ¿Qué es el aprendizaje no supervisado?

1.1 La definición y el cambio de mentalidad

En el aprendizaje supervisado tienes pares (X, y): features y etiqueta. El objetivo es aprender la función que mapea X → y.

En el aprendizaje no supervisado solo tienes X. El objetivo cambia radicalmente: ya no predices nada, sino que descubres la estructura interna de los datos:

  • ¿Hay grupos naturales? → Clustering
  • ¿Hay direcciones de variación dominantes? ¿Puedo comprimir sin perder mucho? → Reducción de dimensionalidad
  • ¿Hay puntos que no encajan con el resto? → Detección de anomalías
flowchart TD
    A[Datos sin etiquetas X] --> B{¿Qué pregunta hago?}
    B -->|"¿Hay grupos?"|C[Clustering]
    B -->|"¿Puedo comprimir/visualizar?"|D[Reducción de dimensionalidad]
    B -->|"¿Qué no encaja?"|E[Detección de anomalías]
    C --> C1[K-Means]
    C --> C2[Jerárquico]
    C --> C3[DBSCAN]
    D --> D1[PCA]
    D --> D2["t-SNE / UMAP<br/>(solo visualización)"]
    E --> E1[Isolation Forest]
    E --> E2[Local Outlier Factor]
    E --> E3[One-Class SVM]

1.2 Por qué es más difícil de evaluar

Este es el punto filosófico central del capítulo y conviene grabarlo a fuego:

En no supervisado no existe la "respuesta correcta". No hay y_test contra el que calcular accuracy. Dos segmentaciones distintas de los mismos clientes pueden ser ambas "válidas".

Consecuencias prácticas:

  1. La evaluación es indirecta. Usamos métricas internas (cohesión/separación de clusters, como la silueta) que miden geometría, no verdad.
  2. La validación final es humana y de negocio. Un clustering de clientes es "bueno" si el equipo de marketing puede actuar sobre los segmentos, no si maximiza un número.
  3. Los hiperparámetros importan más. Elegir k en K-Means o eps en DBSCAN cambia por completo el resultado, y no hay grid search con y_test que te salve.
  4. Es fácil autoengañarse. Todo algoritmo de clustering siempre devuelve clusters, incluso sobre ruido puro. Que el algoritmo devuelva 5 grupos no significa que existan 5 grupos.

Advertencia

K-Means aplicado a datos uniformemente aleatorios te devolverá k clusters con total tranquilidad. La existencia de output no demuestra la existencia de estructura. Siempre valida con métricas internas Y con inspección de negocio.

1.3 Problemas de negocio que resuelve

Problema de negocio Técnica típica Qué se descubre Valor
Segmentación de clientes Clustering (K-Means, jerárquico) Grupos de clientes con comportamiento similar Marketing dirigido, pricing, retención
Detección de fraude / anomalías Isolation Forest, LOF, DBSCAN Transacciones o eventos que "no encajan" Prevención de pérdidas, seguridad
Sistemas de recomendación Clustering + factorización de matrices Usuarios/productos similares Cross-selling, engagement
Organización de documentos Clustering de embeddings Temas latentes en textos Búsqueda, triaje de tickets, knowledge management
Compresión de datos PCA Representación compacta con mínima pérdida Menos almacenamiento, modelos más rápidos
Mantenimiento predictivo Detección de anomalías en sensores Comportamiento anómalo de máquinas Evitar paradas de producción
Exploración inicial (EDA) PCA + clustering Estructura general del dataset Hipótesis para modelos supervisados

Consejo profesional

en la industria, el no supervisado casi nunca es el producto final. Es el paso de descubrimiento que alimenta decisiones (segmentos → campañas) o modelos supervisados (anomalías detectadas → etiquetas para un clasificador futuro). Piensa en él como el microscopio, no como la medicina.

Ejercicio rápido 1

Tu empresa de streaming quiere: (a) predecir si un usuario cancelará el mes que viene, (b) descubrir tipos de espectadores para diseñar perfiles de contenido, (c) detectar cuentas compartidas ilegalmente sin ejemplos previos. Clasifica cada tarea como supervisada o no supervisada y justifica.

Ver solución - **(a) Supervisada (clasificación binaria).** Tienes histórico: sabes qué usuarios cancelaron (etiqueta `churn = 1/0`). - **(b) No supervisada (clustering).** Nadie ha definido a priori los "tipos de espectadores"; quieres descubrirlos a partir del comportamiento (horas de visionado, géneros, horarios). - **(c) No supervisada (detección de anomalías).** No hay etiquetas de "cuenta compartida" confirmadas. Buscas cuentas cuyo patrón (ubicaciones simultáneas, dispositivos, horarios imposibles) se desvía de lo normal. Si más adelante confirmas casos y acumulas etiquetas, podrías convertirlo en un problema supervisado.

2. K-Means a fondo

2.1 Qué es y para qué sirve

K-Means es el algoritmo de clustering más usado del mundo. Dado un número k (que TÚ eliges), particiona los datos en k grupos, de forma que cada punto pertenece al grupo cuyo centroide (punto medio) está más cerca.

  • Qué optimiza: la inercia (WCSS, within-cluster sum of squares): la suma de distancias al cuadrado de cada punto a su centroide. Menor inercia = clusters más compactos.
  • Por qué existe: es simple, rápido (escala a millones de puntos), y para muchos problemas de negocio (segmentación) sus supuestos son "suficientemente verdad".
  • Para qué sirve: segmentación de clientes, cuantización de colores en imágenes, agrupación de embeddings, inicialización de modelos más complejos.

2.2 El algoritmo paso a paso

K-Means es un bucle de dos pasos que se repite hasta converger:

flowchart LR
    A["1. Inicialización:<br/>elegir k centroides"] --> B["2. Asignación:<br/>cada punto va al<br/>centroide más cercano"]
    B --> C["3. Actualización:<br/>cada centroide se mueve<br/>a la media de sus puntos"]
    C --> D{"¿Los centroides<br/>se movieron?"}
    D -->|Sí|B
    D -->|"No (convergencia)"|E["Fin: clusters finales"]
  1. Inicialización: coloca k centroides (al azar, o mejor, con k-means++, ver §2.4).
  2. Asignación: asigna cada punto al centroide más cercano (distancia euclídea).
  3. Actualización: recalcula cada centroide como la media de los puntos asignados a él. (De ahí el nombre: K medias.)
  4. Convergencia: repite 2–3 hasta que las asignaciones no cambien (o los centroides se muevan menos que una tolerancia).

Nota

cada iteración nunca aumenta la inercia, por eso K-Means siempre converge. Pero converge a un óptimo local: distintas inicializaciones pueden dar resultados distintos. Por eso sklearn ejecuta el algoritmo varias veces (n_init) y se queda con la mejor.

2.3 Ejemplo numérico 2D a mano

Trabajemos con 6 puntos y k = 2. Cada punto es (x, y):

A=(1,1)  B=(1,2)  C=(2,1)      ← parecen un grupo abajo-izquierda
D=(8,8)  E=(9,8)  F=(8,9)      ← parecen un grupo arriba-derecha

Visualmente:

 y
 9 |                          F
 8 |                       D  E
 7 |
 6 |
 5 |
 4 |
 3 |
 2 |  B
 1 |  A  C
 0 +--------------------------------
    0  1  2  3  4  5  6  7  8  9   x

Iteración 0 — Inicialización (deliberadamente mala, para ver cómo se corrige): centroides iniciales μ1 = A = (1,1) y μ2 = B = (1,2).

Iteración 1 — Asignación. Distancia euclídea de cada punto a cada centroide:

Punto dist a μ1=(1,1) dist a μ2=(1,2) Asignado a
A=(1,1) 0.00 1.00 μ1
B=(1,2) 1.00 0.00 μ2
C=(2,1) 1.00 1.41 μ1
D=(8,8) 9.90 9.22 μ2
E=(9,8) 10.63 10.00 μ2
F=(8,9) 10.63 9.90 μ2

Iteración 1 — Actualización. - μ1 = media(A, C) = ((1+2)/2, (1+1)/2) = (1.5, 1.0) - μ2 = media(B, D, E, F) = ((1+8+9+8)/4, (2+8+8+9)/4) = (6.5, 6.75)

Fíjate: μ2 ya "voló" hacia el grupo de arriba, arrastrado por D, E y F.

Iteración 2 — Asignación:

Punto dist a μ1=(1.5,1.0) dist a μ2=(6.5,6.75) Asignado a
A=(1,1) 0.50 7.94 μ1
B=(1,2) 1.12 7.28 μ1
C=(2,1) 0.50 7.29 μ1
D=(8,8) 9.55 1.95 μ2
E=(9,8) 10.26 2.80 μ2
F=(8,9) 10.31 2.70 μ2

B "cambió de bando": ahora está con A y C, donde intuitivamente debía estar.

Iteración 2 — Actualización: - μ1 = media(A, B, C) = (1.33, 1.33) - μ2 = media(D, E, F) = (8.33, 8.33)

Iteración 3 — Asignación: nadie cambia de cluster → los centroides no se mueven → convergencia.

Resultado final:

 y
 9 |                          F ●
 8 |                       D ●  E ●     ● = cluster 2  (μ2  en 8.33, 8.33)
 7 |                         
 6 |
 5 |
 4 |
 3 |
 2 |  B ○
 1 |  A ○ C ○                          ○ = cluster 1  (μ1  en 1.33, 1.33)
 0 +--------------------------------
    0  1  2  3  4  5  6  7  8  9   x

Tres iteraciones y listo. En datasets reales suelen bastar decenas de iteraciones.

2.4 k-means++: por qué la inicialización importa

Con inicialización puramente aleatoria pueden caer dos centroides dentro del mismo grupo natural, y el algoritmo converge a una partición mala (óptimo local). k-means++ (Arthur y Vassilvitskii, 2007) soluciona esto:

  1. Elige el primer centroide al azar entre los puntos.
  2. Para cada punto restante, calcula D(x)² = distancia al cuadrado al centroide ya elegido más cercano.
  3. Elige el siguiente centroide con probabilidad proporcional a D(x)² → los puntos lejanos de los centroides existentes tienen más probabilidad de ser elegidos.
  4. Repite hasta tener k centroides.

Resultado: centroides iniciales dispersos por todo el espacio, convergencia más rápida y mejores óptimos. Es el default de sklearn (init='k-means++') — no lo cambies sin motivo.

2.5 Elegir k: método del codo

K-Means no elige k por ti. La primera herramienta es el método del codo: entrena para varios k, grafica la inercia, y busca el punto donde añadir clusters deja de reducirla significativamente (el "codo").

# ============================================
# MÉTODO DEL CODO para elegir k
# ============================================
import numpy as np                                  # cálculo numérico
import matplotlib.pyplot as plt                     # gráficos
from sklearn.datasets import make_blobs             # generador de datos sintéticos con grupos
from sklearn.preprocessing import StandardScaler    # escalado (¡obligatorio en K-Means!)
from sklearn.cluster import KMeans                  # el algoritmo K-Means

# 1) Datos sintéticos: 500 puntos, 4 grupos reales, 2 features
X, _ = make_blobs(n_samples=500,      # número de puntos
                  centers=4,          # número REAL de grupos (en la vida real no lo sabrías)
                  cluster_std=1.0,    # dispersión de cada grupo
                  random_state=42)    # semilla para reproducibilidad

# 2) Escalar SIEMPRE antes de K-Means: usa distancias euclídeas,
#    y una feature con rango grande dominaría el cálculo
scaler = StandardScaler()             # media 0, desviación 1 por columna
X_scaled = scaler.fit_transform(X)    # ajusta con los datos y transforma

# 3) Entrenar K-Means para k = 1..10 y guardar la inercia de cada uno
inercias = []                                       # lista para acumular resultados
rango_k = range(1, 11)                              # probamos de 1 a 10 clusters
for k in rango_k:
    km = KMeans(n_clusters=k,        # número de clusters de esta prueba
                init='k-means++',    # inicialización inteligente (default)
                n_init=10,           # 10 reinicios aleatorios; se queda el mejor
                random_state=42)     # reproducibilidad
    km.fit(X_scaled)                 # ejecuta asignación/actualización hasta converger
    inercias.append(km.inertia_)     # inercia = suma de distancias² a centroides

# 4) Graficar inercia vs k y buscar el "codo"
plt.figure(figsize=(8, 4))
plt.plot(rango_k, inercias, 'o-')                   # línea con marcadores
plt.xlabel('Número de clusters (k)')
plt.ylabel('Inercia (WCSS)')
plt.title('Método del codo')
plt.axvline(x=4, color='red', linestyle='--',       # marcamos el codo esperado
            label='Codo en k=4')
plt.legend()
plt.tight_layout()
plt.show()

Interpretación: la inercia siempre baja al aumentar k (más centroides = puntos más cerca de alguno). Lo que buscas es el punto donde la curva pasa de caer en picado a caer suavemente. En este ejemplo, de k=1 a k=4 la inercia se desploma; a partir de k=4 la mejora es marginal → codo en k=4.

Advertencia

el codo es a menudo ambiguo en datos reales (curvas suaves sin codo claro). Nunca lo uses como criterio único; combínalo con la silueta y, sobre todo, con la interpretabilidad de negocio.

2.6 Elegir k: índice de silueta

La silueta de un punto mide, de −1 a +1, si está bien colocado en su cluster:

s(i) = (b(i) − a(i)) / max(a(i), b(i))

a(i) = distancia media de i a los puntos de SU cluster (cohesión)
b(i) = distancia media de i a los puntos del cluster vecino más próximo (separación)
  • s ≈ +1: punto muy dentro de su cluster, lejos de los demás. Perfecto.
  • s ≈ 0: punto en la frontera entre dos clusters.
  • s < 0: punto probablemente asignado al cluster equivocado.

La silueta media de todo el dataset es una nota global del clustering. A diferencia de la inercia, tiene máximo: puedes elegir el k que la maximiza.

# ============================================
# ÍNDICE DE SILUETA para elegir k
# ============================================
from sklearn.metrics import silhouette_score       # silueta media global

siluetas = []                                       # acumulador de resultados
rango_k = range(2, 11)                              # ¡la silueta requiere k >= 2!
for k in rango_k:
    km = KMeans(n_clusters=k, n_init=10, random_state=42)  # modelo para este k
    etiquetas = km.fit_predict(X_scaled)            # entrena y devuelve el cluster de cada punto
    score = silhouette_score(X_scaled, etiquetas)   # silueta media de todos los puntos
    siluetas.append(score)                          # guardamos para graficar
    print(f"k={k}: silueta media = {score:.3f}")    # log legible

# El mejor k es el que MAXIMIZA la silueta
mejor_k = rango_k[int(np.argmax(siluetas))]         # índice del máximo → k correspondiente
print(f"\nMejor k según silueta: {mejor_k}")

plt.figure(figsize=(8, 4))
plt.plot(rango_k, siluetas, 'o-')
plt.xlabel('Número de clusters (k)')
plt.ylabel('Silueta media')
plt.title('Análisis de silueta (más alto = mejor)')
plt.tight_layout()
plt.show()

Interpretación práctica de la silueta media:

Silueta media Lectura
> 0.7 Estructura fuerte y clara
0.5 – 0.7 Estructura razonable
0.25 – 0.5 Estructura débil, posiblemente artificial
< 0.25 Probablemente no hay clusters reales

Consejo profesional

cuando codo y silueta discrepan, la silueta suele ser más fiable (mide separación real, no solo compacidad). Y si el negocio dice "necesitamos entre 3 y 6 segmentos porque marketing no puede gestionar más", ese es un criterio tan legítimo como cualquier métrica.

2.7 Limitaciones de K-Means

Limitación Por qué ocurre Mitigación
Asume clusters esféricos y de tamaño similar Usa distancia euclídea a un centroide; una "media" no puede representar una luna o un anillo DBSCAN o clustering espectral para formas arbitrarias
Sensible a la escala Una feature en miles (salario) domina a una en unidades (edad) StandardScaler SIEMPRE antes de ajustar
Sensible a outliers La media se desplaza hacia valores extremos Eliminar/tratar outliers antes, o usar K-Medoids
Hay que fijar k a priori El algoritmo no lo descubre Codo + silueta + criterio de negocio
Óptimos locales Depende de la inicialización k-means++ y n_init >= 10
Todo punto recibe cluster (no hay "ruido") Es una partición exhaustiva DBSCAN si necesitas detectar outliers

Advertencia — escalar siempre

este es EL error número uno con K-Means. Si agrupas clientes por edad (20–70) e ingresos (20.000–200.000 €), la distancia euclídea estará dominada al 99,9% por los ingresos: la edad será decorativa. StandardScaler antes de fit no es opcional, es parte del algoritmo en la práctica.

Cuándo NO usar K-Means: - Clusters con formas alargadas, anillos, medias lunas → usa DBSCAN. - Datos con muchos outliers que no puedes limpiar → DBSCAN o K-Medoids. - Features categóricas dominantes → K-Modes/K-Prototypes o embeddings previos. - Cuando necesitas jerarquía de grupos (subsegmentos dentro de segmentos) → clustering jerárquico.

2.8 Código completo de K-Means comentado

# ============================================
# K-MEANS completo: entrenar, inspeccionar, predecir
# ============================================
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_blobs
import numpy as np

# 1) Datos de ejemplo
X, _ = make_blobs(n_samples=500, centers=4, random_state=42)  # 4 grupos reales

# 2) Escalado (imprescindible: K-Means opera con distancias euclídeas)
scaler = StandardScaler()                # estandarizador: (x - media) / std
X_scaled = scaler.fit_transform(X)       # aprende parámetros y transforma

# 3) Modelo con el k elegido tras el análisis de codo/silueta
km = KMeans(
    n_clusters=4,        # número de clusters (decidido en §2.5–2.6)
    init='k-means++',    # inicialización dispersa e inteligente
    n_init=10,           # 10 ejecuciones con semillas distintas; gana la de menor inercia
    max_iter=300,        # tope de iteraciones asignación/actualización por ejecución
    tol=1e-4,            # tolerancia de movimiento de centroides para declarar convergencia
    random_state=42,     # reproducibilidad de los reinicios aleatorios
)

# 4) Entrenar y obtener etiquetas en un solo paso
etiquetas = km.fit_predict(X_scaled)     # array de enteros 0..3: cluster de cada punto

# 5) Inspeccionar el modelo entrenado
print("Centroides (en espacio escalado):")
print(km.cluster_centers_)               # matriz (k, n_features): coordenadas de cada centroide
print(f"Inercia final: {km.inertia_:.2f}")        # WCSS del mejor reinicio
print(f"Iteraciones hasta converger: {km.n_iter_}")  # cuántas vueltas dio el bucle

# 6) Centroides en unidades ORIGINALES (para interpretarlos con el negocio)
centroides_originales = scaler.inverse_transform(km.cluster_centers_)  # deshace el escalado
print("Centroides en unidades originales:")
print(centroides_originales)

# 7) Asignar cluster a datos NUEVOS (por ejemplo, un cliente recién llegado)
X_nuevo = np.array([[2.0, 3.5]])                     # nuevo punto en unidades originales
X_nuevo_scaled = scaler.transform(X_nuevo)           # transform (NO fit_transform: ¡no re-ajustar!)
print(f"Cluster del nuevo punto: {km.predict(X_nuevo_scaled)[0]}")

Nota

observa la línea 7: sobre datos nuevos se usa scaler.transform, nunca fit_transform. Re-ajustar el scaler con datos nuevos cambiaría el espacio y las asignaciones dejarían de ser coherentes. Es el mismo principio de fuga de datos que viste en el capítulo 3.

Ejercicio rápido 2

Tienes clientes con features edad (18–80) y gasto_anual_eur (100–50.000). Ejecutas K-Means con k=3 sin escalar y obtienes tres clusters. ¿Qué habrán capturado esos clusters casi con total seguridad? ¿Cómo lo arreglas?

Ver solución Los clusters serán, casi con seguridad, **tres franjas de gasto** (bajo/medio/alto), ignorando la edad por completo. Razón: la distancia euclídea entre dos clientes está dominada por la feature de mayor rango. Una diferencia de 40 años aporta como mucho 40² = 1.600 a la distancia²; una diferencia de 10.000 € aporta 100.000.000. La edad es matemáticamente irrelevante. **Arreglo:** `StandardScaler().fit_transform(X)` antes de `KMeans.fit`. Con ambas features en media 0 y desviación 1, contribuyen por igual. Después, para interpretar los centroides, usa `scaler.inverse_transform(km.cluster_centers_)` para volver a unidades de negocio.

3. Clustering jerárquico

3.1 Qué es y por qué existe

El clustering jerárquico no produce una única partición: produce un árbol completo de agrupaciones, desde "cada punto es su propio cluster" hasta "todo es un único cluster". Existe porque muchas estructuras reales son jerárquicas: especies dentro de géneros, productos dentro de categorías, subsegmentos dentro de segmentos de clientes.

La variante estándar es la aglomerativa (bottom-up):

  1. Empieza con N clusters (cada punto es uno).
  2. Encuentra los dos clusters más cercanos y fúndelos.
  3. Repite hasta que quede un solo cluster.
  4. El historial de fusiones forma el dendrograma.

3.2 El dendrograma: cómo leerlo

Un dendrograma es el árbol de fusiones. El eje vertical es la distancia a la que se fusionaron dos grupos: fusiones bajas = grupos muy similares; fusiones altas = grupos que se unieron "a regañadientes".

 distancia
 10 ┤            ┌────────────┴────────────┐
    │            │                         │
  6 ┤      ┌─────┴─────┐                   │
    │      │           │                   │
  3 ┤      │           │             ┌─────┴─────┐
    │      │           │             │           │
  1 ┤   ┌──┴──┐     ┌──┴──┐       ┌──┴──┐        │
    │   │     │     │     │       │     │        │
  0 ┴───A─────B─────C─────D───────E─────F────────G

Lectura de este dendrograma: - A-B, C-D y E-F se fusionan a distancia 1: son parejas muy similares. - {A,B} y {C,D} se unen a distancia 6. - G se une a {E,F} a distancia 3: G es afín a ese grupo, pero no gemelo. - La última fusión, a distancia 10, une los dos macro-grupos.

Para obtener k clusters, "cortas" horizontalmente: un corte a altura 5 (entre 3 y 6) atraviesa 3 ramas → 3 clusters: {A,B}, {C,D}, {E,F,G}. Un corte a altura 8 da 2 clusters. Los saltos verticales grandes indican cortes naturales: si entre la fusión a distancia 3 y la de distancia 10 no ocurre nada, cortar en ese hueco es una elección robusta.

3.3 Linkages: cómo se mide la distancia entre clusters

Cuando los clusters tienen varios puntos, ¿qué es "la distancia entre dos clusters"? La respuesta es el linkage, y cambia el comportamiento del algoritmo:

Linkage Distancia entre clusters Comportamiento Cuándo usarlo
ward (default sklearn) Fusión que minimiza el aumento de varianza interna Clusters compactos y de tamaño similar (parecido a K-Means) Opción por defecto; solo con distancia euclídea
complete Máxima distancia entre pares de puntos Clusters compactos de diámetro pequeño Cuando quieres grupos muy homogéneos
average Distancia media entre pares de puntos Compromiso equilibrado Buena opción general con métricas no euclídeas
single Mínima distancia entre pares de puntos Efecto "cadena": puede formar clusters alargados Formas alargadas; sensible a ruido

3.4 Código: dendrograma + clustering aglomerativo

# ============================================
# CLUSTERING JERÁRQUICO: dendrograma y modelo
# ============================================
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import dendrogram, linkage   # utilidades jerárquicas de SciPy
from sklearn.cluster import AgglomerativeClustering       # versión sklearn del aglomerativo
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_blobs

# 1) Datos y escalado (igual de obligatorio que en K-Means: se usan distancias)
X, _ = make_blobs(n_samples=150, centers=3, random_state=42)  # 150 puntos, 3 grupos
X_scaled = StandardScaler().fit_transform(X)                  # media 0, desviación 1

# 2) Matriz de fusiones con SciPy (necesaria para dibujar el dendrograma)
Z = linkage(X_scaled,        # datos escalados
            method='ward')   # linkage Ward: minimiza el aumento de varianza al fusionar
# Z es una matriz (n-1, 4): [cluster_i, cluster_j, distancia_de_fusión, tamaño_resultante]

# 3) Dibujar el dendrograma
plt.figure(figsize=(10, 5))
dendrogram(Z,
           truncate_mode='lastp',   # no dibujar las 150 hojas: resumir
           p=20,                    # mostrar solo las últimas 20 fusiones
           show_leaf_counts=True)   # nº de puntos dentro de cada hoja resumida
plt.title('Dendrograma (linkage Ward)')
plt.xlabel('Puntos o (tamaño del grupo)')
plt.ylabel('Distancia de fusión')
plt.axhline(y=10, color='red', linestyle='--',  # línea de corte propuesta
            label='Corte → 3 clusters')
plt.legend()
plt.tight_layout()
plt.show()

# 4) Modelo sklearn con el k decidido mirando el dendrograma
agg = AgglomerativeClustering(
    n_clusters=3,       # cortamos el árbol para obtener 3 grupos
    linkage='ward',     # mismo criterio que el dendrograma (¡coherencia!)
)
etiquetas = agg.fit_predict(X_scaled)   # cluster asignado a cada punto (0, 1 o 2)
print("Tamaños de cluster:",
      [int((etiquetas == c).sum()) for c in range(3)])  # cuántos puntos por grupo

Nota

AgglomerativeClustering no tiene método predict para puntos nuevos: la jerarquía se construye sobre el dataset completo y añadir un punto la cambiaría. Si necesitas asignar clientes nuevos a segmentos existentes, entrena después un clasificador supervisado (k-NN, por ejemplo) usando las etiquetas del clustering como y.

3.5 Ventajas, desventajas y cuándo preferirlo a K-Means

Ventajas: - No exiges k a priori: el dendrograma te muestra TODAS las granularidades y eliges mirándolo. - El dendrograma es una herramienta de comunicación excelente con negocio ("mirad cómo se agrupan las tiendas"). - Determinista: sin inicialización aleatoria, mismo resultado siempre. - Con el linkage adecuado captura estructuras no esféricas mejor que K-Means.

Desventajas y limitaciones: - Coste O(n²) en memoria y ~O(n² log n) en tiempo: con 100.000 puntos, la matriz de distancias ocupa ~40 GB. Inviable en datasets grandes (K-Means llega a millones sin despeinarse). - Las fusiones son irreversibles: un error temprano se arrastra hasta el final. - Sin predict para datos nuevos.

Prefiere jerárquico a K-Means cuando: - El dataset es pequeño o mediano (< ~10.000 puntos). - Sospechas estructura jerárquica real (taxonomías, sub-segmentos). - No tienes ni idea de cuántos grupos hay y quieres ver el paisaje completo antes de decidir. - Necesitas explicar visualmente la agrupación a una audiencia no técnica.


4. DBSCAN: clustering por densidad

4.1 La idea: clusters = zonas densas

DBSCAN (Density-Based Spatial Clustering of Applications with Noise, 1996) cambia la definición de cluster: un cluster no es "puntos cerca de un centroide", sino una región densa de puntos separada de otras regiones densas por zonas vacías. Consecuencias revolucionarias:

  1. Encuentra formas arbitrarias: lunas, anillos, espirales — cosas donde K-Means fracasa estrepitosamente.
  2. No necesitas fijar el número de clusters: emerge de los datos.
  3. Marca outliers explícitamente (etiqueta -1): los puntos en zonas poco densas no pertenecen a ningún cluster. Esta es su ventaja clave y lo convierte en detector de anomalías gratuito.
K-Means con 2 lunas:  FRACASO          DBSCAN con 2 lunas:  ÉXITO

   ●●●●●                                   ●●●●●
  ●●   ●●● ○○                             ●●   ●●●
 ●●      ●●●○○○                          ●●      ●●●
 ●●    ○○ ●●●○○○                         ●●    ○○ ●●
  ●   ○○○   ●●○○                          ●   ○○○
      ○○○○ ○○○○     ← corta las lunas         ○○○○ ○○○○   ← respeta las formas
       ○○○○○○         por la mitad             ○○○○○○
                                                         ← punto aislado: ruido (-1)

4.2 Los dos hiperparámetros: eps y min_samples

  • eps (epsilon): radio de vecindad. Dos puntos son "vecinos" si distan menos de eps.
  • min_samples: número mínimo de puntos (incluido él mismo) dentro del radio eps para que un punto sea considerado núcleo (core point).

Con esto se clasifican los puntos en tres tipos:

Tipo Definición Papel
Núcleo (core) Tiene ≥ min_samples vecinos en radio eps Forma el "esqueleto" del cluster
Frontera (border) No es núcleo, pero está en el radio de algún núcleo Se pega al cluster de su núcleo
Ruido (noise) Ni núcleo ni frontera Etiqueta -1: outlier

El algoritmo: toma un núcleo no visitado, crea un cluster, y lo expande añadiendo todos los núcleos alcanzables en cadena (vecino de vecino de vecino...) y sus fronteras. Repite con el siguiente núcleo no visitado. Lo que quede sin cluster es ruido.

4.3 Código comentado

# ============================================
# DBSCAN sobre datos con forma de lunas
# ============================================
import numpy as np
from sklearn.datasets import make_moons             # dataset clásico de 2 medias lunas
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import DBSCAN
from sklearn.neighbors import NearestNeighbors      # para la heurística de elegir eps

# 1) Datos: dos lunas entrelazadas + ruido gaussiano
X, _ = make_moons(n_samples=400,   # 400 puntos
                  noise=0.08,      # ruido para hacerlo realista
                  random_state=42)
X_scaled = StandardScaler().fit_transform(X)        # escalar: DBSCAN también usa distancias

# 2) Heurística para elegir eps: gráfico k-distancia
#    Para cada punto, distancia a su k-ésimo vecino (k = min_samples).
#    Se ordenan y se busca el "codo": ahí está un buen eps.
k = 5                                               # usaremos min_samples = 5
nn = NearestNeighbors(n_neighbors=k)                # buscador de vecinos más cercanos
nn.fit(X_scaled)                                    # indexa los puntos
distancias, _ = nn.kneighbors(X_scaled)             # distancias a los k vecinos de cada punto
dist_k = np.sort(distancias[:, -1])                 # distancia al k-ésimo vecino, ordenada
# → grafica dist_k con plt.plot(dist_k) y busca el codo; aquí ronda 0.25

# 3) Modelo DBSCAN con los parámetros elegidos
db = DBSCAN(
    eps=0.25,          # radio de vecindad (del gráfico k-distancia)
    min_samples=5,     # regla práctica: >= 2 * n_dimensiones (aquí 2D → min 4-5)
    metric='euclidean' # distancia usada para la vecindad
)
etiquetas = db.fit_predict(X_scaled)   # etiquetas: 0,1,2,... para clusters; -1 para ruido

# 4) Analizar el resultado
n_clusters = len(set(etiquetas)) - (1 if -1 in etiquetas else 0)  # nº de clusters sin contar ruido
n_ruido = int((etiquetas == -1).sum())                            # nº de outliers detectados
print(f"Clusters encontrados: {n_clusters}")        # esperamos 2 (las dos lunas)
print(f"Puntos marcados como ruido: {n_ruido}")     # puntos aislados por el noise añadido

Reglas prácticas de ajuste: - min_samples: empieza con 2 × n_dimensiones. Súbelo si hay mucho ruido o el dataset es grande. - eps: usa el gráfico k-distancia (código arriba). Si te salen demasiados puntos de ruido → sube eps; si todo acaba en un mega-cluster → baja eps.

4.4 Cuándo falla DBSCAN

  • Densidades variables: su talón de Aquiles. Un solo eps global no puede capturar a la vez un cluster denso y otro disperso: o parte el disperso en ruido, o fusiona los densos. Solución moderna: HDBSCAN (jerárquico por densidad, sin eps, disponible en sklearn.cluster.HDBSCAN desde la versión 1.3), que es hoy la opción preferida para clustering de embeddings.
  • Alta dimensionalidad: en muchas dimensiones todas las distancias se parecen (maldición de la dimensionalidad) y el concepto de "densidad" se degrada. Reduce dimensión antes (PCA/UMAP).
  • Elección de eps delicada: pequeños cambios pueden alterar mucho el resultado.
  • Sin predict nativo para puntos nuevos (igual que el jerárquico).

4.5 Tabla comparativa: K-Means vs jerárquico vs DBSCAN

Criterio K-Means Jerárquico (aglomerativo) DBSCAN
¿Requiere fijar nº de clusters? Sí (k) No (cortas el dendrograma) No (emerge de la densidad)
Formas de cluster Esféricas/convexas Depende del linkage Arbitrarias
Maneja outliers No (todos reciben cluster) No Sí (etiqueta -1)
Escalabilidad Excelente (millones de puntos) Mala (O(n²) memoria) Buena (con índices espaciales)
Determinista No (inicialización aleatoria) Casi (orden puede afectar fronteras)
predict en datos nuevos No No
Hiperparámetros clave k linkage + punto de corte eps, min_samples
Sensible a escala Sí (escalar siempre) Sí (escalar siempre) Sí (escalar siempre)
Ideal para Segmentación general, datasets grandes Datasets pequeños, estructura jerárquica, comunicación Formas raras, ruido, densidad, anomalías
Falla con Formas no convexas, outliers Datasets grandes Densidades variables, alta dimensión

Ejercicio rápido 3

Un dataset de ubicaciones GPS de repartidores contiene rutas que serpentean por la ciudad (formas alargadas y curvas) y algunos puntos GPS erróneos en medio del mar. ¿Qué algoritmo eliges y por qué? ¿Qué harías con las coordenadas antes de clusterizar?

Ver solución **DBSCAN** (o HDBSCAN), por dos razones que encajan exactamente con sus fortalezas: (1) las rutas son formas **alargadas y arbitrarias**, imposibles para K-Means, que las trocearía en bolas; (2) los puntos GPS erróneos en el mar son **outliers**, y DBSCAN los marcará automáticamente con `-1` en vez de contaminar algún cluster. Sobre las coordenadas: latitud/longitud no son euclídeas (un grado de longitud mide distinto según la latitud). Para áreas pequeñas basta proyectar a metros (UTM); para áreas grandes, usar `metric='haversine'` con coordenadas en radianes, que DBSCAN soporta. Y ojo: aquí NO conviene StandardScaler, porque destruiría la geometría real — la excepción que confirma la regla: escala cuando las features son magnitudes heterogéneas, no cuando ya comparten unidad física.

5. Reducción de dimensionalidad: PCA

5.1 El problema: demasiadas features

Los datasets reales tienen decenas, cientos o miles de columnas. Eso causa tres dolores concretos:

  1. No puedes visualizar más de 3 dimensiones. ¿Cómo "miras" un dataset de 50 columnas?
  2. Maldición de la dimensionalidad: con muchas dimensiones, los puntos se vuelven equidistantes, las distancias pierden significado y los modelos necesitan exponencialmente más datos.
  3. Redundancia y ruido: muchas features están correlacionadas (altura y peso; gasto total y nº de compras). Cargar con columnas redundantes ralentiza los modelos y añade ruido.

PCA (Principal Component Analysis, análisis de componentes principales) ataca los tres: encuentra un número reducido de direcciones (combinaciones lineales de las features originales) que retienen la máxima información posible, entendiendo "información" como varianza.

5.2 Intuición geométrica: proyectar sombras

Imagina una nube de puntos 3D con forma de baguette flotando en una habitación, y una linterna con la que proyectas su sombra sobre la pared:

  • Si iluminas la baguette a lo largo, la sombra es un circulito: has destruido casi toda la información.
  • Si la iluminas de lado, la sombra es una barra larga: la forma esencial sobrevive en 2D.

PCA es el algoritmo que encuentra automáticamente el ángulo de linterna que produce la sombra más informativa: la proyección que conserva la máxima varianza.

        Nube de puntos 2D correlacionados          PC1 = dirección de máxima varianza

  y                                            y
  │            . .··                           │            . .··
  │         .·.···                             │         .·.···      ↗ PC1
  │       ···●··                               │       ···●··   ↗
  │    ··.···                                  │    ··.··· ↗
  │  ··.··                                     │  ··.·↗
  │ ··                                         │ ↗·        ↖ PC2 (perpendicular,
  └──────────────── x                          └──────────────── x   poca varianza)

Paso a paso, qué hace PCA por dentro:

  1. Centra los datos (resta la media de cada columna; con StandardScaler además divides por la desviación, recomendado).
  2. Calcula la matriz de covarianzas entre features (quién varía junto con quién).
  3. Calcula sus autovectores y autovalores (o, en la práctica, la descomposición SVD, numéricamente más estable — es lo que hace sklearn).
  4. Los autovectores son las componentes principales: direcciones perpendiculares entre sí, ordenadas por el autovalor (= cuánta varianza captura cada una).
  5. Proyecta los datos sobre las primeras n_components direcciones: tus nuevas coordenadas comprimidas.

Nota

PC1 es la dirección de máxima varianza; PC2 es la de máxima varianza perpendicular a PC1; y así sucesivamente. Como son perpendiculares, las nuevas coordenadas están descorrelacionadas: PCA elimina la redundancia lineal por construcción.

5.3 Varianza explicada y cómo elegir componentes

Cada componente captura un porcentaje de la varianza total. La pregunta "¿cuántas componentes conservo?" se responde con el gráfico de varianza acumulada:

# ============================================
# PCA: varianza explicada y elección de componentes
# ============================================
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_breast_cancer     # dataset real: 30 features numéricas
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

# 1) Cargar datos: 569 muestras, 30 features de morfología celular
datos = load_breast_cancer()
X = datos.data                                      # matriz (569, 30)

# 2) Escalar SIEMPRE antes de PCA: la varianza depende de las unidades.
#    Sin escalar, la feature con números más grandes acapararía PC1
#    por pura escala, no por informativa.
X_scaled = StandardScaler().fit_transform(X)

# 3) PCA completo (todas las componentes) para estudiar la varianza
pca_full = PCA()                                    # sin n_components: calcula las 30
pca_full.fit(X_scaled)                              # aprende las direcciones principales

# 4) Varianza explicada por componente y acumulada
var_exp = pca_full.explained_variance_ratio_        # array: fracción de varianza de cada PC
var_acum = np.cumsum(var_exp)                       # suma acumulada: total retenido con n PCs
print(f"PC1 explica: {var_exp[0]:.1%}")             # ~44% en este dataset
print(f"PC1+PC2 explican: {var_acum[1]:.1%}")       # ~63%
print(f"Componentes para retener 95%: {int(np.argmax(var_acum >= 0.95)) + 1}")  # ~10

# 5) Gráfico de varianza acumulada: el "codo" de PCA
plt.figure(figsize=(8, 4))
plt.plot(range(1, len(var_acum) + 1), var_acum, 'o-')  # curva acumulada
plt.axhline(y=0.95, color='red', linestyle='--',    # umbral típico del 95%
            label='95% de varianza')
plt.xlabel('Número de componentes')
plt.ylabel('Varianza explicada acumulada')
plt.title('¿Cuántas componentes necesito?')
plt.legend()
plt.tight_layout()
plt.show()

# 6) Atajo de sklearn: pedir directamente "el 95%"
pca_95 = PCA(n_components=0.95)                     # float en (0,1) = fracción de varianza deseada
X_reducido = pca_95.fit_transform(X_scaled)         # proyecta a las PCs necesarias
print(f"De {X.shape[1]} features a {X_reducido.shape[1]} componentes "
      f"reteniendo {pca_95.explained_variance_ratio_.sum():.1%} de la varianza")

# 7) PCA a 2D para VISUALIZAR el dataset completo en un plano
pca_2d = PCA(n_components=2)                        # solo las 2 primeras direcciones
X_2d = pca_2d.fit_transform(X_scaled)               # coordenadas (569, 2)
plt.figure(figsize=(7, 5))
plt.scatter(X_2d[:, 0], X_2d[:, 1],                 # cada punto = una muestra
            c=datos.target,                          # coloreamos con la etiqueta SOLO para verificar
            cmap='coolwarm', alpha=0.6, s=15)
plt.xlabel(f'PC1 ({pca_2d.explained_variance_ratio_[0]:.0%} varianza)')
plt.ylabel(f'PC2 ({pca_2d.explained_variance_ratio_[1]:.0%} varianza)')
plt.title('30 dimensiones proyectadas a 2 con PCA')
plt.tight_layout()
plt.show()

Interpretación: en este dataset, 30 features se comprimen a ~10 componentes reteniendo el 95% de la varianza — un tercio del tamaño con pérdida mínima. Y con solo 2 componentes (63%) ya se ven los dos grupos de tumores separados en el plano: la estructura esencial era de baja dimensión.

Criterios para elegir componentes:

Criterio Regla Cuándo usarlo
Umbral de varianza Retener 90–99% acumulado Compresión previa a un modelo
Codo del scree plot Cortar donde la curva se aplana Análisis exploratorio
Fijo a 2–3 n_components=2 Visualización
Validación cruzada El nº que maximiza la métrica del modelo posterior Cuando PCA es un paso del pipeline supervisado

5.4 Usos reales de PCA

  1. Visualizar datos de alta dimensión (incluidos embeddings de LLMs) en 2D/3D para inspección humana.
  2. Acelerar modelos: entrenar sobre 50 componentes en vez de 500 features reduce tiempo y memoria; con features muy correlacionadas incluso mejora la generalización.
  3. Eliminar ruido: las últimas componentes suelen capturar ruido; proyectar y reconstruir (inverse_transform) actúa como filtro (se usa en imágenes y señales).
  4. Descorrelacionar features para algoritmos sensibles a la multicolinealidad (regresión lineal, por ejemplo).
  5. Whitening previo a otros algoritmos de ML.

5.5 Limitaciones de PCA

  • Es lineal: solo encuentra direcciones rectas. Si la estructura es una espiral o una variedad curvada, PCA no la "desenrolla" (para eso existen t-SNE/UMAP o kernel PCA).
  • Componentes poco interpretables: PC1 puede ser 0.3·edad − 0.5·ingresos + 0.2·antigüedad + .... Matemáticamente óptimo, pero difícil de explicar a negocio ("¿qué significa PC1?" es una pregunta incómoda en reuniones).
  • Varianza ≠ importancia: PCA maximiza varianza, no relevancia para tu objetivo. Una feature de poca varianza puede ser la más predictiva, y PCA puede diluirla.
  • Sensible a la escala (como todo lo de este capítulo): estandariza antes.
  • Sensible a outliers: un outlier extremo puede secuestrar la dirección de PC1.

5.6 t-SNE y UMAP: solo para mirar

Para visualización de estructuras no lineales (especialmente embeddings), existen dos técnicas superiores a PCA:

  • t-SNE: preserva vecindarios locales — puntos parecidos quedan juntos en el plano 2D. Excelente para "ver clusters". Lento en datasets grandes.
  • UMAP: idea similar, mucho más rápido, preserva algo mejor la estructura global. El estándar actual para visualizar embeddings.

Advertencia

t-SNE y UMAP son microscopios, no transformadores de datos para pipelines:

  • Las distancias y tamaños en el plano no son fiables: que dos clusters se vean lejos o cerca en t-SNE no significa nada cuantitativo. La distancia inter-cluster es en gran parte un artefacto.
  • Pueden "inventar" clusters en datos sin estructura si el hiperparámetro perplexity/n_neighbors es inadecuado.
  • t-SNE no tiene transform para datos nuevos (UMAP sí, pero con matices).
  • Regla de la casa: para features de un modelo → PCA. Para hacer un gráfico bonito y explorar → t-SNE/UMAP. Nunca al revés.
# t-SNE en dos líneas, solo para visualizar (nunca como features)
from sklearn.manifold import TSNE
X_tsne = TSNE(n_components=2,        # proyección a 2D
              perplexity=30,         # ~nº de vecinos efectivos; probar 5-50
              random_state=42).fit_transform(X_scaled)  # coordenadas para el scatter

Ejercicio rápido 4

Aplicas PCA a un dataset con ingresos_eur (varianza enorme, ~10⁹) y 20 features más de rango 0–1, sin escalar. ¿Qué contendrá PC1? ¿Qué porcentaje de varianza explicada reportará aproximadamente? ¿Es un buen resultado?

Ver solución PC1 será, esencialmente, **la propia feature `ingresos_eur`** (coeficiente ≈ 1 para ella, ≈ 0 para el resto), y explicará >99% de la varianza total — porque la varianza total está dominada numéricamente por esa columna de escala gigante. Parece un resultado espectacular ("¡una componente explica el 99%!") pero es un **artefacto de unidades**: no has aprendido nada sobre la estructura; solo que los euros son números grandes. Las otras 20 features quedaron aplastadas. Arreglo: `StandardScaler` antes de PCA, para que "varianza" signifique variación relativa comparable, no magnitud de las unidades.

6. Detección de anomalías

6.1 El problema

Una anomalía (outlier) es una observación que se desvía tanto del resto que levanta sospechas de haber sido generada por un mecanismo distinto: un fraude entre compras legítimas, un rodamiento a punto de fallar entre lecturas normales de sensores, una intrusión entre logs rutinarios.

¿Por qué no supervisado? Porque en los casos más valiosos no hay etiquetas suficientes: el fraude nuevo no se parece al viejo, los fallos de máquina son rarísimos, y los ataques cambian cada semana. En lugar de aprender "cómo es el fraude", aprendemos "cómo es lo normal" y señalamos lo que se sale.

6.2 Isolation Forest: lo raro se aísla rápido

La intuición de Isolation Forest (2008) es de una elegancia memorable:

Si haces cortes aleatorios en el espacio de features, un punto anómalo queda aislado en pocos cortes (está solo en su rincón), mientras que un punto normal, rodeado de vecinos, necesita muchos cortes para quedar separado de todos ellos.

El algoritmo construye árboles con particiones aleatorias (feature al azar, umbral al azar) y mide la profundidad media a la que cada punto queda aislado. Profundidad pequeña → anomalía.

Aislar un punto NORMAL (●, en zona densa):    Aislar una ANOMALÍA (, alejada):

┌───────┬───┬───────┐                         ┌───────────┬─────────┐
│  ●●●  │●●●│ corte4│                         │   ●●●●    │         │
│ ●●●●●━┿━●●┿━━━━━━━│                         │  ●●●●●●   │        │
│  ●●● 3│●● │corte5 │                         │   ●●●●    │ 1 corte │
├───────┴───┤       │                         │           │  basta  │
│ corte 1   │corte 2│                         └───────────┴─────────┘
└───────────┴───────┘
  ~5+ cortes para aislarlo                       profundidad 1 → MUY anómalo

Ventajas: rápido, escala bien, funciona en alta dimensión, pocos hiperparámetros, no asume ninguna distribución. Es el default industrial para anomalías tabulares. Limitaciones: el parámetro contamination (fracción esperada de anomalías) fija el umbral y hay que estimarlo; menos sensible a anomalías "locales" (raras solo respecto a su vecindario).

6.3 Local Outlier Factor (LOF): raro respecto a los vecinos

LOF compara la densidad local de cada punto con la de sus k vecinos. Si tus vecinos viven en zona densa y tú en zona rala, tu LOF es alto → anomalía local.

  • Fortaleza: detecta puntos que son normales globalmente pero raros en su contexto (una compra de 300 € es normal en general, pero anómala para un cliente que siempre gasta 10 €).
  • Debilidades: O(n²) sin índices (lento en datasets grandes), sensible a n_neighbors, y por defecto no predice sobre datos nuevos (necesita novelty=True).

6.4 One-Class SVM (mención)

Aprende una frontera alrededor de la región "normal" en un espacio transformado por un kernel; lo que cae fuera es anomalía. Potente en problemas pequeños y bien escalados, pero delicado de ajustar (nu, gamma) y costoso en datasets grandes. Hoy suele preferirse Isolation Forest como primera opción.

Método Idea Fortaleza Debilidad Escala a datos grandes
Isolation Forest Lo raro se aísla en pocos cortes aleatorios Rápido, robusto, default industrial contamination a ciegas; anomalías locales
LOF Densidad local vs vecinos Anomalías contextuales/locales O(n²), sensible a k Regular
One-Class SVM Frontera kernel alrededor de lo normal Fronteras flexibles Ajuste delicado, lento No
DBSCAN (bonus) Ruido = puntos fuera de zonas densas Clustering y anomalías a la vez No diseñado como scorer Regular

6.5 Código completo con datos sintéticos

# ============================================
# DETECCIÓN DE ANOMALÍAS: Isolation Forest + LOF
# ============================================
import numpy as np
from sklearn.ensemble import IsolationForest        # bosque de aislamiento
from sklearn.neighbors import LocalOutlierFactor    # factor de outlier local
from sklearn.preprocessing import StandardScaler

# 1) Datos sintéticos: 500 transacciones normales + 15 anómalas
rng = np.random.RandomState(42)                     # generador reproducible
# Normales: importe medio 50€ (desv 15), a una hora media 14h (desv 3)
normales = np.column_stack([
    rng.normal(50, 15, 500),    # columna 0: importe en euros
    rng.normal(14, 3, 500),     # columna 1: hora del día (0-23)
])
# Anómalas: importes enormes de madrugada (patrón típico de fraude)
anomalas = np.column_stack([
    rng.normal(900, 200, 15),   # importes ~900€: 18x lo normal
    rng.normal(3.5, 1, 15),     # ~3:30 de la madrugada
])
X = np.vstack([normales, anomalas])                 # dataset combinado (515, 2)
y_real = np.array([0] * 500 + [1] * 15)             # verdad oculta SOLO para evaluar al final

# 2) Escalar: ambos métodos usan distancias/particiones sensibles a escala
X_scaled = StandardScaler().fit_transform(X)

# 3) ISOLATION FOREST
iso = IsolationForest(
    n_estimators=200,        # nº de árboles aleatorios (más = score más estable)
    contamination=0.03,      # fracción esperada de anomalías (~15/515); fija el umbral
    random_state=42,         # reproducibilidad
)
pred_iso = iso.fit_predict(X_scaled)        # +1 = normal, -1 = anomalía (convención sklearn)
scores_iso = iso.decision_function(X_scaled)  # score continuo: cuanto MÁS NEGATIVO, más anómalo
# El score continuo permite RANKEAR alertas en vez de decidir sí/no: clave en producción

# 4) LOCAL OUTLIER FACTOR
lof = LocalOutlierFactor(
    n_neighbors=20,          # tamaño del vecindario para medir densidad local
    contamination=0.03,      # misma fracción esperada, para comparar en igualdad
)
pred_lof = lof.fit_predict(X_scaled)        # +1 normal / -1 anomalía (no hay predict posterior)

# 5) Evaluación (posible aquí solo porque los datos son sintéticos y
#    conocemos la verdad; en producción esto lo hace la revisión humana)
def evaluar(nombre, pred):
    detectadas = (pred == -1)                       # máscara de puntos marcados como anómalos
    verdaderos = int((detectadas & (y_real == 1)).sum())  # anomalías reales detectadas
    falsos = int((detectadas & (y_real == 0)).sum())      # normales marcados por error
    print(f"{nombre}: detectó {int(detectadas.sum())} anomalías → "
          f"{verdaderos}/15 verdaderas, {falsos} falsas alarmas")

evaluar("IsolationForest", pred_iso)
evaluar("LOF", pred_lof)

# 6) Puntuar una transacción NUEVA con Isolation Forest (LOF por defecto no puede)
nueva = np.array([[850.0, 4.0]])                    # 850€ a las 4 AM... sospechosa
nueva_scaled = StandardScaler().fit(X).transform(nueva)  # mismo scaler del entrenamiento
print(f"Predicción: {iso.predict(nueva_scaled)[0]} "     # -1 esperado (anomalía)
      f"(score: {iso.decision_function(nueva_scaled)[0]:.3f})")

Consejo profesional

en producción, no uses la predicción binaria: usa el score continuo (decision_function) para ordenar los casos de más a menos sospechoso, y deja que el umbral lo fije la capacidad del equipo humano de revisión ("podemos revisar 200 alertas/día"), no el parámetro contamination. Veremos esto en acción en el caso empresarial del §9.


7. Conexión con la era LLM: clustering de embeddings

7.1 El no supervisado vive en los embeddings

Aquí está la razón por la que este capítulo importa más en 2026 que en 2016. Los modelos de lenguaje modernos convierten cualquier texto en un embedding: un vector de cientos o miles de números donde textos con significado parecido quedan geométricamente cerca.

Y en cuanto tienes vectores donde "cerca = parecido"... todo este capítulo se enciende:

  • K-Means / HDBSCAN sobre embeddings de tickets de soporte → descubres los temas reales de queja de tus clientes, sin definir categorías a priori. Esto es LA técnica moderna de descubrimiento de temas (topic discovery), y ha sustituido en gran medida a métodos clásicos como LDA.
  • PCA/UMAP sobre embeddings → visualizas tu base de conocimiento en 2D y ves los "continentes" temáticos.
  • Detección de anomalías sobre embeddings → encuentras el ticket que no se parece a nada (¿nuevo tipo de problema? ¿ataque?).
flowchart LR
    A["Textos crudos<br/>(tickets, docs, reseñas)"] --> B["Modelo de embeddings<br/>(p. ej. API de embeddings)"]
    B --> C["Vectores<br/>(n_docs × 1536 dims)"]
    C --> D["Reducción opcional<br/>PCA/UMAP"]
    D --> E["Clustering<br/>K-Means / HDBSCAN"]
    E --> F["Clusters = TEMAS descubiertos"]
    F --> G["Etiquetar cada tema<br/>(muestra + LLM o humano)"]

7.2 Ejemplo con vectores de juguete

Simulemos embeddings minúsculos (3 dimensiones en vez de 1536) para ver la mecánica exacta:

# ============================================
# CLUSTERING DE "EMBEDDINGS" DE JUGUETE
# ============================================
import numpy as np
from sklearn.cluster import KMeans
from sklearn.preprocessing import normalize          # normalización L2 (norma 1 por fila)

# 1) Seis tickets de soporte y sus embeddings inventados de 3 dims.
#    Imagina que cada dimensión captura un aspecto semántico latente:
#    dim0 ~ "tema facturación", dim1 ~ "tema acceso", dim2 ~ "tema envíos"
tickets = [
    "Me han cobrado dos veces la suscripción",       # facturación
    "Quiero factura de mi último pago",              # facturación
    "No puedo iniciar sesión en mi cuenta",          # acceso
    "He olvidado mi contraseña",                     # acceso
    "Mi pedido lleva 2 semanas sin llegar",          # envíos
    "El paquete llegó dañado",                       # envíos
]
embeddings = np.array([
    [0.92, 0.08, 0.11],   # ticket 0 → carga alta en dim "facturación"
    [0.88, 0.15, 0.05],   # ticket 1 → facturación
    [0.10, 0.90, 0.07],   # ticket 2 → acceso
    [0.14, 0.85, 0.12],   # ticket 3 → acceso
    [0.07, 0.10, 0.93],   # ticket 4 → envíos
    [0.12, 0.06, 0.89],   # ticket 5 → envíos
])

# 2) Normalización L2: cada vector pasa a tener norma 1.
#    Con vectores normalizados, la distancia euclídea equivale (monótonamente)
#    a la SIMILITUD COSENO, que es la métrica natural de los embeddings.
emb_norm = normalize(embeddings)                     # norma L2 = 1 por fila

# 3) K-Means sobre los embeddings normalizados
km = KMeans(n_clusters=3,       # esperamos 3 temas (en la práctica: silueta o HDBSCAN)
            n_init=10,
            random_state=42)
temas = km.fit_predict(emb_norm)                     # tema asignado a cada ticket

# 4) Ver qué descubrió: agrupamos los textos por cluster
for t in sorted(set(temas)):
    print(f"\n— TEMA {t} —")
    for texto, tema in zip(tickets, temas):
        if tema == t:
            print(f"   · {texto}")
# Salida esperada: los 2 de facturación juntos, los 2 de acceso juntos,
# los 2 de envíos juntos — SIN haber definido categorías nunca.

El pipeline real es idéntico, cambiando dos cosas: los embeddings vienen de un modelo real (p. ej. la API de embeddings de tu proveedor de LLM, con 1024–3072 dimensiones) y el último paso suele pedir a un LLM que ponga nombre a cada cluster a partir de una muestra de sus textos.

Nota

este pipeline (embeddings → clustering → etiquetado) es la puerta de entrada conceptual al módulo 06-RAG, donde los mismos embeddings alimentan la búsqueda semántica. La geometría es la misma; solo cambia la pregunta: aquí preguntamos "¿qué grupos hay?"; en RAG preguntaremos "¿qué documentos están cerca de esta consulta?".

Consejo profesional

para embeddings reales, la receta 2026 es: normalizar L2 → reducir con UMAP a ~10-50 dims → HDBSCAN (maneja clusters de densidad variable y marca ruido, cosas que abundan en el espacio semántico). K-Means sigue siendo válido cuando necesitas velocidad o un k fijado por negocio.


8. Ejemplo integrador: segmentación RFM de clientes

Vamos a unir todo el capítulo en el caso de uso rey del clustering: segmentar clientes de un e-commerce con el modelo RFM:

  • Recency: días desde la última compra (menos = mejor).
  • Frequency: número de compras en el período (más = mejor).
  • Monetary: gasto total (más = mejor).
flowchart LR
    A["Transacciones<br/>crudas"] --> B["Agregar por cliente:<br/>R, F, M con Pandas"]
    B --> C["Tratar asimetría<br/>(log1p) y escalar"]
    C --> D["Elegir k<br/>con silueta"]
    D --> E["K-Means final"]
    E --> F["Perfilar segmentos<br/>(medianas por cluster)"]
    F --> G["Nombres de negocio<br/>+ acciones de marketing"]

8.1 Generar el dataset sintético de transacciones

# ============================================
# PASO 1: dataset sintético de transacciones
# ============================================
import numpy as np
import pandas as pd

rng = np.random.RandomState(42)                      # semilla global del ejemplo

HOY = pd.Timestamp("2026-07-01")                     # fecha de referencia del análisis
n_clientes = 500                                     # tamaño del ejemplo

filas = []                                           # acumulador de transacciones
for cliente_id in range(1, n_clientes + 1):
    # Sorteamos un "arquetipo" oculto que gobierna el comportamiento.
    # OJO: esto es solo para GENERAR datos realistas; el modelo NO lo verá.
    arquetipo = rng.choice(
        ["campeon", "leal", "ocasional", "dormido"], # 4 comportamientos latentes
        p=[0.10, 0.25, 0.40, 0.25])                  # proporciones en la base de clientes
    if arquetipo == "campeon":                       # compra mucho, a menudo y hace poco
        n_compras = rng.randint(15, 30)              # nº de compras del cliente
        max_dias = 30                                # última compra hace <30 días
        ticket_medio = rng.uniform(80, 200)          # gasto por compra
    elif arquetipo == "leal":                        # regular, ticket medio
        n_compras = rng.randint(6, 15)
        max_dias = 90
        ticket_medio = rng.uniform(40, 90)
    elif arquetipo == "ocasional":                   # pocas compras, esporádico
        n_compras = rng.randint(1, 5)
        max_dias = 250
        ticket_medio = rng.uniform(20, 60)
    else:                                            # "dormido": no compra hace mucho
        n_compras = rng.randint(1, 4)
        max_dias = 365
        ticket_medio = rng.uniform(15, 50)

    for _ in range(n_compras):                       # generar cada transacción
        dias_atras = rng.randint(                    # cuándo ocurrió la compra
            5 if arquetipo == "campeon" else max_dias // 3,  # mínimo según arquetipo
            max_dias + 1)                            # máximo según arquetipo
        filas.append({
            "cliente_id": cliente_id,                              # quién compró
            "fecha": HOY - pd.Timedelta(days=int(dias_atras)),     # cuándo
            "importe": round(float(rng.normal(ticket_medio,        # cuánto: normal
                                              ticket_medio * 0.3)),# con 30% de desv.
                             2),
        })

transacciones = pd.DataFrame(filas)                  # DataFrame de transacciones
transacciones["importe"] = transacciones["importe"].clip(lower=5)  # sin importes negativos
print(transacciones.shape)                           # ~4000-5000 transacciones
print(transacciones.head())

8.2 Calcular R, F y M con Pandas

# ============================================
# PASO 2: tabla RFM (una fila por cliente)
# ============================================
rfm = transacciones.groupby("cliente_id").agg(       # agregamos por cliente
    ultima_compra=("fecha", "max"),                  # fecha de la compra más reciente
    frequency=("fecha", "count"),                    # F: nº de transacciones
    monetary=("importe", "sum"),                     # M: gasto total acumulado
).reset_index()                                      # cliente_id vuelve a ser columna

# R: días transcurridos desde la última compra hasta HOY
rfm["recency"] = (HOY - rfm["ultima_compra"]).dt.days  # timedelta → días enteros
rfm = rfm[["cliente_id", "recency", "frequency", "monetary"]]  # columnas finales, en orden

print(rfm.describe().round(1))                       # sanity check de rangos
# recency: ~5-365 días | frequency: 1-29 compras | monetary: ~20-5000 €

8.3 Transformar y escalar

# ============================================
# PASO 3: log-transformación + escalado
# ============================================
from sklearn.preprocessing import StandardScaler

# frequency y monetary tienen fuerte asimetría a la derecha (pocos clientes
# gastan muchísimo). Sin corregirla, esos pocos clientes extremos "estiran"
# el espacio y secuestran los clusters. log1p = log(1+x) comprime la cola
# y es seguro con ceros.
rfm_t = rfm.copy()                                   # copia para no pisar los datos crudos
rfm_t["frequency"] = np.log1p(rfm_t["frequency"])    # comprime la cola derecha de F
rfm_t["monetary"] = np.log1p(rfm_t["monetary"])      # ídem para M
# recency ya es razonablemente uniforme; la dejamos lineal

features = ["recency", "frequency", "monetary"]      # columnas que entran al modelo
scaler = StandardScaler()                            # media 0, desviación 1
X_rfm = scaler.fit_transform(rfm_t[features])        # matriz (500, 3) lista para K-Means

8.4 Elegir k con la silueta

# ============================================
# PASO 4: ¿cuántos segmentos? — silueta
# ============================================
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

for k in range(2, 8):                                # rango razonable para marketing
    km = KMeans(n_clusters=k, n_init=10, random_state=42)  # modelo candidato
    labels = km.fit_predict(X_rfm)                   # entrenar y etiquetar
    sil = silhouette_score(X_rfm, labels)            # calidad geométrica global
    print(f"k={k} → silueta={sil:.3f}")
# Salida típica: k=4 con la silueta más alta (los datos tienen 4 arquetipos ocultos).
# Si k=3 y k=4 empatan, decide por interpretabilidad de negocio.

K_FINAL = 4                                          # decisión: 4 segmentos
km_final = KMeans(n_clusters=K_FINAL, n_init=10, random_state=42)
rfm["segmento"] = km_final.fit_predict(X_rfm)        # etiqueta de segmento por cliente

8.5 Perfilar los segmentos y ponerles nombre

# ============================================
# PASO 5: perfil de cada segmento (¡en unidades originales!)
# ============================================
perfil = rfm.groupby("segmento").agg(                # estadísticas por segmento
    n_clientes=("cliente_id", "count"),              # tamaño del segmento
    recency_med=("recency", "median"),               # mediana: robusta a extremos
    frequency_med=("frequency", "median"),
    monetary_med=("monetary", "median"),
    monetary_total=("monetary", "sum"),              # cuánta facturación concentra
).round(1)
perfil["pct_ingresos"] = (perfil["monetary_total"] /             # % de la facturación
                          perfil["monetary_total"].sum() * 100).round(1)
print(perfil.sort_values("monetary_med", ascending=False))

Salida típica (los números exactos varían con la semilla):

Segmento Clientes Recency med. (días) Frequency med. Monetary med. (€) % ingresos Nombre de negocio
2 ~50 12 21 2.700 ~45% Campeones
0 ~125 45 9 590 ~30% Leales
3 ~200 140 3 120 ~17% Ocasionales
1 ~125 280 2 65 ~8% En riesgo / dormidos

Cómo se leen y se bautizan: se ordenan los clusters por sus medianas y se traduce geometría a negocio. R baja + F alta + M alta = tus mejores clientes ("campeones"). R altísima + F baja = clientes que probablemente ya perdiste ("dormidos"). El nombre NO lo da el algoritmo: lo das tú mirando el perfil.

8.6 Acciones de marketing por segmento

Segmento Diagnóstico Acción de marketing KPI a vigilar
Campeones Máximo valor, activos Programa VIP, acceso anticipado, pedir reseñas y referidos. NO bombardear con descuentos (comprarían igual: sería regalar margen) Retención, NPS
Leales Buen valor, regulares Cross-selling, suscripción, subir ticket medio con bundles Ticket medio, frecuencia
Ocasionales Compran poco, recientes ma non troppo Email educativo, segunda-compra con incentivo suave, remarketing Tasa de 2ª compra
En riesgo Sin comprar hace meses Campaña de reactivación agresiva (descuento fuerte, "te echamos de menos"); si no responden, dejar de invertir Tasa de reactivación, coste por reactivado

Consejo profesional

el entregable de una segmentación nunca es "el modelo": es esta tabla. Si no puedes rellenar la columna de acciones, la segmentación no está terminada — o los clusters no son accionables y hay que iterar (otras features, otro k).

Ejercicio rápido 5

Tras entregar la segmentación, marketing pregunta: "¿en qué segmento cae este cliente nuevo que se registró ayer e hizo una compra de 35 €?". ¿Qué problema conceptual hay, y cómo asignarías mecánicamente un cliente nuevo a un segmento existente?

Ver solución **Problema conceptual:** un cliente de un día tiene R=1, F=1, M=35. Sus métricas RFM apenas tienen historia: caerá probablemente en "ocasionales" por defecto, pero es indistinguible de un futuro campeón en su primera compra. La segmentación RFM describe comportamiento acumulado; para clientes nuevos suele mantenerse un segmento aparte ("nuevos", p. ej. antigüedad < 30-60 días) hasta que acumulan señal. **Mecánica de asignación:** guardar el `scaler` y el `km_final` entrenados; para el cliente nuevo, calcular (R, F, M) → aplicar la **misma** transformación `log1p` a F y M → `scaler.transform` (nunca `fit_transform`) → `km_final.predict`. Exactamente el mismo pipeline de transformación que en entrenamiento, congelado.

9. Caso empresarial: banco detectando transacciones anómalas

Caso empresarial

BancoNorte (mediano, ~2 millones de clientes) sufre fraude en tarjetas. Tiene un sistema de reglas ("bloquear si > 3.000 € en el extranjero") que los defraudadores ya conocen y esquivan. Sigue la historia real-típica de un proyecto así.

9.1 Por qué no supervisado

El equipo de datos plantea primero un clasificador supervisado con el histórico de fraudes confirmados. El responsable de riesgo lo frena con un argumento clave:

"El fraude que nos preocupa es el que todavía no hemos visto. Nuestras etiquetas describen el fraude de 2024; los defraudadores de 2026 hacen otra cosa. Un clasificador entrenado con fraude viejo es un guardián que solo reconoce a los ladrones ya fichados."

Además: las etiquetas disponibles son escasas (0,1% de transacciones), tardías (el cliente reclama semanas después) y sesgadas (solo se etiquetó lo que las reglas viejas cazaron). Decisión: modelar lo normal con Isolation Forest y alertar sobre lo que se desvía — complementando (no sustituyendo) las reglas y el histórico.

9.2 Arquitectura de la solución

  Transacción entrante
  ┌──────────────────┐   features: importe relativo al histórico del cliente,
  │ Features por     │   hora, país vs país habitual, comercio nuevo (sí/no),
  │ cliente-contexto │   velocidad (nº ops última hora), distancia geográfica
  └────────┬─────────┘   respecto a la operación anterior...
  ┌──────────────────┐
  │ Isolation Forest │──► score de anomalía continuo (no un sí/no)
  └────────┬─────────┘
  ┌──────────────────┐   umbral fijado por CAPACIDAD del equipo:
  │ Cola priorizada  │──► los analistas revisan ~400 alertas/día,
  │ de alertas       │    ordenadas de más a menos sospechosa
  └────────┬─────────┘
  ┌──────────────────┐   cada resolución (fraude/legítima) se guarda:
  │ Revisión humana  │──► ¡las etiquetas nuevas alimentarán un futuro
  └──────────────────┘    modelo supervisado! (círculo virtuoso)

Detalle crucial de diseño: las features son relativas al cliente ("importe / mediana de importes del cliente", "¿país habitual?"), no absolutas. 500 € es normal para un cliente y estratosférico para otro: la anomalía es contextual.

9.3 Errores del primer intento (y sus lecciones)

  1. Entrenaron con features absolutas (importe en euros, sin contexto del cliente). Resultado: el modelo marcaba como "anómalos" a todos los clientes de renta alta. Miles de falsas alarmas de gente rica comprando normal. Lección: en anomalías, el contexto lo es todo; ingeniería de features relativa al sujeto.
  2. No escalaron las features. El importe (0–20.000) aplastaba a la hora (0–23) y al flag de país (0/1): el modelo era, en la práctica, un detector de importes altos, redundante con las reglas existentes. Lección: la de siempre — escalar.
  3. Usaron la predicción binaria con contamination=0.01. Eso marcaba ~17.000 alertas/día para un equipo de 6 analistas que puede revisar 400. Las alertas se acumulaban y se ignoraban en bloque: un sistema que alerta demasiado es un sistema apagado. Lección: usar el score continuo y fijar el umbral por capacidad operativa.
  4. Entrenaron una sola vez. A los 3 meses, el "comportamiento normal" había cambiado (campaña de Navidad → importes altos nocturnos legítimos) y las falsas alarmas se dispararon. Lección: lo "normal" deriva con el tiempo (concept drift); reentrenar con ventana móvil (p. ej. últimos 90 días) y monitorizar la tasa de alertas.

9.4 Métricas operativas (no hay accuracy que valga)

Sin etiquetas completas, el éxito se mide operativamente:

Métrica Definición Valor inicial A los 6 meses
Tasa de alertas revisables Alertas/día vs capacidad del equipo 17.000 (inviable) 400 (100% revisadas)
Precisión de alerta (hit rate) % de alertas revisadas que resultan fraude 0,4% 6,5%
Fraude nuevo detectado Casos sin patrón previo en reglas 14 esquemas nuevos
Tiempo hasta detección Desde la transacción hasta la alerta días (reclamación) < 1 minuto
Fricción con clientes % de operaciones legítimas bloqueadas alto (reglas rígidas) −38%

Nota

fíjate en el 6,5% de hit rate: ¡el 93,5% de las alertas revisadas son falsas alarmas y aun así el sistema es rentable! Un fraude evitado ahorra cientos o miles de euros; una revisión cuesta minutos de analista. En detección de anomalías, la economía de los errores es asimétrica y el punto de operación se elige con esa aritmética, no buscando un accuracy bonito.

El círculo se cierra donde empezó el capítulo: las resoluciones de los analistas (fraude sí/no) van generando etiquetas frescas que, con el tiempo, permiten añadir un clasificador supervisado sobre los patrones ya conocidos, mientras Isolation Forest sigue vigilando lo desconocido. No supervisado y supervisado no compiten: se alimentan mutuamente.


10. Buenas prácticas

  1. Escala siempre antes de cualquier algoritmo basado en distancias (K-Means, jerárquico, DBSCAN, PCA, LOF). Es la práctica nº 1 de este capítulo por una razón.
  2. Combina criterios para elegir k: codo + silueta + interpretabilidad de negocio. Si los tres apuntan a sitios distintos, desconfía de que haya clusters reales.
  3. Perfila los clusters en unidades originales (inverse_transform o agregaciones sobre los datos crudos) y ponles nombres de negocio. Un cluster sin nombre ni acción es un número, no un insight.
  4. Trata la asimetría antes de clusterizar (log1p en variables de dinero/conteos): las colas largas secuestran la geometría.
  5. Fija random_state y guarda scaler + modelo juntos (mismo pipeline en entrenamiento y en asignación de puntos nuevos).
  6. Valida la estabilidad: re-ejecuta con otras semillas y submuestras (bootstrap). Si los clusters cambian radicalmente, no eran estructura, eran ruido.
  7. En anomalías, entrega scores rankeados, no binarios, y fija el umbral según la capacidad de revisión humana.
  8. Reentrena periódicamente: lo "normal" y los segmentos derivan con el tiempo (drift).
  9. Para embeddings: normaliza L2, considera reducir dimensión, y valora HDBSCAN antes que K-Means.
  10. Documenta las decisiones subjetivas (k elegido, features incluidas, transformaciones): en no supervisado hay muchas, y el "yo futuro" las agradecerá.

11. Malas prácticas

  1. Clusterizar sin escalar y presentar los resultados como hallazgo (has descubierto qué columna tiene los números más grandes).
  2. Creer en los clusters porque el algoritmo los devolvió. K-Means sobre ruido devuelve k clusters, siempre.
  3. Usar t-SNE/UMAP como features de un modelo o interpretar sus distancias inter-cluster cuantitativamente.
  4. Elegir k=17 porque maximiza una métrica cuando marketing solo puede gestionar 5 segmentos: optimizar el número equivocado.
  5. Aplicar fit_transform del scaler a datos nuevos, rompiendo la coherencia del espacio.
  6. Meter el ID de cliente o variables casi-constantes como features del clustering.
  7. Ignorar los outliers antes de K-Means: tres devoluciones millonarias pueden convertirse en su propio "segmento" y desplazar todos los centroides.
  8. En anomalías, fijar contamination sin pensar y saturar al equipo con 17.000 alertas/día.
  9. Interpretar componentes de PCA como si fueran features físicas sin mirar sus cargas (pca.components_).
  10. No re-validar nunca: entregar la segmentación de 2024 en 2026 como si los clientes no hubieran cambiado.

12. Errores comunes

Error Síntoma Causa Solución
No escalar Los clusters replican la feature de mayor rango Distancia euclídea dominada por esa columna StandardScaler antes de fit
k mal elegido Segmentos ininterpretables o triviales Solo codo, o ningún criterio Codo + silueta + negocio
Outliers en K-Means Un cluster de 3 puntos; centroides desplazados La media es sensible a extremos Limpiar/log-transformar, o DBSCAN
eps demasiado pequeño en DBSCAN 80% de puntos marcados como ruido Vecindad más estrecha que la densidad real Gráfico k-distancia; subir eps
eps demasiado grande Todo en un único mega-cluster Vecindad conecta clusters distintos Bajar eps
PCA sin escalar PC1 = la feature de varianza gigante, "99% explicado" Varianza en unidades no comparables Estandarizar antes
Interpretar distancias de t-SNE "Estos clusters están el doble de lejos" t-SNE no preserva distancias globales Solo topología local; usar PCA para geometría
fit_transform en datos nuevos Asignaciones incoherentes en producción Re-ajustar el scaler cambia el espacio transform con el scaler guardado
Silueta con k=1 ValueError de sklearn La silueta necesita ≥ 2 clusters Rango de k desde 2
Fraude: features absolutas Falsas alarmas masivas en clientes atípicos legítimos Anomalía global ≠ anomalía contextual Features relativas al historial del sujeto
Confundir cluster con causa "El segmento 3 cancela más, el cluster lo explica" El clustering describe, no explica Análisis causal o experimentos aparte
Clusterizar embeddings sin normalizar Grupos por longitud del texto, no por tema Norma del vector como señal espuria normalize() L2 antes del clustering

13. FAQ — Preguntas frecuentes

1. ¿Cómo sé si mis datos tienen clusters "de verdad" antes de empezar? Mira la silueta media para varios k (si nunca supera ~0.25, mala señal), visualiza con PCA/UMAP buscando grumos, y compara la inercia con la de datos aleatorios del mismo rango (gap statistic). Y re-ejecuta con submuestras: los clusters reales son estables.

2. ¿K-Means puede usarse con variables categóricas? No directamente: la media de "Madrid" y "Sevilla" no existe. Opciones: one-hot + escalado (aceptable con pocas categorías), K-Modes/K-Prototypes (librería kmodes), o convertir las entidades a embeddings y clusterizar estos.

3. ¿Qué hago con los puntos que DBSCAN marca como ruido (-1)? Depende del objetivo: si buscas anomalías, ¡son tu resultado! Si buscas segmentos, puedes dejarlos sin segmento (honesto), asignarlos al cluster más cercano (pragmático) o revisar eps/min_samples si son demasiados.

4. ¿PCA antes o después de escalar? ¿Y PCA antes de clustering? Escalar SIEMPRE antes de PCA. Y PCA antes de clustering es una buena idea cuando hay muchas features correlacionadas o dimensión alta: reduce ruido y acelera; conserva ~90-95% de varianza y clusteriza las componentes.

5. ¿Cuál es la diferencia práctica entre reducir con PCA y seleccionar features? Selección elige un subconjunto de columnas originales (interpretables, descarta información de las no elegidas). PCA crea combinaciones nuevas (retiene información de todas, pero pierde interpretabilidad). Para explicar a negocio: selección. Para comprimir señal: PCA.

6. ¿La silueta sirve para DBSCAN? Con matices: la silueta asume clusters convexos y no contempla el ruido. Puedes calcularla excluyendo los puntos -1, pero penalizará injustamente las formas arbitrarias que son justo la gracia de DBSCAN. Para DBSCAN pesa más la validación visual y de dominio (o métricas como DBCV).

7. ¿Isolation Forest necesita datos "limpios" (sin anomalías) para entrenar? No: está diseñado para entrenarse con datos contaminados (por eso el parámetro contamination). Si dispones de un conjunto garantizadamente normal, también puedes entrenar solo con él y puntuar lo nuevo (régimen novelty detection, como One-Class SVM o LOF con novelty=True).

8. ¿Puedo evaluar mi clustering contra unas pocas etiquetas que sí tengo? Sí: con etiquetas parciales puedes usar métricas externas como ARI (Adjusted Rand Index) o NMI (Normalized Mutual Information) de sklearn.metrics sobre el subconjunto etiquetado. Es un excelente sanity check, pero recuerda que el clustering puede descubrir una estructura distinta (y válida) a la de tus etiquetas.

9. ¿Cuántos datos necesito para clusterizar? Menos de lo que crees para K-Means (cientos de puntos bastan si la estructura es clara), pero la dimensionalidad manda: con 50 features y 200 puntos, las distancias son casi ruido. Regla informal: muchas más filas que columnas; si no, reduce dimensión primero.

10. En la era de los LLMs, ¿sigue mereciendo la pena aprender K-Means? Más que nunca: los LLMs producen embeddings, y los embeddings se exploran con... clustering, PCA/UMAP y detección de anomalías. Este capítulo es la caja de herramientas para navegar el espacio semántico. Lo verás en el módulo 06-RAG.

14. Resumen

  • El aprendizaje no supervisado descubre estructura sin etiquetas; su evaluación es indirecta (métricas internas + juicio de negocio) porque no hay respuesta correcta.
  • K-Means: rápido y escalable; itera asignación→actualización hasta converger; exige elegir k (codo + silueta), asume clusters esféricos y requiere escalar siempre. k-means++ y n_init mitigan los óptimos locales.
  • Jerárquico: construye un dendrograma con todas las granularidades; ideal para datasets pequeños, estructuras anidadas y comunicación; no escala.
  • DBSCAN: clusters = zonas densas; encuentra formas arbitrarias y marca outliers (-1); falla con densidades variables (→ HDBSCAN).
  • PCA: proyecta a las direcciones de máxima varianza; elige componentes con la varianza acumulada; lineal y poco interpretable, pero imprescindible para comprimir y visualizar. t-SNE/UMAP: solo para mirar, nunca para pipelines.
  • Anomalías: modela lo normal y señala lo raro. Isolation Forest (lo raro se aísla rápido) es el default; LOF para anomalías locales. En producción: score continuo + umbral por capacidad humana.
  • En la era LLM, el no supervisado vive en los embeddings: clusterizar vectores semánticos es la técnica moderna de descubrimiento de temas.
  • El flujo profesional completo: agregar features → transformar y escalar → elegir k con silueta → clusterizar → perfilar en unidades de negocio → nombrar → accionar.

15. Bibliografía y recursos

  • scikit-learn — Clustering (guía oficial): https://scikit-learn.org/stable/modules/clustering.html
  • scikit-learn — Decomposition (PCA): https://scikit-learn.org/stable/modules/decomposition.html
  • scikit-learn — Novelty and Outlier Detection: https://scikit-learn.org/stable/modules/outlier_detection.html
  • Arthur, D. & Vassilvitskii, S. (2007). k-means++: The Advantages of Careful Seeding: https://theory.stanford.edu/~sergei/papers/kMeansPP-soda.pdf
  • Ester, M. et al. (1996). A Density-Based Algorithm for Discovering Clusters (DBSCAN): https://www.aaai.org/Papers/KDD/1996/KDD96-037.pdf
  • Liu, F. T., Ting, K. M. & Zhou, Z.-H. (2008). Isolation Forest: https://cs.nju.edu.cn/zhouzh/zhouzh.files/publication/icdm08b.pdf
  • van der Maaten, L. & Hinton, G. (2008). Visualizing Data using t-SNE: https://jmlr.org/papers/v9/vandermaaten08a.html
  • McInnes, L. et al. (2018). UMAP: Uniform Manifold Approximation and Projection: https://arxiv.org/abs/1802.03426
  • Wattenberg, M. et al. How to Use t-SNE Effectively (Distill): https://distill.pub/2016/misread-tsne/
  • James, G. et al. An Introduction to Statistical Learning (cap. 12, no supervisado, gratuito): https://www.statlearning.com/
  • Géron, A. (2022). Hands-On Machine Learning with Scikit-Learn, Keras & TensorFlow (3ª ed., caps. 8–9). O'Reilly.
  • HDBSCAN — documentación: https://hdbscan.readthedocs.io/

Anterior Módulo Siguiente
Capítulo 3: Clasificación 02-MACHINE-LEARNING Capítulo 5: Features, pipelines y validación