๐Ÿง  PCA Fundamentals

๐ŸŽฏ What is PCA?

Principal Component Analysis (PCA) is a dimensionality reduction technique that transforms data into a new coordinate system where the greatest variance lies on the first axis (first principal component), the second greatest on the second axis, and so on. It's one of the most widely used techniques in data science and machine learning.

Why PCA Matters

Dimensionality reduction: Reduce features while preserving information. Visualization: Project high-dimensional data to 2D/3D. Noise reduction: Keep signal, discard noise. Speed: Faster training for ML models. Multicollinearity: Removes correlated features. Compression: Store data more efficiently.

๐Ÿ“Œ Dimensionality Reduction๐Ÿ“Œ Variance๐Ÿ“Œ Orthogonal๐Ÿ“Œ Linear
# PCA fundamentals (conceptual) import numpy as np from sklearn.decomposition import PCA from sklearn.datasets import load_iris # Load data iris = load_iris() X = iris.data # (150, 4) y = iris.target print(f"Original shape: {X.shape}") # Apply PCA pca = PCA(n_components=2) X_pca = pca.fit_transform(X) print(f"Reduced shape: {X_pca.shape}") print(f"Explained variance ratio: {pca.explained_variance_ratio_}") print(f"Total variance explained: {sum(pca.explained_variance_ratio_):.4f}") # PCA in one line: # 1. Center the data # 2. Compute covariance matrix # 3. Find eigenvectors/eigenvalues # 4. Sort by eigenvalue # 5. Project data onto top K eigenvectors # Manual implementation X_centered = X - X.mean(axis=0) cov_matrix = np.cov(X_centered.T) eigenvalues, eigenvectors = np.linalg.eig(cov_matrix) # Sort by eigenvalue (descending) idx = np.argsort(eigenvalues)[::-1] eigenvalues = eigenvalues[idx] eigenvectors = eigenvectors[:, idx] # Project X_manual = X_centered @ eigenvectors[:, :2] print(f"\nManual PCA shape: {X_manual.shape}")

๐Ÿ“Š Variance & Covariance

๐ŸŽฏ The Heart of PCA

Variance measures how spread out data is along a direction. Covariance measures how two variables vary together. PCA finds directions (principal components) that maximize variance and minimize covariance.

Variance & Covariance Formulas

Variance: Var(X) = (1/n) ฮฃ (x_i - ฮผ)ยฒ. Covariance: Cov(X,Y) = (1/n) ฮฃ (x_i - ฮผ_x)(y_i - ฮผ_y). Covariance matrix: nร—n symmetric matrix. Diagonal: Variances. Off-diagonal: Covariances. Goal: Diagonalize the covariance matrix.

๐Ÿ“Š Variance๐Ÿ”— Covariance๐Ÿ“ Matrix๐ŸŽฏ Diagonal
# Variance and covariance in PCA import numpy as np # Sample data np.random.seed(42) X = np.random.randn(100, 3) @ np.array([ [1.0, 0.7, 0.3], [0.0, 0.5, 0.2], [0.0, 0.0, 0.1] ]) print("=== Data Statistics ===") print(f"Shape: {X.shape}") print(f"Mean: {X.mean(axis=0)}") print(f"Standard deviation: {X.std(axis=0)}") # 1. Variance of each feature print("\n=== Variance ===") variance = X.var(axis=0) print(f"Variance per feature: {variance}") print(f"Total variance: {variance.sum()}") # 2. Covariance matrix print("\n=== Covariance Matrix ===") cov_matrix = np.cov(X.T) print(cov_matrix) # Interpretation: # - Diagonal: variance of each feature # - Off-diagonal: covariance between features # - Symmetric matrix # 3. Correlation matrix (normalized covariance) print("\n=== Correlation Matrix ===") corr_matrix = np.corrcoef(X.T) print(corr_matrix) # 4. Eigendecomposition of covariance eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix) # Sort descending idx = np.argsort(eigenvalues)[::-1] eigenvalues = eigenvalues[idx] eigenvectors = eigenvectors[:, idx] print("\n=== Eigendecomposition ===") print(f"Eigenvalues: {eigenvalues}") print(f"Sum of eigenvalues: {eigenvalues.sum()}") print(f"Variance explained: {eigenvalues / eigenvalues.sum()}") print(f"\nEigenvectors (columns):") print(eigenvectors) # 5. Verify: eigenvectors are orthogonal print("\n=== Orthogonality Check ===") for i in range(len(eigenvectors)): for j in range(i+1, len(eigenvectors)): dot = np.dot(eigenvectors[:, i], eigenvectors[:, j]) print(f"Dot(eig{i}, eig{j}) = {dot:.6f}") # 6. Project data print("\n=== Projected Data ===") X_centered = X - X.mean(axis=0) X_pca = X_centered @ eigenvectors print(f"Original shape: {X.shape}") print(f"Projected shape: {X_pca.shape}") print(f"Variance of projected: {X_pca.var(axis=0)}") print(f"Matches eigenvalues: {np.allclose(X_pca.var(axis=0), eigenvalues)}")

๐Ÿงฎ Eigenvalues & Eigenvectors

๐ŸŽฏ The Mathematical Foundation

An eigenvector of a matrix is a vector that doesn't change direction when the matrix is applied. The eigenvalue is how much it's scaled. In PCA, eigenvectors of the covariance matrix are the principal components; eigenvalues are their variances.

Eigendecomposition

Definition: A v = ฮป v. A: Covariance matrix. v: Eigenvector (principal direction). ฮป: Eigenvalue (variance). Ordering: Sort by ฮป descending. Interpretation: Largest ฮป = most important direction.

๐Ÿงฎ Eigenvalue๐Ÿงญ Eigenvector๐Ÿ“Š Variance๐ŸŽฏ Principal
# Eigenvalues and eigenvectors in PCA import numpy as np import matplotlib.pyplot as plt # Generate correlated 2D data np.random.seed(42) n = 500 mean = [0, 0] cov = [[3, 2], [2, 2]] X = np.random.multivariate_normal(mean, cov, n) # 1. Center data X_centered = X - X.mean(axis=0) # 2. Compute covariance matrix cov_matrix = np.cov(X_centered.T) # 3. Eigen decomposition eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix) idx = np.argsort(eigenvalues)[::-1] eigenvalues = eigenvalues[idx] eigenvectors = eigenvectors[:, idx] print("=== Covariance Matrix ===") print(cov_matrix) print("\n=== Eigenvalues (variances) ===") print(f"ฮป1 = {eigenvalues[0]:.4f} (PC1 - largest variance)") print(f"ฮป2 = {eigenvalues[1]:.4f} (PC2 - smallest variance)") print(f"Total variance: {eigenvalues.sum():.4f}") print(f"PC1 explains: {eigenvalues[0]/eigenvalues.sum()*100:.2f}%") print(f"PC2 explains: {eigenvalues[1]/eigenvalues.sum()*100:.2f}%") print("\n=== Eigenvectors (principal directions) ===") print(f"PC1: {eigenvectors[:, 0]}") print(f"PC2: {eigenvectors[:, 1]}") # 4. Verify: A @ v = ฮป @ v for i in range(2): v = eigenvectors[:, i] lam = eigenvalues[i] A_v = cov_matrix @ v lambda_v = lam * v print(f"\nVerify PC{i+1}: A@v = {A_v.round(4)}, ฮป*v = {lambda_v.round(4)}") # 5. Project data X_pca = X_centered @ eigenvectors print("\n=== Projected Data ===") print(f"Original variance: {X_centered.var(axis=0)}") print(f"PC1 variance: {X_pca[:, 0].var():.4f} (should โ‰ˆ ฮป1)") print(f"PC2 variance: {X_pca[:, 1].var():.4f} (should โ‰ˆ ฮป2)") # 6. Dimensionality reduction X_reduced = X_pca[:, :1] # Keep only PC1 X_reconstructed = X_reduced @ eigenvectors[:, :1].T + X.mean(axis=0) print("\n=== Dimensionality Reduction ===") print(f"Original shape: {X.shape}") print(f"Reduced shape: {X_reduced.shape}") print(f"Reconstruction error: {np.mean((X - X_reconstructed)**2):.4f}") # How much info did we lose? loss = eigenvalues[1] / eigenvalues.sum() print(f"\nInformation lost: {loss*100:.2f}%") print(f"Information kept: {(1-loss)*100:.2f}%") # Visualization plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1) plt.scatter(X_centered[:, 0], X_centered[:, 1], alpha=0.5) for i, (eigval, eigvec) in enumerate(zip(eigenvalues, eigenvectors.T)): plt.arrow(0, 0, eigvec[0]*np.sqrt(eigval)*2, eigvec[1]*np.sqrt(eigval)*2, color=['red', 'blue'][i], width=0.05, head_width=0.3) plt.text(eigvec[0]*np.sqrt(eigval)*2.2, eigvec[1]*np.sqrt(eigval)*2.2, f'PC{i+1}', color=['red', 'blue'][i], fontsize=12) plt.title('Original Data with Principal Components') plt.axis('equal') plt.grid(True, alpha=0.3) plt.subplot(1, 2, 2) plt.scatter(X_pca[:, 0], X_pca[:, 1], alpha=0.5, c='green') plt.title('Data in PC Space (Decorrelated)') plt.axis('equal') plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() # Eigenvalue decomposition properties print("\n=== Properties ===") print("1. Eigenvectors are orthogonal (perpendicular)") print("2. Eigenvalues = variance along each PC") print("3. Sum of eigenvalues = total variance") print("4. Covariance matrix in PC space is diagonal") print(f"5. Diagonal covariance: {np.diag(eigenvalues)}")

๐Ÿ”ข SVD Approach

๐ŸŽฏ Singular Value Decomposition

SVD is a more numerically stable way to compute PCA. Instead of computing the covariance matrix (which squares the condition number), SVD decomposes the data matrix directly.

SVD vs Eigendecomposition

SVD: X = U ฮฃ V^T. Eigendecomposition: Cov = V ฮ› V^T. Relationship: V = principal components, ฮฃยฒ / (n-1) = eigenvalues. Advantage: More numerically stable. Speed: Faster for tall matrices. Used by: scikit-learn's PCA.

