Skip to content

Linear Regression

After this lesson, you will:

  • Understand how linear regression makes predictions, and why the best line is the one with the smallest mean squared error (MSE).
  • Be able to derive the formulas for ww and bb yourself when there is one feature, and read the normal equation X⊤Xθ=X⊤y\mathbf{X}^\top\mathbf{X}\boldsymbol{\theta} = \mathbf{X}^\top\mathbf{y}.
  • Implement it with NumPy and scikit-learn, and explain what ww and bb mean.
  • Recognize its main limitations, such as outliers, extrapolation, and the case where the normal equation has infinitely many solutions.

You are looking for an apartment to rent. You ask about the rent of 10 apartments in the area where you want to live, and write them down in the table below. The whole lesson uses this table.

Rent by floor area for 10 apartments. The numbers are made up for the lesson, not market prices.
Floor area (m²)Rent (million VND/month)
256.7
306.5
368.8
428.8
489.4
5512.2
6012.0
6814.1
7513.9
8516.8

Then a friend tells you that a 65 m² apartment will be free soon. What would be a fair rent?

One look at the table tells you that larger apartments cost more. But not evenly: the 30 m² apartment is cheaper than the 25 m² one, and the 75 m² apartment is cheaper than the 68 m² one. Rent also depends on location, floor, furniture, things the table does not record. So no straight line goes through all 10 points.

What you can do is find the line that runs closest to the points, then use it to guess the rent of the 65 m² apartment. That is the whole idea of linear regression.

Try two simple ways to guess:

  • Line A: ignore the area and guess the mean rent of the table, 10.92 million VND, for every apartment. This is a flat line.
  • Line B: “200,000 VND per m²”. It guesses 5.0 million for the 25 m² apartment and 17.0 million for the 85 m² one.

For each apartment, take the actual rent minus the rent the line guesses. This difference is called the residual.

For line B, the 25 m² apartment has a residual of 6.7 − 5.0 = 1.7: the point lies above the line. The 75 m² apartment has a residual of 13.9 − 15.0 = −1.1: the point lies below the line. On a plot, a residual is the vertical segment between a data point and the line. To compare two lines, you need one number that sums up the residuals of all 10 apartments.

Your first thought might be to add up the residuals. But the residuals of line A add up to exactly 0, because the positive and negative residuals around the mean cancel out perfectly. Yet line A is very bad: it guesses the same rent for the 25 m² apartment and the 85 m² one.

The fix is to square each residual, then take the mean. This number is the mean squared error, or MSE for short. There are three reasons to choose the square:

  1. Nothing cancels. A square is never negative, so an error counts whichever side of the line it is on.
  2. Large errors cost more. Being off by 1 million costs 1 point; being off by 2 million costs 4 points. So two apartments that are each off by 1 million (2 points) beat one exact guess plus one apartment off by 2 million (4 points). The downside: a single point that lies very far away pulls the whole line toward it, as you will see in the third code cell.
  3. You can take its derivative. The square function is smooth, while the absolute value has a sharp corner at 0, where it has no derivative. Derivatives let you find the best line with algebra instead of trial and error.

The MSE of each line, computed with Python on the table above:

LineMSE
A: flat at the mean rent 10.9210.50
B: 200,000 VND per m²0.88
The best line, found later in this lesson0.36

Line B is much better than line A, but an even better line exists. The problem is now clear: among all straight lines, find the one with the smallest MSE.

The table has mm samples, one per apartment, so m=10m = 10. Sample ii has a feature x(i)x^{(i)}, the floor area, and a label y(i)y^{(i)}, the actual rent. The superscript (i)(i) means “apartment number ii”, not a power.

The model guesses the rent with a straight line:

y^=wx+b\hat{y} = wx + b
  • y^\hat{y} (read “y hat”) is the predicted rent, to tell it apart from the actual rent yy.
  • ww is the weight. With only one feature, ww is the slope of the line: when the area grows by 1 m², the predicted rent grows by ww million.
  • bb is the bias, also called the intercept: where the line crosses the vertical axis.

Most of this lesson uses one feature, because it is easy to picture. The section on the matrix form extends this to many features.

