Skip to content
RgramHeld-out prediction

Predicting demand from temperature

Question and data

Can a nonlinear temperature curve predict demand on unseen days better than a constant training mean? We generate 160 independent synthetic days with a U-shaped mean and Gaussian noise. This controlled example needs no download; it does not establish how real demand behaves.

Configuration

Hold out 25% of days before selecting a bandwidth. GridSearchCV compares three Gaussian bandwidths in a Pipeline: each fold learns its own StandardScaler. The test set influences neither scaling nor selection. The bandwidth is measured in standard deviations of training temperature. Gaussian support permits finite predictions throughout this dataset; extrapolation is explicitly allowed and warned about, rather than being evidence of accuracy outside the training range.

Random splitting assumes independent days. Use chronological or grouped splits when observations are dependent. Unsorted-input warnings are expected because splitting preserves randomly selected rows rather than sorting them.

Download the standalone source.

Run the complete example

python examples/held_out_prediction.py
"""Can a temperature curve predict demand for unseen observations?"""

import numpy as np
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

from rgram import KernelSmoother

# Independent synthetic days; no network access or hidden data cleaning.
rng = np.random.default_rng(42)
temperature = rng.uniform(5, 30, 160)
demand = 20 + 0.15 * (temperature - 18) ** 2 + rng.normal(0, 2, 160)
# One column is required by sklearn preprocessing; rows are independent days.
X = temperature.reshape(-1, 1)
X_train, X_test, y_train, y_test = train_test_split(
    X,
    demand,
    test_size=0.25,
    random_state=42,
)
# The scaler learns only from each training fold; bandwidth uses scaled units.
pipeline = make_pipeline(
    StandardScaler(),
    KernelSmoother(kernel="gaussian", bandwidth="manual", extrapolation="allow"),
)
search = GridSearchCV(
    pipeline,
    {"kernelsmoother__bandwidth_value": [0.1, 0.3, 0.7]},
    scoring="neg_mean_squared_error",
    cv=4,
).fit(X_train, y_train)
prediction = search.predict(X_test)
print(
    f"Selected bandwidth: {search.best_params_['kernelsmoother__bandwidth_value']:.1f}"
)
print(f"Test RMSE: {np.sqrt(mean_squared_error(y_test, prediction)):.2f}")
print(
    f"Training-mean baseline RMSE: {np.sqrt(mean_squared_error(y_test, np.full_like(y_test, y_train.mean()))):.2f}"
)
Selected bandwidth: 0.1
Test RMSE: 2.11
Training-mean baseline RMSE: 7.14

Interpretation and next steps

RMSE is in demand units. On this seeded dataset, the selected smoother improves on the constant baseline. These numbers describe this simulation and split; they are not a performance guarantee. Selection uses negative MSE, so larger CV scores mean less error. The final test RMSE is evaluated only once.

Next inspect held-out residuals and support, repeat with appropriate resampling, and compare a domain baseline. Do not tune further using this test set and still report it as an untouched evaluation. See pipelines and theory for assumptions and limitations.