Apéndice B — Caso aplicado: comparar PCA, t-SNE y UMAP en los mismos datos

estadistica
reduccion-dimensionalidad
caso-practico
tsne
umap
Autor/a

Wilder Ramírez Delgado

Fecha de publicación

2 de octubre de 2026

De la intuición a los números

El capítulo anterior mostró, de forma visual, cómo PCA, MDS, t-SNE y UMAP proyectan el mismo conjunto de vinos a 2 dimensiones, y advirtió que las distancias en t-SNE y UMAP no son literales. Este caso aplicado reemplaza esa intuición visual por dos métricas concretas de qué tanta estructura conserva cada método, y usa esas métricas para responder una pregunta de producción: si el objetivo es alimentar un modelo predictivo en vez de solo mirar un gráfico, ¿cuál conviene usar?

Objetivos de este caso

Al terminar vas a ser capaz de:

  • calcular y leer el trustworthiness de una proyección, una métrica de preservación local;
  • comparar la preservación global de distancias entre métodos mediante correlación de rangos;
  • explicar por qué t-SNE y MDS clásico no son aptos para un pipeline que debe transformar datos nuevos;
  • decidir, con una comparación numérica, si conviene proyectar a 2D para un modelo predictivo o no.

Preparación del entorno

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.spatial.distance import pdist
from scipy.stats import spearmanr
from sklearn.datasets import load_wine
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.manifold import TSNE, trustworthiness
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
import umap

SEMILLA = 2026
COLORES = ['#2A6F97', '#D97706', '#2D6A4F']
plt.rcParams.update({
    'figure.figsize': (9, 5),
    'axes.spines.top': False,
    'axes.spines.right': False,
    'axes.titleweight': 'bold',
})
C:\Users\wilde\.conda\envs\ANALITICA\Lib\site-packages\tqdm\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

Los datos

Mismo conjunto que en el capítulo conceptual: 178 vinos, 13 variables químicas, 3 clases. Igual que siempre, estandarizamos antes de proyectar.

wine = load_wine(as_frame=True)
X = wine.data.copy()
y = wine.target.copy()

escalador = StandardScaler()
X_std = escalador.fit_transform(X)

print(f'Observaciones: {X.shape[0]}, variables: {X.shape[1]}')
Observaciones: 178, variables: 13

Métrica 1: trustworthiness (preservación local)

El trustworthiness mide si los vecinos más cercanos de un punto en la proyección también eran sus vecinos más cercanos en el espacio original. Va de 0 a 1: 1 significa que la vecindad local se preservó perfectamente; valores bajos indican que la proyección “inventó” vecinos que no lo eran. Es la métrica natural para evaluar t-SNE y UMAP, porque es justo lo que esos métodos intentan optimizar — a diferencia del estrés de MDS o la varianza explicada de PCA, que miden preservación global.

proyecciones = {
    'PCA': PCA(n_components=2, random_state=SEMILLA).fit_transform(X_std),
    't-SNE': TSNE(n_components=2, perplexity=30, random_state=SEMILLA, init='pca', learning_rate='auto').fit_transform(X_std),
    'UMAP': umap.UMAP(n_components=2, n_neighbors=15, min_dist=0.1, random_state=SEMILLA).fit_transform(X_std),
}

filas_confianza = []
for nombre, proy in proyecciones.items():
    for k in (5, 10):
        t = trustworthiness(X_std, proy, n_neighbors=k)
        filas_confianza.append({'metodo': nombre, 'k_vecinos': k, 'trustworthiness': t})