For apartment ii, the predicted rent is y^(i)=wx(i)+b\hat{y}^{(i)} = wx^{(i)} + b, and the residual is

e(i)=y(i)−y^(i)e^{(i)} = y^{(i)} - \hat{y}^{(i)}

The loss function L\mathcal{L} is the MSE, the mean of the squared residuals over all mm apartments:

L(w,b)=1m∑i=1m(y^(i)−y(i))2=1m∑i=1m(wx(i)+b−y(i))2\begin{aligned} \mathcal{L}(w, b) &= \frac{1}{m}\sum_{i=1}^{m}\left(\hat{y}^{(i)} - y^{(i)}\right)^2 \\ &= \frac{1}{m}\sum_{i=1}^{m}\left(wx^{(i)} + b - y^{(i)}\right)^2 \end{aligned}

A few things to notice:

  • y^(i)−y(i)\hat{y}^{(i)} - y^{(i)} is just the residual with its sign flipped. Once squared, the two are the same.
  • The data is fixed, so L\mathcal{L} is a function of only the two parameters ww and bb.
  • Many books use 12m\frac{1}{2m} so that the 2 cancels when you take the derivative. Multiplying L\mathcal{L} by a positive constant does not move its lowest point, so the solution stays the same. This lesson uses 1m\frac{1}{m} to match the MSE of scikit-learn.

The task is to find the ww and bb that make L(w,b)\mathcal{L}(w, b) as small as possible. The answer is called the least squares solution.

L\mathcal{L} is a sum of squares of expressions that are linear in ww and bb, so its graph over (w,b)(w, b) is a bowl that opens upward. The only place where the bowl is flat (every partial derivative is 0) is its bottom, so you only need to set the two partial derivatives to 0 and solve. This requires that the apartments do not all have the same area (see the section “When the solution is not unique”).

Let xˉ=1m∑i=1mx(i)\bar{x} = \frac{1}{m}\sum_{i=1}^{m} x^{(i)} be the mean area and yˉ=1m∑i=1my(i)\bar{y} = \frac{1}{m}\sum_{i=1}^{m} y^{(i)} the mean rent.

Step 1: the partial derivative with respect to bb. By the chain rule, the derivative of u2u^2 is 2u2u times the derivative of uu. Here u=wx(i)+b−y(i)u = wx^{(i)} + b - y^{(i)}, and its derivative with respect to bb is 1. So:

∂L∂b=2m∑i=1m(wx(i)+b−y(i))\frac{\partial\mathcal{L}}{\partial b} = \frac{2}{m}\sum_{i=1}^{m}\left(wx^{(i)} + b - y^{(i)}\right)

Set this derivative to 0 and divide both sides by 2. The sum splits into w∑ix(i)+mb−∑iy(i)w\sum_{i} x^{(i)} + mb - \sum_{i} y^{(i)}. Dividing by mm turns the three parts into wxˉw\bar{x}, bb and yˉ\bar{y}:

0=1m∑i=1m(wx(i)+b−y(i))=wxˉ+b−yˉ\begin{aligned} 0 &= \frac{1}{m}\sum_{i=1}^{m}\left(wx^{(i)} + b - y^{(i)}\right) \\ &= w\bar{x} + b - \bar{y} \end{aligned}

Rearranging:

b=yˉ−wxˉb = \bar{y} - w\bar{x}

In words: yˉ=wxˉ+b\bar{y} = w\bar{x} + b, so the best line always passes through the mean point (xˉ,yˉ)(\bar{x}, \bar{y}), and the residuals average to 0. Line A also passes through this point (which is why its residuals add up to 0), and it is still bad: this condition is necessary but not sufficient. You also have to pick the right slope.

Step 2: the partial derivative with respect to ww. This time, the derivative of uu with respect to ww is x(i)x^{(i)}:

∂L∂w=2m∑i=1mx(i)(wx(i)+b−y(i))\frac{\partial\mathcal{L}}{\partial w} = \frac{2}{m}\sum_{i=1}^{m}x^{(i)}\left(wx^{(i)} + b - y^{(i)}\right)

