Reducción de dimensionalidad
1 La maldición de la dimensionalidad
Cuando un dataset tiene muchas variables (dimensiones), los modelos sufren varios problemas:
- Necesitan exponencialmente más datos para generalizar.
- Las distancias se vuelven uniformes: en 100 dimensiones, la distancia entre cualquier par de puntos tiende a ser casi la misma, lo que arruina métodos como KNN.
- El sobreajuste es casi inevitable: el modelo memoriza el ruido en lugar de aprender patrones.
- La visualización es imposible: no puedes graficar 100 dimensiones.
La reducción de dimensionalidad proyecta los datos a un espacio de menor dimensión preservando la mayor información posible.
Nota
Diferencia con la selección de características: La selección elige un subconjunto de las variables originales. La reducción de dimensionalidad crea nuevas variables (componentes) que son combinaciones lineales o no lineales de las originales.
2 PCA: análisis de componentes principales
El Análisis de Componentes Principales (PCA) es el método más clásico y ampliamente usado. Encuentra los ejes (componentes principales) a lo largo de los cuales los datos tienen la máxima varianza.
2.1 Intuición geométrica
Imagina un dataset 2D donde los puntos forman una elipse alargada. PCA encuentra la dirección de la elipse (PC1, mayor varianza) y la dirección perpendicular (PC2, menor varianza). Si proyectas los puntos solo sobre PC1, pierdes poca información pero reduces las dimensiones de 2 a 1.
2.2 Matemáticas
Dado un dataset \(X\) de \(n\) observaciones y \(p\) variables (centrado para que cada columna tenga media cero):
- Calcular la matriz de covarianza:
- Descomponer en autovalores y autovectores:
donde \(V\) contiene los autovectores (direcciones) y \(\Lambda\) los autovalores (varianza explicada).
- Proyectar sobre los \(k\) componentes principales:
donde \(V_k\) son los \(k\) autovectores con mayor autovalor.
2.3 Ejemplo: California Housing (regresión)
Vamos a usar PCA para predecir el precio medio de viviendas en California, un dataset real con 8 features numéricas y objetivos que vamos a reducir:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
# Cargar dataset real
housing = fetch_california_housing()
X = pd.DataFrame(housing.data, columns=housing.feature_names)
y = housing.target
print(f"Features: {X.columns.tolist()}")
print(f"Dimensiones: {X.shape}")
print(f"\nPrimeras filas:")
print(X.head())
# MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude Longitude
# 0 8.3252 41.0 6.984127 1.023810 322.0 2.555556 37.88 -122.23
# 1 8.3014 21.0 6.238137 0.971880 2401.0 2.109842 37.86 -122.22
# 2 7.2574 52.0 8.288136 1.073446 496.0 2.802260 37.85 -122.24
print(f"\nTarget (precio medio en $100k): media={y.mean():.2f}, std={y.std():.2f}")
2.3.1 Paso 1: visualizar la varianza explicada
pca_full = PCA().fit(X)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Varianza por componente
axes[0].bar(range(1, len(pca_full.explained_variance_ratio_) + 1),
pca_full.explained_variance_ratio_, alpha=0.7, label='Individual')
axes[0].set_xlabel('Componente principal')
axes[0].set_ylabel('Varianza explicada')
axes[0].set_title('Varianza por componente')
axes[0].set_xticks(range(1, len(pca_full.explained_variance_ratio_) + 1))
axes[0].legend()
# Varianza acumulada
var_acumulada = np.cumsum(pca_full.explained_variance_ratio_)
axes[1].plot(range(1, len(var_acumulada) + 1), var_acumulada, 'bo-')
axes[1].axhline(y=0.95, color='r', linestyle='--', label='95% varianza')
axes[1].axhline(y=0.99, color='g', linestyle='--', label='99% varianza')
axes[1].set_xlabel('Número de componentes')
axes[1].set_ylabel('Varianza acumulada')
axes[1].set_title('Scree plot')
axes[1].legend()
axes[1].grid(True)
plt.tight_layout()
# ¿Cuántos componentes necesitamos?
n_95 = np.argmax(var_acumulada >= 0.95) + 1
n_99 = np.argmax(var_acumulada >= 0.99) + 1
print(f"Componentes para 95% varianza: {n_95}")
print(f"Componentes para 99% varianza: {n_99}")
print(f"Varianza por componente: {pca_full.explained_variance_ratio_.round(4)}")
En California Housing, las primeras 2-3 componentes suelen capturar ~70-80% de la varianza, ya que Latitude y Longitude están altamente correlacionadas (la ubicación geográfica es un factor dominante).
2.3.2 Paso 2: comparar regresión con y sin PCA
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# --- Modelo baseline: todas las features ---
lr_full = LinearRegression()
lr_full.fit(X_train, y_train)
r2_full = r2_score(y_test, lr_full.predict(X_test))
# --- Modelo con PCA ---
n_components = 4 # Elegido según scree plot
pca = PCA(n_components=n_components)
X_train_pca = pca.fit_transform(X_train)
X_test_pca = pca.transform(X_test)
lr_pca = LinearRegression()
lr_pca.fit(X_train_pca, y_train)
r2_pca = r2_score(y_test, lr_pca.predict(X_test_pca))
print(f"--- Regresión Lineal ---")
print(f"Baseline (8 features): R² = {r2_full:.4f}")
print(f"Con PCA ({n_components} components): R² = {r2_pca:.4f}")
print(f"Pérdida de información: {r2_full - r2_pca:.4f}")
print(f"Reducción dimensionalidad: {X_train.shape[1]} → {n_components} ({n_components/X_train.shape[1]:.0%})")
2.3.3 Paso 3: ¿Qué significan los componentes?
# Loadings: contribución de cada feature original a cada componente
loadings = pd.DataFrame(
pca.components_.T,
columns=[f'PC{i+1}' for i in range(n_components)],
index=X.columns
)
print("Loadings (contribución de cada feature):")
print(loadings.round(3))
# Interpretación típica en California Housing:
# PC1 → proxy de ubicación geográfica (Latitude, Longitude altos)
# PC2 → tamaño de la vivienda (AveRooms, AveBedrms altos)
# PC3 → demografía (Population, AveOccup altos)
# PC4 → nivel socioeconómico (MedInc alto)
Tip
¿Por qué PCA funciona aquí? Las 8 features originales tienen correlaciones ocultas: la ubicación (Latitude, Longitude) explica gran parte del precio; el tamaño de la vivienda (AveRooms, AveBedrms) está correlacionado. PCA descubre estas correlaciones y crea features compuestas más informativas.
Advertencia
PCA solo captura relaciones lineales. Si los datos tienen estructura no lineal (como un “swiss roll”), PCA producirá proyecciones poco informativas. Para datos no lineales, usa t-SNE o UMAP.
3 t-SNE: incrustación estocástica de vecinos distribuidos
t-SNE (t-distributed Stochastic Neighbor Embedding) es una técnica de visualización que preserva la estructura local de los datos: puntos que están cerca en el espacio original siguen cerca en la proyección.
3.1 Cómo funciona
- Calcula similitudes entre pares de puntos en el espacio original (usando distribuciones gaussianas).
- Calcula similitudes en el espacio de baja dimensión (usando distribuciones t de Student).
- Minimiza la divergencia Kullback-Leibler entre ambas distribuciones usando gradiente descendente.
from sklearn.manifold import TSNE
from sklearn.datasets import load_digits
import matplotlib.pyplot as plt
digits = load_digits()
X, y = digits.data, digits.target
tsne = TSNE(n_components=2, random_state=42, perplexity=30, n_iter=1000)
X_tsne = tsne.fit_transform(X)
plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap='tab10', alpha=0.7, s=10)
plt.colorbar(scatter, label='Dígito')
plt.title('t-SNE: dígitos manuscritos (64D → 2D)')
plt.tight_layout()
3.2 Parámetros importantes
| Parámetro | Qué controla | Efecto |
|---|---|---|
perplexity |
Número de vecinos considerados | bajo = estructura local, alto = estructura global |
n_iter |
Iteraciones del optimizador | más = mejor convergencia |
learning_rate |
Tamaño de paso | 200-500 suele funcionar bien |
Advertencia
t-SNE es solo para visualización, no para reducción de dimensionalidad real. No se puede usar para transformar datos nuevos (no tiene transform()), las distancias absolutas no son interpretables, y los resultados varían con la semilla. Para reducción de dimensionalidad en producción, usa PCA o UMAP.
4 UMAP: Uniform Manifold Approximation and Projection
UMAP es una alternativa moderna a t-SNE basada en teoría de variedades algebraicas y topología. Preserva tanto la estructura local como la global, y es significativamente más rápido.
import umap
reducer = umap.UMAP(n_components=2, random_state=42)
X_umap = reducer.fit_transform(X)
plt.figure(figsize=(10, 8))
plt.scatter(X_umap[:, 0], X_umap[:, 1], c=y, cmap='tab10', alpha=0.7, s=10)
plt.colorbar(label='Dígito')
plt.title('UMAP: dígitos manuscritos (64D → 2D)')
plt.tight_layout()
4.1 Comparación t-SNE vs. UMAP
| Característica | t-SNE | UMAP |
|---|---|---|
| Velocidad | Lento | Rápido |
| Preserva estructura global | Mal | Bien |
Soporte para transform() |
No | Sí |
| Escalabilidad | ~10k puntos | ~1M puntos |
| Reproducibilidad | Baja | Alta |
Tip
Para exploración de datos, UMAP es generalmente superior a t-SNE. Para reducción de dimensionalidad antes de un modelo, PCA sigue siendo la opción estándar por su simplicidad y eficiencia.
5 LDA: análisis discriminante lineal
El Análisis Discriminante Lineal (LDA) es una técnica de reducción de dimensionalidad supervisada: utiliza las etiquetas de clase (\(y\)) para encontrar los ejes que maximizan la separación entre clases.
5.1 PCA vs. LDA
| Característica | PCA | LDA |
|---|---|---|
| Supervisión | No supervisado | Supervisado |
| Objetivo | Maximizar varianza total | Maximizar separación entre clases |
| Requiere \(y\) | No | Sí |
| Ideal para | Exploración, preprocesado | Clasificación |
PCA encuentra ejes con mayor varianza; LDA encuentra ejes que mejor discriminan entre clases. Si las clases están bien separadas en cierta dirección, LDA la encontrará aunque esa dirección no tenga la mayor varianza total.
5.2 Matemáticas
LDA busca los vectores \(w\) que maximizan el cociente:
donde: - \(S_B\) es la matriz de dispersión entre clases ( Between-class scatter): mide cuán separadas están las medias de las clases. - \(S_W\) es la matriz de dispersión dentro de las clases (Within-class scatter): mide cuán dispersos están los puntos dentro de cada clase.
Intuición: LDA busca proyectar los datos de modo que las clases queden lo más separadas posible y los puntos de cada clase lo más agrupados posible.
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
iris = load_iris()
X, y = iris.data, iris.target
lda = LDA(n_components=2)
X_lda = lda.fit_transform(X, y)
print(f"Varianza explicada: {lda.explained_variance_ratio_}")
# [0.9912, 0.0088] → 99.1% de la información discriminativa en 1 componente
plt.figure(figsize=(8, 6))
for clase, nombre in enumerate(iris.target_names):
mask = y == clase
plt.scatter(X_lda[mask, 0], X_lda[mask, 1], label=nombre, alpha=0.7)
plt.xlabel(f'LD1 ({lda.explained_variance_ratio_[0]:.1%})')
plt.ylabel(f'LD2 ({lda.explained_variance_ratio_[1]:.1%})')
plt.title('LDA: Iris dataset (4D → 2D)')
plt.legend()
plt.tight_layout()
Nota
PCA vs. LDA en Iris: PCA proyecta maximizando la varianza (puede mezclar clases). LDA proyecta maximizando la separación (las clases quedan perfectamente separadas). Para un problema de clasificación, LDA es generalmente superior.
5.3 LDA como preprocesado para clasificación
from sklearn.pipeline import Pipeline
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score
# Pipeline: LDA → SVM
pipe_lda = Pipeline([
('lda', LDA(n_components=2)),
('svm', SVC(kernel='rbf'))
])
score = cross_val_score(pipe_lda, X, y, cv=5).mean()
print(f"Accuracy con LDA: {score:.4f}")
6 Análisis Factorial (SVD)
El Análisis Factorial busca factores latentes que expliquen las correlaciones entre las variables observadas. A diferencia de PCA, que crea componentes como combinaciones lineales, el análisis factorial asume que las variables observadas están generadas por factores subyacentes no observados.
6.1 SVD: Descomposición en Valores Singulares
La SVD es la técnica numérica detrás del análisis factorial. Descompone una matriz \(X\) en:
donde: - \(U\) es la matriz de vectores singulares izquierdos. - \(\Sigma\) es la matriz diagonal de valores singulares. - \(V^T\) es la transpuesta de la matriz de vectores singulares derechos.
Los valores singulares en \(\Sigma\) ordenados de mayor a menor indican la importancia de cada factor latente.
from numpy.linalg import svd
import numpy as np
np.random.seed(42)
X = np.random.randn(100, 10)
U, S, Vt = svd(X, full_matrices=False)
print(f"Valores singulares: {S.round(2)}")
print(f"Varianza explicada por factor: {(S**2 / (S**2).sum()).round(3)}")
# Reducir a 3 factores
X_reduced = U[:, :3] @ np.diag(S[:3])
print(f"Dimensiones: {X.shape} → {X_reduced.shape}")
from sklearn.decomposition import TruncatedSVD
# Equivalente usando scikit-learn
svd = TruncatedSVD(n_components=3, random_state=42)
X_reduced = svd.fit_transform(X)
print(f"Varianza explicada: {svd.explained_variance_ratio_.round(3)}")
print(f"Varianza total: {svd.explained_variance_ratio_.sum():.2%}")
Tip
¿Cuándo usar SVD en lugar de PCA? SVD es útil cuando la matriz \(X\) es muy grande y dispersa (por ejemplo, matrices TF-IDF de texto). TruncatedSVD no necesita centrar los datos, lo que lo hace más eficiente para datos dispersos. PCA centrará los datos, destruyendo la dispersidad.
6.2 PCA vs. Análisis Factorial vs. SVD
| Aspecto | PCA | Análisis Factorial | SVD |
|---|---|---|---|
| Modelo | Componentes = combinaciones lineales | Factores latentes generan variables | Descomposición matricial |
| Objetivo | Maximizar varianza | Explicar correlaciones | Aproximar matriz |
| Requiere correlación | No | Sí (entre variables) | No |
| Implementación | PCA |
FactorAnalysis |
TruncatedSVD |
from sklearn.decomposition import FactorAnalysis
fa = FactorAnalysis(n_components=3, random_state=42)
X_fa = fa.fit_transform(X)
print(f"Componentes (loadings):\n{fa.components_.round(3)}")
7 Apilamiento de K-means para reducción dimensional
Otra forma de reducir dimensiones es usar K-means como transformador: cada punto se reprsenta por su distancia a los centroides, en lugar de las coordenadas originales.
7.1 Cómo funciona
- Se aplica K-means a los datos de entrada con \(k\) centroides.
- Cada punto se transforma en un vector de \(k\) dimensiones: las distancias a cada centroide.
- El resultado es un dataset con \(k\) features en lugar de las \(p\) originales.
from sklearn.cluster import KMeans
from sklearn.datasets import load_digits
import pandas as pd
digits = load_digits()
X, y = digits.data, digits.target
print(f"Dimensiones originales: {X.shape}") # (1797, 64)
# Apilar K-means: 20 centroides → 20 features
kmeans = KMeans(n_clusters=20, n_init=10, random_state=42)
X_kmeans = kmeans.fit_transform(X)
print(f"Dimensiones tras K-means: {X_kmeans.shape}") # (1797, 20)
print(f"Reducción: {X.shape[1]} → {X_kmeans.shape[1]} features")
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
# Comparar rendimiento
rf = RandomForestClassifier(n_estimators=100, random_state=42)
score_original = cross_val_score(rf, X, y, cv=5).mean()
score_kmeans = cross_val_score(rf, X_kmeans, y, cv=5).mean()
print(f"Original (64 features): {score_original:.4f}")
print(f"K-means (20 features): {score_kmeans:.4f}")
Tip
Ventaja sobre PCA: K-means captura estructuras no lineales (los centroides pueden representar agrupaciones complejas). Desventaja: requiere elegir \(k\) y es más lento que PCA.
8 Autoencoders: reducción no lineal con redes neuronales
Los autoencoders son redes neuronales que aprenden una representación comprimida de los datos. Consisten en:
- Codificador (encoder): comprime la entrada a un vector de baja dimensión (cuello de botella).
- Cuello de botella (latent space): la representación reducida.
- Decodificador (decoder): reconstruye la entrada original desde la representación comprimida.
El objetivo es minimizar la reconstrucción: la salida debe ser lo más parecida posible a la entrada.
import torch
import torch.nn as nn
class Autoencoder(nn.Module):
def __init__(self, input_dim, latent_dim):
super().__init__()
self.encoder = nn.Sequential(
nn.Linear(input_dim, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, latent_dim)
)
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 64),
nn.ReLU(),
nn.Linear(64, 128),
nn.ReLU(),
nn.Linear(128, input_dim)
)
def forward(self, x):
z = self.encoder(x)
x_recon = self.decoder(z)
return x_recon
# Entrenar
model = Autoencoder(input_dim=784, latent_dim=32)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(100):
x_recon = model(X_train)
loss = criterion(x_recon, X_train)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Extraer representación reducida
X_compressed = model.encoder(X_test).detach().numpy()
9 Resumen
| Técnica | Tipo | Supervisión | Para qué sirve | Velocidad |
|---|---|---|---|---|
| PCA | Lineal | No supervisado | Reducción general + visualización | Muy rápida |
| LDA | Lineal | Supervisado | Separación entre clases | Rápida |
| SVD/Análisis Factorial | Lineal | No supervisado | Datos dispersos, factores latentes | Rápida |
| t-SNE | No lineal | No supervisado | Solo visualización | Lenta |
| UMAP | No lineal | No supervisado | Visualización + reducción | Rápida |
| Autoencoders | No lineal | No supervisado | Reducción compleja | Lenta |
¿Cuándo usar qué?
- PCA: Primera opción siempre. Rápido, interpretable, funciona bien en la mayoría de casos.
- LDA: Cuando tienes etiquetas de clase y quieres maximizar la separación entre clases.
- SVD/Análisis Factorial: Cuando trabajas con datos dispersos (texto TF-IDF) o buscas factores latentes interpretables.
- UMAP: Cuando PCA no captura la estructura (datos no lineales) o necesitas visualizar.
- t-SNE: Solo para exploración visual de datasets pequeños.
- Autoencoders: Cuando tienes muchos datos y la relación entrada→representación es muy compleja.