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¶
- ¿Qué es el aprendizaje no supervisado?
- K-Means a fondo
- Clustering jerárquico
- DBSCAN: clustering por densidad
- Reducción de dimensionalidad: PCA
- Detección de anomalías
- Conexión con la era LLM: clustering de embeddings
- Ejemplo integrador: segmentación RFM de clientes
- Caso empresarial: banco detectando transacciones anómalas
- Buenas prácticas
- Malas prácticas
- Errores comunes
- FAQ — Preguntas frecuentes
- Resumen
- 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_testcontra el que calcular accuracy. Dos segmentaciones distintas de los mismos clientes pueden ser ambas "válidas".
Consecuencias prácticas:
- La evaluación es indirecta. Usamos métricas internas (cohesión/separación de clusters, como la silueta) que miden geometría, no verdad.
- 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.
- Los hiperparámetros importan más. Elegir
ken K-Means oepsen DBSCAN cambia por completo el resultado, y no hay grid search cony_testque te salve. - 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"]
- Inicialización: coloca
kcentroides (al azar, o mejor, con k-means++, ver §2.4). - Asignación: asigna cada punto al centroide más cercano (distancia euclídea).
- Actualización: recalcula cada centroide como la media de los puntos asignados a él. (De ahí el nombre: K medias.)
- 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:
- Elige el primer centroide al azar entre los puntos.
- Para cada punto restante, calcula
D(x)²= distancia al cuadrado al centroide ya elegido más cercano. - Elige el siguiente centroide con probabilidad proporcional a
D(x)²→ los puntos lejanos de los centroides existentes tienen más probabilidad de ser elegidos. - Repite hasta tener
kcentroides.
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):
- Empieza con N clusters (cada punto es uno).
- Encuentra los dos clusters más cercanos y fúndelos.
- Repite hasta que quede un solo cluster.
- 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:
- Encuentra formas arbitrarias: lunas, anillos, espirales — cosas donde K-Means fracasa estrepitosamente.
- No necesitas fijar el número de clusters: emerge de los datos.
- 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 deeps.min_samples: número mínimo de puntos (incluido él mismo) dentro del radioepspara 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
epsglobal 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, sineps, disponible ensklearn.cluster.HDBSCANdesde 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
epsdelicada: pequeños cambios pueden alterar mucho el resultado. - Sin
predictnativo 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) | Sí | Casi (orden puede afectar fronteras) |
predict en datos nuevos |
Sí | 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:
- No puedes visualizar más de 3 dimensiones. ¿Cómo "miras" un dataset de 50 columnas?
- 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.
- 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:
- Centra los datos (resta la media de cada columna; con
StandardScalerademás divides por la desviación, recomendado). - Calcula la matriz de covarianzas entre features (quién varía junto con quién).
- Calcula sus autovectores y autovalores (o, en la práctica, la descomposición SVD, numéricamente más estable — es lo que hace sklearn).
- Los autovectores son las componentes principales: direcciones perpendiculares entre sí, ordenadas por el autovalor (= cuánta varianza captura cada una).
- Proyecta los datos sobre las primeras
n_componentsdirecciones: 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¶
- Visualizar datos de alta dimensión (incluidos embeddings de LLMs) en 2D/3D para inspección humana.
- Acelerar modelos: entrenar sobre 50 componentes en vez de 500 features reduce tiempo y memoria; con features muy correlacionadas incluso mejora la generalización.
- 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). - Descorrelacionar features para algoritmos sensibles a la multicolinealidad (regresión lineal, por ejemplo).
- 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_neighborses inadecuado. - t-SNE no tiene
transformpara 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 (necesitanovelty=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 |
Sí |
| 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)¶
- 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.
- 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.
- 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. - 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¶
- 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.
- Combina criterios para elegir k: codo + silueta + interpretabilidad de negocio. Si los tres apuntan a sitios distintos, desconfía de que haya clusters reales.
- Perfila los clusters en unidades originales (
inverse_transformo agregaciones sobre los datos crudos) y ponles nombres de negocio. Un cluster sin nombre ni acción es un número, no un insight. - Trata la asimetría antes de clusterizar (log1p en variables de dinero/conteos): las colas largas secuestran la geometría.
- Fija
random_statey guarda scaler + modelo juntos (mismo pipeline en entrenamiento y en asignación de puntos nuevos). - Valida la estabilidad: re-ejecuta con otras semillas y submuestras (bootstrap). Si los clusters cambian radicalmente, no eran estructura, eran ruido.
- En anomalías, entrega scores rankeados, no binarios, y fija el umbral según la capacidad de revisión humana.
- Reentrena periódicamente: lo "normal" y los segmentos derivan con el tiempo (drift).
- Para embeddings: normaliza L2, considera reducir dimensión, y valora HDBSCAN antes que K-Means.
- Documenta las decisiones subjetivas (k elegido, features incluidas, transformaciones): en no supervisado hay muchas, y el "yo futuro" las agradecerá.
11. Malas prácticas¶
- Clusterizar sin escalar y presentar los resultados como hallazgo (has descubierto qué columna tiene los números más grandes).
- Creer en los clusters porque el algoritmo los devolvió. K-Means sobre ruido devuelve k clusters, siempre.
- Usar t-SNE/UMAP como features de un modelo o interpretar sus distancias inter-cluster cuantitativamente.
- Elegir k=17 porque maximiza una métrica cuando marketing solo puede gestionar 5 segmentos: optimizar el número equivocado.
- Aplicar
fit_transformdel scaler a datos nuevos, rompiendo la coherencia del espacio. - Meter el ID de cliente o variables casi-constantes como features del clustering.
- Ignorar los outliers antes de K-Means: tres devoluciones millonarias pueden convertirse en su propio "segmento" y desplazar todos los centroides.
- En anomalías, fijar
contaminationsin pensar y saturar al equipo con 17.000 alertas/día. - Interpretar componentes de PCA como si fueran features físicas sin mirar sus cargas (
pca.components_). - 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++yn_initmitigan 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 |