Linear regression, binary logistic regression, gradient descent, K-Means and PCA, implemented from first principles with NumPy as the only runtime dependency. The package connects mathematical derivations to readable implementations and validates their behavior against scikit-learn on six offline datasets.
Browse the algorithms · Read the mathematics · Inspect generated benchmarks · Explore the tests
High-level libraries make model fitting convenient while hiding the choices that determine correctness: loss scaling, intercept handling, numerical stability, initialization and stopping criteria. This project makes those decisions explicit and tests them as software. It combines numerical computing and mathematical reasoning with a small, installable Python package designed for inspection and reproducible experiments.
| Algorithm | Key concepts | API | Reference validation |
|---|---|---|---|
| Linear regression | MSE gradients; centered least squares |
fit, predict, score
|
Held-out MSE, |
| Logistic regression | Stable sigmoid and cross entropy; optional L2 |
fit, predict, predict_proba, score
|
Accuracy, F1, log loss, probability differences |
| Gradient descent | Full-batch fixed steps; gradient-norm stopping | minimize |
Analytic quadratics, gradient checks, regression optimum |
| K-Means | Lloyd updates; K-Means++; multistart selection |
fit, predict, fit_predict
|
Inertia and permutation-invariant ARI |
| PCA | Centered thin SVD; variance; reconstruction |
fit, transform, fit_transform, inverse_transform
|
Variance, reconstruction and subspace projectors |
- Shared validation, fitted-state errors, typed APIs and explicit convergence reporting.
- Stable logit-space loss, rank-deficient least squares and empty-cluster handling.
- Analytic, edge-case and scikit-learn comparison tests; a 90% branch-inclusive coverage gate.
- Executed CSV/JSON/Markdown benchmarks with seeds, preprocessing, solver settings and environment metadata.
- Five educational notebooks and five scripted figures using the same tested package code.
- GitHub Actions for lint, formatting, types, tests, notebooks and artifact generation.
src/ml_from_scratch/
├── linear_model/ # Linear and binary logistic regression
├── optimization/ # Shared gradient descent
├── cluster/ # K-Means
├── decomposition/ # PCA
├── metrics/ # Regression, classification and clustering scores
└── utils/ # Validation and numerical primitives
tests/ # Correctness, API, numerical and reference tests
benchmarks/ # Six offline datasets and independent sklearn metrics
notebooks/ # Five sequential educational demonstrations
docs/ # Derivations, architecture and methodology
scripts/ # Figure generation and executable-documentation checks
assets/ # Reproducible selected figures
.github/workflows/ # CI
The architecture guide explains dependency boundaries and API contracts. Core modules import only NumPy, the standard library and this package; an automated test enforces that boundary.
Use Python 3.11 or newer. From the repository directory after cloning or downloading it:
python -m venv .venvActivate the environment on macOS/Linux:
source .venv/bin/activateOr in Windows PowerShell:
.venv\Scripts\Activate.ps1Then install the package and development tools:
python -m pip install -e ".[dev]"
python -m pytest --cov=ml_from_scratch --cov-report=term-missing --cov-report=xmlActivation is optional: on Windows you can use .venv\Scripts\python.exe in place of
python in every command. python -m pip install . installs only the runtime package
and NumPy. Datasets require no downloads after dependencies are installed.
Each example below runs independently; the documentation verifier executes these exact blocks. Inputs are dense numeric matrices with samples in rows and one-dimensional targets.
import numpy as np
from ml_from_scratch import LinearRegression
X = np.array([[-2.0], [-1.0], [0.0], [1.0], [2.0]])
y = 3.0 * X[:, 0] + 2.0
model = LinearRegression(solver="gd", learning_rate=0.1, tol=1e-8).fit(X, y)
np.testing.assert_allclose(model.predict([[3.0]]), [11.0], atol=1e-6)
assert model.converged_solver="normal" (the default) uses centered np.linalg.lstsq, avoiding explicit matrix
inversion. Both solvers support multiple features and an optional intercept.
import numpy as np
from ml_from_scratch import LogisticRegression
X = np.array([[-2.0], [-1.0], [1.0], [2.0]])
y = np.array([0, 0, 1, 1])
model = LogisticRegression(l2=0.1, threshold=0.5).fit(X, y)
probabilities = model.predict_proba(X) # Columns are P(y=0), P(y=1).
np.testing.assert_allclose(probabilities.sum(axis=1), 1.0)
np.testing.assert_array_equal(model.predict(X), y)Training requires both binary labels 0 and 1. The objective is mean binary cross entropy
plus l2 / 2 * ||coef_||²; the intercept is unpenalized. Scale features for fixed-step GD.
import numpy as np
from ml_from_scratch import GradientDescent
def objective(theta):
residual = theta - np.array([2.0, -1.0])
return float(residual @ residual), 2.0 * residual
optimizer = GradientDescent(learning_rate=0.2, tol=1e-8).minimize(objective, np.zeros(2))
np.testing.assert_allclose(optimizer.params_, [2.0, -1.0], atol=1e-7)
assert len(optimizer.loss_history_) == optimizer.n_iter_ + 1Iterations stop when the gradient norm is at most tol. Exhausting max_iter emits
ConvergenceWarning; a materially increasing loss or nonfinite arithmetic raises an error
with guidance to lower the step size or scale features.
import numpy as np
from ml_from_scratch import KMeans
X = np.array([[0.0, 0.0], [0.0, 1.0], [8.0, 8.0], [8.0, 9.0]])
model = KMeans(n_clusters=2, n_init=10, random_state=42)
labels = model.fit_predict(X)
np.testing.assert_array_equal(labels, model.predict(X))
assert model.cluster_centers_.shape == (2, 2)
assert np.isclose(model.inertia_, 1.0)Cluster IDs are arbitrary. Empty clusters are reseeded; duplicate observations can yield fewer occupied clusters than requested. The seed controls a local NumPy generator.
import numpy as np
from ml_from_scratch import PCA
X = np.array([[-2.0, -4.0], [-1.0, -2.0], [1.0, 2.0], [2.0, 4.0]])
model = PCA(n_components=1)
projected = model.fit_transform(X)
reconstructed = model.inverse_transform(projected)
assert projected.shape == (4, 1)
np.testing.assert_allclose(reconstructed, X, atol=1e-12)PCA centers features but does not standardize or whiten. Component signs are arbitrary; the projection subspace and reconstruction are the meaningful comparison objects.
The mathematics guide derives both regression gradients, explains learning-rate and convergence conditions, derives Lloyd's centroid updates, and connects PCA variance maximization to SVD and reconstruction error. It also documents objective normalization, L2 scaling, metric edge cases and sign/label ambiguity.
These are executed results, with seed 42, from the checked-in machine-readable run. Supervised rows evaluate a held-out 25% split. K-Means and PCA rows evaluate whole-dataset exploratory fits. Preprocessing and settings are fixed before comparison; see methodology.
| Algorithm | Dataset | Metric | From scratch | scikit-learn | Absolute difference | Relative difference |
|---|---|---|---|---|---|---|
| LinearRegression(gd) | synthetic_regression | test_mse | 93.242 | 93.242 | 2.09727e-08 | 2.24927e-10 |
| LinearRegression(gd) | diabetes | test_mse | 2848.31 | 2848.31 | 1.96955e-06 | 6.91481e-10 |
| LogisticRegression | breast_cancer | test_accuracy | 0.986014 | 0.986014 | 0 | 0 |
| KMeans | iris | inertia | 139.82 | 139.82 | 8.52651e-14 | 6.09819e-16 |
| KMeans | wine | inertia | 1277.93 | 1277.93 | 9.09495e-13 | 7.11695e-16 |
| PCA | iris | reconstruction_mse | 0.0418679 | 0.0418679 | 6.93889e-18 | 1.65733e-16 |
| PCA | wine | reconstruction_mse | 0.198377 | 0.198377 | 5.55112e-17 | 2.79826e-16 |
| PCA | digits | reconstruction_mse | 0.00775101 | 0.00775101 | 8.67362e-19 | 1.11903e-16 |
The full table includes
Runtime is recorded for context, not as a speed claim. Fixed-step GD, NumPy linear algebra, reference solvers and K-Means initialization use different numerical paths. Matching integer seeds does not imply matching random draws across implementations.
Regenerate every result and refresh this table:
python -m benchmarks.run_all --update-readmeThe six unique datasets are synthetic regression, diabetes, breast cancer, Iris, Wine and Digits. See results metadata for the executed environment.
The gradient descent figure compares step sizes on a quadratic.
Regenerate all five figures with python scripts/generate_figures.py.
| Notebook | Focus |
|---|---|
| 01 — Linear regression | Closed-form and gradient-based fitting with reference predictions |
| 02 — Logistic regression | Probabilities, regularized loss and classification |
| 03 — Gradient descent | Step size, convergence and an analytical optimum |
| 04 — K-Means | Unsupervised structure and label-invariant evaluation |
| 05 — PCA | Variance, projection and reconstruction |
Each notebook imports the package, develops the mathematics, runs an experiment, plots
results and compares a reference. To explore interactively, install .[notebooks] and run
python -m jupyterlab. To verify all five in clean kernels, use the command below.
Latest local acceptance: 241 tests passed, 98.41% branch-inclusive coverage, with all five notebooks and all five README examples executed successfully.
All commands run from the repository root after installing .[dev]. Make is optional;
the Python equivalents work in PowerShell, Bash and CI.
| Task | Command | Make shortcut |
|---|---|---|
| Install | python -m pip install -e ".[dev]" |
make install |
| Tests and coverage | python -m pytest --cov=ml_from_scratch --cov-report=term-missing --cov-report=xml |
make test |
| Lint | python -m ruff check . |
make lint (also checks formatting) |
| Check formatting | python -m ruff format --check . |
make lint |
| Format | python -m ruff format . |
make format (also fixes lint) |
| Types | python -m mypy |
make typecheck |
| Benchmarks | python -m benchmarks.run_all |
make benchmark |
| Figures | python scripts/generate_figures.py |
make figures |
| README, links and CI structure | python scripts/verify_artifacts.py |
make verify |
| Also execute notebooks | python scripts/verify_artifacts.py --notebooks |
make notebooks |
| Build wheel and source distribution | python -m build |
make build |
Tests cover known coefficients, finite-difference gradients, probability bounds, singular designs, duplicate clusters, reconstruction, invalid inputs, numerical failures, failed refits, determinism and reference agreement. Warnings are errors unless explicitly expected. The CI workflow tests Python 3.11 and 3.13 on Linux without secrets. Local acceptance evidence is recorded in verification.
NumPy supplies array operations and linear algebra; every estimator's objective, gradients, updates, convergence and API are implemented here. scikit-learn is confined to independent validation, dataset loading and demonstrations. Shared utilities keep validation consistent, and one optimizer connects the mathematical objectives to the supervised estimators.
No remote repository URL, hosted CI badge or performance claim is invented. Suggested GitHub description and topics are available in repository metadata.
This is an educational implementation with software-engineering checks. It does not aim to replace optimized production libraries such as scikit-learn.
- Dense, real, finite arrays only; no sparse matrices, missing-value imputation or sample weights.
- Single-target linear regression and binary 0/1 logistic regression; no multiclass solver.
- Fixed-step full-batch GD can converge slowly on poorly conditioned features. Features are not automatically standardized.
- K-Means can reach local optima; absolute stopping tolerance and initialization differ from scikit-learn.
- Full thin-SVD PCA has no whitening, randomized solver or incremental mode.
- The API follows familiar estimator conventions but does not implement sklearn cloning, parameter search or pipeline integration.
- Benchmarks use one fixed supervised split and selected small datasets; they do not estimate generalization uncertainty or establish runtime superiority.
Repeated-split evaluation with uncertainty estimates, a documented backtracking line search, weighted objectives, and incremental PCA would each add distinct engineering or mathematical value. These are outside the implemented scope.
MIT.



