Skip to content
AI360XpertBeta

Ordinary Least Squares

Ordinary least squares finds the straight-line fit that makes squared misses as small as possible. Geometry says the leftover error stands at a right angle to every feature.

Three points at heights 1, 2 and 4 fit a line of slope 1.5, leaving residuals 0.17, minus 0.33 and 0.17 that sum to zero and stand square to every column
Three points at heights 1, 2 and 4 fit a line of slope 1.5, leaving residuals 0.17, minus 0.33 and 0.17 that sum to zero and stand square to every column

Why Does This Exist?

Linear regression hands you a formula for the weights and moves on. Two questions get skipped: why that formula, and why no serious library computes it that way.

Both answers come out of one picture. A greenhouse logs its water use for three days alongside the sunlight hours each day got, so two columns go into the fit: a column of ones for the intercept, and sunlight hours.

Now the shift that makes everything follow. A list of three numbers is a point in three-dimensional space, so your water readings are one specific point out there, call it yy. Your two columns are two other points, which you may scale and add, and every combination sweeps out a flat plane through the origin. That plane is the column space of XX: every prediction your features can ever make, everything else unreachable (vector spaces and rank generalises it).

So your data is a point, your reachable predictions are a plane, and yy is not on it. Find the closest point on the plane.

Think of It Like This

The shortest string from a fly to the windowpane

A fly hovers a hand width off a window. Run a string from the fly to any point on the glass and measure it. Slide the far end around: the string shortens, bottoms out, lengthens again.

Look at where the shortest one lands. It meets the glass square on, at a right angle, and nobody imposed that. It falls out of being shortest. Tilt the string a few degrees off perpendicular and you have bolted a sideways leg onto a right triangle, so by Pythagoras it is longer. The perpendicular wins because every other string is a hypotenuse.

That is least squares in full. The glass is every prediction your features can produce, the fly is what happened, the fit is the nearest point on the glass, and the string is the error left over.

Where it stops: glass never moves and string never stretches, while data keeps arriving and features get added or dropped. The analogy covers one fixed fit, not model choice or new columns.

How It Actually Works

Write y^=Xw\hat{y} = Xw for the fitted values and r=y−y^r = y - \hat{y} for the residual. Whatever ww you pick, y^\hat{y} lands on the plane. Making rr as short as possible means making it perpendicular to the plane, and a vector is perpendicular to a plane exactly when it is perpendicular to everything spanning it, so to every column of XX:

X⊤(y−Xw)=0⟹X⊤Xw=X⊤yX^\top (y - Xw) = 0 \quad \Longrightarrow \quad X^\top X w = X^\top y

Those are the normal equations, where normal means perpendicular, nothing to do with the normal distribution. The condition is a list of dot products, one per column, each required to be zero.

Worked example: three days, slope 1.5

Take sunlight hours x=[0,1,2]x = [0, 1, 2] with water use y=[1,2,4]y = [1, 2, 4]. Sums are n=3n = 3, ∑x=3\sum x = 3, ∑y=7\sum y = 7, ∑x2=5\sum x^2 = 5, ∑xy=0+2+8=10\sum xy = 0 + 2 + 8 = 10. The slope is:

b1=3×10−3×73×5−9=96=1.5b_1 = \frac{3 \times 10 - 3 \times 7}{3 \times 5 - 9} = \frac{9}{6} = 1.5

The intercept is b0=(7−1.5×3)/3=2.5/3≈0.83b_0 = (7 - 1.5 \times 3) / 3 = 2.5 / 3 \approx 0.83. Predictions are 0.830.83, 2.332.33 and 3.833.83. Residuals are 0.170.17, −0.33-0.33 and 0.170.17. Add them: 0.17−0.33+0.17=00.17 - 0.33 + 0.17 = 0. Dot them with xx: 0×0.17+1×(−0.33)+2×0.17=0.01≈00 \times 0.17 + 1 \times (-0.33) + 2 \times 0.17 = 0.01 \approx 0, which is zero up to rounding. Both columns sit square to the leftover, exactly as the diagram shows.

One consequence you can check by hand

The intercept column is all ones, and its dot product with the residual is the sum of the residuals. So residuals sum to exactly zero whenever the model has an intercept. Duplicate columns fall out of the same picture: two identical columns buy no second dimension, so the plane stays a line and X⊤XX^\top X cannot be inverted. The nearest point is still one point, but the weights reaching it are not unique, decided by matrix rank. Correlated features generalise this headache in multicollinearity.

