Linear regression
In short: Linear regression predicts a number with a line y = w · x + b, and the mean squared error (MSE) says in one number how well the line fits the data.
Problem
Two colleagues, two lines
The warehouse wants to promise hours, not days. Here are 10 past orders: the distance and how many hours they took.
# past orders: distance in km and hours to deliveryKM = [30, 50, 80, 100, 130, 160, 200, 240, 270, 300]HOURS = [10, 11, 17, 17, 23, 24, 31, 33, 40, 44]Two colleagues look at the points and each proposes a rule:
- line A: each kilometre adds 0.08 hours, plus 12 hours in the warehouse;
- line B: each kilometre adds 0.12 hours, plus 6 hours in the warehouse.
On the chart both look decent. The colleagues argue all afternoon. The argument will not end until they have one number that says how good a line is.
The idea
The line as a model
Both proposals are of one kind: hours = w · km + b.
- w is the slope: how many hours each kilometre adds;
- b is the intercept: the hours at 0 km, that is the handling in the warehouse.
A model that predicts a number with a line is called linear regression. Training the model means choosing w and b.
# the line: hours = w · km + bdef predict(w, b, km): return w * km + bUnlike k nearest neighbours, the line does not keep the orders. It is two numbers, and a forecast is one multiplication and one addition. And it continues its slope: for 500 km it says 0.12 · 500 + 6 = 66 hours, while the neighbours of 500 km are the orders from 300 km.
The idea
The error of each order
For every order the line says some number. The error is the difference: what the line says minus what really happened. It is positive when the line promises more, negative when it promises less.
Line B, order by order:
w, b = 0.12, 6for km, hours in zip(KM, HOURS): guess = predict(w, b, km) print(f"{km} km: actual {hours} h, the line {guess:.2f} h, error {guess - hours:.2f}")print(f"MSE: {mse(w, b):.2f}")30 km: actual 10 h, the line 9.60 h, error -0.4050 km: actual 11 h, the line 12.00 h, error 1.0080 km: actual 17 h, the line 15.60 h, error -1.40100 km: actual 17 h, the line 18.00 h, error 1.00130 km: actual 23 h, the line 21.60 h, error -1.40160 km: actual 24 h, the line 25.20 h, error 1.20200 km: actual 31 h, the line 30.00 h, error -1.00240 km: actual 33 h, the line 34.80 h, error 1.80270 km: actual 40 h, the line 38.40 h, error -1.60300 km: actual 44 h, the line 42.00 h, error -2.00MSE: 1.83Ten errors, some plus, some minus. We need one number for the whole line.
The idea
One number: the mean square
Each error is squared, the squares are added and divided by the number of orders. This is the mean squared error, MSE. A number that says how bad a model is, is called a loss function (loss). Less is better, and 0 means the line goes through every point.
# the mean of the squared errors (MSE)def mse(w, b): total = 0 for km, hours in zip(KM, HOURS): error = predict(w, b, km) - hours total += error * error return total / len(KM)zip(KM, HOURS) walks both lists at once: at each step it gives one pair of km and hours.Why a square? Compare three lines. C is the flat line through the mean, 25 hours for every order:
# the mean error without the squaredef mean_error(w, b): total = 0 for km, hours in zip(KM, HOURS): total += predict(w, b, km) - hours return total / len(KM)lines = {"A": (0.08, 12), "B": (0.12, 6), "C": (0, 25)}for name, (w, b) in lines.items(): print(f"line {name}: MSE {mse(w, b):.2f}, mean error {mean_error(w, b):.2f}")line A: MSE 17.35, mean error -0.52line B: MSE 1.83, mean error -0.28line C: MSE 124.00, mean error 0.00Without the square line C has a mean error of 0.00: it is above the points as much as below them, and the pluses cancel the minuses. The worst line "wins". With the square nothing cancels: C is 124, A is 17.35, and B is 1.83. The colleague with line B wins the argument.
The square does something else too: a big error weighs more. An error of 4 hours gives 16, while two errors of 2 hours give 8 in total. A line that is a little wrong everywhere is better than one that is exact on half the orders and very wrong on the other half.
See it
The errors as squares
def predict(w, b, km): return w * km + b
def mse(w, b): total = 0 for km, hours in zip(KM, HOURS): error = predict(w, b, km) - hours total += error * error return total / len(KM)Step by step, in words
- 10 past orders: how many km, and how many hours until delivery.
- Line A: 0.08 hours per km plus 12 hours.
- The error of each order: what the line says minus what really happened. Some points are above the line, some below.
- Each error squared is a square with the error as its side. The mean area is the MSE: 17.35.
- Line B: 0.12 hours per km plus 6 hours. The line turns, and the squares shrink.
- The MSE of B is 1.83, against 17.35 for A.
- The line with the smaller MSE is closer to the points. One number instead of an argument.
Each square is one error squared: its side is the distance from the point to the line. The MSE is their mean area. Line B is better because its squares are smaller, not because someone thought so.
Try it
Predict the output
The courier charges €2 per kilogram. Three parcels of 1, 2 and 3 kg cost €3, €5 and €7. What will the program print?
kg = [1, 2, 3]price = [3, 5, 7]total = 0for x, y in zip(kg, price): error = 2 * x - y total += error * errorprint(f"{total / 3:.2f}")The line 2 · kg gives €2, €4 and €6, and the truth is €3, €5 and €7. Each error is −1, and its square is 1. The mean of 1, 1 and 1 is 1.00. The MSE cannot be negative.
Try it
Fill in
What is added to the sum for each order?
error = predict(w, b, km) - hours
total += The square of the error: error * error. A multiplication is clearer and faster than raising to a power, and the result is the same.
Try it
Find the bug
A colleague writes the loss function and compares the lines. Their program says the best one is the flat line C. Which line has the bug?
Pick the line with the bug.
The error is added without the square. The pluses and minuses cancel, and the line through the mean comes out with an error of 0. It should be error * error.
The idea
When the line lies
The line is the simplest model and is often enough. But:
- The world is not straight. If after 300 km the order goes through another warehouse and waits another day, the points make a step. The line goes between the two parts and is wrong on both.
- One odd point. An order lost for a week is an error of hundreds of hours, and its square weighs as much as all the others together. The line tilts towards it and starts lying about the normal orders. Such points are checked and removed, or you measure with the mean absolute error.
- Far outside the data. The orders go up to 300 km. For 2,000 km the line says 246 hours, but nobody has checked whether such a delivery even exists.
- A relation is not a cause. The line says that farther orders travel longer. It does not say why. If the farther towns are simply served by a slower courier, a faster courier there will break the line.
The line the colleagues picked by eye is just one of countless w and b. Which one is best, and how to find it without guessing, is in the lesson on gradient descent.
More precisely, if you are curious
The formula for the best line. For a line there is an exact formula (the method of least squares). w is how much km and hours move together, divided by how much km move on their own. b is such that the line goes through the mean point:
w = Σ (km − mean km) · (hours − mean hours) / Σ (km − mean km)², b = mean hours − w · mean km.
n = len(KM)mean_km = 0mean_hours = 0for km, hours in zip(KM, HOURS): mean_km += km mean_hours += hoursmean_km /= nmean_hours /= ntop = 0bottom = 0for km, hours in zip(KM, HOURS): top += (km - mean_km) * (hours - mean_hours) bottom += (km - mean_km) * (km - mean_km)w = top / bottomb = mean_hours - w * mean_kmprint(f"the best line: {w:.4f} h/km, {b:.2f} h")print(f"MSE: {mse(w, b):.2f}")the best line: 0.1241 h/km, 5.64 hMSE: 1.62The best line is slightly better than B (1.62 against 1.83). The colleague judged well.
Why not always the formula? It is for a line with the squared error. With many features, with another loss function and with neural networks there is no exact formula, or it is too slow. That is why gradient descent is used.
More features. The line can have one w per feature: hours = w₁ · km + w₂ · kg + w₃ · parcels + b. The idea and the MSE are the same.
The mean absolute error (MAE) adds |error| instead of the square. It does not punish a big error as much and is calmer about odd points. The MSE is smoother and so more convenient for gradient descent.
In the libraries: LinearRegression in scikit-learn computes exactly this formula. In ML.NET it is the Sdca or Ols regression.
Exercise
The courier's price
Solve the task in your editor.
The courier does not publish its price list, but the shop keeps what it paid for eight parcels:
# parcels: weight in kg and the courier price in €KG = [0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0]EUR = [4.10, 4.60, 5.00, 5.60, 6.40, 7.50, 8.30, 9.40]The colleagues propose three lines for the price: A is €0.90 per kg plus €3.80, B is €1.10 per kg plus €3.20, C is €0.70 per kg plus €4.60. Write the loss function (MSE), print it for all three and say which one is best.
line A (€0.90/kg + €3.80): MSE 0.0144line B (€1.10/kg + €3.20): MSE 0.0769line C (€0.70/kg + €4.60): MSE 0.2869smallest MSE: line AMore tasks
- The mean absolute error. Replace the square with the absolute value. Does the winner change?
- The formula. Compute the best line with the formula from "More precisely". How much better than A is it?
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Functions · Parameters and return · What all objects share: class attributes · What machine learning is
- Continues in: Gradient descent · Classification: fraud or not · Overfitting: the model that memorises · The neuron
- Tasks on this topic: How a model learns