Set this derivative to 0, then drop the factor 2m\frac{2}{m}, since it is not 0:

∑i=1mx(i)(wx(i)+b−y(i))=0\sum_{i=1}^{m}x^{(i)}\left(wx^{(i)} + b - y^{(i)}\right) = 0

Step 3: substitute bb from step 1. Write the deviations from the mean as x~(i)=x(i)−xˉ\tilde{x}^{(i)} = x^{(i)} - \bar{x} and y~(i)=y(i)−yˉ\tilde{y}^{(i)} = y^{(i)} - \bar{y}. The expression in parentheses becomes:

wx(i)+b−y(i)=wx(i)+(yˉ−wxˉ)−y(i)=w(x(i)−xˉ)−(y(i)−yˉ)=wx~(i)−y~(i)\begin{aligned} & wx^{(i)} + b - y^{(i)} \\ &= wx^{(i)} + \left(\bar{y} - w\bar{x}\right) - y^{(i)} \\ &= w\left(x^{(i)} - \bar{x}\right) - \left(y^{(i)} - \bar{y}\right) \\ &= w\tilde{x}^{(i)} - \tilde{y}^{(i)} \end{aligned}

The third line groups the two terms with ww together, and the other two terms together. The equation from step 2 becomes:

∑i=1mx(i)(wx~(i)−y~(i))=0\sum_{i=1}^{m}x^{(i)}\left(w\tilde{x}^{(i)} - \tilde{y}^{(i)}\right) = 0

Step 4: turn the x(i)x^{(i)} in front of the parentheses into x~(i)\tilde{x}^{(i)}. Deviations from the mean always add up to 0: ∑ix~(i)=∑ix(i)−mxˉ=0\sum_{i} \tilde{x}^{(i)} = \sum_{i} x^{(i)} - m\bar{x} = 0, and likewise ∑iy~(i)=0\sum_{i} \tilde{y}^{(i)} = 0. Therefore:

xˉ∑i=1m(wx~(i)−y~(i))=xˉ (w⋅0−0)=0\begin{aligned} & \bar{x}\sum_{i=1}^{m}\left(w\tilde{x}^{(i)} - \tilde{y}^{(i)}\right) \\ &= \bar{x}\,(w \cdot 0 - 0) = 0 \end{aligned}

Subtract this identity from the equation of step 3. Each x(i)x^{(i)} in front of the parentheses becomes x(i)−xˉx^{(i)} - \bar{x}, which is x~(i)\tilde{x}^{(i)}:

∑i=1mx~(i)(wx~(i)−y~(i))=0\sum_{i=1}^{m}\tilde{x}^{(i)}\left(w\tilde{x}^{(i)} - \tilde{y}^{(i)}\right) = 0

Step 5: solve for ww. Expanding the parentheses splits the equation into two sums:

w∑i=1m(x~(i))2−∑i=1mx~(i)y~(i)=0w\sum_{i=1}^{m}\left(\tilde{x}^{(i)}\right)^2 - \sum_{i=1}^{m}\tilde{x}^{(i)}\tilde{y}^{(i)} = 0

Move the second sum to the right-hand side, divide by the first sum, and write the result in terms of the original xx and yy:

w=∑i=1m(x(i)−xˉ)(y(i)−yˉ)∑i=1m(x(i)−xˉ)2w = \frac{\sum_{i=1}^{m}\left(x^{(i)} - \bar{x}\right)\left(y^{(i)} - \bar{y}\right)}{\sum_{i=1}^{m}\left(x^{(i)} - \bar{x}\right)^2}

Once you have ww, step 1 gives b=yˉ−wxˉb = \bar{y} - w\bar{x}.

Reading the formula. An apartment whose two deviations have the same sign (larger and pricier than average, or smaller and cheaper) adds a positive number to the numerator; opposite signs add a negative number. When the positive numbers outweigh the negative ones, w>0w > 0 and the line slopes upward. The denominator measures how spread out the areas are, and it is 0 only when every apartment has the same area.

On the apartment table. Here xˉ=52.4\bar{x} = 52.4, yˉ=10.92\bar{y} = 10.92, the numerator is 598.32 and the denominator is 3530.4. So:

