Curso de ML EN

Capítulo 27 de 37 · intermedio

Aprender sin etiquetas

Qué cubre este capítulo

Todo lo anterior venía con hoja de respuestas. Había una columna de etiquetas, un objetivo que igualar y un número — accuracy, RMSE, R² — que te decía sin rodeos si el modelo le atinaba. Este capítulo te quita la hoja de respuestas y ya no te la regresa. De aquí en adelante los datos son nada más puntos, sin verdad adjunta, y el trabajo cambia: ya no se trata de predecir una respuesta sino de encontrar la estructura que ya está ahí, dentro de los datos.

Eso es el aprendizaje no supervisado, y es otro trabajo con otra forma de fallar. Cuando no hay etiqueta no hay accuracy, así que lo difícil deja de ser "¿puedo ajustarlo?" y se convierte en "¿cómo sé si funcionó siquiera?". Este es el capítulo que le pone nombre a las familias que vas a conocer — clustering, reducción de dimensionalidad, estimación de densidad, detección de anomalías — y luego se pasa la mayor parte del tiempo en la pregunta honesta que ninguna de ellas te contesta gratis: cómo juzgas un resultado cuando nada te dice cuál es el correcto. Vamos a construir a mano, en NumPy, la única métrica que califica un clustering sin etiquetas, el silhouette score, y la vamos a usar para hacer algo que jamás vas a poder hacer con datos reales sin etiquetar: ver un número de calidad llegar a su máximo justo en la estructura que de verdad estaba ahí.

Los cuatro capítulos que siguen son los métodos mismos. El análisis de componentes principales comprime. K-means, clustering jerárquico y las mezclas gaussianas agrupan. Este capítulo es el marco de los cuatro: para qué sirve todo el asunto y cómo distingues una buena respuesta de una equivocada muy segura de sí misma.

Un poco de historia

La idea de encontrar grupos sin que nadie te diga cuáles son los grupos es más vieja que el machine learning y salió de las ciencias que tenían montones de mediciones y ninguna teoría para ordenarlas. En 1939 el psicólogo Robert Tryon publicó Cluster Analysis, un método del tamaño de un libro para agrupar variables según qué tanto correlacionaban, resuelto a mano para una disciplina que se estaba ahogando en datos de cuestionarios. Por esas mismas fechas los antropólogos Harold Driver y Alfred Kroeber hacían prácticamente lo mismo con rasgos culturales: calculaban similitudes entre tribus y las agrupaban. La frase "análisis de clusters" es de ellos y de Tryon, décadas antes de que una computadora corriera uno.

La otra raíz es el análisis factorial, y es todavía más antigua. En 1904 Charles Spearman notó que las calificaciones de los niños en materias escolares sin relación entre sí correlacionaban todas, y postuló un único factor oculto — la inteligencia general — que las movía, factor que dedujo a partir de la matriz de correlaciones. Louis Thurstone lo generalizó a múltiples factores en los años treinta. Quítale la psicología y el análisis factorial es reducción de dimensionalidad: muchas variables observadas explicadas por unas pocas latentes, que es exactamente la idea que se convierte en análisis de componentes principales en el siguiente capítulo. Toda la tradición no supervisada creció de este mismo impulso: hay más estructura en los datos de la que admiten las columnas, y la puedes recuperar sin que nadie la etiquete primero.

La intuición

El aprendizaje no supervisado no es un método, es un puñado de trabajos que comparten la misma limitante. Ayuda tener el mapa en la cabeza antes de entrarle a los capítulos individuales, porque todos son respuestas a "los datos no tienen etiquetas, ¿y ahora qué?".

El clustering agrupa puntos parecidos: k-means, clustering jerárquico, mezclas gaussianas. Crees que los datos caen en tipos y quieres que la máquina encuentre los tipos. La reducción de dimensionalidad comprime: PCA agarra cincuenta columnas correlacionadas y te entrega dos ejes que conservan la mayor parte de la variación, para que puedas graficar lo que antes no podías o meterle una tabla más chica a un modelo supervisado. La estimación de densidad aprende dónde viven los datos — la forma misma de la distribución — lo cual se convierte directamente en detección de anomalías: un punto en una zona de baja densidad, en medio de la nada, es candidato a outlier, a fraude, a sensor descompuesto. Trabajos distintos, una cosa en común: no hay columna objetivo, así que no hay accuracy.

