A single-feature linear regression that predicts a car's price from its
mileage, trained with gradient descent — implemented from scratch in pure
Python. No library does the regression (no numpy.polyfit, lstsq, or
scikit-learn), so every step is inspectable and defensible at evaluation.
python3 train.py # gradient descent over data.csv -> theta.csv
python3 predict.py # prompts for a mileage -> estimated price
python3 predict.py 50000 # or pass the mileage directlypredict.py uses the subject's hypothesis exactly:
estimatePrice(mileage) = theta0 + theta1 * mileage
Before training, theta0 = theta1 = 0, so the estimate is 0.
train.py applies the subject's update rules verbatim, updating both thetas
simultaneously:
tmp_theta0 = learningRate * (1/m) * Σ (estimatePrice(x[i]) - y[i])
tmp_theta1 = learningRate * (1/m) * Σ (estimatePrice(x[i]) - y[i]) * x[i]
Real mileages are ~10⁵, so the x[i] factor in tmp_theta1 makes raw gradient
descent diverge for any usable learning rate. The trainer min-max normalises
the feature and target, runs the exact update above in that scaled space, then
de-normalises the thetas back to real units before saving:
theta1 = t1 * yspan / xspan
theta0 = ymin + t0 * yspan - theta1 * xmin
The algorithm is unchanged — only the feature scale is. The saved thetas work directly on real kilometres and match the closed-form least-squares solution to 5+ significant figures (the test suite asserts this).
For the 24-point dataset training is instant either way (~1.5 ms). The default
trainer is O(iterations × samples); train.py --fast uses the fact that both
gradient sums are linear in the data and collapse to four precomputed moments,
making each step O(1) — training time then stops depending on dataset size, for
an identical result (make bench):
| samples | naive | --fast |
speedup |
|---|---|---|---|
| 100 | 8 ms | 0.16 ms | 50× |
| 10 000 | 812 ms | 1.15 ms | 706× |
| 100 000 | 8.2 s | 9.5 ms | 867× |
| 1 000 000 | ~80 s | 93 ms | ~860× |
python3 precision.py # R² (coefficient of determination) + MSE / RMSE / MAE
python3 plot.py # matplotlib PNG if installed, else SVG (+ ASCII preview)
python3 plot.py --show # matplotlib window
python3 plot.py --svg # force the dependency-free SVGplot.py draws the data and the fitted line. It uses matplotlib when it is
installed (regression.png, or a window with --show) and otherwise falls back
to a hand-written, dependency-free SVG — so the visualisation works whether
or not a plotting library is present. A quick ASCII scatter always prints to the
terminal.
On this dataset: R² ≈ 0.733 (73 % of the price variance explained),
theta0 ≈ 8499.6, theta1 ≈ -0.0214.
make test # ~108 cases: core unit tests + end-to-end CLI stress (zero deps)
make bench # naive vs --fast trainer benchmark
make lint # ruff check + ruff format --check
make format # ruff format + autofix
make fclean # remove theta.csv / regression.svg / cachesThe stress suite (tests/test_cli.py) drives the real programs through valid,
invalid, weird and long inputs and asserts none ever crash. Requires only
Python 3.10+ (developed on 3.12); ruff is the sole dev tool.
| File | Role |
|---|---|
linear_regression.py |
core: load, normalise→GD→de-normalise, metrics, IO |
train.py |
mandatory: train and save theta.csv (--fast path) |
predict.py |
mandatory: mileage → estimated price |
precision.py |
bonus: model precision (R²/MSE/RMSE/MAE) |
plot.py |
bonus: SVG + ASCII visualisation |
tests/test_core.py |
core unit tests (fast≡naive, vs closed-form, edges) |
tests/test_cli.py |
end-to-end CLI stress (~108 input scenarios) |
bench.py |
naive vs --fast trainer benchmark |
data.csv |
the provided dataset (km,price) |