w=598.323530.4≈0.1695b≈10.92−0.1695×52.4≈2.04\begin{aligned} w &= \frac{598.32}{3530.4} \approx 0.1695 \\ b &\approx 10.92 - 0.1695 \times 52.4 \approx 2.04 \end{aligned}

The best line is y^=0.1695x+2.04\hat{y} = 0.1695x + 2.04, with an MSE of about 0.3615. Each extra m² raises the predicted rent by about 0.17 million (170,000 VND). As for b≈2.04b \approx 2.04, it is just where the line crosses the vertical axis, not “the rent of a 0 m² apartment”: the data has no apartment anywhere near that small.

With several features, for example the number of bedrooms as well, each apartment is a vector x=[x1,…,xn]⊤\mathbf{x} = [x_1, \dots, x_n]^\top, each xjx_j has its own weight wjw_j, and the prediction is a dot product plus bb:

y^=w⊤x+b\hat{y} = \mathbf{w}^\top\mathbf{x} + b

Matrix notation lets you write the problem compactly for any nn. Arrange the data into a design matrix X\mathbf{X}: one row per apartment, one column per feature, plus a column of ones at the end. The actual rents form a vector y\mathbf{y}. For the apartment table:

X=[251301⋮⋮851],y=[6.76.5⋮16.8]\mathbf{X} = \begin{bmatrix} 25 & 1 \\ 30 & 1 \\ \vdots & \vdots \\ 85 & 1 \end{bmatrix}, \qquad \mathbf{y} = \begin{bmatrix} 6.7 \\ 6.5 \\ \vdots \\ 16.8 \end{bmatrix}

The parameters go into a vector θ=[w,b]⊤\boldsymbol{\theta} = [w, b]^\top. With nn features, row ii of X\mathbf{X} is [ x1(i),…,xn(i),1 ][\,x^{(i)}_1, \dots, x^{(i)}_n, 1\,] (xj(i)x^{(i)}_j is feature jj of apartment ii) and θ=[w1,…,wn,b]⊤\boldsymbol{\theta} = [w_1, \dots, w_n, b]^\top, so X\mathbf{X} has mm rows and n+1n + 1 columns.

Why the column of ones? The dot product of row ii with θ\boldsymbol{\theta} is x(i)w+1⋅b=y^(i)x^{(i)}w + 1 \cdot b = \hat{y}^{(i)}: the column of ones is there so that bb gets added to every prediction. So Xθ\mathbf{X}\boldsymbol{\theta} holds all mm predicted rents, and Xθ−y\mathbf{X}\boldsymbol{\theta} - \mathbf{y} holds the y^(i)−y(i)\hat{y}^{(i)} - y^{(i)}. The squared length of a vector, ∥v∥2=v12+⋯+vm2\lVert\mathbf{v}\rVert^2 = v_1^2 + \dots + v_m^2, is the sum of the squares of its entries, so the MSE becomes simply:

L(θ)=1m∥Xθ−y∥2\mathcal{L}(\boldsymbol{\theta}) = \frac{1}{m}\left\lVert\mathbf{X}\boldsymbol{\theta} - \mathbf{y}\right\rVert^2

Set every partial derivative of this function (with respect to w1,…,wnw_1, \dots, w_n and bb) to 0, and you get n+1n + 1 linear equations. In matrix form, the whole system fits on one line, called the normal equation:

X⊤X θ=X⊤y\mathbf{X}^\top\mathbf{X}\,\boldsymbol{\theta} = \mathbf{X}^\top\mathbf{y}

You do not need matrix calculus to understand it. Move the right-hand side to the left and factor out X⊤\mathbf{X}^\top: the equation becomes X⊤(Xθ−y)=0\mathbf{X}^\top\left(\mathbf{X}\boldsymbol{\theta} - \mathbf{y}\right) = \mathbf{0}. Row jj of the left-hand side is the dot product of column jj of X\mathbf{X} with Xθ−y\mathbf{X}\boldsymbol{\theta} - \mathbf{y}, which is ∑ixj(i)(y^(i)−y(i))\sum_{i} x^{(i)}_j\left(\hat{y}^{(i)} - y^{(i)}\right). Multiplied by 2m\frac{2}{m}, row jj is exactly ∂L/∂wj\partial\mathcal{L}/\partial w_j (the chain rule, as in step 2), and the last row, which belongs to the column of ones, is ∂L/∂b\partial\mathcal{L}/\partial b. So the normal equation is just the “derivative equals 0” equations stacked into a matrix. For the apartment table, it is a system with two unknowns:

30988 w+524 b=6320.4524 w+10 b=109.2\begin{aligned} 30988\,w + 524\,b &= 6320.4 \\ 524\,w + 10\,b &= 109.2 \end{aligned}

On the left, 30988, 524 and 10 are ∑i(x(i))2\sum_{i} \left(x^{(i)}\right)^2, ∑ix(i)\sum_{i} x^{(i)} and mm: the entries of X⊤X\mathbf{X}^\top\mathbf{X}. On the right, 6320.4 is ∑ix(i)y(i)\sum_{i} x^{(i)}y^{(i)} and 109.2 is ∑iy(i)\sum_{i} y^{(i)}: the entries of X⊤y\mathbf{X}^\top\mathbf{y}. Dividing the second equation by 10 gives 52.4 w+b=10.9252.4\,w + b = 10.92, which is exactly xˉw+b=yˉ\bar{x}w + b = \bar{y} from step 1.

If X⊤X\mathbf{X}^\top\mathbf{X} is invertible, you can write the solution as:

θ=(X⊤X)−1X⊤y\boldsymbol{\theta} = \left(\mathbf{X}^\top\mathbf{X}\right)^{-1}\mathbf{X}^\top\mathbf{y}

This is how you write the solution, not how you should compute it. In code, use np.linalg.solve(X.T @ X, X.T @ y), or better, np.linalg.lstsq(X, y), because it works on X\mathbf{X} directly and never forms X⊤X\mathbf{X}^\top\mathbf{X} (scikit-learn’s LinearRegression does the same). Forming this matrix squares the condition number: how much a relative error in the input, rounding errors included, can be magnified in the result. For the apartment table, the condition number in the 2-norm (the default of np.linalg.cond) goes from about 165 to about 165² ≈ 27,000, but the inverse matrix, solve and lstsq still give the same result to about 14 digits.

The normal equation always has a solution. The solution is unique when X⊤X\mathbf{X}^\top\mathbf{X} is invertible, that is, when the columns of X\mathbf{X} are linearly independent: no column can be written as a weighted sum of the other columns. Three common cases break this:

  • Every sample has the same value of a feature. If every apartment is 40 m², the area column is 40 times the column of ones.
  • Duplicate features. For example, the area in both m² and ft²: the ft² column is the m² column times about 10.76. This kind of linear dependence between columns, exact or near, is called collinearity.
  • Fewer samples than parameters, that is m<n+1m < n + 1, for example 5 apartments but 8 features.

Then X⊤X\mathbf{X}^\top\mathbf{X} is singular, and infinitely many θ\boldsymbol{\theta} give the same predictions on the training set, with the same smallest MSE.

With one feature, the first case shows up clearly in the determinant. Using the entries of X⊤X\mathbf{X}^\top\mathbf{X} from the system above:

det⁡(X⊤X)=m∑i(x(i))2−(∑ix(i))2=m(∑i(x(i))2−mxˉ2)=m∑i(x(i)−xˉ)2\begin{aligned} \det\left(\mathbf{X}^\top\mathbf{X}\right) &= m\sum_{i}\left(x^{(i)}\right)^2 - \Big(\sum_{i} x^{(i)}\Big)^2 \\ &= m\Big(\sum_{i}\left(x^{(i)}\right)^2 - m\bar{x}^2\Big) \\ &= m\sum_{i}\left(x^{(i)} - \bar{x}\right)^2 \end{aligned}