Aquí está toda la dificultad en una sola imagen. A la izquierda están los datos crudos con los que trabaja este capítulo: una nube de puntos, en gris, sin etiquetas, exactamente lo que ve el algoritmo. A la derecha está la misma nube después de un clustering, coloreada por los grupos que descubrió. No entró ninguna etiqueta. Salió estructura.

Tu ojo hizo lo mismo que el algoritmo: encontró cuatro grupos en la versión gris antes de que leyeras la coloreada. El problema empieza cuando haces la pregunta obvia que sigue: ¿de verdad son cuatro? Podrían ser tres, podrían ser seis, y los datos no te lo van a decir. Esa pregunta es todo el capítulo, y la herramienta para contestarla es una métrica que califica un clustering usando nada más la geometría, sin necesidad de etiquetas.

Las matemáticas

La métrica es el silhouette, y califica un punto a la vez preguntando si ese punto está más cómodo en su propio cluster que en el rival más cercano. Toma un punto y llámale aa a la distancia promedio de ese punto a los demás puntos de su propio cluster: qué tan apretada está su casa. Luego mira cada uno de los otros clusters, promedia la distancia del punto a todos los miembros de cada uno, y quédate con el más chico de esos promedios; llámale bb: qué tan lejos está el cluster vecino más cercano. El coeficiente de silhouette de ese punto es:

s=bamax(a,b)s = \frac{b - a}{\max(a, b)}

Lee los dos extremos directo de la fórmula. Si el cluster propio del punto está mucho más apretado que el más cercano, aba \ll b, la fracción tiende a b/b=1b/b = 1: encaje perfecto. Si el punto cae justo en la frontera entre dos clusters, aba \approx b y s0s \approx 0. Si en realidad está más cerca de un cluster vecino que del suyo, a>ba > b y ss se va a negativo: lo más probable es que el punto esté mal asignado. Entonces cada punto recibe una calificación en [1,1][-1, 1], sin unidades, y el silhouette score de todo el clustering es simplemente el promedio sobre todos los puntos:

S=1Ni=1NsiS = \frac{1}{N} \sum_{i=1}^{N} s_i

El único caso borde es un cluster con un solo punto adentro: no hay más miembros con los cuales promediar una distancia, así que aa queda indefinida. Por convención ese punto saca s=0s = 0, que es lo mismo que hace scikit-learn. Todo en el silhouette son distancias entre puntos que ya tienes: sin objetivo, sin verdad. Eso es lo que lo hace usable en el único tipo de datos donde no tienes hoja de respuestas, que son todos los datos no supervisados.

En qué es bueno y en qué no

Lo genuinamente bueno aquí es que el aprendizaje no supervisado funciona con los datos que de verdad tienes. Las etiquetas son caras — alguien se tiene que sentar a etiquetar ejemplos — y la abrumadora mayoría de los datos llega sin ninguna. El clustering, la compresión y la detección de anomalías corren sobre tablas crudas sin etiquetar, y por eso esta familia es donde empieza la mayor parte del trabajo exploratorio real. Es como encuentras los segmentos de clientes que nadie definió, como exprimes una matriz de features ancha hasta dejarla en algo que puedas graficar, o como marcas la transacción que no se parece a las demás. Brilla para exploración, para compresión y como paso de features que alimenta a un modelo supervisado más adelante.

Lo difícil es justo lo que lo hace poderoso. Sin etiqueta no existe un número único que signifique "correcto", así que la evaluación se parte en dos opciones honestas pero parciales. Las métricas internas juzgan un clustering por su propia geometría y no necesitan verdad: el silhouette (sin unidades, en [1,1][-1, 1]), o la inercia, la suma de distancias al cuadrado dentro de cada cluster (en unidades de datos²). Te dicen si los grupos están apretados y separados, no si significan algo. Las métricas externas como el índice de Rand ajustado (sin unidades, en [0.5,1][-0.5, 1], donde ≈0 es un clustering aleatorio y 1 es coincidencia perfecta) comparan tu clustering contra etiquetas conocidas, pero si tuvieras las etiquetas probablemente no estarías haciendo clustering. La trampa es tomar una buena calificación interna como prueba. Un silhouette limpio dice que los grupos son geométricamente nítidos; no dice nada sobre si esos grupos son los segmentos que le importan a tu negocio. La métrica es necesaria y ni de cerca suficiente, y olvidarlo es la forma clásica en que el trabajo no supervisado se echa a perder.

Los datos