tabla_confianza = pd.DataFrame(filas_confianza).pivot(index='metodo', columns='k_vecinos', values='trustworthiness')
display(tabla_confianza.round(3))
C:\Users\wilde\.conda\envs\ANALITICA\Lib\site-packages\umap\umap_.py:1952: UserWarning: n_jobs value 1 overridden to 1 by setting random_state. Use no seed for parallelism.
  warn(
k_vecinos 5 10
metodo
PCA 0.871 0.888
UMAP 0.959 0.951
t-SNE 0.967 0.956

Cómo leer esta tabla

Cada celda es el trustworthiness de ese método con ese número de vecinos considerados. Con 5 vecinos, t-SNE llega a 0.967, UMAP a 0.959 y PCA a 0.871; con 10 vecinos el orden se mantiene (0.956, 0.951 y 0.888). En este dataset los no lineales sí mejoran la vecindad local respecto de PCA, y la brecha (unos 8 a 10 puntos porcentuales) se sostiene tanto con 5 como con 10 vecinos, así que no es ruido de una sola elección de $. Aun así, con solo 13 variables de partida (una dimensionalidad moderada) y clases ya bastante separables de forma lineal, la ventaja de los métodos no lineales acá es más modesta que la que suelen mostrar en datasets de miles de variables con estructura fuertemente curva — el vino no fuerza demasiado los límites de PCA.

Métrica 2: correlación de distancias (preservación global)

Trustworthiness mide vecindad local; para preservación global comparamos directamente las distancias por pares. Tomamos todas las distancias euclidianas entre pares de observaciones en el espacio original de 13 variables, las mismas distancias entre pares en cada proyección 2D, y calculamos la correlación de Spearman entre ambas listas: si vale 1, el orden de “quién está más lejos de quién” se preservó exactamente; si vale 0, no hay relación.

distancias_originales = pdist(X_std)

filas_correlacion = []
for nombre, proy in proyecciones.items():
    distancias_proyectadas = pdist(proy)
    rho, _ = spearmanr(distancias_originales, distancias_proyectadas)
    filas_correlacion.append({'metodo': nombre, 'correlacion_spearman': rho})

tabla_correlacion = pd.DataFrame(filas_correlacion).set_index('metodo')
display(tabla_correlacion.round(3))
correlacion_spearman
metodo
PCA 0.824
t-SNE 0.780
UMAP 0.755

Cómo leer este resultado

PCA conserva mejor el orden global de distancias (0.824), delante de t-SNE (0.780) y UMAP (0.755). Es esperable que PCA puntúe más alto acá que en trustworthiness: está optimizado exactamente para este tipo de preservación global, mientras que t-SNE sacrifica distancia global a propósito para ganar nitidez en la vecindad local — las dos tablas de esta sección miden objetivos distintos, no la misma cosa con otra fórmula.

La pregunta que de verdad separa “visualizar” de “predecir”

Las dos métricas anteriores comparan qué tan fiel es cada proyección. Pero para usar una proyección dentro de un modelo predictivo hace falta algo más básico: poder aplicarle la misma transformación ya aprendida a observaciones nuevas, sin tener que volver a ajustar el método desde cero cada vez que llega un dato. Esto es justamente lo que distingue a fit_transform de fit + transform por separado:

  • PCA: aprende una matriz de proyección fija; .transform() sobre datos nuevos es directo.
  • UMAP: también aprende una función reutilizable; soporta .transform() sobre datos nuevos (con alguna pérdida de fidelidad frente a reajustar todo, pero es una operación válida y rápida).
  • MDS clásico y t-SNE: no aprenden una función reutilizable. Solo saben proyectar el conjunto exacto con el que fueron ajustados; no hay .transform() para datos nuevos sin reajustar todo el modelo con los datos viejos y nuevos juntos.

Por esta razón, en la comparación predictiva que sigue solo entran PCA y UMAP — son los dos métodos de este capítulo que se pueden meter dentro de un Pipeline de scikit-learn sin incurrir en fuga de información ni en tener que reajustar sobre el conjunto de prueba.

Comparación predictiva: PCA(2D) frente a UMAP(2D) frente a las 13 variables originales

Partimos los datos en entrenamiento y prueba, igual que en el capítulo de PCA, y comparamos tres pipelines de clasificación. Fijamos 2 componentes en los tres casos a propósito — no es un valor optimizado por validación cruzada, es el mismo número de dimensiones que usamos para graficar arriba, para que la comparación sea justa contra esas imágenes.

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=SEMILLA, stratify=y
)