๐Ÿ”ข SVD๐ŸŽฏ Stableโšก Fast๐Ÿ”ง Scikit-learn
# SVD-based PCA import numpy as np # Generate data np.random.seed(42) X = np.random.randn(100, 5) @ np.diag([5, 3, 1, 0.5, 0.1]) # Center data X_centered = X - X.mean(axis=0) # 1. SVD approach print("=== SVD Approach ===") U, S, Vt = np.linalg.svd(X_centered, full_matrices=False) print(f"U shape: {U.shape}") print(f"S shape: {S.shape}") print(f"Vt shape: {Vt.shape}") print(f"Singular values: {S}") # Principal components are rows of Vt # Variances are Sยฒ / (n-1) n = X_centered.shape[0] explained_variance = (S ** 2) / (n - 1) print(f"Explained variance: {explained_variance}") print(f"Explained variance ratio: {explained_variance / explained_variance.sum()}") # Project data X_pca_svd = U * S # or X_centered @ Vt.T # 2. Covariance approach (comparison) print("\n=== Covariance Approach ===") cov_matrix = np.cov(X_centered.T) eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix) idx = np.argsort(eigenvalues)[::-1] eigenvalues = eigenvalues[idx] eigenvectors = eigenvectors[:, idx] print(f"Eigenvalues: {eigenvalues}") print(f"Match SVD: {np.allclose(eigenvalues, explained_variance)}") # 3. Why SVD is better print("\n=== SVD vs Eigendecomposition ===") print("SVD:") print(" - More numerically stable") print(" - Works directly on data matrix") print(" - Avoids squaring condition number") print(" - Handles rank-deficient matrices") print(" - Faster for tall matrices (n >> p)") print("\nEigendecomposition:") print(" - Requires computing covariance matrix") print(" - Squares condition number (numerical issue)") print(" - Slower for tall matrices") # 4. Demonstrate numerical stability print("\n=== Numerical Stability Test ===") # Create ill-conditioned matrix np.random.seed(42) X_ill = np.random.randn(50, 10) X_ill[:, 9] = X_ill[:, 0] + 1e-10 * np.random.randn(50) # Nearly dependent # SVD U, S, Vt = np.linalg.svd(X_ill - X_ill.mean(axis=0), full_matrices=False) print(f"SVD singular values: {S[:5]}") # Covariance cov = np.cov((X_ill - X_ill.mean(axis=0)).T) eigs = np.linalg.eigvalsh(cov)[::-1] print(f"Cov eigenvalues: {eigs[:5]}") # 5. Truncated SVD (for sparse matrices) from scipy.sparse.linalg import svds from scipy.sparse import random as sparse_random # Sparse matrix X_sparse = sparse_random(1000, 100, density=0.05, format='csr', random_state=42) # Truncated SVD U_k, S_k, Vt_k = svds(X_sparse, k=10) # Sort descending idx = np.argsort(S_k)[::-1] S_k = S_k[idx] print(f"\nTruncated SVD:") print(f"Original shape: {X_sparse.shape}") print(f"U shape: {U_k.shape}") print(f"S shape: {S_k.shape}") print(f"Vt shape: {Vt_k.shape}") # 6. Randomized SVD (for very large matrices) from sklearn.utils.extmath import randomized_svd X_large = np.random.randn(10000, 100) U_r, S_r, Vt_r = randomized_svd(X_large, n_components=10, random_state=42) print(f"\nRandomized SVD:") print(f"Original: {X_large.shape}") print(f"Components: {len(S_r)}") print(f"Speed: Much faster than exact SVD for large matrices") # scikit-learn PCA uses SVD by default from sklearn.decomposition import PCA pca = PCA(n_components=3, svd_solver='full') # 'full', 'randomized', 'arpack' pca.fit(X_centered) print(f"\nSklearn PCA explained variance: {pca.explained_variance_}") print(f"Matches SVD: {np.allclose(pca.explained_variance_, explained_variance[:3])}")

๐Ÿ“‹ PCA Step-by-Step

๐ŸŽฏ The Complete Algorithm

PCA is a linear transformation with clearly defined steps. Understanding each step helps you apply PCA correctly and interpret the results.

The 6 Steps of PCA

1. Standardize: Center (and scale) the data. 2. Covariance: Compute covariance matrix. 3. Eigen: Find eigenvalues and eigenvectors. 4. Sort: Order by eigenvalue descending. 5. Select: Choose top K components. 6. Transform: Project data onto components.

๐Ÿ“Š Standardize๐Ÿ“ Covariance๐Ÿงฎ Eigen๐ŸŽฏ Project
# PCA from scratch โ€” complete implementation import numpy as np import matplotlib.pyplot as plt class PCAFromScratch: def __init__(self, n_components=None): self.n_components = n_components self.components_ = None self.explained_variance_ = None self.explained_variance_ratio_ = None self.mean_ = None self.scale_ = None def fit(self, X): """Fit PCA to data.""" n_samples, n_features = X.shape # Step 1: Standardize (center and optionally scale) self.mean_ = X.mean(axis=0) X_centered = X - self.mean_ # Optional: Scale to unit variance # self.scale_ = X.std(axis=0) # X_centered = X_centered / self.scale_ # Step 2: Compute covariance matrix cov_matrix = np.cov(X_centered, rowvar=False) # Step 3: Eigendecomposition eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix) # Step 4: Sort by eigenvalue descending idx = np.argsort(eigenvalues)[::-1] eigenvalues = eigenvalues[idx] eigenvectors = eigenvectors[:, idx] # Step 5: Select top K components if self.n_components is None: self.n_components = n_features self.components_ = eigenvectors[:, :self.n_components].T self.explained_variance_ = eigenvalues[:self.n_components] self.explained_variance_ratio_ = ( self.explained_variance_ / eigenvalues.sum() ) return self def transform(self, X): """Project data onto principal components.""" X_centered = X - self.mean_ return X_centered @ self.components_.T def fit_transform(self, X): """Fit and transform in one step.""" self.fit(X) return self.transform(X) def inverse_transform(self, X_transformed): """Reconstruct original data from PCA.""" X_reconstructed = X_transformed @ self.components_ + self.mean_ return X_reconstructed # Example with iris data from sklearn.datasets import load_iris iris = load_iris() X = iris.data # Apply PCA pca = PCAFromScratch(n_components=2) X_pca = pca.fit_transform(X) print("=== PCA Results ===") print(f"Original shape: {X.shape}") print(f"Reduced shape: {X_pca.shape}") print(f"\nExplained variance: {pca.explained_variance_}") print(f"Explained variance ratio: {pca.explained_variance_ratio_}") print(f"Total variance explained: {pca.explained_variance_ratio_.sum():.4f}") print(f"\nPrincipal components (top 2):") print(pca.components_) # Step-by-step demonstration print("\n\n=== Step-by-Step Demonstration ===") # Step 1: Center X_centered = X - X.mean(axis=0) print(f"Step 1 - Centered mean: {X_centered.mean(axis=0).round(10)}") # Step 2: Covariance cov_matrix = np.cov(X_centered, rowvar=False) print(f"\nStep 2 - Covariance matrix shape: {cov_matrix.shape}") print(f"Covariance matrix:\n{cov_matrix}") # Step 3: Eigen eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix) print(f"\nStep 3 - Eigenvalues (unsorted): {eigenvalues}") print(f"Eigenvectors shape: {eigenvectors.shape}") # Step 4: Sort idx = np.argsort(eigenvalues)[::-1] eigenvalues = eigenvalues[idx] eigenvectors = eigenvectors[:, idx] print(f"\nStep 4 - Sorted eigenvalues: {eigenvalues}") # Step 5: Select top K k = 2 top_k_components = eigenvectors[:, :k] print(f"\nStep 5 - Top {k} components shape: {top_k_components.shape}") # Step 6: Project X_pca_step = X_centered @ top_k_components print(f"\nStep 6 - Projected data shape: {X_pca_step.shape}") # Verify match print(f"\nMatches our implementation: {np.allclose(X_pca, X_pca_step)}") # Reconstruction X_reconstructed = X_pca @ pca.components_ + pca.mean_ reconstruction_error = np.mean((X - X_reconstructed) ** 2) print(f"\nReconstruction error: {reconstruction_error:.4f}") # Visualize plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1) plt.scatter(X[:, 0], X[:, 1], c=iris.target, cmap='viridis', alpha=0.7) plt.xlabel(iris.feature_names[0]) plt.ylabel(iris.feature_names[1]) plt.title('Original Data (first 2 features)') plt.colorbar(label='Class') plt.subplot(1, 2, 2) plt.scatter(X_pca[:, 0], X_pca[:, 1], c=iris.target, cmap='viridis', alpha=0.7) plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]*100:.1f}%)') plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]*100:.1f}%)') plt.title('PCA Projection') plt.colorbar(label='Class') plt.tight_layout() plt.show()

๐ŸŽฏ Principal Components

๐ŸŽฏ What Are They?

Principal components are the new axes of the transformed space. Each PC is a linear combination of original features, and they are ordered by the amount of variance they explain. They are orthogonal (uncorrelated) to each other.

Properties of Principal Components

Ordered: PC1 has max variance, PC2 next, etc. Orthogonal: Perpendicular to each other. Uncorrelated: Zero correlation between PCs. Linear: Combinations of original features. Loadings: Coefficients of original features. Interpretation: Can analyze what each PC means.