Cuatro blobs gaussianos generados con make_blobs de scikit-learn, 180 puntos en 2-D, con una semilla aleatoria fija para que la instantánea sea estable. Dos features para que todo quepa en una página y puedas ver cada punto; cuatro centros lo bastante separados como para que un humano encuentre los grupos al instante, que es exactamente lo que quieres cuando el objetivo es verificar si un método no supervisado coincide con la respuesta obvia. El generador también regresa el id verdadero del blob de cada punto. Esos los guardamos, pero los tratamos como un sobre sellado: las coordenadas entran al clustering, y los ids salen hasta el mero final para calcular una métrica externa, para poder calificar un resultado como casi nunca puedes hacerlo con datos reales.

Constrúyelo, una función a la vez

El código desde cero esta vez no es un algoritmo de clustering: es la regla de medir. Cinco funciones cortas que convierten un conjunto de puntos y un clustering en un silhouette score, sin etiquetas por ningún lado. El clustering en sí lo tomamos prestado de la librería; juzgarlo es la parte que vale la pena construir a mano.

Todo descansa sobre las distancias entre puntos, así que las calculamos todas de una vez. Dados los puntos, queremos la matriz completa de distancias punto a punto — cada ii contra cada jj — y de ahí nada más indexamos:

def pairwise_distances(X):
    """Euclidean distance between every pair of points.

    X is (N, D); the result is (N, N), where entry (i, j) is ||x_i - x_j||.
    Broadcasting builds the whole matrix at once — the silhouette needs every
    point-to-point distance, so we pay for it once and index into it after.
    """
    diff = X[:, None, :] - X[None, :, :]        # (N, N, D)
    return np.sqrt((diff ** 2).sum(axis=2))     # (N, N)

Esa matriz (N, N) es todo el costo del método; el resto es contabilidad encima de ella. La primera cantidad es aa, la distancia promedio de un punto a los demás miembros de su propio cluster: qué tan apretada está su casa:

def intra_cluster(D, labels, i):
    """a_i: the mean distance from point i to the OTHER points in its cluster.

    Distances to points in a different cluster are ignored, and the point's
    distance to itself (a zero on the diagonal) is excluded from the average.
    A cluster of size one has no such neighbours, so a_i is undefined and we
    return NaN — the caller turns that into a silhouette of 0.
    """
    same = (labels == labels[i])
    same[i] = False                     # drop the point itself
    n = same.sum()
    if n == 0:
        return np.nan                   # singleton cluster
    return D[i, same].mean()

Fíjate en las dos exclusiones: quitamos el renglón del propio punto para que no esté midiendo la distancia a sí mismo, y si el cluster tiene únicamente ese punto no hay nada que promediar, así que regresamos NaN y dejamos que quien llama lo califique con cero. Sigue bb, la distancia al cluster rival más cercano: para cada uno de los otros clusters, la distancia promedio a sus miembros, y luego la más chica de esas:

def nearest_cluster(D, labels, i):
    """b_i: the smallest mean distance from point i to any OTHER cluster.

    For each cluster that isn't i's own, average i's distance to all its
    points; b_i is the minimum of those averages — the nearest rival cluster,
    the one i would defect to if it left home.
    """
    own = labels[i]
    best = np.inf
    for c in np.unique(labels):
        if c == own:
            continue
        mean_d = D[i, labels == c].mean()
        best = min(best, mean_d)
    return best

Con aa y bb en la mano, el silhouette por punto es la fórmula tal cual sale del papel. Recorremos cada punto, manejamos el caso del cluster de un solo elemento como cero, y combinamos en s=(ba)/max(a,b)s = (b - a) / \max(a, b):

def silhouette_samples(X, labels):
    """The silhouette coefficient s_i for every point.

    Build the distance matrix once, then for each point combine its own-cluster
    tightness a and its nearest-rival distance b into s = (b - a) / max(a, b).
    Points in a singleton cluster score 0 by convention. Returns an (N,) array
    in [-1, 1].
    """
    D = pairwise_distances(X)
    labels = np.asarray(labels)
    s = np.zeros(len(X))
    for i in range(len(X)):
        a = intra_cluster(D, labels, i)
        if np.isnan(a):
            s[i] = 0.0                  # singleton: no self-comparison possible
            continue
        b = nearest_cluster(D, labels, i)
        s[i] = (b - a) / max(a, b)
    return s

Eso regresa una calificación por punto, cada una en [1,1][-1, 1]. La calificación de todo el clustering es el último paso, y es nada más el promedio:

