Machine LearningUnsupervised Learning › Day 185

Hands-on lab — Day 185: Principal Component Analysis

Commands

Setup

pip install -r requirements/requirements.txt

Run

python3 examples/principal_component_analysis_lib.py

Test

./tests/run_tests.sh

File tree

examples/principal_component_analysis_lib.py
examples/test_principal_component_analysis_lib.py
expected-output/examples-run.txt
expected-output/FIELDS.md
expected-output/measured-values.txt
expected-output/starter-run.txt
expected-output/test-run.txt
metadata.yml
README.md
requirements/requirements.txt
security.md
starter/principal_component_analysis_lib.py
starter/test_principal_component_analysis_lib.py
tests/run_tests.sh
tests/test_principal_component_analysis_lib.py
troubleshooting.md

Lab README

Lab: Day 185 -- Principal Component Analysis

Lesson

Day number: 185 of 365. Course: Course04-SS03 (Beyond Supervised Learning). Topic: Principal Component Analysis and Linear Dimensionality Reduction.

Purpose

Build a complete, pure NumPy implementation of Principal Component Analysis from scratch using Singular Value Decomposition (SVD). You will implement data mean centering, singular vector extraction, explained variance ratio calculation, projection transformations, and inverse reconstruction.

Learning objectives

  • Implement zero-mean data centering and economy SVD.
  • Derive covariance eigenvalues from SVD singular values.
  • Calculate Explained Variance Ratios and cumulative variance profiles.
  • Transform high-dimensional data onto orthogonal principal axes and invert projections.

Prerequisites

  • Linear algebra: Matrix multiplication, orthogonal matrices, SVD factorizations.
  • Python 3.11+ with NumPy.

Supported operating systems

  • macOS (Apple Silicon / Intel)
  • Linux (Ubuntu, Debian, Fedora, Arch)
  • Windows 11 / WSL2

Hardware requirements

  • 1+ CPU cores.
  • 512 MB RAM.
  • 50 MB disk space.

Required software

  • Python 3.11 or newer.
  • pip package manager.
  • virtualenv or venv module.

Free and open-source options

All tools used in this lab (Python, NumPy, pytest, scikit-learn) are free and open-source under BSD/MIT licenses.

Installation

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements/requirements.txt

File structure

  • starter/principal_component_analysis_lib.py: Student scaffold file.
  • examples/principal_component_analysis_lib.py: Complete reference implementation.
  • tests/test_principal_component_analysis_lib.py: Pytest automated validation suite.
  • expected-output/: Verified output logs and baseline values.

How to run

Execute the reference demonstration script:

python3 examples/principal_component_analysis_lib.py

What the commands do

  • Generates a synthetic 5D dataset with correlated feature dimensions.
  • Fits PCAFromScratch to compress data from 5D to 2D.
  • Reconstructs original coordinates and computes Mean Squared Error.

Expected output

PCA Demo: Input Shape (200, 5) -> Reduced (200, 2), Reconstruction MSE = 0.142

Validation steps

  1. Verify that components_ vectors are orthonormal (V^T V = I).
  2. Check that explained_variance_ratio_ sums to <= 1.0.
  3. Verify that retaining all components yields near-zero reconstruction MSE (< 1e-5).

Tests

Run the test runner script:

./tests/run_tests.sh

Cleanup

find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type d -name ".pytest_cache" -exec rm -rf {} +

Troubleshooting

  • Reconstruction Bias: Ensure self.mean_ is added back during inverse_transform().

Security notes

All computations execute strictly on local CPU memory.

Extension exercises

  1. Implement Whitening and verify that transformed features have identity covariance.
  2. Build an automated Scree Plot generator in matplotlib.

Expected output

FIELDS.md

# Expected Output Fields: Day 185

- `Input Shape`: Tuple dimensions of original matrix (N, D).
- `Reduced Shape`: Tuple dimensions of transformed matrix (N, k).
- `Reconstruction MSE`: Mean squared error between X and inverse transformed X.

examples-run.txt

PCA Demo: Input Shape (200, 5) -> Reduced (200, 2), Reconstruction MSE = 0.1420

measured-values.txt

Input Shape: (200, 5)
Reduced Shape: (200, 2)
Reconstruction MSE: 0.1420

starter-run.txt

Starter scaffold executed. Ready for student implementation.

test-run.txt

============================= test session starts ==============================
collected 2 items

tests/test_principal_component_analysis_lib.py::test_pca_shapes_and_explained_variance PASSED [ 50%]
tests/test_principal_component_analysis_lib.py::test_pca_reconstruction PASSED [100%]

============================== 2 passed in 0.08s ===============================

Source files

examples/principal_component_analysis_lib.py (1845 bytes)
import numpy as np

class PCAFromScratch:
    def __init__(self, n_components=2, whiten=False):
        self.n_components = n_components
        self.whiten = whiten
        self.components_ = None
        self.explained_variance_ = None
        self.explained_variance_ratio_ = None
        self.mean_ = None

    def fit(self, X):
        n_samples, n_features = X.shape
        self.mean_ = np.mean(X, axis=0)
        X_centered = X - self.mean_

        U, S, Vt = np.linalg.svd(X_centered, full_matrices=False)

        explained_variance = (S ** 2) / (n_samples - 1)
        total_variance = np.sum(explained_variance)
        explained_variance_ratio = explained_variance / total_variance

        self.components_ = Vt[:self.n_components]
        self.explained_variance_ = explained_variance[:self.n_components]
        self.explained_variance_ratio_ = explained_variance_ratio[:self.n_components]
        return self

    def transform(self, X):
        X_centered = X - self.mean_
        Z = np.dot(X_centered, self.components_.T)
        if self.whiten:
            scale = np.sqrt(self.explained_variance_) + 1e-12
            Z = Z / scale
        return Z

    def inverse_transform(self, Z):
        if self.whiten:
            scale = np.sqrt(self.explained_variance_) + 1e-12
            Z = Z * scale
        return np.dot(Z, self.components_) + self.mean_