๐Ÿ“Š Ordered๐Ÿ“ Orthogonal๐ŸŽฏ Uncorrelated๐Ÿ“ˆ Loadings
# Understanding principal components import numpy as np import pandas as pd from sklearn.decomposition import PCA from sklearn.datasets import load_iris # Load iris iris = load_iris() X = iris.data y = iris.target feature_names = iris.feature_names # Apply PCA pca = PCA() X_pca = pca.fit_transform(X) print("=== Explained Variance ===") for i, (var, ratio) in enumerate(zip(pca.explained_variance_, pca.explained_variance_ratio_)): print(f"PC{i+1}: variance={var:.4f}, ratio={ratio:.4f} ({ratio*100:.2f}%)") print(f"\nCumulative variance:") cumsum = np.cumsum(pca.explained_variance_ratio_) for i, cs in enumerate(cumsum): print(f" PC1-{i+1}: {cs:.4f} ({cs*100:.2f}%)") # 1. Components (loadings) print("\n=== Components (Loadings) ===") loadings_df = pd.DataFrame( pca.components_.T, columns=[f'PC{i+1}' for i in range(len(pca.components_))], index=feature_names ) print(loadings_df.round(4)) # 2. Interpretation print("\n=== Interpretation ===") for i in range(min(4, len(pca.components_))): component = pca.components_[i] # Find most important features importance = np.abs(component) top_features = np.argsort(importance)[::-1][:3] print(f"\nPC{i+1}:") for feat_idx in top_features: direction = "+" if component[feat_idx] > 0 else "-" print(f" {direction} {feature_names[feat_idx]}: {component[feat_idx]:+.4f}") # 3. Loadings visualization import matplotlib.pyplot as plt fig, axes = plt.subplots(1, 2, figsize=(14, 6)) # Scree plot axes[0].bar(range(1, len(pca.explained_variance_) + 1), pca.explained_variance_, alpha=0.7, color='steelblue') axes[0].plot(range(1, len(pca.explained_variance_) + 1), pca.explained_variance_, 'ro-') axes[0].set_xlabel('Principal Component') axes[0].set_ylabel('Explained Variance') axes[0].set_title('Scree Plot') axes[0].grid(alpha=0.3) # Cumulative variance axes[1].plot(range(1, len(cumsum) + 1), cumsum, 'bo-') axes[1].axhline(y=0.95, color='r', linestyle='--', label='95% threshold') axes[1].axhline(y=0.99, color='g', linestyle='--', label='99% threshold') axes[1].set_xlabel('Number of Components') axes[1].set_ylabel('Cumulative Explained Variance') axes[1].set_title('Cumulative Variance') axes[1].legend() axes[1].grid(alpha=0.3) plt.tight_layout() plt.show() # 4. Biplot โ€” how features relate to PCs fig, ax = plt.subplots(figsize=(10, 8)) # Project data X_2d = X_pca[:, :2] # Plot data points scatter = ax.scatter(X_2d[:, 0], X_2d[:, 1], c=y, cmap='viridis', alpha=0.5, s=50) # Plot loading vectors for i, feature in enumerate(feature_names): ax.arrow(0, 0, pca.components_[0, i] * 3, pca.components_[1, i] * 3, color='red', alpha=0.7, head_width=0.15) ax.text(pca.components_[0, i] * 3.3, pca.components_[1, i] * 3.3, feature, color='red', fontsize=10, fontweight='bold') ax.set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]*100:.1f}%)') ax.set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]*100:.1f}%)') ax.set_title('PCA Biplot') ax.axhline(0, color='gray', linestyle='--', alpha=0.5) ax.axvline(0, color='gray', linestyle='--', alpha=0.5) ax.grid(alpha=0.3) plt.colorbar(scatter, label='Class') plt.tight_layout() plt.show() # 5. Feature contributions to PC1 print("\n=== Feature Contributions to PC1 ===") contributions = pd.DataFrame({ 'feature': feature_names, 'loading': pca.components_[0], 'abs_loading': np.abs(pca.components_[0]) }).sort_values('abs_loading', ascending=False) print(contributions.to_string(index=False)) # 6. Reconstructed data with different numbers of PCs print("\n=== Reconstruction Quality ===") for k in [1, 2, 3, 4]: pca_k = PCA(n_components=k) X_reduced = pca_k.fit_transform(X) X_reconstructed = pca_k.inverse_transform(X_reduced) error = np.mean((X - X_reconstructed) ** 2) variance_kept = pca_k.explained_variance_ratio_.sum() print(f"k={k}: MSE={error:.6f}, Variance kept={variance_kept:.4f} ({variance_kept*100:.1f}%)")

โš–๏ธ Feature Scaling

๐ŸŽฏ Why Standardization Matters

PCA is sensitive to feature scales. If one feature ranges 0-1 and another 0-1000, PCA will be dominated by the larger-scale feature. Standardization (z-score normalization) is usually essential before PCA.

When to Scale

Always scale: When features have different units. Don't scale: When features are already comparable. StandardScaler: Mean=0, Std=1. MinMaxScaler: Range [0, 1]. RobustScaler: Uses median and IQR. Impact: Changes which directions are "principal".

๐Ÿ“ Different Units๐ŸŽฏ Z-Scoreโš–๏ธ Balance๐Ÿ” Impact
# Feature scaling before PCA import numpy as np import pandas as pd from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler # Create data with different scales np.random.seed(42) X = np.column_stack([ np.random.randn(100) * 0.1, # Small scale np.random.randn(100) * 100, # Large scale np.random.randn(100) * 10 + 50, # Different mean and scale np.random.randn(100) * 1 # Normal scale ]) print("=== Original Data Statistics ===") print(f"Mean: {X.mean(axis=0)}") print(f"Std: {X.std(axis=0)}") print(f"Range: {X.max(axis=0) - X.min(axis=0)}") # 1. PCA without scaling print("\n=== PCA WITHOUT Scaling ===") pca_no_scale = PCA() X_pca_no_scale = pca_no_scale.fit_transform(X) print(f"Explained variance ratios: {pca_no_scale.explained_variance_ratio_}") print(f"PC1 variance: {pca_no_scale.explained_variance_[0]:.2f}") print(f"Feature with largest variance dominates PC1!") # Find which feature dominates dominant_feature = np.argmax(np.abs(pca_no_scale.components_[0])) print(f"Dominant feature in PC1: Feature {dominant_feature} (scale: {X[:, dominant_feature].std():.2f})") # 2. PCA with StandardScaler print("\n=== PCA WITH StandardScaler ===") scaler = StandardScaler() X_scaled = scaler.fit_transform(X) print(f"Scaled mean: {X_scaled.mean(axis=0).round(10)}") print(f"Scaled std: {X_scaled.std(axis=0)}") pca_scaled = PCA() X_pca_scaled = pca_scaled.fit_transform(X_scaled) print(f"\nExplained variance ratios: {pca_scaled.explained_variance_ratio_}") print(f"PC1 variance: {pca_scaled.explained_variance_[0]:.2f}") print(f"Now variance is more balanced!") # 3. Compare components print("\n=== Component Comparison ===") print("Without scaling - PC1 loadings:") print(pca_no_scale.components_[0].round(4)) print("\nWith scaling - PC1 loadings:") print(pca_scaled.components_[0].round(4)) # 4. Different scalers scalers = { 'StandardScaler': StandardScaler(), 'MinMaxScaler': MinMaxScaler(), 'RobustScaler': RobustScaler(), } print("\n=== Different Scalers ===") for name, scaler in scalers.items(): X_scaled = scaler.fit_transform(X) pca = PCA(n_components=2) pca.fit(X_scaled) print(f"\n{name}:") print(f" Explained variance: {pca.explained_variance_ratio_}") print(f" Total: {pca.explained_variance_ratio_.sum():.4f}") # 5. When NOT to scale print("\n=== When NOT to Scale ===") print(""" 1. All features already have same units/scale Example: pixel intensities (0-255), word counts 2. You want to preserve relative importance Example: financial data where larger values = more important 3. Data is already standardized Example: z-scored data from previous step 4. PCA is used for compression, not interpretation Example: image compression where all pixels are comparable """) # 6. Real-world example print("\n=== Real-World Example ===") from sklearn.datasets import load_wine wine = load_wine() X_wine = wine.data feature_names = wine.feature_names print(f"Feature scales in wine dataset:") for i, name in enumerate(feature_names): print(f" {name}: mean={X_wine[:, i].mean():.2f}, std={X_wine[:, i].std():.2f}") # Without scaling pca_no = PCA(n_components=2) X_pca_no = pca_no.fit_transform(X_wine) print(f"\nWithout scaling: PC1 explains {pca_no.explained_variance_ratio_[0]*100:.1f}%") # With scaling scaler = StandardScaler() X_wine_scaled = scaler.fit_transform(X_wine) pca_yes = PCA(n_components=2) X_pca_yes = pca_yes.fit_transform(X_wine_scaled) print(f"With scaling: PC1 explains {pca_yes.explained_variance_ratio_[0]*100:.1f}%") # 7. Best practices print("\n=== Best Practices ===") print(""" 1. Always visualize feature distributions first 2. Scale if features have different units or ranges 3. StandardScaler is the most common choice 4. Use RobustScaler for data with outliers 5. Fit scaler on training data only (not test data) 6. Store scaler parameters for production 7. Consider if scaling is meaningful for your problem """) # 8. Pipeline print("\n=== Recommended Pipeline ===") from sklearn.pipeline import Pipeline pipeline = Pipeline([ ('scaler', StandardScaler()), ('pca', PCA(n_components=0.95)) # Keep 95% variance ]) X_pipeline = pipeline.fit_transform(X_wine) print(f"Pipeline result shape: {X_pipeline.shape}") print(f"Components kept: {pipeline.named_steps['pca'].n_components_}") print(f"Variance explained: {pipeline.named_steps['pca'].explained_variance_ratio_.sum():.4f}")

๐Ÿ” Choosing K Components

๐ŸŽฏ How Many Components to Keep?

Choosing the right number of components (K) is critical. Too few and you lose information; too many and you don't reduce dimensionality. Several methods help: variance threshold, scree plot, Kaiser rule, and cross-validation.

Methods for Choosing K

Variance threshold: Keep 95% or 99% of variance. Scree plot: Look for the "elbow". Kaiser rule: Keep eigenvalues > 1. Cross-validation: Optimize downstream task. Domain knowledge: Interpretability concerns. Reconstruction error: Minimize error.