modelo_completo = Pipeline([
    ('escalador', StandardScaler()),
    ('clasificador', LogisticRegression(max_iter=2000)),
])
modelo_pca = Pipeline([
    ('escalador', StandardScaler()),
    ('pca', PCA(n_components=2, random_state=SEMILLA)),
    ('clasificador', LogisticRegression(max_iter=2000)),
])
modelo_umap = Pipeline([
    ('escalador', StandardScaler()),
    ('umap', umap.UMAP(n_components=2, n_neighbors=15, min_dist=0.1, random_state=SEMILLA)),
    ('clasificador', LogisticRegression(max_iter=2000)),
])

resultados = []
for nombre, modelo in [('13 variables', modelo_completo), ('PCA (2D)', modelo_pca), ('UMAP (2D)', modelo_umap)]:
    modelo.fit(X_train, y_train)
    exactitud = accuracy_score(y_test, modelo.predict(X_test))
    resultados.append({'modelo': nombre, 'dimensiones': 13 if nombre == '13 variables' else 2, 'exactitud_test': exactitud})

tabla_resultados = pd.DataFrame(resultados)
display(tabla_resultados.style.format({'exactitud_test': '{:.2%}'}))
C:\Users\wilde\.conda\envs\ANALITICA\Lib\site-packages\umap\umap_.py:1952: UserWarning: n_jobs value 1 overridden to 1 by setting random_state. Use no seed for parallelism.
  warn(
  modelo dimensiones exactitud_test
0 13 variables 13 98.15%
1 PCA (2D) 2 94.44%
2 UMAP (2D) 2 96.30%

Cómo leer esta comparación

En el conjunto de prueba, las 13 variables llegan a 98.15% de exactitud; UMAP en 2D se queda en 96.30% y PCA en 2D en 94.44%. Pasar de 13 columnas a 2 cuesta unos pocos puntos: UMAP pierde menos que PCA en esta partición, y ninguno supera al modelo completo. Fíjate que el modelo con las 13 variables originales es, por construcción, al menos tan bueno como cualquier proyección a 2D podría serlo en el mejor de los casos — reducir dimensiones nunca agrega información, como mucho la reorganiza de forma más compacta. La pregunta relevante no es “¿la proyección gana?”, sino “¿la pérdida de exactitud al proyectar es aceptable a cambio de trabajar con menos columnas?” — y esa respuesta depende del problema, no de una regla general.

Decisión práctica: ¿cuántas dimensiones retener?

En el capítulo de PCA, la pregunta “¿cuántas dimensiones?” tenía una respuesta basada en un número objetivo: la curva de varianza explicada acumulada. UMAP no ofrece un equivalente directo — no existe una noción de “varianza explicada por componente” para n_neighbors o min_dist, porque UMAP no está optimizando varianza, está optimizando preservación de vecindad. Esto tiene una consecuencia práctica importante: si tu objetivo final es predecir, PCA te da una forma principiada de decidir cuántos componentes retener (como en el capítulo anterior); si tu objetivo es visualizar, fijar 2 o 3 dimensiones en UMAP o t-SNE es razonable porque ahí el número lo impone la pantalla, no un criterio estadístico. Usar UMAP o t-SNE como paso de reducción dentro de un pipeline predictivo es posible — y UMAP, a diferencia de t-SNE, lo permite técnicamente — pero conviene tratarlo como una decisión de ingeniería a validar con la métrica final del modelo (como hicimos arriba), no como una optimización de varianza.

Lo que debes llevarte de este caso

Método Preserva mejor Sirve para predecir datos nuevos Criterio para elegir dimensiones
PCA Estructura global (distancias/varianza) Sí Varianza explicada acumulada
MDS Distancias globales No Estrés frente a número de dimensiones
t-SNE Vecindad local No No aplica — valor fijado por visualización
UMAP Vecindad local, algo de estructura global Sí No aplica — validar con la métrica final

Si tienes que elegir uno solo para explorar datos nuevos sin mucho contexto previo, UMAP suele ser el mejor punto de partida: combina vecindad local razonable, rapidez y la posibilidad — poco común entre los métodos no lineales — de aplicarse a datos nuevos.