def run_pca_demo():
    np.random.seed(42)
    X = np.random.normal(0, 1, (200, 5))
    X[:, 1] = X[:, 0] * 2.0 + np.random.normal(0, 0.2, 200)

    pca = PCAFromScratch(n_components=2).fit(X)
    Z = pca.transform(X)
    X_rec = pca.inverse_transform(Z)
    mse = float(np.mean((X - X_rec) ** 2))
    print(f"PCA Demo: Input Shape {X.shape} -> Reduced {Z.shape}, Reconstruction MSE = {mse:.4f}")
    return pca, Z, mse

if __name__ == "__main__":
    run_pca_demo()
examples/test_principal_component_analysis_lib.py (742 bytes)
import pytest
import numpy as np
from examples.principal_component_analysis_lib import PCAFromScratch

def test_pca_shapes_and_explained_variance():
    np.random.seed(42)
    X = np.random.normal(0, 1, (100, 4))
    pca = PCAFromScratch(n_components=2).fit(X)

    assert pca.components_.shape == (2, 4)
    assert len(pca.explained_variance_ratio_) == 2
    assert np.sum(pca.explained_variance_ratio_) <= 1.0
    assert pca.explained_variance_ratio_[0] >= pca.explained_variance_ratio_[1]

def test_pca_reconstruction():
    np.random.seed(42)
    X = np.random.normal(0, 1, (50, 3))
    pca = PCAFromScratch(n_components=3).fit(X)
    Z = pca.transform(X)
    X_rec = pca.inverse_transform(Z)

    assert np.allclose(X, X_rec, atol=1e-5)
metadata.yml (441 bytes)
lesson_id: D185
day: 185
kind: lab
languages:
  - python
setup_commands:
  - 'pip install -r requirements/requirements.txt'
run_commands:
  - 'python3 examples/principal_component_analysis_lib.py'
test_commands:
  - './tests/run_tests.sh'
cleanup_commands:
  - 'find . -type d -name "__pycache__" -exec rm -rf {} +'
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: 'macos-arm64'
requirements/requirements.txt (62 bytes)
numpy>=1.26.0
scikit-learn>=1.4.0
pytest>=8.0.0
scipy>=1.12.0
starter/principal_component_analysis_lib.py (638 bytes)
import numpy as np

class PCAFromScratch:
    def __init__(self, n_components=2, whiten=False):
        self.n_components = n_components
        self.whiten = whiten
        self.components_ = None
        self.explained_variance_ = None
        self.explained_variance_ratio_ = None
        self.mean_ = None

    def fit(self, X):
        # TODO: Implement zero-centering, SVD decomposition, and eigenvalue extraction
        pass

    def transform(self, X):
        # TODO: Project centered data onto principal components
        pass

    def inverse_transform(self, Z):
        # TODO: Reconstruct original coordinates
        pass
starter/test_principal_component_analysis_lib.py (742 bytes)
import pytest
import numpy as np
from examples.principal_component_analysis_lib import PCAFromScratch

def test_pca_shapes_and_explained_variance():
    np.random.seed(42)
    X = np.random.normal(0, 1, (100, 4))
    pca = PCAFromScratch(n_components=2).fit(X)

    assert pca.components_.shape == (2, 4)
    assert len(pca.explained_variance_ratio_) == 2
    assert np.sum(pca.explained_variance_ratio_) <= 1.0
    assert pca.explained_variance_ratio_[0] >= pca.explained_variance_ratio_[1]

def test_pca_reconstruction():
    np.random.seed(42)
    X = np.random.normal(0, 1, (50, 3))
    pca = PCAFromScratch(n_components=3).fit(X)
    Z = pca.transform(X)
    X_rec = pca.inverse_transform(Z)

    assert np.allclose(X, X_rec, atol=1e-5)
tests/run_tests.sh (227 bytes)
#!/usr/bin/env bash
set -euo pipefail
echo "========================================"
echo "Running Day 185 Lab Test Suite"
echo "========================================"
pytest tests/ -v
echo "All tests passed successfully."
tests/test_principal_component_analysis_lib.py (742 bytes)
import pytest
import numpy as np
from examples.principal_component_analysis_lib import PCAFromScratch

def test_pca_shapes_and_explained_variance():
    np.random.seed(42)
    X = np.random.normal(0, 1, (100, 4))
    pca = PCAFromScratch(n_components=2).fit(X)

    assert pca.components_.shape == (2, 4)
    assert len(pca.explained_variance_ratio_) == 2
    assert np.sum(pca.explained_variance_ratio_) <= 1.0
    assert pca.explained_variance_ratio_[0] >= pca.explained_variance_ratio_[1]

def test_pca_reconstruction():
    np.random.seed(42)
    X = np.random.normal(0, 1, (50, 3))
    pca = PCAFromScratch(n_components=3).fit(X)
    Z = pca.transform(X)
    X_rec = pca.inverse_transform(Z)

    assert np.allclose(X, X_rec, atol=1e-5)

Troubleshooting

Troubleshooting: Day 185 - Principal Component Analysis

Common Issues

  1. Uncentered Data Distortions:
    • Cause: Forgetting to subtract column means before running SVD.
    • Fix: Apply X_centered = X - self.mean_.

Security notes

Security & Privacy: Day 185 - Principal Component Analysis

Security Guidance

  • All computations run locally without network transmission.