๐Ÿ“Š Variance๐Ÿ“ˆ Scree๐Ÿ”ข Kaiser๐ŸŽฏ CV
# Choosing K components import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.datasets import load_digits, load_iris from sklearn.model_selection import cross_val_score from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler # Load a dataset with more features digits = load_digits() X = digits.data # 64 features y = digits.target print(f"Original features: {X.shape[1]}") # Fit PCA with all components pca = PCA().fit(X) explained_variance = pca.explained_variance_ explained_variance_ratio = pca.explained_variance_ratio_ cumulative_variance = np.cumsum(explained_variance_ratio) # 1. Variance threshold print("\n=== Variance Threshold ===") for threshold in [0.90, 0.95, 0.99, 0.999]: n_components = np.argmax(cumulative_variance >= threshold) + 1 print(f" {threshold*100:.1f}% variance โ†’ {n_components} components") # 2. Scree plot (elbow method) print("\n=== Scree Plot ===") # Find elbow using second derivative diffs = np.diff(explained_variance) diffs2 = np.diff(diffs) elbow = np.argmax(diffs2) + 2 print(f"Elbow at component: {elbow}") print(f"Variance at elbow: {cumulative_variance[elbow-1]:.4f}") # 3. Kaiser rule print("\n=== Kaiser Rule ===") kaiser_components = np.sum(explained_variance > 1) print(f"Components with eigenvalue > 1: {kaiser_components}") print(f"Variance explained: {cumulative_variance[kaiser_components-1]:.4f}") # 4. Reconstruction error print("\n=== Reconstruction Error ===") errors = [] for k in range(1, 65): pca_k = PCA(n_components=k) X_reduced = pca_k.fit_transform(X) X_reconstructed = pca_k.inverse_transform(X_reduced) mse = np.mean((X - X_reconstructed) ** 2) errors.append(mse) # Find k where error stops decreasing rapidly errors = np.array(errors) error_diffs = np.diff(errors) elbow_error = np.argmax(np.abs(np.diff(error_diffs))) + 2 print(f"Elbow in reconstruction error: {elbow_error}") # 5. Cross-validation print("\n=== Cross-Validation ===") cv_scores = [] k_range = range(1, 65, 5) for k in k_range: pipeline = Pipeline([ ('scaler', StandardScaler()), ('pca', PCA(n_components=k)), ('clf', LogisticRegression(max_iter=1000)) ]) scores = cross_val_score(pipeline, X, y, cv=5, scoring='accuracy') cv_scores.append(scores.mean()) print(f" k={k}: accuracy={scores.mean():.4f} (ยฑ{scores.std():.4f})") best_k_cv = list(k_range)[np.argmax(cv_scores)] print(f"\nBest k by CV: {best_k_cv}") # 6. Visualization fig, axes = plt.subplots(2, 2, figsize=(14, 10)) # Scree plot axes[0, 0].bar(range(1, len(explained_variance) + 1), explained_variance, alpha=0.7, color='steelblue') axes[0, 0].plot(range(1, len(explained_variance) + 1), explained_variance, 'ro-', markersize=4) axes[0, 0].axhline(y=1, color='red', linestyle='--', label='Kaiser (ฮป=1)') axes[0, 0].axvline(x=elbow, color='green', linestyle='--', label=f'Elbow ({elbow})') axes[0, 0].set_xlabel('Principal Component') axes[0, 0].set_ylabel('Eigenvalue') axes[0, 0].set_title('Scree Plot') axes[0, 0].legend() axes[0, 0].grid(alpha=0.3) # Cumulative variance axes[0, 1].plot(range(1, len(cumulative_variance) + 1), cumulative_variance, 'bo-', markersize=4) axes[0, 1].axhline(y=0.95, color='r', linestyle='--', label='95%') axes[0, 1].axhline(y=0.99, color='g', linestyle='--', label='99%') for thresh, color in [(0.95, 'r'), (0.99, 'g')]: k = np.argmax(cumulative_variance >= thresh) + 1 axes[0, 1].axvline(x=k, color=color, linestyle=':', alpha=0.5) axes[0, 1].text(k+1, thresh-0.05, f'k={k}', color=color) axes[0, 1].set_xlabel('Number of Components') axes[0, 1].set_ylabel('Cumulative Variance') axes[0, 1].set_title('Cumulative Explained Variance') axes[0, 1].legend() axes[0, 1].grid(alpha=0.3) # Reconstruction error axes[1, 0].plot(range(1, 65), errors, 'go-', markersize=4) axes[1, 0].axvline(x=elbow_error, color='red', linestyle='--', label=f'Elbow ({elbow_error})') axes[1, 0].set_xlabel('Number of Components') axes[1, 0].set_ylabel('Reconstruction MSE') axes[1, 0].set_title('Reconstruction Error') axes[1, 0].legend() axes[1, 0].grid(alpha=0.3) # Cross-validation axes[1, 1].plot(k_range, cv_scores, 'mo-', markersize=6) axes[1, 1].axvline(x=best_k_cv, color='red', linestyle='--', label=f'Best k={best_k_cv}') axes[1, 1].set_xlabel('Number of Components') axes[1, 1].set_ylabel('CV Accuracy') axes[1, 1].set_title('Cross-Validation Score') axes[1, 1].legend() axes[1, 1].grid(alpha=0.3) plt.tight_layout() plt.show() # 7. Recommendations print("\n=== Recommendations ===") print(""" Rule of thumb: - 95% variance: Most common choice - 99% variance: When accuracy is critical - Kaiser rule: Good default for exploration - Elbow method: Visual, subjective - CV: Best for supervised learning For different use cases: - Visualization: 2-3 components - Noise reduction: 90-95% variance - Feature engineering: 95-99% variance - Compression: As few as possible - Downstream ML: Optimize via CV """) # 8. Practical function def choose_k(X, method='variance', threshold=0.95): """Choose K components based on method.""" pca = PCA().fit(X) if method == 'variance': cumsum = np.cumsum(pca.explained_variance_ratio_) return np.argmax(cumsum >= threshold) + 1 elif method == 'kaiser': return np.sum(pca.explained_variance_ > 1) elif method == 'elbow': diffs2 = np.diff(np.diff(pca.explained_variance_)) return np.argmax(diffs2) + 2 else: raise ValueError(f"Unknown method: {method}") print("\n=== Practical Usage ===") for method in ['variance', 'kaiser', 'elbow']: k = choose_k(X, method=method) print(f"{method}: k={k}")

๐ŸŒ€ Kernel PCA

๐ŸŽฏ Non-Linear Dimensionality Reduction

Kernel PCA extends PCA to non-linear data by using the kernel trick. It implicitly maps data to a higher-dimensional space where linear PCA can separate non-linear structures.

Kernel PCA Features

Kernel trick: Compute dot products without explicit mapping. RBF kernel: Gaussian โ€” most common. Polynomial: For polynomial relationships. Sigmoid: Neural network-like. Use cases: Non-linear manifolds, clustering. Cost: Slower than linear PCA.

๐ŸŒ€ Non-linear๐ŸŽฏ Kernel Trick๐Ÿ“ RBF๐Ÿ”ฌ Manifolds
# Kernel PCA for non-linear dimensionality reduction import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA, KernelPCA from sklearn.datasets import make_circles, make_moons, make_swiss_roll # 1. Concentric circles (non-linear) X_circles, y_circles = make_circles(n_samples=400, factor=0.3, noise=0.05, random_state=42) # Linear PCA fails pca_linear = PCA(n_components=2) X_linear = pca_linear.fit_transform(X_circles) # Kernel PCA works kpca_rbf = KernelPCA(n_components=2, kernel='rbf', gamma=15) X_kpca_rbf = kpca_rbf.fit_transform(X_circles) # 2. Moons (non-linear) X_moons, y_moons = make_moons(n_samples=400, noise=0.05, random_state=42) pca_moons = PCA(n_components=2) X_pca_moons = pca_moons.fit_transform(X_moons) kpca_moons = KernelPCA(n_components=2, kernel='rbf', gamma=15) X_kpca_moons = kpca_moons.fit_transform(X_moons) # 3. Swiss roll (3D โ†’ 2D) X_swiss, color_swiss = make_swiss_roll(n_samples=1000, noise=0.05, random_state=42) pca_swiss = PCA(n_components=2) X_pca_swiss = pca_swiss.fit_transform(X_swiss) kpca_swiss = KernelPCA(n_components=2, kernel='rbf', gamma=0.1) X_kpca_swiss = kpca_swiss.fit_transform(X_swiss) # 4. Comparison plot fig, axes = plt.subplots(3, 4, figsize=(16, 12)) # Circles axes[0, 0].scatter(X_circles[:, 0], X_circles[:, 1], c=y_circles, cmap='viridis', alpha=0.6) axes[0, 0].set_title('Circles - Original') axes[0, 1].scatter(X_linear[:, 0], X_linear[:, 1], c=y_circles, cmap='viridis', alpha=0.6) axes[0, 1].set_title('Circles - Linear PCA (fails)') axes[0, 2].scatter(X_kpca_rbf[:, 0], X_kpca_rbf[:, 1], c=y_circles, cmap='viridis', alpha=0.6) axes[0, 2].set_title('Circles - Kernel PCA RBF') axes[0, 3].axis('off') # Moons axes[1, 0].scatter(X_moons[:, 0], X_moons[:, 1], c=y_moons, cmap='viridis', alpha=0.6) axes[1, 0].set_title('Moons - Original') axes[1, 1].scatter(X_pca_moons[:, 0], X_pca_moons[:, 1], c=y_moons, cmap='viridis', alpha=0.6) axes[1, 1].set_title('Moons - Linear PCA') axes[1, 2].scatter(X_kpca_moons[:, 0], X_kpca_moons[:, 1], c=y_moons, cmap='viridis', alpha=0.6) axes[1, 2].set_title('Moons - Kernel PCA RBF') axes[1, 3].axis('off') # Swiss roll axes[2, 0].scatter(X_swiss[:, 0], X_swiss[:, 1], c=color_swiss, cmap='viridis', alpha=0.6) axes[2, 0].set_title('Swiss Roll - Original (3D)') axes[2, 1].scatter(X_pca_swiss[:, 0], X_pca_swiss[:, 1], c=color_swiss, cmap='viridis', alpha=0.6) axes[2, 1].set_title('Swiss Roll - Linear PCA') axes[2, 2].scatter(X_kpca_swiss[:, 0], X_kpca_swiss[:, 1], c=color_swiss, cmap='viridis', alpha=0.6) axes[2, 2].set_title('Swiss Roll - Kernel PCA RBF') axes[2, 3].axis('off') plt.tight_layout() plt.show() # 5. Different kernels print("=== Different Kernels ===") kernels = ['linear', 'poly', 'rbf', 'sigmoid', 'cosine'] for kernel in kernels: try: kpca = KernelPCA(n_components=2, kernel=kernel) X_kpca = kpca.fit_transform(X_circles) print(f"Kernel: {kernel}") print(f" Explained variance: {kpca.lambdas_[:3]}") except Exception as e: print(f"Kernel: {kernel} โ€” {e}") # 6. Gamma parameter (for RBF) print("\n=== RBF Gamma Parameter ===") gammas = [0.1, 1, 5, 15, 50, 100] fig, axes = plt.subplots(1, len(gammas), figsize=(20, 4)) for i, gamma in enumerate(gammas): kpca = KernelPCA(n_components=2, kernel='rbf', gamma=gamma) X_kpca = kpca.fit_transform(X_circles) axes[i].scatter(X_kpca[:, 0], X_kpca[:, 1], c=y_circles, cmap='viridis', alpha=0.6) axes[i].set_title(f'gamma={gamma}') axes[i].axis('off') plt.tight_layout() plt.show() print(""" Gamma interpretation: - Small gamma: Smooth, underfitting - Medium gamma: Good balance - Large gamma: Complex, overfitting - Rule of thumb: gamma โ‰ˆ 1 / (n_features * X.var()) """) # 7. Kernel PCA vs other non-linear methods print("\n=== Kernel PCA vs Other Methods ===") from sklearn.manifold import TSNE, Isomap, LocallyLinearEmbedding # Apply different methods tsne = TSNE(n_components=2, random_state=42, perplexity=30) X_tsne = tsne.fit_transform(X_circles) isomap = Isomap(n_components=2, n_neighbors=10) X_isomap = isomap.fit_transform(X_circles) lle = LocallyLinearEmbedding(n_components=2, n_neighbors=10) X_lle = lle.fit_transform(X_circles) fig, axes = plt.subplots(1, 4, figsize=(20, 5)) axes[0].scatter(X_kpca_rbf[:, 0], X_kpca_rbf[:, 1], c=y_circles, cmap='viridis', alpha=0.6) axes[0].set_title('Kernel PCA (RBF)') axes[1].scatter(X_tsne[:, 0], X_tsne[:, 1], c=y_circles, cmap='viridis', alpha=0.6) axes[1].set_title('t-SNE') axes[2].scatter(X_isomap[:, 0], X_isomap[:, 1], c=y_circles, cmap='viridis', alpha=0.6) axes[2].set_title('Isomap') axes[3].scatter(X_lle[:, 0], X_lle[:, 1], c=y_circles, cmap='viridis', alpha=0.6) axes[3].set_title('LLE') plt.tight_layout() plt.show() # 8. Practical example print("\n=== Practical Usage ===") from sklearn.datasets import load_digits from sklearn.model_selection import cross_val_score from sklearn.svm import SVC from sklearn.pipeline import Pipeline digits = load_digits() X_digits = digits.data y_digits = digits.target # Compare linear vs kernel PCA for name, reducer in [ ('Linear PCA', PCA(n_components=30)), ('Kernel PCA (RBF)', KernelPCA(n_components=30, kernel='rbf', gamma=0.001)), ('Kernel PCA (poly)', KernelPCA(n_components=30, kernel='poly', degree=3)), ]: pipeline = Pipeline([ ('reduce', reducer), ('clf', SVC(kernel='linear')) ]) scores = cross_val_score(pipeline, X_digits, y_digits, cv=3, scoring='accuracy') print(f"{name}: {scores.mean():.4f} (ยฑ{scores.std():.4f})")

