Sesión 5: Análisis Discriminante y Pipelines - Explicación Exhaustiva

Linear Discriminant Analysis (LDA)

Linear Discriminant Analysis (LDA) es un algoritmo de machine learning supervisado que combina dos funciones principales: clasificación y reducción de dimensionalidad. A diferencia de PCA, LDA utiliza las etiquetas de clase para encontrar las direcciones que mejor separan las diferentes clases en el espacio de características.

Fundamentos Matemáticos del LDA

LDA busca maximizar la separación entre clases minimizando la varianza dentro de cada clase. Matemáticamente, esto se logra encontrando los eigenvectores de la matriz S_W^(-1) × S_B, donde:

  • S_W: Matriz de dispersión intra-clase (within-class scatter matrix)
  • S_B: Matriz de dispersión inter-clase (between-class scatter matrix)

La fórmula matemática para LDA implica:

S_W = Σ(i=1 to c) Σ(x∈Ci) (x - μi)(x - μi)^T
S_B = Σ(i=1 to c) ni(μi - μ)(μi - μ)^T

Donde c es el número de clases, μi es la media de la clase i, y μ es la media global.

Implementación Práctica de LDA

# Ejemplo 1: Linear Discriminant Analysis (LDA) básico
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, accuracy_score

# Cargar dataset
iris = load_iris()
X = iris.data
y = iris.target

# Dividir datos
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# Escalar datos
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# Aplicar LDA
lda = LinearDiscriminantAnalysis(n_components=2)  # Reducir a 2 componentes
X_train_lda = lda.fit_transform(X_train_scaled, y_train)
X_test_lda = lda.transform(X_test_scaled)

print("Dimensiones originales:", X_train.shape)
print("Dimensiones después de LDA:", X_train_lda.shape)
print("Varianza explicada por componente:", lda.explained_variance_ratio_)

# Usar LDA para clasificación
lda_classifier = LinearDiscriminantAnalysis()
lda_classifier.fit(X_train_scaled, y_train)
y_pred = lda_classifier.predict(X_test_scaled)

print("\nAccuracy:", accuracy_score(y_test, y_pred))
print("\nReporte de clasificación:")
print(classification_report(y_test, y_pred, target_names=iris.target_names))

Limitaciones del LDA

Una limitación importante del LDA es que puede extraer como máximo c-1 componentes discriminantes, donde c es el número de clases. Esto significa que:

  • Para 2 clases: máximo 1 componente discriminante
  • Para 3 clases: máximo 2 componentes discriminantes
  • Para k clases: máximo k-1 componentes discriminantes

Diferencias Fundamentales: PCA vs LDA

La distinción entre PCA y LDA es crucial para seleccionar la técnica apropiada:

PCA (Principal Component Analysis)

  • Método no supervisado: No utiliza etiquetas de clase
  • Objetivo: Maximizar la varianza total de los datos
  • Uso: Exploración de datos, visualización, compresión
  • Componentes: Basados en la matriz de covarianza de X

LDA (Linear Discriminant Analysis)

  • Método supervisado: Requiere etiquetas de clase
  • Objetivo: Maximizar la separación entre clases
  • Uso: Clasificación, reducción dimensional supervisada
  • Componentes: Basados en la separabilidad de clases

Ejemplo Comparativo PCA vs LDA

# Ejemplo 2: Comparación PCA vs LDA
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_wine
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.model_selection import train_test_split

# Cargar dataset de vinos (3 clases)
wine = load_wine()
X = wine.data
y = wine.target

# Escalar datos
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Aplicar PCA
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)

# Aplicar LDA
lda = LinearDiscriminantAnalysis(n_components=2)
X_lda = lda.fit_transform(X_scaled, y)

print("Dataset de vinos - Comparación PCA vs LDA")
print("=" * 50)
print(f"Dimensiones originales: {X.shape}")
print(f"Dimensiones después de PCA: {X_pca.shape}")
print(f"Dimensiones después de LDA: {X_lda.shape}")

print(f"\nPCA - Varianza explicada: {pca.explained_variance_ratio_}")
print(f"PCA - Varianza total explicada: {pca.explained_variance_ratio_.sum():.3f}")

print(f"\nLDA - Varianza explicada: {lda.explained_variance_ratio_}")
print(f"LDA - Varianza total explicada: {lda.explained_variance_ratio_.sum():.3f}")

Pipelines en Scikit-Learn

Los Pipelines son una herramienta fundamental para automatizar el flujo de trabajo de machine learning. Permiten encadenar múltiples pasos de preprocesamiento y modelado en una secuencia ordenada.