def silhouette_score(X, labels):
    """The silhouette score of a clustering: the mean of s over all points.

    A single unitless number in [-1, 1]. Higher is better — points sit closer
    to their own cluster than to the nearest other. This is the whole label-free
    verdict on how well the clustering carved the data.
    """
    return float(silhouette_samples(X, labels).mean())

Un número sin unidades que dice qué tan bien el clustering partió los datos, calculado a partir de nada más que los puntos y las asignaciones de grupo. Ese es el veredicto sin etiquetas que vamos a barrer a lo largo de k.

Míralo funcionar

Aquí está la recompensa, y lo que nunca vas a poder hacer en el mundo real. No sabemos cuántos clusters tienen los datos — o hagamos como que no sabemos — así que probamos cada k del 2 al 8. Para cada uno hacemos clustering con k-means y calificamos el resultado con el silhouette que acabamos de construir. Dale play y observa dos cosas al mismo tiempo: el panel de arriba recolorea los puntos en k grupos, y el de abajo grafica el silhouette score de cada k conforme se va calculando. El pie de figura te dice la k y la calificación.

Mira la curva de abajo subir, llegar a su pico y caer. En k=2 el silhouette es 0.597 (sin unidades, en [1,1][-1, 1]): el clustering está fusionando grupos reales. Sube a 0.754 en k=3, luego a su pico de 0.788 en k=4, y de ahí se desploma: 0.654 en k=5, 0.566 en k=6, hasta llegar a los 0.44 y algo en k=8, donde k-means anda rebanando los blobs reales en pedazos arbitrarios. La métrica llega a su máximo en cuatro, y cuatro es la cantidad de blobs que construimos. Nada en ese barrido vio una sola etiqueta. El silhouette encontró el número verdadero de grupos usando pura geometría, que es toda la promesa de la validación interna: un número sin etiquetas que se dobla justo donde está la estructura real.

El mismo barrido como un par de curvas estáticas, para que puedas leer el doblez directamente. La curva de arriba es el silhouette (sin unidades, en [1,1][-1, 1], más alto es mejor, el pico va marcado en naranja); la de abajo es la inercia (en unidades de datos², la suma de distancias al cuadrado dentro de cada cluster, siempre bajando conforme crece k, así que lo que lees es su codo):

Las dos curvas apuntan a cuatro. El silhouette tiene ahí un pico claro — 0.788, la calificación más alta del barrido. La inercia no tiene pico, nada más cae, de 4,346 en k=2 a 970.9 en k=3 a 286.6 en k=4 (unidades de datos² en todos los casos), y luego la caída se aplana: k=5 apenas te baja a 254.2. Ese aplanamiento es el codo, y cae en la misma k que eligió el silhouette. Dos métricas internas distintas, calculadas con pura geometría, coincidiendo sobre la estructura. Cuando coinciden así de limpio puedes confiar. En el mundo real seguido no coinciden, y ahí regresas a usar criterio.

La implementación completa

El silhouette entero, sin librería, de arriba abajo. Esto es exactamente con lo que el barrido de arriba calificó cada clustering:

"""The silhouette score, built from scratch.

This is a methodology chapter, not an algorithm chapter, so the from-scratch
piece isn't a clustering method — it's the ruler you use to judge one. The
silhouette score asks, for every point, a purely geometric question: does this
point sit closer to its own cluster than to the nearest other cluster? It needs
no labels and no ground truth, only the points and a clustering someone already
produced. Pure NumPy — no ML library in this file.

For point i, with a = its mean distance to the other points in its own cluster
and b = the mean distance to the points of the nearest neighbouring cluster:

    s_i = (b - a) / max(a, b)         in [-1, 1]

s near 1 means the point fits its cluster far better than any other; s near 0
means it sits on a boundary; s below 0 means it would be happier somewhere else.
The silhouette score of a whole clustering is the mean of s over all points.
Singleton clusters have no intra-cluster distance to average, so their s is
defined to be 0 — the same convention scikit-learn uses.

Every function below appears in the chapter one step at a time (the
`# region:` markers are what the book's include directives pull in).
"""

import numpy as np
import pandas as pd


# region: pairwise
def pairwise_distances(X):
    """Euclidean distance between every pair of points.

    X is (N, D); the result is (N, N), where entry (i, j) is ||x_i - x_j||.
    Broadcasting builds the whole matrix at once — the silhouette needs every
    point-to-point distance, so we pay for it once and index into it after.
    """
    diff = X[:, None, :] - X[None, :, :]        # (N, N, D)
    return np.sqrt((diff ** 2).sum(axis=2))     # (N, N)