๐Ÿ“ˆ Incremental PCA

๐ŸŽฏ PCA for Large Datasets

Incremental PCA processes data in batches, allowing PCA on datasets that don't fit in memory. It's essential for big data applications.

When to Use Incremental PCA

Large datasets: Data doesn't fit in RAM. Streaming data: Continuous incoming data. Online learning: Model updates incrementally. Memory constraints: Limited RAM. Trade-off: Slightly less accurate than full PCA. Batch size: Balance memory and accuracy.

๐Ÿ“Š Batches๐Ÿ’พ Memory๐ŸŒŠ Streamingโšก Online
# Incremental PCA for large datasets import numpy as np import time from sklearn.decomposition import PCA, IncrementalPCA from sklearn.datasets import make_classification from sklearn.preprocessing import StandardScaler # Generate a large dataset n_samples = 100000 n_features = 100 X, y = make_classification( n_samples=n_samples, n_features=n_features, n_informative=50, random_state=42 ) print(f"Dataset: {X.shape}, size: {X.nbytes / 1e6:.1f} MB") # 1. Regular PCA (needs all data in memory) print("\n=== Regular PCA ===") scaler = StandardScaler() X_scaled = scaler.fit_transform(X) t0 = time.time() pca_full = PCA(n_components=50) X_pca_full = pca_full.fit_transform(X_scaled) full_time = time.time() - t0 print(f"Time: {full_time:.2f}s") print(f"Explained variance: {pca_full.explained_variance_ratio_.sum():.4f}") # 2. Incremental PCA (processes in batches) print("\n=== Incremental PCA ===") batch_size = 1000 n_batches = n_samples // batch_size t0 = time.time() ipca = IncrementalPCA(n_components=50, batch_size=batch_size) for i in range(n_batches): start = i * batch_size end = start + batch_size batch = X_scaled[start:end] ipca.partial_fit(batch) X_pca_inc = ipca.transform(X_scaled) inc_time = time.time() - t0 print(f"Time: {inc_time:.2f}s") print(f"Explained variance: {ipca.explained_variance_ratio_.sum():.4f}") # 3. Compare results print("\n=== Comparison ===") print(f"Full PCA variance: {pca_full.explained_variance_ratio_.sum():.4f}") print(f"Incremental PCA variance: {ipca.explained_variance_ratio_.sum():.4f}") print(f"Difference: {abs(pca_full.explained_variance_ratio_.sum() - ipca.explained_variance_ratio_.sum()):.6f}") # Compare components component_diff = np.abs(pca_full.components_ - ipca.components_).mean() print(f"Component difference: {component_diff:.6f}") # 4. Different batch sizes print("\n=== Effect of Batch Size ===") batch_sizes = [100, 500, 1000, 5000, 10000] for bs in batch_sizes: ipca = IncrementalPCA(n_components=50, batch_size=bs) n_batches = max(1, n_samples // bs) for i in range(n_batches): batch = X_scaled[i*bs:(i+1)*bs] ipca.partial_fit(batch) variance = ipca.explained_variance_ratio_.sum() print(f"Batch size {bs}: variance={variance:.4f}") # 5. Streaming data example print("\n=== Streaming Data ===") ipca_stream = IncrementalPCA(n_components=50, batch_size=500) # Simulate streaming data all_variances = [] for i in range(0, n_samples, 1000): batch = X_scaled[i:i+1000] ipca_stream.partial_fit(batch) # Track cumulative variance if i % 10000 == 0: total_var = ipca_stream.explained_variance_ratio_.sum() all_variances.append(total_var) print(f"After {i+1000} samples: variance={total_var:.4f}") # 6. Memory comparison print("\n=== Memory Usage ===") print(f"Full dataset: {X.nbytes / 1e6:.1f} MB") print(f"Per batch ({batch_size} samples): {X[:batch_size].nbytes / 1e6:.2f} MB") print(f"Memory savings: {X.nbytes / X[:batch_size].nbytes:.0f}x") # 7. Real-world example: Image data print("\n=== Real-World: Image Data ===") from sklearn.datasets import load_digits digits = load_digits() X_img = digits.data print(f"Image data: {X_img.shape}") # Incremental PCA on images ipca_img = IncrementalPCA(n_components=32, batch_size=100) for i in range(0, len(X_img), 100): ipca_img.partial_fit(X_img[i:i+100]) X_img_pca = ipca_img.transform(X_img) print(f"Reduced shape: {X_img_pca.shape}") print(f"Compression ratio: {X_img.shape[1] / X_img_pca.shape[1]:.1f}x") # Reconstruct X_img_reconstructed = ipca_img.inverse_transform(X_img_pca) mse = np.mean((X_img - X_img_reconstructed) ** 2) print(f"Reconstruction MSE: {mse:.4f}") # 8. Best practices print("\n=== Best Practices ===") print(""" 1. Choose batch size carefully: - Too small: slow convergence - Too large: memory issues - Rule: 1000-10000 samples per batch 2. Always fit scaler on full data first - Or use partial_fit on scaler too 3. Watch for numerical stability: - Use float64 for accumulation - Normalize data 4. Validate with holdout set - Compare to full PCA when possible 5. Use for: - Datasets > RAM - Streaming data - Online learning - Memory-constrained environments 6. Consider alternatives: - Randomized PCA (faster for many features) - Truncated SVD (for sparse data) """) # 9. Complete pipeline example print("\n=== Complete Pipeline ===") from sklearn.pipeline import Pipeline # Simulate loading data in chunks chunk_sizes = [1000] * 10 pipeline_ipca = IncrementalPCA(n_components=20) print("Processing chunks:") for i, chunk_size in enumerate(chunk_sizes): chunk = X_scaled[i*chunk_size:(i+1)*chunk_size] pipeline_ipca.partial_fit(chunk) print(f" Chunk {i+1}: {chunk_size} samples, " f"variance={pipeline_ipca.explained_variance_ratio_.sum():.4f}") print(f"\nFinal variance: {pipeline_ipca.explained_variance_ratio_.sum():.4f}") print(f"Components: {pipeline_ipca.n_components_}")

๐Ÿค PCA Whitening

๐ŸŽฏ Decorrelation + Normalization

Whitening transforms data so that it has identity covariance matrix โ€” features are uncorrelated and have unit variance. It's used in image processing and as preprocessing for ML algorithms.

Two Types of Whitening

PCA whitening: PCA then divide by eigenvalues. ZCA whitening: PCA whitening then rotate back to original space. Benefit: Removes redundancy. Use case: Image preprocessing, ICA, deep learning. Effect: Identity covariance matrix.