The second line substitutes ∑ix(i)=mxˉ\sum_{i} x^{(i)} = m\bar{x} and factors out mm. The third line holds because expanding ∑i(x(i)−xˉ)2\sum_{i}\left(x^{(i)} - \bar{x}\right)^2 gives ∑i(x(i))2−2xˉ⋅mxˉ+mxˉ2\sum_{i}\left(x^{(i)}\right)^2 - 2\bar{x} \cdot m\bar{x} + m\bar{x}^2, which is ∑i(x(i))2−mxˉ2\sum_{i}\left(x^{(i)}\right)^2 - m\bar{x}^2, exactly the expression in parentheses. The determinant is 0 exactly when all the x(i)x^{(i)} are equal, which is also when the denominator in the formula for ww is 0. For what this does to your code and how to handle it, see the third box under Common mistakes.

Linear regression is simple, fast and easy to explain, but it has four limitations:

  • It is linear only in the features you give it. To follow a curve, add features such as x2x^2 (the first box under Common mistakes). How many to add is the topic of the lesson Overfitting and Underfitting.
  • It is sensitive to outliers. One odd point can pull the whole line off course (the third code cell).
  • Extrapolation easily goes wrong. The data only covers 25 to 85 m² (the second box under Common mistakes).
  • It gets expensive when nn is large. Forming the (n+1)×(n+1)(n + 1) \times (n + 1) matrix X⊤X\mathbf{X}^\top\mathbf{X} takes on the order of mn2mn^2 operations, and solving the system about n3n^3 more. The lesson Gradient Descent finds the bottom of the same L\mathcal{L} in small steps.

Now find the best line yourself, by hand. The figure below is a scatter plot of the 10 apartments. The ww slider turns the line, and the bb slider raises or lowers the whole line. The vertical segments are the residuals, and the MSE updates as you drag. The × mark is the mean point (52.4,10.92)(52.4, 10.92). Try this:

  1. At the start, w=0w = 0 and b=10.9b = 10.9: a flat line, almost line A, with MSE 10.5020.
  2. Raise ww a little, then lower bb until the line passes through the × again. The MSE goes down. Repeat a few times.
  3. Do not keep raising ww. At w=0.4w = 0.4, b=−10.0b = -10.0, the line still passes close to the × but is far too steep, and the MSE shoots up to 19.1240. Passing through the mean point is necessary, but not enough.
  4. When the MSE is small enough, the figure shows the line “Very close to the best line”. Press Show the best line to compare with y^=0.1695x+2.04\hat{y} = 0.1695x + 2.04, MSE 0.3615. The sliders move in steps of 0.005 and 0.1, so the closest you can get is w=0.17w = 0.17, b=2.0b = 2.0, MSE 0.3617.
Drag the two sliders to place a line. Try to find the line with the smallest MSE.
  • One apartment
  • Mean point (52.4, 10.92)
  • Residual: the vertical segment from a point to your line
Your liney^\hat{y} = 0.0000xx + 10.90MSE = 10.5020
Best line (least squares)y^\hat{y} = 0.1695xx + 2.04MSE = 0.3615

Ten points rise from left to right, from a 25 m² apartment at 6.7 million to an 85 m² one at 16.8 million. Larger apartments tend to cost more, but the points do not lie on one straight line. An X-shaped mark shows the mean point (52.4, 10.92). The best line always passes through it.

The three code cells below redo the steps above in Python. Each cell builds the apartment table itself, so you can run them in any order. The second cell takes longer the first time, because your browser has to download scikit-learn as well.

The first cell builds the design matrix, solves the normal equation with np.linalg.solve, then computes the MSE.

The normal equation with NumPy

import numpy as np
# The 10 apartments (example data, made up for the lesson).
# area: floor area (m²), rent: rent (million VND/month).
# dataset: apartments
area = np.array([25, 30, 36, 42, 48, 55, 60, 68, 75, 85])
rent = np.array([6.7, 6.5, 8.8, 8.8, 9.4, 12.2, 12.0, 14.1, 13.9, 16.8])
# Design matrix X: the area column, plus a column of ones for the bias b.
m = len(area)
X = np.column_stack([area, np.ones(m)])
# Normal equation: (X^T X) theta = X^T y.
# np.linalg.solve solves the system directly, without computing an inverse.
theta = np.linalg.solve(X.T @ X, X.T @ rent)
w, b = theta # same order as the columns of X
# MSE: the mean of the squared residuals.
y_hat = X @ theta
mse = np.mean((y_hat - rent) ** 2)
print(f"b = {b:.4f}")
print(f"w = {w:.4f}")
print(f"MSE = {mse:.4f}")