# endregion


# region: intra
def intra_cluster(D, labels, i):
    """a_i: the mean distance from point i to the OTHER points in its cluster.

    Distances to points in a different cluster are ignored, and the point's
    distance to itself (a zero on the diagonal) is excluded from the average.
    A cluster of size one has no such neighbours, so a_i is undefined and we
    return NaN — the caller turns that into a silhouette of 0.
    """
    same = (labels == labels[i])
    same[i] = False                     # drop the point itself
    n = same.sum()
    if n == 0:
        return np.nan                   # singleton cluster
    return D[i, same].mean()
# endregion


# region: nearest
def nearest_cluster(D, labels, i):
    """b_i: the smallest mean distance from point i to any OTHER cluster.

    For each cluster that isn't i's own, average i's distance to all its
    points; b_i is the minimum of those averages — the nearest rival cluster,
    the one i would defect to if it left home.
    """
    own = labels[i]
    best = np.inf
    for c in np.unique(labels):
        if c == own:
            continue
        mean_d = D[i, labels == c].mean()
        best = min(best, mean_d)
    return best
# endregion


# region: samples
def silhouette_samples(X, labels):
    """The silhouette coefficient s_i for every point.

    Build the distance matrix once, then for each point combine its own-cluster
    tightness a and its nearest-rival distance b into s = (b - a) / max(a, b).
    Points in a singleton cluster score 0 by convention. Returns an (N,) array
    in [-1, 1].
    """
    D = pairwise_distances(X)
    labels = np.asarray(labels)
    s = np.zeros(len(X))
    for i in range(len(X)):
        a = intra_cluster(D, labels, i)
        if np.isnan(a):
            s[i] = 0.0                  # singleton: no self-comparison possible
            continue
        b = nearest_cluster(D, labels, i)
        s[i] = (b - a) / max(a, b)
    return s
# endregion


# region: score
def silhouette_score(X, labels):
    """The silhouette score of a clustering: the mean of s over all points.

    A single unitless number in [-1, 1]. Higher is better — points sit closer
    to their own cluster than to the nearest other. This is the whole label-free
    verdict on how well the clustering carved the data.
    """
    return float(silhouette_samples(X, labels).mean())
# endregion


def load_data(path="../data/blobs.csv"):
    """make_blobs snapshot: 180 2-D points (x, y) plus the true blob id.

    The blob column is ground truth we NEVER cluster on — it exists only so we
    can compute an external metric (the adjusted Rand index) at the very end,
    the honest check you almost never get on real unlabeled data.
    """
    return pd.read_csv(path)

La versión con librería

En producción no lo escribirías a mano, y una vez que ya lo construiste no necesitas hacerlo. sklearn.metrics.silhouette_score calcula la misma geometría en un loop de C:

def sklearn_silhouette(X, labels):
    """The library silhouette: mean s over all points, unitless in [-1, 1].

    Same objective our from-scratch version computes, so the two numbers should
    agree to within floating-point noise on identical inputs.
    """
    return float(sk_silhouette(X, labels))

Los clusterings que juzgó el barrido salieron de sklearn.cluster.KMeans, la herramienta que el capítulo de aquí a dos construye a mano; aquí es una llamada de dos líneas que regresa etiquetas e inercia:

def cluster(X, k, seed=0):
    """Cluster X into k groups with KMeans. Returns labels and inertia.

    Inertia is the within-cluster sum of squared distances (data units^2) —
    an INTERNAL metric that always falls as k grows, so you read its elbow, not
    its minimum.
    """
    km = KMeans(n_clusters=k, n_init=10, random_state=seed).fit(X)
    return km.labels_, float(km.inertia_)

Esos dos — una herramienta de clustering y una métrica interna — son todo el loop de la evaluación no supervisada sin etiquetas. El lado de reducción de dimensionalidad de la familia es sklearn.decomposition.PCA, el tema del siguiente capítulo, referenciado aquí como lo que es, una llamada de una línea: entran muchas columnas, salen unos pocos ejes informativos:

def pca_project(X, n_components=2, seed=0):
    """Compress X to n_components with PCA — the dimensionality-reduction tool.

    Returns the projected coordinates. On already-2-D data this is a rotation,
    but it's the same one-line call you make to squeeze a 50-column table down
    to two axes you can actually plot. Its own chapter builds it from scratch.
    """
    return PCA(n_components=n_components, random_state=seed).fit_transform(X)