๐Ÿค Whitening๐Ÿ”„ ZCA๐Ÿ“Š Unit Variance๐Ÿ–ผ๏ธ Images
# PCA whitening and ZCA whitening import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA # Generate correlated data np.random.seed(42) n = 500 X = np.random.randn(n, 2) @ np.array([[3, 2], [1, 1]]) # 1. Standard PCA (no whitening) pca = PCA(n_components=2, whiten=False) X_pca = pca.fit_transform(X) # 2. PCA whitening pca_white = PCA(n_components=2, whiten=True) X_pca_white = pca_white.fit_transform(X) # 3. Manual whitening # Step 1: Center X_centered = X - X.mean(axis=0) # Step 2: Compute covariance cov = np.cov(X_centered.T) # Step 3: Eigendecomposition eigenvalues, eigenvectors = np.linalg.eigh(cov) idx = np.argsort(eigenvalues)[::-1] eigenvalues = eigenvalues[idx] eigenvectors = eigenvectors[:, idx] # Step 4: PCA whitening (rotate + scale) X_pca_manual = X_centered @ eigenvectors X_pca_manual_white = X_pca_manual / np.sqrt(eigenvalues) # Step 5: ZCA whitening (scale + rotate back) X_zca_white = X_pca_manual_white @ eigenvectors.T # Verify print("=== Covariance Matrices ===") print(f"Original covariance:\n{np.cov(X.T)}") print(f"\nAfter PCA (no whitening):\n{np.cov(X_pca.T)}") print(f"\nAfter PCA whitening:\n{np.cov(X_pca_white.T)}") print(f"\nAfter ZCA whitening:\n{np.cov(X_zca_white.T)}") # 4. Compare transformations fig, axes = plt.subplots(1, 4, figsize=(20, 5)) datasets = [ (X, 'Original Data'), (X_pca, 'PCA (no whitening)'), (X_pca_white, 'PCA Whitened'), (X_zca_white, 'ZCA Whitened'), ] for ax, (data, title) in zip(axes, datasets): ax.scatter(data[:, 0], data[:, 1], alpha=0.5, s=20) ax.set_title(title) ax.axis('equal') ax.grid(alpha=0.3) # Show covariance cov = np.cov(data.T) text = f"cov:\n[{cov[0,0]:.2f}, {cov[0,1]:.2f}]\n[{cov[1,0]:.2f}, {cov[1,1]:.2f}]" ax.text(0.05, 0.95, text, transform=ax.transAxes, verticalalignment='top', fontsize=8, bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5)) plt.tight_layout() plt.show() # 5. Eigenvalue analysis print("\n=== Eigenvalue Analysis ===") print(f"Eigenvalues: {eigenvalues}") print(f"Eigenvectors:\n{eigenvectors}") # PCA whitening: divide by sqrt(eigenvalue) print(f"\nPCA whitened variance: {X_pca_manual_white.var(axis=0)}") print("Expected: [1, 1]") # ZCA whitening print(f"ZCA whitened variance: {X_zca_white.var(axis=0)}") # 6. Application: Image whitening print("\n=== Image Whitening ===") from sklearn.datasets import load_digits digits = load_digits() X_img = digits.data # Normalize to [0, 1] X_img_norm = X_img / 16.0 # PCA whitening pca_img = PCA(n_components=64, whiten=True) X_img_white = pca_img.fit_transform(X_img_norm) # ZCA whitening (manual) X_img_centered = X_img_norm - X_img_norm.mean(axis=0) cov_img = np.cov(X_img_centered.T) eigenvalues_img, eigenvectors_img = np.linalg.eigh(cov_img) idx = np.argsort(eigenvalues_img)[::-1] eigenvalues_img = eigenvalues_img[idx] eigenvectors_img = eigenvectors_img[:, idx] # PCA whitening X_img_pca_white = X_img_centered @ eigenvectors_img X_img_pca_white = X_img_pca_white / np.sqrt(eigenvalues_img + 1e-5) # ZCA whitening X_img_zca_white = X_img_pca_white @ eigenvectors_img.T # Visualize a sample sample_idx = 0 fig, axes = plt.subplots(1, 4, figsize=(16, 4)) axes[0].imshow(X_img_norm[sample_idx].reshape(8, 8), cmap='gray') axes[0].set_title('Original') axes[1].imshow(X_img_centered[sample_idx].reshape(8, 8), cmap='gray') axes[1].set_title('Centered') axes[2].imshow(X_img_pca_white[sample_idx][:64].reshape(8, 8), cmap='gray') axes[2].set_title('PCA Whitened') axes[3].imshow(X_img_zca_white[sample_idx].reshape(8, 8), cmap='gray') axes[3].set_title('ZCA Whitened') for ax in axes: ax.axis('off') plt.tight_layout() plt.show() # 7. When to use which print("\n=== When to Use Which ===") print(""" PCA Whitening: + Removes correlations + Unit variance per component + Good for ICA, clustering - Doesn't preserve original space ZCA Whitening: + Removes correlations + Unit variance + Preserves original orientation + Better for CNNs, image processing - More computation Use PCA whitening when: - You want maximally decorrelated features - Downstream algorithm assumes independent features - Doing ICA or independent component analysis Use ZCA whitening when: - Preserving visual structure matters - Preprocessing images for CNNs - You want decorrelated but recognizable features - Working with spatial data Don't use whitening when: - Feature scale is meaningful - Sparse data (whitening destroys sparsity) - Interpretability is important """) # 8. Whitening in deep learning print("\n=== Whitening in Deep Learning ===") print(""" Used in: - CNN input preprocessing (ZCA) - Batch normalization (a form of whitening) - Style transfer (whitening and coloring) - Domain adaptation - Self-supervised learning Modern alternatives: - Batch Normalization (per-layer) - Layer Normalization - Instance Normalization - Group Normalization Batch Norm is essentially: 1. Compute mean and variance per batch 2. Normalize 3. Learn scale and shift parameters """) # 9. Complete whitening function def whiten(X, method='zca', epsilon=1e-5): """Whiten data using PCA or ZCA.""" # Center X_centered = X - X.mean(axis=0) # Covariance cov = np.cov(X_centered.T) # Eigendecomposition eigenvalues, eigenvectors = np.linalg.eigh(cov) idx = np.argsort(eigenvalues)[::-1] eigenvalues = eigenvalues[idx] eigenvectors = eigenvectors[:, idx] # Whitening matrix D = np.diag(1.0 / np.sqrt(eigenvalues + epsilon)) W_pca = D @ eigenvectors.T W_zca = eigenvectors @ D @ eigenvectors.T if method == 'pca': return X_centered @ W_pca.T elif method == 'zca': return X_centered @ W_zca.T else: raise ValueError(f"Unknown method: {method}") # Test X_test = np.random.randn(100, 3) @ np.array([[2, 1, 0], [0, 2, 1], [0, 0, 1]]) X_pca_w = whiten(X_test, method='pca') X_zca_w = whiten(X_test, method='zca') print("\n=== Whitening Function Test ===") print(f"Original covariance:\n{np.cov(X_test.T).round(3)}") print(f"\nPCA whitened covariance:\n{np.cov(X_pca_w.T).round(3)}") print(f"\nZCA whitened covariance:\n{np.cov(X_zca_w.T).round(3)}")

๐Ÿ’ผ Applications (Vision, NLP, Finance)

๐ŸŽฏ Where PCA Shines

PCA is used across many domains: computer vision (Eigenfaces, image compression), NLP (word embeddings, topic modeling), finance (risk factors, portfolio optimization), and more.

Domain Applications

Vision: Eigenfaces, image compression, denoising. NLP: Latent Semantic Analysis, word embeddings. Finance: Risk factors, portfolio optimization. Biology: Gene expression, population genetics. Robotics: Sensor fusion, state estimation. Time series: Trend extraction, anomaly detection.

๐Ÿ‘๏ธ Vision๐Ÿ“ NLP๐Ÿ’ฐ Finance๐Ÿงฌ Biology
# Real-world PCA applications import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler from sklearn.datasets import fetch_olivetti_faces, load_digits # 1. EIGENFACES (Face Recognition) print("=== Eigenfaces ===") faces = fetch_olivetti_faces(shuffle=True, random_state=42) X_faces = faces.data y_faces = faces.target print(f"Face data: {X_faces.shape}") # Apply PCA n_components = 50 pca_faces = PCA(n_components=n_components, whiten=True) X_faces_pca = pca_faces.fit_transform(X_faces) print(f"Reduced shape: {X_faces_pca.shape}") print(f"Variance explained: {pca_faces.explained_variance_ratio_.sum():.4f}") # Visualize eigenfaces fig, axes = plt.subplots(4, 5, figsize=(12, 10)) for i, ax in enumerate(axes.flat): eigenface = pca_faces.components_[i].reshape(64, 64) ax.imshow(eigenface, cmap='gray') ax.set_title(f'Eigenface {i+1}') ax.axis('off') plt.suptitle('Top 20 Eigenfaces', fontsize=16) plt.tight_layout() plt.show() # Face recognition with PCA from sklearn.model_selection import train_test_split from sklearn.svm import SVC from sklearn.metrics import accuracy_score X_train, X_test, y_train, y_test = train_test_split( X_faces_pca, y_faces, test_size=0.3, random_state=42 ) clf = SVC(kernel='rbf', C=10, gamma=0.001) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print(f"Face recognition accuracy: {accuracy_score(y_test, y_pred):.4f}") # 2. IMAGE COMPRESSION print("\n=== Image Compression ===") digits = load_digits() X_digits = digits.data / 16.0 # Normalize # Try different compression levels k_values = [5, 10, 20, 30, 50, 64] fig, axes = plt.subplots(2, len(k_values), figsize=(20, 6)) sample_idx = 0 original = X_digits[sample_idx].reshape(8, 8) # Original axes[0, 0].imshow(original, cmap='gray') axes[0, 0].set_title('Original') axes[0, 0].axis('off') for i, k in enumerate(k_values): if k == 5: continue pca_k = PCA(n_components=k) X_compressed = pca_k.fit_transform(X_digits) X_reconstructed = pca_k.inverse_transform(X_compressed) reconstructed = X_reconstructed[sample_idx].reshape(8, 8) ax = axes[0, i] ax.imshow(reconstructed, cmap='gray') ax.set_title(f'k={k}') ax.axis('off') # Compression ratio original_size = 64 compressed_size = k * 2 + k # components + mean ratio = original_size / compressed_size ax = axes[1, i] ax.bar(['Original', 'Compressed'], [original_size, compressed_size]) ax.set_title(f'Ratio: {ratio:.1f}x') plt.tight_layout() plt.show() # 3. LATENT SEMANTIC ANALYSIS (NLP) print("\n=== Latent Semantic Analysis ===") from sklearn.feature_extraction.text import TfidfVectorizer documents = [ "The cat sat on the mat", "The dog played in the park", "Machine learning is fascinating", "Deep learning uses neural networks", "Cats and dogs are pets", "AI and ML are related fields", "The cat chased the mouse", "Neural networks learn patterns", "Natural language processing is hard", "Pets need love and care" ] vectorizer = TfidfVectorizer(stop_words='english') X_tfidf = vectorizer.fit_transform(documents).toarray() print(f"TF-IDF matrix: {X_tfidf.shape}") # Apply LSA (PCA on TF-IDF) pca_lsa = PCA(n_components=3) X_lsa = pca_lsa.fit_transform(X_tfidf) print(f"LSA shape: {X_lsa.shape}") print(f"Variance explained: {pca_lsa.explained_variance_ratio_.sum():.4f}") # Visualize documents in 2D fig, ax = plt.subplots(figsize=(10, 8)) ax.scatter(X_lsa[:, 0], X_lsa[:, 1], s=100, alpha=0.6) for i, doc in enumerate(documents): ax.annotate(f"Doc {i+1}", (X_lsa[i, 0], X_lsa[i, 1]), fontsize=9) ax.set_xlabel(f'PC1 ({pca_lsa.explained_variance_ratio_[0]*100:.1f}%)') ax.set_ylabel(f'PC2 ({pca_lsa.explained_variance_ratio_[1]*100:.1f}%)') ax.set_title('Document Clustering with LSA') ax.grid(alpha=0.3) plt.tight_layout() plt.show() # 4. FINANCE: PORTFOLIO RISK FACTORS print("\n=== Finance: Portfolio Risk ===") np.random.seed(42) n_days = 1000 n_stocks = 50 # Simulate stock returns # Market factor + sector factors + idiosyncratic market_factor = np.random.randn(n_days) * 0.01 sector_factors = np.random.randn(n_days, 5) * 0.005 idiosyncratic = np.random.randn(n_days, n_stocks) * 0.02 # Stock returns = exposure to factors + noise X_stocks = (market_factor[:, None] * np.random.randn(n_stocks) + sector_factors @ np.random.randn(5, n_stocks) * 0.5 + idiosyncratic) # Apply PCA pca_finance = PCA() X_finance = pca_finance.fit_transform(X_stocks) print(f"Stock returns: {X_stocks.shape}") print(f"Top 5 PC variances: {pca_finance.explained_variance_[:5]}") # Variance explained by first few factors cumulative = np.cumsum(pca_finance.explained_variance_ratio_) print(f"\nVariance explained by top factors:") for i in [1, 3, 5, 10, 20]: print(f" Top {i:2d} factors: {cumulative[i-1]*100:.2f}%") # 5. TIME SERIES: TREND EXTRACTION print("\n=== Time Series: Trend Extraction ===") np.random.seed(42) n_points = 500 # Generate multiple time series with common trend common_trend = np.sin(np.linspace(0, 4*np.pi, n_points)) + np.linspace(0, 1, n_points) noise = np.random.randn(n_points, 10) * 0.3 X_timeseries = noise + common_trend[:, None] # Apply PCA pca_ts = PCA(n_components=3) X_ts_pca = pca_ts.fit_transform(X_timeseries) print(f"Time series: {X_timeseries.shape}") print(f"Variance explained: {pca_ts.explained_variance_ratio_}") # Plot fig, axes = plt.subplots(2, 1, figsize=(14, 8)) axes[0].plot(X_timeseries[:, :5], alpha=0.5) axes[0].set_title('Original Time Series (5 shown)') axes[0].set_xlabel('Time') axes[0].set_ylabel('Value') axes[0].grid(alpha=0.3) axes[1].plot(X_ts_pca[:, :3], linewidth=2) axes[1].set_title('First 3 Principal Components') axes[1].set_xlabel('Time') axes[1].set_ylabel('Component Value') axes[1].legend([f'PC{i+1}' for i in range(3)]) axes[1].grid(alpha=0.3) plt.tight_layout() plt.show() # 6. BIOLOGY: GENE EXPRESSION print("\n=== Biology: Gene Expression ===") # Simulate gene expression data n_samples = 200 n_genes = 1000 # Simulate: few biological processes + noise n_processes = 5 process_activity = np.random.randn(n_samples, n_processes) gene_loadings = np.random.randn(n_processes, n_genes) noise = np.random.randn(n_samples, n_genes) * 0.5 X_genes = process_activity @ gene_loadings + noise # PCA pca_genes = PCA(n_components=20) X_genes_pca = pca_genes.fit_transform(X_genes) print(f"Gene expression: {X_genes.shape}") print(f"Top PCs explain:") cumsum_genes = np.cumsum(pca_genes.explained_variance_ratio_) for k in [1, 3, 5, 10]: print(f" PC1-{k}: {cumsum_genes[k-1]*100:.2f}%") # 7. ROBOTICS: SENSOR FUSION print("\n=== Robotics: Sensor Fusion ===") # Simulate redundant sensor readings n_timesteps = 1000 n_sensors = 20 # True state + sensor noise true_state = np.cumsum(np.random.randn(n_timesteps) * 0.1) sensor_readings = true_state[:, None] + np.random.randn(n_timesteps, n_sensors) * 0.5 # PCA to extract common signal pca_sensors = PCA(n_components=1) X_sensors_pca = pca_sensors.fit_transform(sensor_readings) # The first PC should recover the true state correlation = np.corrcoef(X_sensors_pca[:, 0], true_state)[0, 1] print(f"Sensor data: {sensor_readings.shape}") print(f"PC1 correlation with true state: {correlation:.4f}") print(f"Variance explained: {pca_sensors.explained_variance_ratio_[0]*100:.2f}%") # 8. Applications summary print("\n\n=== PCA Applications Summary ===") applications = { 'Computer Vision': [ 'Eigenfaces for face recognition', 'Image compression', 'Image denoising', 'Feature extraction' ], 'NLP': [ 'Latent Semantic Analysis', 'Word embeddings', 'Topic modeling', 'Document similarity' ], 'Finance': [ 'Risk factor modeling', 'Portfolio optimization', 'Interest rate curves', 'Credit scoring' ], 'Biology': [ 'Gene expression analysis', 'Population genetics', 'Protein structure', 'Single-cell RNA-seq' ], 'Robotics': [ 'Sensor fusion', 'State estimation', 'SLAM', 'Motion planning' ], 'Time Series': [ 'Trend extraction', 'Seasonality decomposition', 'Anomaly detection', 'Factor models' ] } for domain, uses in applications.items(): print(f"\n{domain}:") for use in uses: print(f" โ€ข {use}")