Ventajas de los Pipelines

  1. Prevención de Data Leakage: Garantiza que las transformaciones se ajusten solo en datos de entrenamiento
  2. Código Limpio: Organiza el flujo de trabajo en pasos claros
  3. Reutilización: Fácil aplicación a nuevos datos
  4. Validación Cruzada: Integración seamless con técnicas de evaluación

Implementación de Pipeline Básico

# Ejemplo 3: Pipeline básico con scikit-learn
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.datasets import load_digits
from sklearn.metrics import classification_report

# Cargar dataset (dígitos manuscritos)
digits = load_digits()
X = digits.data  # 64 características (8x8 píxeles)
y = digits.target

# Dividir datos
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Crear pipeline - Automatización de pasos
pipeline = Pipeline([
    ('scaler', StandardScaler()),           # Paso 1: Escalar datos
    ('pca', PCA(n_components=20)),          # Paso 2: Reducción dimensional
    ('classifier', RandomForestClassifier(n_estimators=100, random_state=42))  # Paso 3: Clasificador
])

print("PIPELINE - Automatización del flujo de trabajo")
print("=" * 50)
print("Pasos del pipeline:")
for i, (name, transformer) in enumerate(pipeline.steps, 1):
    print(f"{i}. {name}: {type(transformer).__name__}")

# Entrenar pipeline (todos los pasos se ejecutan automáticamente)
pipeline.fit(X_train, y_train)

# Hacer predicciones (preprocessing automático)
y_pred = pipeline.predict(X_test)

# Evaluación
accuracy = pipeline.score(X_test, y_test)
print(f"\nAccuracy: {accuracy:.3f}")

# Validación cruzada con pipeline
cv_scores = cross_val_score(pipeline, X, y, cv=5, scoring='accuracy')
print(f"CV Accuracy: {cv_scores.mean():.3f} (+/- {cv_scores.std()*2:.3f})")

ColumnTransformers: Manejo de Datos Mixtos

El ColumnTransformer permite aplicar diferentes transformaciones a diferentes subsets de columnas simultáneamente. Es especialmente útil cuando se trabaja con datos que contienen variables numéricas y categóricas.

Funcionalidad del ColumnTransformer

  • Transformaciones Paralelas: Aplica diferentes preprocessors a diferentes columnas
  • Integración con Pipelines: Se combina perfectamente con Pipeline
  • Flexibilidad: Permite especificar transformaciones por tipo de dato

Ejemplo de ColumnTransformer

# Ejemplo 4: ColumnTransformer para diferentes tipos de variables
import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# Crear dataset sintético con variables mixtas
np.random.seed(42)
n_samples = 1000

data = {
    'age': np.random.randint(18, 80, n_samples),
    'income': np.random.normal(50000, 20000, n_samples),
    'education': np.random.choice(['High School', 'Bachelor', 'Master', 'PhD'], n_samples),
    'city': np.random.choice(['Madrid', 'Barcelona', 'Valencia', 'Sevilla'], n_samples),
    'experience': np.random.randint(0, 40, n_samples),
}

# Variable objetivo (basada en algunas características)
data['approved'] = (
    (data['age'] > 25) & 
    (data['income'] > 40000) & 
    (data['experience'] > 2)
).astype(int)

df = pd.DataFrame(data)

# Separar características y objetivo
X = df.drop('approved', axis=1)
y = df['approved']

# Identificar tipos de columnas
numerical_columns = ['age', 'income', 'experience']
categorical_columns = ['education', 'city']

# Crear ColumnTransformer
preprocessor = ColumnTransformer(
    transformers=[
        ('num', StandardScaler(), numerical_columns),
        ('cat', OneHotEncoder(drop='first', sparse_output=False), categorical_columns)
    ],
    remainder='passthrough'  # Mantener otras columnas sin cambios
)

# Pipeline completo
pipeline = Pipeline([
    ('preprocessor', preprocessor),
    ('classifier', RandomForestClassifier(n_estimators=100, random_state=42))
])

# Entrenar y evaluar
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
pipeline.fit(X_train, y_train)
accuracy = pipeline.score(X_test, y_test)

Escaladores: Normalización de Características

StandardScaler

El StandardScaler implementa la normalización Z-score:

Fórmula: z = (x - μ) / σ

  • Resultado: Media = 0, Desviación estándar = 1
  • Uso: Datos con distribución aproximadamente normal
  • Limitación: Sensible a outliers

MinMaxScaler