Squared error is an assumption about noise

Squared error looks like the default. It is a claim.

Suppose each day's water use is the true linear value plus independent noise from a bell curve of the same width every day. The probability of your three readings, as a function of ww, is a product of those densities, and its logarithm is a sum of squared gaps between observed and predicted. Maximising that log is minimising squared error. Same problem. So squared loss is maximum likelihood under Gaussian noise of constant variance, and heavy tails would make absolute error the likelihood-maximising choice instead. You were never neutral. You were assuming.

Why nobody forms the normal equations

The condition number of a matrix measures how much it amplifies small relative errors, so a large one means digits get eaten during the solve. Forming X⊤XX^\top X squares it: a design matrix at a condition number of a million becomes a system at a trillion, and double precision carries roughly sixteen digits, so you have spent twelve before the solver starts. QR and SVD work on XX directly and never form that product, paying the condition number once rather than twice. That is why library solvers factor instead of using the formula everyone learns, and why ridge regression adds a penalty when the columns nearly coincide.

Code

Both claims at once with plain arithmetic: slope 1.5, residuals summing to zero, residual square to the sunlight column.

def ols_line(x: list[float], y: list[float]) -> tuple[float, float]:    n: int = len(x)    sx: float = sum(x)    sy: float = sum(y)    sxx: float = sum(v * v for v in x)    sxy: float = sum(a * b for a, b in zip(x, y))    slope: float = (n * sxy - sx * sy) / (n * sxx - sx * sx)    intercept: float = (sy - slope * sx) / n    return intercept, slope

xs: list[float] = [0.0, 1.0, 2.0]ys: list[float] = [1.0, 2.0, 4.0]b0, b1 = ols_line(xs, ys)pred: list[float] = [b0 + b1 * v for v in xs]resid: list[float] = [b - a for a, b in zip(pred, ys)]print(f"{b1:.1f} {b0:.2f}")  # -> 1.5 0.83print(f"{sum(resid):.1f}")  # -> -0.0print(f"{sum(a * b for a, b in zip(xs, resid)):.1f}")  # -> -0.0

The printout matches the hand computation: slope 1.5 with intercept 0.83, residual sum negative zero, dot product with the feature negative zero. Change the last reading from 4 to 10 and rerun: the slope jumps, which shows how one point drags a squared-error fit.

Watch Out For

Solving the normal equations on data you never conditioned

The symptom is coefficients that swing between runs, or a solver that reports success and returns weights of magnitude ten to the ninth on data measured in litres. Raw polynomial features do it, as does mixing a column in metres with one in nanoseconds. The formula still evaluates. It answers a question a long way from yours.

Fix: use a factorising solver rather than solving the cross-product, and centre and scale columns first. Then check the condition number of XX: past about ten to the eighth, treat individual coefficients as unreadable whatever solver you used, and either drop columns or move to ridge regression.

Quoting Gauss-Markov as if it said OLS is best

The symptom is a review slide claiming OLS is the best estimator, then surprise when a biased model wins on held-out error. The theorem is real and narrower than its reputation. Under uncorrelated errors of equal variance, OLS has the smallest variance among linear unbiased estimators. Three qualifiers, all load-bearing.

Fix: drop unbiasedness and biased estimators routinely win on held-out error, which is the whole argument for shrinkage. Time-ordered data has correlated errors, spend data has variance growing with the level. Both break the guarantee while the arithmetic keeps working, so nothing complains. Check residuals for correlation and a widening fan.

The Quick Version

  • Ordinary least squares projects the target onto the column space of the features, and the residual leaves at a right angle.
  • The normal equations say one thing: make the residual perpendicular to every column.
  • On readings 1, 2 and 4 at inputs 0, 1 and 2, the slope is 1.5 with intercept about 0.83.
  • Those residuals are 0.17, minus 0.33 and 0.17, summing to zero and orthogonal to the feature.
  • Minimising squared error assumes Gaussian noise of constant variance.
  • Forming the normal equations squares the condition number, so libraries factor instead.
  • Gauss-Markov gives best linear unbiased only, which evaporates under unequal or correlated errors.