The first run downloads Python, about 10 MB.

The result is b=2.0394b = 2.0394, w=0.1695w = 0.1695 and MSE =0.3615= 0.3615, which matches the one-feature formula and the best line in the Illustration section.

The second cell uses scikit-learn’s LinearRegression, a class that solves exactly the ordinary least squares (OLS) problem above. The cell checks it against the hand formula, predicts the rent of two new apartments, then draws the line.

scikit-learn and predicting new apartments

import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
# The 10 apartments (example data, made up for the lesson).
# area: floor area (m²), rent: rent (million VND/month).
# dataset: apartments
area = np.array([25, 30, 36, 42, 48, 55, 60, 68, 75, 85])
rent = np.array([6.7, 6.5, 8.8, 8.8, 9.4, 12.2, 12.0, 14.1, 13.9, 16.8])
# scikit-learn wants X as m rows (samples) by n columns (features). Here n = 1.
X = area.reshape(-1, 1)
model = LinearRegression().fit(X, rent) # adds the bias b by itself
print(f"sklearn: w = {model.coef_[0]:.4f}, b = {model.intercept_:.4f}")
# Check against the one-feature formula from the Theory section.
dx = area - area.mean()
dy = rent - rent.mean()
w = np.sum(dx * dy) / np.sum(dx**2)
b = rent.mean() - w * area.mean()
print(f"formula: w = {w:.4f}, b = {b:.4f}")
# Predict two new apartments: 65 m² (inside the data) and 300 m² (far outside).
new_area = np.array([[65], [300]])
for a, p in zip(new_area[:, 0], model.predict(new_area)):
print(f"{a} m² -> {p:.2f}")
# Plot the data and the best line, only over the range of the data.
xs = np.array([[20], [90]])
plt.scatter(area, rent, label="apartments")
plt.plot(xs, model.predict(xs), color="C1", label="best line")
plt.xlabel("Area (m²)")
plt.ylabel("Rent (million VND/month)")
plt.legend()
plt.show()

The first run downloads Python, about 10 MB.

The first two lines match: scikit-learn and the hand formula give the same ww and bb. Two things to remember when you use scikit-learn:

  • X must be a two-dimensional table, mm rows by nn columns, even when there is only one feature. That is why the code calls reshape(-1, 1).
  • The weights are in coef_, and bb is in intercept_. You do not need to add the column of ones yourself.

The 65 m² apartment gets a predicted rent of 13.06 million VND/month, between the rents of the 60 m² apartment (12.0) and the 68 m² one (14.1). That sounds reasonable. The 300 m² apartment gets 52.88 million, a suspicious number (see Common mistakes).

The third cell tries an outlier: a 110 m² apartment whose rent was mistyped as 9.0 instead of 19.0. It uses np.linalg.lstsq to fit the line before and after adding that point.

One outlier

import matplotlib.pyplot as plt
import numpy as np
# The 10 apartments (example data, made up for the lesson).
# area: floor area (m²), rent: rent (million VND/month).
# dataset: apartments
area = np.array([25, 30, 36, 42, 48, 55, 60, 68, 75, 85])
rent = np.array([6.7, 6.5, 8.8, 8.8, 9.4, 12.2, 12.0, 14.1, 13.9, 16.8])
def fit(x, y):
# np.linalg.lstsq solves least squares on X itself, without forming X^T X.
X = np.column_stack([x, np.ones(len(x))])
w, b = np.linalg.lstsq(X, y)[0]
return w, b
# Add a 110 m² apartment, the largest of all, with rent 19.0 mistyped as 9.0.
area_out = np.append(area, 110)
rent_out = np.append(rent, 9.0)
w1, b1 = fit(area, rent)
w2, b2 = fit(area_out, rent_out)
print(f"without outlier: w = {w1:.4f}, b = {b1:.2f}")
print(f"with outlier: w = {w2:.4f}, b = {b2:.2f}")
# Plot both lines to compare them.
xs = np.array([20, 115])
plt.scatter(area, rent, label="apartments")
plt.scatter([110], [9.0], color="C3", marker="x", s=80, label="outlier")
plt.plot(xs, w1 * xs + b1, color="C1", label="without outlier")
plt.plot(xs, w2 * xs + b2, color="C3", linestyle="--", label="with outlier")
plt.xlabel("Area (m²)")
plt.ylabel("Rent (million VND/month)")
plt.legend()
plt.show()

