Linear regression
In short: Linear regression predicts a number as a sum of the features with coefficients; MAE says how far off it is on average, and RMSE weighs big errors more, both in the same units.
Problem
“How much money do I expect on Friday?”
Accounting plans the money for the next weeks. It asks how much revenue to expect on Friday, 6 November, and on Sunday, 8 November. Here is the revenue by day of the year, without the cancelled orders, because they bring no money:
A day brings €304.62 on average. Friday brings €350.79, and Sunday €250.85. December days bring €513.42 on average. The mean of every day of the week is a start, but December and the slow growth over the year are mixed into it. You need a model that puts them together.
The idea
A sum of features
Linear regression predicts as a sum: a starting number plus one number for each feature, multiplied by its value. These numbers are the coefficients. The features here are of three kinds:
- the day of the week, as columns of 0 and 1. With
drop_first=Truethe column for Monday is dropped: Monday is the base, and every other day is “how much more than Monday”; day, the running number of the day: 0 on 1 November 2025. This is the trend;december: 1 for the days in December.
The model learns from November to June, 242 days. July–October, 123 days, are for the test. Why not shuffled is at the end of the lesson:
The starting number is €273.98: a Monday on day 0, outside December. Friday adds €63.58, and Sunday takes away €45.44. December adds €233.05 a day. Every day adds €0.06, that is about €21.33 a year. So the forecast for a Friday in December is 273.98 + 63.58 + 233.05 plus the trend.
More precisely, if you are curious
The coefficients are estimates and carry noise, like the mean in the lesson on samples. Trained on all 365 days, the model gives Friday +€44.70, not +€63.58. How regression finds the numbers (the smallest sum of squared errors) is in the lesson Linear regression of the AI course, without a library.
Try it
Which is the base
The idea
How much it misses: MAE and RMSE
The error is measured on the test. There are two usual measures:
- MAE (mean absolute error): the mean error without the sign. In euros, and the manager understands it: “we miss by this much a day on average”.
- RMSE (root mean squared error): the errors are squared, the mean is taken and then the root. It is in euros too, but a big miss weighs more.
sklearn.metrics has both:
The model misses by €59.88 a day on average (MAE), and the RMSE is €75.91. The two small calculations show the difference. Misses of 10 and 10 give an MAE of 10 and an RMSE of 10. Misses of 0 and 20 give the same MAE of 10, but an RMSE of 14.14. If one big miss costs you more than a few small ones, for example an empty till on Friday, you look at the RMSE. Otherwise the MAE is clearer for the manager.
Try it
And the baseline?
Does the model beat “always the average day”? Put in guess the mean revenue of the training days, and in mae_base and rmse_base the errors of this forecast on the test days.
Solution
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
orders = orders[orders['status'] != 'cancelled']
orders['ordered_at'] = pd.to_datetime(orders['ordered_at'])
items['revenue'] = items['quantity'] * items['unit_price']
lines = items.merge(orders, left_on='order_id', right_on='id')
daily = lines.set_index('ordered_at').resample('D')['revenue'].sum().to_frame()
daily['weekday'] = daily.index.dayofweek
daily['day'] = np.arange(len(daily))
daily['december'] = (daily.index.month == 12).astype(int)
X = pd.get_dummies(daily[['weekday', 'day', 'december']], columns=['weekday'], drop_first=True, dtype=int)
y = daily['revenue']
train = daily.index < '2026-07-01'
X_train, X_test = X[train], X[~train]
y_train, y_test = y[train], y[~train]
model = LinearRegression().fit(X_train, y_train)
guess = y_train.mean()
mae_base = np.abs(y_test - guess).mean()
rmse_base = np.sqrt(((y_test - guess) ** 2).mean())
print(round(guess, 2), round(mae_base, 2), round(rmse_base, 2))The baseline says €314.34 for every day. It misses with an MAE of €66.24 and an RMSE of €82.67. The model has €59.88 and €75.91: €6–7 a day less on both measures. That is not much: the revenue of one day is noisy, and most of the error comes from which customers happened to come that day. Without the root you get 6,834.32: euros squared, which compare with nothing. With the mean of the test (€285.49) the MAE is €61.02 and the baseline looks almost as good as the model, but it uses the answer.
The idea
The residuals
A residual is the actual value minus the forecast. Plus means the shop sold more than the model said. With a good model the residuals are scattered around 0, with no pattern. A pattern is something the model does not know:
The biggest miss is on 30 July 2026: €239.28 above the forecast. It is an unusually strong Thursday with €535.40 of revenue. By month, July, August and October are close to 0 (€3.99, −€8.03 and €20.42). September is −€58.85 a day: for a whole month the shop sold less than the day and the trend explain. This is not noise, something happened. What, we will find out in the course project.
The idea
What the model cannot do
The model knows only what its features tell it. Without the december column:
The trend becomes −€0.40 a day, −€147.35 a year. December is at the start of the training data and “lifts” the start of the line, and the model has no other way to explain it. The test error becomes €69.43, worse than the baseline (€66.24). The jump from the gift cards in December shows only if a feature says so.
Two more limits:
- No extrapolation. The line goes on forever. From 8 months the growth is €21.33 a year, but whether it continues next year the data does not say. We have seen only one December.
- A coefficient is not a cause. “+€63.58 on Friday” does not mean that a promotion moved to Friday will bring €63.58. The coefficient describes the data. How much an action brings, an experiment tells you, as in the lesson Correlation and cause.
Try it
Where the model is blind
Fit the model without the december column on the training days. Compute its residuals on the same days and the mean residual by month in by_month (group by res.index.strftime('%Y-%m')). Draw by_month as bars, with a title and a label on the vertical axis.
Solution
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
orders = orders[orders['status'] != 'cancelled']
orders['ordered_at'] = pd.to_datetime(orders['ordered_at'])
items['revenue'] = items['quantity'] * items['unit_price']
lines = items.merge(orders, left_on='order_id', right_on='id')
daily = lines.set_index('ordered_at').resample('D')['revenue'].sum().to_frame()
daily['weekday'] = daily.index.dayofweek
daily['day'] = np.arange(len(daily))
daily['december'] = (daily.index.month == 12).astype(int)
X = pd.get_dummies(daily[['weekday', 'day', 'december']], columns=['weekday'], drop_first=True, dtype=int)
y = daily['revenue']
train = daily.index < '2026-07-01'
X_train, X_test = X[train], X[~train]
y_train, y_test = y[train], y[~train]
import matplotlib.pyplot as plt
no_dec = X.drop(columns='december')
blind = LinearRegression().fit(no_dec[train], y_train)
res = y_train - blind.predict(no_dec[train])
by_month = res.groupby(res.index.strftime('%Y-%m')).mean()
fig, ax = plt.subplots(figsize=(8, 3.5))
ax.bar(by_month.index, by_month.values)
ax.axhline(0, color='black', linewidth=1)
ax.set_title('Model without December: mean residual by month')
ax.set_ylabel('€ per day')December is €167.92 a day above the forecast: the model does not see the gift cards and the holidays. January is −€100.63: the line, tilted down because of December, is too high at the start of the year. With the december column the December bar is almost 0. The residual is actual minus forecast. If you flip it, December comes out below 0 and the chart says the opposite.
The idea
Split by time, not shuffled
The model in this lesson learns from November to June and is checked on July–October. Why not with train_test_split? Here is a model with the day and the month, checked both ways. The second number on each line is the baseline:
Shuffled, the model misses by €59.81, and the baseline by €81.48: 27% better. But 87 of the 92 test days have a neighbouring day, the one before or the one after, in the training data. The model has learned how the days around the test day went, the later ones too.
Checked by time, the same model misses by €71.98, more than the baseline (€66.24). The months July–October are not in the training data. Their columns are 0 in all the training rows, and the model gives them the level of January, the weakest month.
A real forecast is always for days after all the examples. So the test must come after the training too. Shuffling days lets the future into the training. This is information leakage, and a whole lesson further on is about it.
Exercise
Friday and Sunday
Now the answer for accounting. Fit the model again on all 365 days. Make X_new for Friday 6 November 2026 and Sunday 8 November 2026 (day continues the count from 1 November 2025, december is 0) and put in forecast the forecast for the two days (a Series with the dates as the index).
Solution
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
orders = orders[orders['status'] != 'cancelled']
orders['ordered_at'] = pd.to_datetime(orders['ordered_at'])
items['revenue'] = items['quantity'] * items['unit_price']
lines = items.merge(orders, left_on='order_id', right_on='id')
daily = lines.set_index('ordered_at').resample('D')['revenue'].sum().to_frame()
daily['weekday'] = daily.index.dayofweek
daily['day'] = np.arange(len(daily))
daily['december'] = (daily.index.month == 12).astype(int)
X = pd.get_dummies(daily[['weekday', 'day', 'december']], columns=['weekday'], drop_first=True, dtype=int)
y = daily['revenue']
train = daily.index < '2026-07-01'
X_train, X_test = X[train], X[~train]
y_train, y_test = y[train], y[~train]
final = LinearRegression().fit(X, y)
dates = pd.to_datetime(['2026-11-06', '2026-11-08'])
new = pd.DataFrame({'weekday': dates.dayofweek, 'day': (dates - daily.index[0]).days, 'december': 0}, index=dates)
X_new = pd.get_dummies(new, columns=['weekday'], dtype=int).reindex(columns=X.columns, fill_value=0)
forecast = pd.Series(final.predict(X_new), index=dates).round(2)
print(forecast)Friday is €336.38, and Sunday €236.58. With an error of about €60 a day, the honest answer is “about €340 on Friday and about €240 on Sunday, give or take €60”. The test was the check. For the forecast you fit on all the days, because the last months are the closest to November. So Friday is +€44.70 compared with Monday, not +€63.58. day is 370 and 372, counted exactly as in training. With day 0 the forecast is €329.70 and €229.86, and with the old model trained until June, €359.17 and €250.27.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: The first chart · Distributions · Correlation and cause · Models: learning from examples
- Continues in: Classification · Data leakage · Cross-validation
- Tasks on this topic: Models with scikit-learn