We want to predict how many bicycles are rented in a given hour in Washington DC, from the weather, the time of the day, the day of the week and the season.
Loading
import numpy as npimport pandas as pdimport matplotlib.pyplot as pltfrom sklearn.datasets import fetch_openmlbikes = fetch_openml(data_id=42712, as_frame=True, parser="auto").frameprint(bikes.shape)bikes.head()
(17379, 13)
season
year
month
hour
holiday
weekday
workingday
weather
temp
feel_temp
humidity
windspeed
count
0
spring
0
1
0
False
6
False
clear
9.84
14.395
0.81
0.0
16
1
spring
0
1
1
False
6
False
clear
9.02
13.635
0.80
0.0
40
2
spring
0
1
2
False
6
False
clear
9.02
13.635
0.80
0.0
32
3
spring
0
1
3
False
6
False
clear
9.84
14.395
0.75
0.0
13
4
spring
0
1
4
False
6
False
clear
9.84
14.395
0.75
0.0
1
Every row is one hour of the years 2011 (year 0) and 2012 (year 1).
fig, axes = plt.subplots(1, 2, figsize=(9, 3.2))for wd, label in [("True", "working day"), ("False", "weekend or holiday")]: m = bikes[bikes["workingday"].astype(str) == wd].groupby("hour")["count"].mean() axes[0].plot(m.index, m.values, "o-", ms=3, label=label)axes[0].set(xlabel="hour of day", ylabel="mean rentals")axes[0].legend()for year in [0, 1]: m = bikes[bikes["year"] == year].groupby("month")["count"].mean() axes[1].plot(m.index, m.values, "o-", ms=3, label=str(2011+ year))axes[1].set(xlabel="month", ylabel="mean rentals")axes[1].legend()plt.tight_layout()plt.show()
Figure 45.1: Left, the mean rentals per hour of the day, on working days and on other days. Right, the mean rentals per month, for the two years.
On working days there are two commuter peaks, at 8 and at 17 to 18 hours. On the other days there is one broad peak in the afternoon. The relation with the hour is neither linear nor the same on all days. There are more rentals in summer than in winter, and more in 2012 than in 2011.
The temperature and the felt temperature carry nearly the same information.
Encoding the hour
Hour 23 is next to hour 0, but far from it on the number line. We can give the model the hour as a number, as \(\sin(2\pi h/24)\), \(\cos(2\pi h/24)\) and the same at twice the frequency, as 23 one-hot columns, or as 23 one-hot columns separately for working days and for the other days.
A Poisson regression on the temperature, the year, the working day indicator and one of four encodings of the hour, fitted to all rows. The points are the observed mean rentals per hour, the line the mean of the fitted rate over the same hours. Only with one-hot columns per day type can the fit have two peaks on working days and one on the other days.
A ladder of models
We start with models that are hard to get wrong and add flexibility step by step. All are scored by the same five-fold cross-validation, with the Poisson deviance, the loss that a Poisson regression minimizes.
from sklearn.compose import ColumnTransformerfrom sklearn.preprocessing import OneHotEncoder, StandardScalerfrom sklearn.pipeline import Pipelinefrom sklearn.linear_model import PoissonRegressorfrom sklearn.model_selection import KFold, cross_val_scorey = bikes["count"].to_numpy()X = bikes.drop(columns=["count", "feel_temp"])X["hour_daytype"] = X["hour"].astype(str) +"_"+ X["workingday"].astype(str)numeric = ["temp", "humidity", "windspeed"]def glm(onehot, raw=[]): pre = ColumnTransformer([ ("onehot", OneHotEncoder(drop="first"), onehot), ("scale", StandardScaler(), numeric + raw)])return Pipeline([("pre", pre), ("glm", PoissonRegressor(alpha=1e-6, max_iter=5000))])cv = KFold(5)ladder = {"hour as a number": glm(["holiday", "workingday", "weather", "season"], raw=["hour", "weekday", "month", "year"]),"one-hot hour": glm(["hour", "weekday", "month", "year", "holiday","workingday", "weather", "season"]),"one-hot hour per day type": glm(["hour_daytype", "weekday", "month", "year","holiday", "weather", "season"]),}for name, model in ladder.items(): dev =-cross_val_score(model, X, y, cv=cv, scoring="neg_mean_poisson_deviance")print(f"{name:27s}{dev.mean():6.1f}")
1
A new categorical column with 48 values, one per hour and day type. Its one-hot code lets a linear model give each hour its own effect on working days and on the other days.
2
The scaler is part of the pipeline, so in every fold of the cross-validation it is fitted on the training part only.
3
Without shuffling, every fold is a contiguous block of hours. Neighbouring hours are similar, so shuffled folds would put the hour before and the hour after every test hour into the training set, and the estimate would be too optimistic.
hour as a number 98.1
one-hot hour 38.1
one-hot hour per day type 18.7
Code
from sklearn.metrics import mean_poisson_devianceprint(f"constant rate: {mean_poisson_deviance(y, np.full(len(y), y.mean())):.1f}")
constant rate: 166.4
The representation matters most. With the hour as one-hot columns the deviance drops from about 98 to about 38, and with an interaction between the hour and the day type to about 19, with the same method each time. The interaction we had to find ourselves, by looking at the data.