El MinMaxScaler escala las características a un rango específico:

Fórmula: X_scaled = (X - X_min) / (X_max - X_min)

  • Resultado: Rango por defecto
  • Uso: Cuando se requiere un rango específico
  • Limitación: Muy sensible a outliers

RobustScaler

El RobustScaler usa estadísticas robustas:

Fórmula: X_scaled = (X - median) / IQR

  • Resultado: No garantiza rango específico
  • Uso: Datos con muchos outliers
  • Ventaja: Resistente a outliers

Ejemplo Comparativo de Escaladores

# Ejemplo 5: Comparación de escaladores StandardScaler vs MinMaxScaler
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

# Crear datos con diferentes escalas y outliers
np.random.seed(42)
n_samples = 100

# Datos con diferentes escalas y algunos outliers
feature1 = np.random.normal(50, 15, n_samples)
feature1 = 150  # outlier
feature2 = np.random.normal(5000, 1000, n_samples) 
feature2 = 15000  # outlier
feature3 = np.random.normal(0.5, 0.1, n_samples)

X = np.column_stack([feature1, feature2, feature3])
feature_names = ['Edad', 'Salario', 'Score']

# Aplicar diferentes escaladores
scalers = {
    'StandardScaler': StandardScaler(),
    'MinMaxScaler': MinMaxScaler(),
    'RobustScaler': RobustScaler()
}

for name, scaler in scalers.items():
    X_scaled = scaler.fit_transform(X)
    df_scaled = pd.DataFrame(X_scaled, columns=feature_names)
    print(f"{name} - Estadísticas:")
    print(df_scaled.describe().round(3))

Pipeline Completo: Integrando Todos los Conceptos

Ejemplo Avanzado con LDA

# Ejemplo 6: Pipeline completo con LDA, ColumnTransformer y escaladores
import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer

# Cargar dataset
cancer = load_breast_cancer()
X = cancer.data
y = cancer.target

# Pipeline completo con múltiples pasos
full_pipeline = Pipeline([
    # Paso 1: Preprocesamiento diferenciado por tipo de variable
    ('preprocessor', ColumnTransformer(
        transformers=[
            ('num', StandardScaler(), numerical_features),
            ('cat', OneHotEncoder(drop='first', sparse_output=False), categorical_features)
        ]
    )),
    
    # Paso 2: Reducción dimensional con LDA
    ('lda', LinearDiscriminantAnalysis(n_components=1)),
    
    # Paso 3: Clasificador final
    ('classifier', RandomForestClassifier(n_estimators=100, random_state=42))
])

# Entrenar y evaluar
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
full_pipeline.fit(X_train, y_train)
test_score = full_pipeline.score(X_test, y_test)

Cuándo Usar Cada Técnica

Selección de Técnica de Reducción Dimensional

CriterioPCALDA
SupervisiónNo supervisadoSupervisado
ObjetivoMaximizar varianzaMaximizar separación de clases
Uso principalExploración, visualizaciónClasificación
LimitacionesIgnora clasesMáximo c-1 componentes
Mejor cuandoDatos sin etiquetasClasificación supervisada

Selección de Escalador

EscaladorMejor usoEvitar cuando
StandardScalerDistribución normalMuchos outliers
MinMaxScalerRango específico necesarioOutliers presentes
RobustScalerDatos con outliersSe necesita rango específico

Mejores Prácticas

Para LDA

  1. Preprocesamiento: Siempre escalar las características antes de aplicar LDA
  2. Validación: Usar validación cruzada para evaluar el rendimiento
  3. Componentes: Recordar la limitación de c-1 componentes máximo

Para Pipelines

  1. Orden: Mantener el orden lógico: preprocesamiento → reducción dimensional → modelado
  2. Naming: Usar nombres descriptivos para cada paso
  3. Validación: Aplicar validación cruzada al pipeline completo

Para ColumnTransformers

  1. Identificación: Identificar correctamente tipos de variables
  2. Transformaciones: Elegir transformaciones apropiadas para cada tipo
  3. Remainder: Decidir qué hacer con columnas no especificadas

La combinación de LDA, pipelines, ColumnTransformers y escaladores apropiados proporciona una base sólida para desarrollar flujos de trabajo de machine learning robustos y eficientes, especialmente en tareas de clasificación supervisada donde la reducción dimensional es beneficiosa.

Análisis Discriminante Linear (LDA) y Pipelines - Machine Learning con Scikit-Learn

Author

Juan Fuentes

Publish Date

06 - 22 - 2023