โš–๏ธ Advantages & Limitations

๐ŸŽฏ When to Use PCA

PCA is a powerful, interpretable technique โ€” but it's not always the right choice. Understanding its strengths and weaknesses helps you decide when to use it and when to reach for alternatives.

Trade-offs

Pros: Fast, interpretable, removes correlation, reduces noise. Cons: Linear only, assumes Gaussian, may lose information, hard to interpret components. Alternatives: t-SNE, UMAP, autoencoders, kernel PCA, ICA. Best for: Linear data, preprocessing, compression, exploration.

โšก Fast๐Ÿ“Š Interpretable๐Ÿ“‰ Linear๐Ÿ” Alternatives
# PCA vs alternatives โ€” comparison import numpy as np import time import matplotlib.pyplot as plt from sklearn.decomposition import PCA, KernelPCA, FastICA, NMF from sklearn.manifold import TSNE, Isomap, LocallyLinearEmbedding from sklearn.datasets import load_digits, make_swiss_roll from sklearn.preprocessing import StandardScaler # Load data digits = load_digits() X = digits.data y = digits.target # Standardize X_scaled = StandardScaler().fit_transform(X) print(f"Data: {X.shape}") # Compare methods methods = { 'PCA': PCA(n_components=2), 'Kernel PCA (RBF)': KernelPCA(n_components=2, kernel='rbf', gamma=0.001), 'FastICA': FastICA(n_components=2, random_state=42), 'NMF': NMF(n_components=2, random_state=42, init='nndsvd', max_iter=500), 't-SNE': TSNE(n_components=2, random_state=42, perplexity=30), 'Isomap': Isomap(n_components=2, n_neighbors=10), 'LLE': LocallyLinearEmbedding(n_components=2, n_neighbors=10), } results = {} timings = {} for name, method in methods.items(): try: t0 = time.time() X_transformed = method.fit_transform(X_scaled) elapsed = time.time() - t0 results[name] = X_transformed timings[name] = elapsed print(f"{name}: {elapsed:.2f}s") except Exception as e: print(f"{name}: failed โ€” {e}") # Visualize n_methods = len(results) fig, axes = plt.subplots(2, 4, figsize=(20, 10)) axes = axes.flatten() for i, (name, X_transformed) in enumerate(results.items()): if i >= len(axes): break axes[i].scatter(X_transformed[:, 0], X_transformed[:, 1], c=y, cmap='tab10', alpha=0.6, s=10) axes[i].set_title(f'{name}\n({timings[name]:.2f}s)') axes[i].axis('off') # Hide extra axes for j in range(len(results), len(axes)): axes[j].axis('off') plt.tight_layout() plt.show() # Comparison table print("\n=== Comparison Table ===") print(f"{'Method':<20} {'Time':<10} {'Linear':<10} {'Preserves':<15}") print("-" * 60) method_props = { 'PCA': ('Yes', 'Variance'), 'Kernel PCA (RBF)': ('No', 'Non-linear structure'), 'FastICA': ('No', 'Independence'), 'NMF': ('No', 'Parts (non-negative)'), 't-SNE': ('No', 'Local structure'), 'Isomap': ('No', 'Geodesic distances'), 'LLE': ('No', 'Local linear structure'), } for name, (linear, preserves) in method_props.items(): if name in timings: print(f"{name:<20} {timings[name]:<10.2f} {linear:<10} {preserves:<15}") # 1. PCA ADVANTAGES print("\n=== PCA Advantages ===") print(""" 1. FAST - O(min(n,p)ยฒ ร— max(n,p)) - Faster than t-SNE, UMAP - Scales to large datasets 2. INTERPRETABLE - Components are linear combinations - Loadings show feature contributions - Explained variance is meaningful 3. DETERMINISTIC - Same input โ†’ same output - No random initialization - Reproducible 4. DECORRELATION - Removes multicollinearity - Uncorrelated features - Diagonal covariance 5. NOISE REDUCTION - Removes directions with low variance - Effective for noisy data - Acts as regularizer 6. COMPRESSION - Reduces storage - Speeds up downstream ML - Preserves most information """) # 2. PCA LIMITATIONS print("\n=== PCA Limitations ===") print(""" 1. LINEAR ONLY - Cannot capture non-linear relationships - Fails on curved manifolds - Use Kernel PCA for non-linear 2. ASSUMES GAUSSIAN - Assumes data from normal distribution - May not work for skewed data - Consider transformations first 3. SENSITIVE TO SCALING - Must standardize features - Results depend on scaling - Different scales โ†’ different PCs 4. LOSES INTERPRETABILITY - PCs don't have physical meaning - Complex feature combinations - Hard to explain to stakeholders 5. LOSES INFORMATION - Reducing dimensions loses info - Choose K carefully - Reconstruction error grows 6. OUTLIER SENSITIVE - Outliers affect covariance - Use RobustScaler - Consider robust PCA 7. ASSUMES LINEAR CORRELATION - Only captures linear relationships - Misses complex dependencies - Consider other methods """) # 3. WHEN TO USE PCA print("\n=== When to Use PCA ===") print(""" USE PCA WHEN: โœ“ Data is linearly correlated โœ“ You need fast dimensionality reduction โœ“ You want to visualize high-dim data โœ“ You need to remove multicollinearity โœ“ You want to reduce noise โœ“ You have large sparse data โœ“ Interpretability is not critical โœ“ You need deterministic results โœ“ Preprocessing for supervised learning โœ“ Feature extraction for clustering DON'T USE PCA WHEN: โœ— Data has non-linear structure (use t-SNE, UMAP) โœ— You need to preserve local structure (use LLE, Isomap) โœ— Features have different meaning (you lose interpretability) โœ— You need to preserve the original features โœ— Data is categorical (use MCA) โœ— Outliers are present (use robust PCA) โœ— You need probabilistic interpretation (use PPCA) โœ— Scale matters (don't standardize) ALTERNATIVES TO CONSIDER: - t-SNE: Visualization, local structure - UMAP: Visualization + preservation - Autoencoders: Non-linear, deep learning - ICA: Independent components - NMF: Non-negative, parts-based - LDA: Supervised dimensionality reduction """) # 4. Practical decision tree print("\n=== Decision Tree ===") print(""" Data type: โ”œโ”€โ”€ Linear + Gaussian โ†’ Use PCA โ”œโ”€โ”€ Non-linear โ†’ Consider Kernel PCA, t-SNE, or UMAP โ”œโ”€โ”€ Sparse โ†’ Consider TruncatedSVD โ”œโ”€โ”€ Non-negative โ†’ Consider NMF โ”œโ”€โ”€ Time series โ†’ Consider Dynamic PCA โ””โ”€โ”€ Supervised โ†’ Consider LDA or PLS Goal: โ”œโ”€โ”€ Visualization โ†’ t-SNE, UMAP, PCA โ”œโ”€โ”€ Preprocessing โ†’ PCA (fast) โ”œโ”€โ”€ Compression โ†’ PCA, autoencoders โ”œโ”€โ”€ Feature extraction โ†’ PCA, autoencoders โ”œโ”€โ”€ Interpretability โ†’ PCA (with limitations) โ””โ”€โ”€ Non-linear embedding โ†’ Kernel PCA, autoencoders """) # 5. Hybrid approaches print("\n=== Hybrid Approaches ===") print(""" Often, the best solution is a combination: 1. PCA โ†’ t-SNE - PCA first to reduce dimensions (e.g., 50) - t-SNE on the reduced data - Faster than t-SNE on original 2. PCA โ†’ Clustering - Reduce dimensions with PCA - Cluster in PC space - Faster and often better 3. PCA โ†’ Neural Network - Preprocess with PCA - Feed to neural network - Faster training 4. PCA โ†’ UMAP - Similar to PCA + t-SNE - Preserves more global structure 5. Randomized PCA for large data - Approximate PCA - Much faster - Minimal accuracy loss """) # 6. Summary recommendation print("\n\n=== RECOMMENDATION ===") print(""" For most cases: 1. START with PCA - Fast, interpretable, well-understood - Good baseline for dimensionality reduction 2. Standardize your features - Unless units are identical 3. Choose K components carefully - 95% variance is a good default - Use cross-validation when possible 4. If PCA doesn't work well: - Check for non-linearity - Try Kernel PCA - Consider t-SNE for visualization - Consider autoencoders for deep learning 5. Combine methods - PCA + t-SNE for visualization - PCA + clustering for analysis - PCA + ML for predictive modeling Remember: - PCA is a tool, not a solution - Understand your data first - Try multiple methods - Validate with downstream tasks """)