The first run downloads Python, about 10 MB.

Adding just one point out of 11 cuts the slope by more than half: ww drops from 0.1695 to 0.0760 (from about 170,000 to 76,000 VND per m²). Meanwhile, bb rises from 2.04 to 6.36. Why does one point have so much pull?

  • Compared with the old line, the new point is off by almost 12 million. Squared, that is more than 130, while the squared residuals of the other 10 apartments add up to only about 3.6. To keep the MSE small, the line has to tilt toward this point.
  • The point sits at the far right, a long way from the mean point horizontally, like someone sitting at the end of a seesaw: the farther from the pivot, the more easily it tips the board. A point like this is called a high-leverage point.

So plot your data and check any points that lie far from the crowd before you fit a model.

Question 1 of 4 Why does MSE square the residuals instead of simply adding them up?
Question 2 of 4 A model learns y^=0.17x+2.04\hat{y} = 0.17x + 2.04, where xx is the floor area (m²) and y^\hat{y} is the rent (million VND/month). Two apartments differ in area by 10 m². How far apart are their predicted rents?
Question 3 of 4 Instead of the 110 m² apartment from the third code cell, you add to the original table a 52 m² apartment, very close to the mean area xˉ=52.4\bar{x} = 52.4, with its rent mistyped as 1.0 instead of 10.0. How does the least squares line change?
Question 4 of 4 Which case makes X⊤X\mathbf{X}^\top\mathbf{X} not invertible?

Key ideas:

  • Linear regression predicts y^=w⊤x+b\hat{y} = \mathbf{w}^\top\mathbf{x} + b and picks the parameters that make the MSE smallest. Squaring keeps errors from cancelling, punishes large errors more, and lets you take derivatives.
  • With one feature: b=yˉ−wxˉb = \bar{y} - w\bar{x}, and ww is the sum of the products of the deviations divided by the sum of the squared deviations of xx. The best line passes through the mean point.
  • The general form is the normal equation X⊤Xθ=X⊤y\mathbf{X}^\top\mathbf{X}\boldsymbol{\theta} = \mathbf{X}^\top\mathbf{y}, where X\mathbf{X} has an extra column of ones. The solution is unique when the columns of X\mathbf{X} are linearly independent. In code, use lstsq or LinearRegression; do not compute the inverse yourself.
  • wjw_j tells you how much the prediction changes when xjx_j goes up by 1 unit and the other features stay the same. Watch out for extrapolation and outliers.

Further reading:

  • Vũ Hữu Tiệp (2016). Machine Learning cơ bản, Bài 3: Linear Regression (in Vietnamese). Includes NumPy and scikit-learn examples. The notation differs slightly: NN is the number of samples, and the column of ones comes first.
  • James, G., Witten, D., Hastie, T., Tibshirani, R. (2013). An Introduction to Statistical Learning (1st edition), chapter 3: Linear Regression. Section 3.1: the one-feature solution. Section 3.3.3: outliers, high-leverage points, collinearity.
  • Ng, A. (2003). CS229 Lecture notes (PDF), Part I: Linear Regression (older version, Stanford). Derives the normal equation with matrix derivatives.
  • Deisenroth, M. P., Faisal, A. A., Ong, C. S. (2020). Mathematics for Machine Learning, chapter 9: Linear Regression. Section 9.4 explains the normal equation as an orthogonal projection.
  • scikit-learn developers (2007–2026). scikit-learn 1.8 User Guide, section 1.1.1 Ordinary Least Squares. The official documentation of LinearRegression.

Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About