Y el día raro en que sí tienes etiquetas contra las cuales verificar, la métrica externa es adjusted_rand_score, que compara dos etiquetados después de corregir por azar:

def sklearn_ari(true_labels, pred_labels):
    """Adjusted Rand index: agreement between two labelings, chance-corrected.

    Unitless; ~0 for a random clustering, 1 for a perfect match, and it can go
    slightly negative for worse-than-random. This is an EXTERNAL metric — it
    needs the true labels, which unsupervised learning almost never has.
    """
    return float(adjusted_rand_score(true_labels, pred_labels))

Desde cero contra la librería

Dos cosas que verificar. Primero, que el silhouette escrito a mano sea el de verdad: sobre el clustering elegido de k=4, nuestra calificación desde cero es 0.7876 y la de sklearn es 0.7876 — idénticas hasta el último dígito, silhouette (sin unidades, en [1,1][-1, 1]). Misma geometría, misma respuesta, así que el número donde la animación llegó a su pico es confiable.

Segundo, el punto de todo el capítulo: ¿una buena calificación interna coincide con de verdad recuperar la estructura? Aquí abrimos el sobre sellado y calificamos el clustering contra los ids verdaderos de los blobs con el índice de Rand ajustado. Esto compara la k=4 elegida contra una k=6 plausible pero equivocada, tanto en una métrica interna (silhouette) como en una externa (ARI):

Coinciden, y ese es el caso tranquilizador. En la k=4 elegida el silhouette es 0.7876 (sin unidades, [1,1][-1, 1]) y el índice de Rand ajustado contra los blobs verdaderos es 1.0 (sin unidades, [0.5,1][-0.5, 1]): recuperación perfecta, cada punto en el grupo del que realmente salió. En la k=6 equivocada el silhouette baja a 0.5657 y el ARI a 0.8329: k-means partió blobs reales para inventarse dos clusters extra, la geometría empeoró, y la coincidencia con la verdad empeoró al parejo. La métrica sin etiquetas y la métrica con etiquetas se movieron juntas, que es el resultado que esperas y la razón por la que vale la pena confiar en el silhouette como sustituto cuando las etiquetas no están.

Pero fíjate en lo que tuvo que ser cierto para que ese párrafo existiera: teníamos etiquetas. La columna del ARI solo aparece porque este dataset venía con un sobre sellado. En los datos reales sin etiquetar que vas a agrupar, la mitad derecha de esa gráfica no existe. Te tocan el silhouette y el codo, y te toca decidir.

Puntos clave

Esta es la bisagra del curso. Todo lo anterior era supervisado — una etiqueta, un objetivo, un accuracy — y todo lo que sigue no lo es. El cambio mental es más grande que las matemáticas: dejas de preguntar "¿el modelo está en lo correcto?" y empiezas a preguntar "¿el resultado sirve?", y ningún número te contesta la segunda. El aprendizaje no supervisado es exploratorio por naturaleza. Lo corres para ver qué hay en los datos, no para pegarle a una calificación.

Lo cual convierte la validación en todo el trabajo, y la versión honesta es: usa métricas internas y tus propios ojos, nunca solo un número. El silhouette y el codo son señal real — cuando coinciden, como aquí, créeles — pero un silhouette nítido solo te dice que los grupos están geométricamente apretados, no que signifiquen algo para el problema. Un clustering puede sacar una calificación preciosa y ser inútil, y lo único que atrapa eso es un experto del dominio mirando los grupos y diciendo "sí, esos son segmentos reales" o "no, eso es un artefacto". Trata la métrica como filtro, no como veredicto.

Échale mano a esta familia cuando tengas datos y no tengas etiquetas, que es la mayoría de las veces. Es donde arranca el análisis exploratorio, es como comprimes una tabla ancha en algo que puedas ver o pasar hacia adelante, es como marcas la anomalía que no encaja, y es una fuente silenciosa de features para los modelos supervisados que vienen después: la membresía a un cluster es una columna más. Los siguientes cuatro capítulos son los métodos, uno por uno. PCA comprime. K-means, clustering jerárquico y mezclas gaussianas agrupan, cada uno relajando una suposición que hizo el anterior. Todos y cada uno te van a entregar una respuesta con total seguridad y sin manera de verificarla incluida. El silhouette que acabas de construir a mano es como la verificas.