Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

pca.py

21_dimensionality_reduction/pca.py  ·  115 lines  ·  view on GitHub

"""
PCA: Principal Component Analysis
Interview question: "Implement PCA from scratch"

Mathematical Formulation:
1. Center data: X_centered = X - mean(X)
2. Covariance matrix: C = (1/n) X_centered^T @ X_centered
3. Eigenvalue decomposition: C = Q Λ Q^T
   - Q: Eigenvectors (principal components)
   - Λ: Eigenvalues (explained variance)
4. Select top k components: Q_k = Q[:, :k]
5. Project: Y = X_centered @ Q_k

Why it works:
- Principal components are directions of maximum variance
- First PC captures most variance
- PCs are orthogonal (uncorrelated)
- Minimizes reconstruction error
"""
import numpy as np

def pca(X: np.ndarray, n_components: int) -> tuple:
    """
    PCA from scratch
    
    Steps:
    1. Center data (subtract mean)
    2. Compute covariance matrix
    3. Eigenvalue decomposition
    4. Select top n_components eigenvectors
    5. Project data
    
    Args:
        X: Data matrix (n_samples, n_features)
        n_components: Number of components to keep
    
    Returns:
        (transformed_data, components, explained_variance)
    """
    # Center data
    X_mean = np.mean(X, axis=0)
    X_centered = X - X_mean
    
    # Covariance matrix: C = (1/n) X^T X
    n_samples = X.shape[0]
    cov_matrix = (X_centered.T @ X_centered) / (n_samples - 1)
    
    # Eigenvalue decomposition: C = Q Λ Q^T
    eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
    
    # Sort by eigenvalue (descending)
    idx = np.argsort(eigenvalues)[::-1]
    eigenvalues = eigenvalues[idx]
    eigenvectors = eigenvectors[:, idx]
    
    # Select top n_components
    components = eigenvectors[:, :n_components]
    explained_variance = eigenvalues[:n_components]
    
    # Project data: Y = X @ components
    X_transformed = X_centered @ components
    
    return X_transformed, components, explained_variance

def explained_variance_ratio(explained_variance: np.ndarray) -> np.ndarray:
    """Compute explained variance ratio"""
    total_variance = np.sum(explained_variance)
    return explained_variance / total_variance

def reconstruct(X_transformed: np.ndarray, components: np.ndarray, 
                X_mean: np.ndarray) -> np.ndarray:
    """
    Reconstruct original data from PCA components
    
    X_reconstructed = X_transformed @ components^T + mean
    """
    return X_transformed @ components.T + X_mean


# Usage Example
if __name__ == "__main__":
    print("PCA from Scratch")
    print("=" * 60)
    
    # Generate sample data
    np.random.seed(42)
    n_samples = 100
    n_features = 10
    
    # Create correlated data
    X = np.random.randn(n_samples, n_features)
    # Add correlation
    X[:, 1] = X[:, 0] + 0.1 * np.random.randn(n_samples)
    
    print(f"Original data shape: {X.shape}")
    
    # Apply PCA
    n_components = 2
    X_transformed, components, explained_variance = pca(X, n_components)
    
    print(f"\nTransformed data shape: {X_transformed.shape}")
    print(f"Components shape: {components.shape}")
    print(f"Explained variance: {explained_variance}")
    
    # Explained variance ratio
    variance_ratio = explained_variance_ratio(explained_variance)
    print(f"\nExplained variance ratio: {variance_ratio}")
    print(f"Total variance explained: {np.sum(variance_ratio):.4f}")
    
    # Reconstruction
    X_mean = np.mean(X, axis=0)
    X_reconstructed = reconstruct(X_transformed, components, X_mean)
    reconstruction_error = np.mean((X - X_reconstructed)**2)
    print(f"\nReconstruction error (MSE): {reconstruction_error:.6f}")