Programming for data science

Python for Data Scientists: Train a Reproducible Demand Model

A pandas and scikit-learn workflow for validation-led model selection, leakage-safe preprocessing, untouched holdout evaluation and reviewable artefacts.

2026 edition · Reproduced 15 August 2026

On this page

Reproduction contract

What this guide lets you reproduce

The workflow, data snapshot and checks below are versioned together. Use the stated commands and compare your results with the tested outputs in the repository.

Before you begin

  • Basic descriptive statistics, regression metrics and command-line execution
  • Understanding of predictors, targets and post-outcome leakage
  • Completion of the SQL feature-table guide or an equivalent validated features.csv artefact

You will be able to

  • Construct a pandas and scikit-learn pipeline whose preprocessing is estimated from the permitted development rows
  • Select a regularisation value on a chronological validation period without opening the test period
  • Refit the selected pipeline on train plus validation and compare it with a simple development-only baseline
  • Save a fitted pipeline, machine-readable metadata, metrics and row-level holdout predictions
  • Interpret predictive error without converting an historical holdout result into a causal or deployment claim

Tested environment

Pinned dataset

UCI Bike Sharing — hour.csv, LF-normalised repository copy

Historical 2011–2012 Washington, DC rentals; no station capacity, bicycle availability, rebalancing, price or reliable basis for present-day or causal claims. The UCI page reports ten more instances than the archived hourly file contains.

Runnable companion

Review and disclosure

AI assistance helped draft the guide and example code. Clean-environment reproduction was verified in GitHub Actions run

A model should not decide how its own final examination is marked. This workflow uses a chronological design: an early period for fitting, a later validation period for choosing the ridge penalty, and an untouched final quarter for one assessment after the choice is fixed. Preprocessing lives inside the fitted pipeline, so category encoding and scaling follow the same boundary.

Python data-analysis illustration introducing the chronological bicycle-demand model.

What this guide assumes

This is not a tour of Python syntax or a catalogue of libraries. It assumes that you can read a function, work in a virtual environment and interpret mean absolute error (MAE) and root mean squared error (RMSE). The SQL guide must first produce artifacts/features.csv from the pinned source.

The analytical target is the hourly total target_rentals. The allowed predictors describe calendar position, working-day status, weather categories, normalised environmental measures and elapsed time. Neither component of the total enters the feature matrix.

The 2011–2012 observations are used as a stable teaching benchmark. They are not current travel evidence and cannot establish the causal effect of a predictor.

Install the executable lock

From the repository root:

python3 -m venv examples/programming-for-data-science/.venv
examples/programming-for-data-science/.venv/bin/python -m pip install \
  -r examples/programming-for-data-science/requirements.txt

The lock pins the direct analytical stack and its runtime dependencies. The central versions are:

ComponentVersionRole
Python3.14.7Publication runtime
NumPy2.5.2Arrays and target transformation
pandas3.0.5Feature input and prediction export
scikit-learn1.9.0Composite pipeline, validation and ridge model
SciPy1.18.0Sparse linear algebra
joblib1.5.3Fitted-pipeline serialisation
DuckDB1.5.5Upstream feature-table generation

Exact pins make the result diagnosable. They do not mean dependencies should never be upgraded. Upgrade them in a separate change, rerun the chain, compare the artefacts and record what changed.

Load only the reviewed feature contract

The source is parsed with observed_at as a timestamp. The script checks the three partition counts and verifies that the last row of each earlier period precedes the first row of the next.

frame = pd.read_csv(FEATURE_PATH, parse_dates=["observed_at"])

counts = frame["split"].value_counts().to_dict()
assert counts == {"train": 13003, "validation": 2208, "test": 2168}

Predictors are declared through an allow-list. A second assertion rejects the target and its known components if they appear in that list. This defence is intentionally duplicated after the SQL handover: upstream quality checks reduce risk, but the model owner remains responsible for the matrix actually fitted.

The script creates one composite category, working_hour, from working-day status and hour. It represents the practical observation that the shape of an hourly rental profile differs between working and non-working days. The interaction is constructed without reading the target.

Put preprocessing inside the estimator

Categorical fields use one-hot encoding with unknown-category handling. Numeric fields are standardised. A ColumnTransformer joins both branches, and Pipeline ensures that each candidate estimates its transformations from the rows supplied to fit.

preprocessor = ColumnTransformer(
    transformers=[
        (
            "categorical",
            OneHotEncoder(handle_unknown="ignore", sparse_output=True),
            CATEGORICAL_FEATURES,
        ),
        ("numeric", StandardScaler(), NUMERIC_FEATURES),
    ],
    remainder="drop",
    sparse_threshold=1.0,
)

regressor = Pipeline(
    steps=[
        ("preprocess", preprocessor),
        ("ridge", Ridge(alpha=alpha, solver="lsqr", tol=1e-8, max_iter=10000)),
    ]
)

This arrangement prevents a common leakage path: fitting the scaler or category vocabulary on the full dataset before splitting. It also serialises the transformation and model together, which reduces discrepancies between training and later prediction.

