DataLens

Linear regression (the math intuition)

Fit a line (and a plane) that minimises squared error, read the coefficients as business effects, compare NumPy's least squares with scikit-learn, and check the residuals.

Level: Intermediate · 8 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

Linear regression predicts a number as a weighted sum of the features plus a constant:

units = w₁·temperature + w₂·weekend + w₃·promo + w₄·ad_spend + b

It's the X @ w + b from the NumPy linear-algebra module, and the "model + loss" from two modules ago, with one special property: the weights with the lowest MSE can be computed exactly, with a formula, no searching needed. It's the first model to try for any regression problem, and often the one that ships, because its coefficients explain themselves.

One feature: a line

Start with the simplest version of the question: does spending more on ads sell more units? With one feature, a linear model is just a straight line through the points, and NumPy can find the best-fitting line directly:

import numpy as np
import pandas as pd

demand = pd.read_csv("daily_demand.csv")
x = demand["ad_spend"].to_numpy()
y = demand["units"].to_numpy()

w, b = np.polyfit(x, y, 1)
print(round(w, 2), round(b, 2))
7.51 44.56

The slope w means: each extra million Rupiah of ads goes with about that many more units sold. The intercept b is the predicted units with zero ad spend.

How is it computed? Least squares has a closed-form answer. For one feature it's short enough to write out:

w_formula = np.sum((x - x.mean()) * (y - y.mean())) / np.sum((x - x.mean()) ** 2)
b_formula = y.mean() - w_formula * x.mean()
print(round(w_formula, 2), round(b_formula, 2))
7.51 44.56

Same answer. The slope is "how much x and y move together" divided by "how much x moves", and the line always passes through the point (mean x, mean y).

Several features: a plane

With several features, NumPy solves the least-squares problem with np.linalg.lstsq. Add a column of ones so the model can learn the intercept:

features = ["temperature", "weekend", "promo", "ad_spend"]
X = demand[features].to_numpy()
X1 = np.column_stack([X, np.ones(len(X))])          # last column: the intercept
coefs, *_ = np.linalg.lstsq(X1, y, rcond=None)
for name, c in zip(features + ["intercept"], coefs):
    print(f"{name:12} {c:8.2f}")
temperature     -1.67
weekend          5.75
promo           18.92
ad_spend         7.62
intercept       85.75

Each coefficient is an effect holding the other features fixed:

That's the attraction of linear regression: a manager can read it. "Promotions are worth about 19 units a day" is a sentence you can act on (compare with what a promotion costs).

Key idea

Linear regression finds the weights that minimise squared error, exactly. Each weight is the effect of one feature with the others held constant, in the feature's units.

The same thing with scikit-learn

In practice you use scikit-learn, which has the same interface for every model: fit, then predict:

from sklearn.linear_model import LinearRegression

model = LinearRegression()
model.fit(demand[features], y)
print(np.round(model.coef_, 2), round(model.intercept_, 2))

tomorrow = pd.DataFrame({"temperature": [29.0], "weekend": [1], "promo": [1], "ad_spend": [2.0]})
print(model.predict(tomorrow).round(1))
[-1.67  5.75 18.92  7.62] 85.75
[77.2]

Identical coefficients to lstsq. predict turns a new day's features into a forecast: a hot Saturday with a promotion and Rp 2M of ads.

R²: how much is explained?

model.score(X, y) returns R²: the share of the variation in y that the model explains (1.0 = perfect, 0 = no better than predicting the average).

print(round(model.score(demand[features], y), 3))
0.811

The regression metrics module explains R² properly. Here it says these four features explain most of the day-to-day variation in sales.

Check the residuals

A model can have a decent score and still be systematically wrong. Look at the residuals (actual − predicted):

residuals = y - model.predict(demand[features])
print("average is ~0:", bool(np.isclose(residuals.mean(), 0)), "| typical size:", round(residuals.std(), 2))
by_weekend = pd.Series(residuals).groupby(demand["weekend"]).mean()
print("weekday and weekend averages ~0:", bool(np.allclose(by_weekend, 0)))
average is ~0: True | typical size: 4.98
weekday and weekend averages ~0: True

Good signs: residuals average zero overall and within groups (no group is consistently over- or under-predicted), and their spread (about ±5 units) matches the noise you'd expect. A pattern in the residuals (for example, always under-predicting on hot days) means the model is missing something: a curved relationship, an interaction, or a feature.

Limitations

Why this matters for clean code

Linear regression is the baseline every other regression model must beat, and the most explainable one you'll ever deploy. Keep the feature list explicit, print coefficients with their names, and always check residuals. A complex model that beats it by 1% often isn't worth losing the explanation.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: Logistic regression and classification →