๐Ÿš€ Future: Deep PCA & Autoencoders

๐ŸŽฏ The Next Generation

PCA is evolving: deep autoencoders, variational autoencoders, self-supervised learning, and neural dimensionality reduction are pushing the boundaries of what's possible.

Modern Alternatives

Autoencoders: Neural networks that learn compressed representations. VAEs: Probabilistic autoencoders with smooth latent space. Diffusion models: For generation and dimensionality reduction. Contrastive learning: SimCLR, MoCo for representation learning. Non-linear PCA: Kernel methods, deep learning.

๐Ÿง  Autoencoders๐ŸŽฒ VAE๐Ÿ“‰ Deep๐Ÿค– Neural
# Deep dimensionality reduction โ€” autoencoders vs PCA import numpy as np import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset from sklearn.decomposition import PCA from sklearn.datasets import load_digits from sklearn.preprocessing import StandardScaler import matplotlib.pyplot as plt # Load data digits = load_digits() X = digits.data y = digits.target # Standardize scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # Convert to tensor X_tensor = torch.FloatTensor(X_scaled) print(f"Data: {X.shape}") # 1. LINEAR AUTOENCODER (equivalent to PCA) class LinearAutoencoder(nn.Module): def __init__(self, input_dim, latent_dim): super().__init__() self.encoder = nn.Linear(input_dim, latent_dim) self.decoder = nn.Linear(latent_dim, input_dim) def forward(self, x): z = self.encoder(x) return self.decoder(z), z # Train linear autoencoder linear_ae = LinearAutoencoder(X.shape[1], 2) optimizer = optim.Adam(linear_ae.parameters(), lr=0.001) criterion = nn.MSELoss() dataset = TensorDataset(X_tensor) loader = DataLoader(dataset, batch_size=64, shuffle=True) losses = [] for epoch in range(100): epoch_loss = 0 for batch in loader: x = batch[0] optimizer.zero_grad() x_reconstructed, _ = linear_ae(x) loss = criterion(x_reconstructed, x) loss.backward() optimizer.step() epoch_loss += loss.item() losses.append(epoch_loss / len(loader)) # Get latent representation with torch.no_grad(): _, X_linear_ae = linear_ae(X_tensor) X_linear_ae = X_linear_ae.numpy() # 2. DEEP AUTOENCODER (non-linear) class DeepAutoencoder(nn.Module): def __init__(self, input_dim, latent_dim=2): super().__init__() self.encoder = nn.Sequential( nn.Linear(input_dim, 128), nn.ReLU(), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, latent_dim) ) self.decoder = nn.Sequential( nn.Linear(latent_dim, 32), nn.ReLU(), nn.Linear(32, 64), nn.ReLU(), nn.Linear(64, 128), nn.ReLU(), nn.Linear(128, input_dim) ) def forward(self, x): z = self.encoder(x) return self.decoder(z), z # Train deep autoencoder deep_ae = DeepAutoencoder(X.shape[1], 2) optimizer = optim.Adam(deep_ae.parameters(), lr=0.001) for epoch in range(200): epoch_loss = 0 for batch in loader: x = batch[0] optimizer.zero_grad() x_reconstructed, _ = deep_ae(x) loss = criterion(x_reconstructed, x) loss.backward() optimizer.step() epoch_loss += loss.item() with torch.no_grad(): _, X_deep_ae = deep_ae(X_tensor) X_deep_ae = X_deep_ae.numpy() # 3. VARIATIONAL AUTOENCODER (VAE) class VAE(nn.Module): def __init__(self, input_dim, latent_dim=2): super().__init__() self.encoder = nn.Sequential( nn.Linear(input_dim, 128), nn.ReLU(), nn.Linear(128, 64), nn.ReLU() ) self.fc_mu = nn.Linear(64, latent_dim) self.fc_logvar = 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 encode(self, x): h = self.encoder(x) return self.fc_mu(h), self.fc_logvar(h) def reparameterize(self, mu, logvar): std = torch.exp(0.5 * logvar) eps = torch.randn_like(std) return mu + eps * std def forward(self, x): mu, logvar = self.encode(x) z = self.reparameterize(mu, logvar) return self.decoder(z), z, mu, logvar # Train VAE vae = VAE(X.shape[1], 2) optimizer = optim.Adam(vae.parameters(), lr=0.001) for epoch in range(200): for batch in loader: x = batch[0] optimizer.zero_grad() x_reconstructed, z, mu, logvar = vae(x) # VAE loss: reconstruction + KL divergence recon_loss = criterion(x_reconstructed, x) kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) kl_loss = kl_loss / x.size(0) loss = recon_loss + 0.001 * kl_loss loss.backward() optimizer.step() with torch.no_grad(): _, X_vae, _, _ = vae(X_tensor) X_vae = X_vae.numpy() # 4. Compare all methods pca = PCA(n_components=2) X_pca = pca.fit_transform(X_scaled) fig, axes = plt.subplots(2, 4, figsize=(20, 10)) methods = { 'PCA (Linear)': X_pca, 'Linear AE': X_linear_ae, 'Deep AE': X_deep_ae, 'VAE': X_vae, } for ax, (name, X_transformed) in zip(axes.flatten(), methods.items()): scatter = ax.scatter(X_transformed[:, 0], X_transformed[:, 1], c=y, cmap='tab10', alpha=0.6, s=10) ax.set_title(name) ax.axis('off') plt.tight_layout() plt.show() # 5. Reconstruction error comparison def reconstruction_error(original, reconstructed): return np.mean((original - reconstructed) ** 2) # For PCA X_pca_reconstructed = pca.inverse_transform(X_pca) pca_error = reconstruction_error(X_scaled, X_pca_reconstructed) # For autoencoders with torch.no_grad(): X_linear_reconstructed, _ = linear_ae(X_tensor) X_deep_reconstructed, _ = deep_ae(X_tensor) X_vae_reconstructed, _, _, _ = vae(X_tensor) linear_ae_error = reconstruction_error(X_scaled, X_linear_reconstructed.numpy()) deep_ae_error = reconstruction_error(X_scaled, X_deep_reconstructed.numpy()) vae_error = reconstruction_error(X_scaled, X_vae_reconstructed.numpy()) print("\n=== Reconstruction Error (MSE) ===") print(f"PCA: {pca_error:.4f}") print(f"Linear AE: {linear_ae_error:.4f}") print(f"Deep AE: {deep_ae_error:.4f}") print(f"VAE: {vae_error:.4f}") # 6. Future of dimensionality reduction print("\n=== Future of Dimensionality Reduction ===") print(""" 1. DEEP LEARNING APPROACHES - Deep autoencoders - Variational autoencoders (VAE) - Generative models (GANs, diffusion) - Self-supervised learning 2. NEURAL DIMENSIONALITY REDUCTION - UMAP with neural networks - Neural PCA - Deep kernel methods - Contrastive learning 3. HYBRID METHODS - PCA + Autoencoders - Linear + Non-linear - Probabilistic methods 4. FOUNDATION MODELS - Pre-trained representations - CLIP, DINOv2 embeddings - Transfer learning - Zero-shot dimensionality reduction 5. ADVANCED METHODS - Topological data analysis - Manifold learning - Sparse coding - Dictionary learning 6. PRACTICAL TRENDS - On-device dimensionality reduction - Federated learning with PCA - Differential privacy - Streaming algorithms """) # 7. Comparison: PCA vs Deep Learning print("\n=== PCA vs Deep Learning ===") comparison = { 'Speed': ('Very fast', 'Slower (needs GPU)'), 'Interpretability': ('High', 'Low'), 'Non-linearity': ('No', 'Yes'), 'Data requirements': ('Any size', 'Large data preferred'), 'Training': ('No training', 'Requires training'), 'Deterministic': ('Yes', 'No (random init)'), 'Hyperparameters': ('Few', 'Many'), 'Deployment': ('Simple', 'Complex'), 'Robustness': ('Good', 'Variable'), 'Best for': ('Linear, fast, interpretable', 'Complex, non-linear, large'), } print(f"{'Aspect':<25} {'PCA':<25} {'Deep Learning':<25}") print("-" * 75) for aspect, (pca_val, dl_val) in comparison.items(): print(f"{aspect:<25} {pca_val:<25} {dl_val:<25}") # 8. Recommendations print("\n=== Recommendations ===") print(""" Use PCA when: โœ“ Data is linearly correlated โœ“ You need fast, interpretable results โœ“ Dataset is small or medium โœ“ You want deterministic output โœ“ You can't afford GPU training โœ“ Interpretability matters โœ“ You need a baseline Use Autoencoders when: โœ“ Data has non-linear structure โœ“ You have large datasets โœ“ You need the best reconstruction โœ“ You can afford training time โœ“ Interpretability is less important โœ“ You have GPU resources Use VAE when: โœ“ You need a probabilistic model โœ“ You want to generate new samples โœ“ You need a smooth latent space โœ“ You're doing representation learning โœ“ You want to interpolate between samples โœ“ You need uncertainty estimates Hybrid approach (recommended): 1. Start with PCA as baseline 2. If results are insufficient, try autoencoders 3. Compare reconstruction error 4. Consider task-specific metrics 5. Combine PCA + deep learning when possible """)