OneHotEncoder(handle_unknown="ignore") keeps a previously unseen category from crashing prediction. It does not establish that the new category is safe or meaningful. Production monitoring should still flag unexpected levels and decide how to handle them.

Model a skewed non-negative outcome carefully

Hourly rentals are non-negative and right-skewed. The example wraps the pipeline in TransformedTargetRegressor, fits log1p(target_rentals) and converts predictions back with expm1. Negative back-transformed values are clipped to zero.

TransformedTargetRegressor(
    regressor=regressor,
    func=np.log1p,
    inverse_func=np.expm1,
    check_inverse=True,
)

This is a pragmatic baseline rather than a complete count model. The log transform changes the loss geometry, ridge remains linear in its transformed features, and clipping can affect error near zero. A Poisson, negative-binomial, gradient-boosting or other model may fit the problem better, but it should enter through the same validation and holdout contract.

Select on validation, then refit

The script compares alpha ∈ {0.1, 1, 10, 100}. Each candidate fits only the 13,003 training rows. Validation MAE selects the penalty:

AlphaExpected validation MAE
0.163.697
163.504
1061.941
10063.147

The chosen alpha = 10 is then refitted on the 15,211 combined train and validation rows. Only after that refit does the script generate predictions for the 2,168 test rows.

This is a small manual grid, suitable for showing the boundary. A larger search still needs a time-respecting design, such as rolling-origin validation. Random cross-validation would mix later and earlier observations and answer a different question.

Compare against a baseline worth beating

The baseline predicts the mean rental count for each weekday-and-hour group. For validation it is estimated from training rows. For the final test it is re-estimated from train plus validation, matching the candidate’s available development period.

A baseline is part of the scientific argument. Reporting only the candidate’s error cannot show whether its preprocessing and model add useful predictive information beyond a stable calendar pattern.

Expected test checkpoints from the pinned workflow are:

MetricCalendar baselineSelected ridge pipeline
MAE79.04949.894
RMSE119.95278.457
Mean signed error−0.927

Lower values are better for MAE and RMSE. The candidate reduces MAE by about 29.16 rentals per recorded hour relative to the baseline within this historical test period. Its small negative mean signed error indicates slight average underprediction, although positive and negative errors can cancel in that summary.

These figures describe one 2012 holdout under this feature contract. They do not establish causal effects, current performance or transferability to another city. The next guide tests whether the paired absolute-error difference remains favourable when whole test days are resampled.

Run the model and preserve the evidence

examples/programming-for-data-science/.venv/bin/python \
  examples/programming-for-data-science/python/train.py

Expected checkpoint:

model_ok selected_alpha=10 validation_mae=61.941 test_rows=2168 baseline_mae=79.049 model_mae=49.894 model_rmse=78.457

The stage writes:

  • python_model.joblib, containing the fitted transformation and ridge estimator;
  • python_model_metadata.json, recording versions, selected alpha, feature groups, row counts and transformed feature count;
  • python_metrics.json, retaining every validation candidate and the final validation/test metrics; and
  • predictions.csv, containing row-level actual, baseline and model values for the validation and test periods.

The display rounds metrics to three decimals. The machine-readable artifact retains full precision, and the golden test permits an absolute difference of 1e-5 rentals so equivalent builds do not fail on negligible floating-point differences in the linear-algebra stack.

Do not treat a joblib file as a permanent, language-neutral model format. It can execute Python objects and should only be loaded from a trusted source in a compatible environment. A deployment decision may require a safer exchange format, feature service or direct reimplementation with parity tests.

Diagnose before adding complexity

Before trying another algorithm, inspect the existing errors by hour, weekday, month, weather category and target magnitude. Check whether rare severe-weather rows dominate RMSE, whether peaks are systematically missed and whether residual structure changes across the quarter. Compare validation and test performance; a large change can indicate seasonality, drift or selection sensitivity.

For production work, add:

  • rolling temporal backtests rather than one validation window;
  • prediction intervals or quantile forecasts where operational decisions need them;
  • capacity, availability and rebalancing variables with prediction-time definitions;
  • drift and unknown-category monitoring;
  • model-card ownership, approval and rollback procedures; and
  • a newer external dataset for transportability assessment.

Do not repeatedly tune against the current test quarter. Once its results influence a choice, reserve a new final holdout.

When Python is the right layer

Python works well here because pandas provides a clear artefact boundary and scikit-learn composes preprocessing, selection and modelling into one fitted object. SQL remains better for the governed upstream table, while R provides a useful independent statistical audit. Keeping those responsibilities explicit reduces the chance that one notebook silently becomes ingestion job, model registry, report and source of truth.

Continue to R for Data Scientists to recalculate the holdout metrics and estimate a day-block bootstrap interval.

Primary references

AI assistance helped draft the guide and code. Exact dependency checks, golden metrics, persisted metadata and the independent R audit make the work reviewable. Clean-environment reproduction was verified on 15 August 2026, and human editorial review followed; the historical-data, causal and deployment limitations remain in force.

Opens in a new tab.