60 minReal data snapshotExecuted reading edition

Build an interpretable rental-count regression

Fit on 2011 and evaluate on 2012, distinguish conditional weather reconstruction from forecasting, and inspect error and drift.

Data science & machine learning · Lesson 05🔋 Offline core analysis⏱ 60 minEmbedded real data📦 numpy · pandas · matplotlib · IPython

Goal and how to run

This lesson assumes basic Python knowledge. Read each method and caption before changing parameters. AI exercises are optional; no AI account is needed for the core workflow. Data lives in notebook metadata, so there is no companion data download. Keep the notebook saved in the active working folder.

Your goal is to understand the research question, execute transparent calculations, check the results, and state the limits of the conclusion.

Setup and parameters

In [1] · Python
from pathlib import Path
import base64, gzip, hashlib, json, platform
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, HTML
from datetime import datetime
from IPython.display import display as _display
def display(value):
    if isinstance(value,pd.DataFrame):
        table=value.to_html(max_rows=12,escape=True)
        style="<style>.vibeit-readable-table{max-width:100%;overflow-x:auto}.vibeit-readable-table table{width:max-content;max-width:none;border-collapse:collapse}.vibeit-readable-table td,.vibeit-readable-table th{white-space:nowrap!important;word-break:normal!important;overflow-wrap:normal!important}</style>"
        return _display(HTML(style+'<div class="vibeit-readable-table">'+table+'</div>'))
    return _display(value)
RECIPE_ID='ds05-bike-regression'
OUTPUT_DIR=Path.cwd()/"results"/RECIPE_ID
plt.rcParams.update({"figure.dpi":120,"font.size":10,"axes.spines.top":False,"axes.spines.right":False,
                      "axes.prop_cycle":plt.cycler(color=["#18785f","#416aa6","#bc6541","#9974a5"])})
print("Python:",platform.python_version(),"; NumPy:",np.__version__,"; Pandas:",pd.__version__)
print("Working folder:",Path.cwd())
print("Core data mode: embedded snapshot; no network requests")
Python: 3.13.13 ; NumPy: 2.5.3 ; Pandas: 3.0.6
Working folder: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads
Core data mode: embedded snapshot; no network requests

Load the embedded snapshot

In [2] · Python
def load_snapshot(recipe_id, expected_sha256, snapshot_key):
    """Read embedded data from this notebook, including a renamed copy."""
    for path in sorted(Path.cwd().glob("*.ipynb")):
        try:
            notebook = json.loads(path.read_text(encoding="utf-8"))
            metadata = notebook.get("metadata", {}).get("vibeit_cookbook", {})
            blob = metadata.get("snapshots", {}).get(snapshot_key)
            if metadata.get("recipe_id") != recipe_id or not blob:
                continue
            if blob.get("sha256") != expected_sha256:
                continue
            raw = gzip.decompress(base64.b64decode(blob["gzip_base64"], validate=True))
            if hashlib.sha256(raw).hexdigest() != expected_sha256:
                raise ValueError("Embedded snapshot checksum mismatch: " + snapshot_key)
            return json.loads(raw)
        except (OSError, json.JSONDecodeError):
            continue
    raise FileNotFoundError(
        "Save/open the complete .ipynb in the active working folder. "
        "The embedded snapshot could not be located for " + recipe_id)

SNAPSHOT_HASHES={'bike': '99d075d1b2d3c2c9ce73f256dcc370081a51d1df60347f59674a8ed3edf92f92'}
def snapshot(key):
    return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['bike']

Read the method functions

In [3] · Python
def audit_numeric_table(frame,columns):
    """Audit observed rows; never silently impute or remove them."""
    if frame.empty or frame.index.has_duplicates:
        raise ValueError('Expected a nonempty table with unique row IDs')
    missing=[name for name in columns if name not in frame.columns]
    if missing:raise ValueError('Missing required columns: '+str(missing))
    values=frame[columns].to_numpy(dtype=float)
    if not np.isfinite(values).all():raise ValueError('Non-finite measurements; inspect missing values explicitly')
    return {'rows':len(frame),'features':len(columns),'finite':True,'duplicate_measurement_rows':int(frame[columns].duplicated().sum())}

print("Method ready: audit_numeric_table")
Method ready: audit_numeric_table
In [4] · Python
def ridge_fit_predict(train_values,train_target,test_values,penalty=1.0):
    """Scale using training statistics and solve ridge by an augmented least-squares system."""
    train_values=np.asarray(train_values,dtype=float);test_values=np.asarray(test_values,dtype=float)
    target=np.asarray(train_target,dtype=float)
    if train_values.ndim!=2 or len(train_values)==0 or target.shape!=(len(train_values),) or penalty<0:raise ValueError('Invalid training data or penalty')
    if test_values.ndim!=2 or train_values.shape[1]!=test_values.shape[1]:raise ValueError('Training/test feature dimensions differ')
    if not np.isfinite(train_values).all() or not np.isfinite(test_values).all() or not np.isfinite(target).all():raise ValueError('Non-finite training data')
    mean=train_values.mean(axis=0);scale=train_values.std(axis=0);scale=np.where(scale==0,1,scale)
    design=np.column_stack([np.ones(len(train_values)),(train_values-mean)/scale])
    test_design=np.column_stack([np.ones(len(test_values)),(test_values-mean)/scale])
    regularizer=np.sqrt(penalty)*np.eye(design.shape[1]);regularizer[0,0]=0
    coefficients=np.linalg.lstsq(np.vstack([design,regularizer]),np.r_[target,np.zeros(design.shape[1])],rcond=None)[0]
    return dict(predictions=test_design@coefficients,coefficients=coefficients,mean=mean,scale=scale,train_predictions=design@coefficients)

print("Method ready: ridge_fit_predict")
Method ready: ridge_fit_predict
In [5] · Python
def regression_metrics(truth,predictions):
    truth=np.asarray(truth,dtype=float);predictions=np.asarray(predictions,dtype=float)
    if truth.shape!=predictions.shape or truth.size==0 or not np.isfinite(truth-predictions).all():raise ValueError('Invalid regression comparison')
    error=predictions-truth;denominator=np.sum((truth-truth.mean())**2)
    return dict(mae=float(np.mean(np.abs(error))),rmse=float(np.sqrt(np.mean(error**2))),r_squared=float(1-np.sum(error**2)/denominator) if denominator>0 else float('nan'))

print("Method ready: regression_metrics")
Method ready: regression_metrics
In [6] · Python
def iso_datetimes(values):
    """Parse source ISO dates with the standard library; keep tables as ISO strings."""
    return [datetime.fromisoformat(str(value)) for value in values]

print("Method ready: iso_datetimes")
Method ready: iso_datetimes

Data and model boundary

The source contains one daily rental count per date in 2011–2012. These are rental events, not unique people. Casual and registered counts sum to the response and are excluded from predictors. Source-normalized daily weather fields are not silently relabelled as degrees Celsius.

Steps

1. Audit dates, targets and source normalization

Daily observations span 2011 and 2012. The target is rentals per day, not unique people. Temperature, humidity and wind fields retain source normalization; the daily temperature conversion is not inferred from an hourly-only source note.

In [7] · Python
bike=snapshot("bike")
frame=pd.DataFrame(bike["rows"],columns=bike["columns"])
frame["date"]=frame["dteday"].astype(str)
frame=frame.sort_values("date").reset_index(drop=True)
assert len(frame)==731 and frame["date"].is_unique
dates=iso_datetimes(frame.date)
assert all((b-a).total_seconds()==86400 for a,b in zip(dates,dates[1:]))
assert (frame["casual"]+frame["registered"]).eq(frame["cnt"]).all()
assert frame["cnt"].ge(0).all()
print("Complete daily observations:",len(frame),"; target: rentals/day")
display(frame[["date","cnt","temp","hum","windspeed"]].head(7))
Complete daily observations: 731 ; target: rentals/day
date cnt temp hum windspeed
0 2011-01-01 985 0.344167 0.805833 0.160446
1 2011-01-02 801 0.363478 0.696087 0.248539
2 2011-01-03 1349 0.196364 0.437273 0.248309
3 2011-01-04 1562 0.200000 0.590435 0.160296
4 2011-01-05 1600 0.226957 0.436957 0.186900
5 2011-01-06 1606 0.204348 0.518261 0.089565
6 2011-01-07 1510 0.196522 0.498696 0.168726

2. Declare the split and permitted predictors

Train on 2011 and test on 2012. Predictors include source-normalized weather, a working-day flag and elapsed time. Casual/registered counts are excluded because they sum to the target. Same-day observed weather makes this conditional regression, not a future-weather forecast.

In [8] · Python
frame["elapsed_days"]=[(value-dates[0]).days for value in dates]
features=["temp","hum","windspeed","workingday","elapsed_days"]
audit_numeric_table(frame,features+["cnt"])
train=np.array([value.year==2011 for value in dates]);test=~train
assert train.sum()==365 and test.sum()==366
fig,ax=plt.subplots(figsize=(9,4));ax.plot(dates,frame.cnt,color="gray",lw=1)
ax.axvline(datetime(2012,1,1),color="red",linestyle="--",label="2011 fit / 2012 test")
ax.set(xlabel="Date",ylabel="Rentals/day",title="Chronological split and observed demand");ax.legend();plt.tight_layout();plt.show()
Computed figure from this code cell

3. Fit with training-only scale estimates

Ridge minimizes squared error plus a penalty on standardized feature coefficients; the intercept is unpenalized. Solve an augmented least-squares system rather than explicitly inverting a matrix. Penalty=1 is declared before examining test outcomes.

In [9] · Python
model=ridge_fit_predict(frame.loc[train,features].to_numpy(),frame.loc[train,"cnt"].to_numpy(),frame.loc[test,features].to_numpy(),penalty=1.0)
prediction=model["predictions"];truth=frame.loc[test,"cnt"].to_numpy(float)
metrics=regression_metrics(truth,prediction)
print("2012 held-out metrics:",metrics)
print("Negative predictions (not silently clipped):",int(np.sum(prediction<0)))
display(pd.DataFrame({"term":["intercept"]+features,"coefficient":model["coefficients"]}))
fig,ax=plt.subplots(figsize=(6,5));ax.scatter(truth,prediction,s=13,alpha=.5)
limit=[min(truth.min(),prediction.min()),max(truth.max(),prediction.max())];ax.plot(limit,limit,"k--")
ax.set(xlabel="Observed rentals/day",ylabel="Predicted rentals/day",title="2012 conditional predictions");plt.tight_layout();plt.show()
2012 held-out metrics: {'mae': 1245.3946847319723, 'rmse': 1479.994175429142, 'r_squared': 0.3134869093527851}
Negative predictions (not silently clipped): 0
term coefficient
0 intercept 3405.761644
1 temp 975.325148
2 hum -273.416546
3 windspeed -250.541176
4 workingday -3.297354
5 elapsed_days 329.429847
Computed figure from this code cell

4. Inspect time-dependent errors

Residuals are predicted minus observed rentals/day. A long run of errors in one direction can indicate dataset shift or missing predictors. R² is a held-out descriptive score and can be negative; it is not proof of causal effects or deployment accuracy.

In [10] · Python
residual=prediction-truth
fig,ax=plt.subplots(figsize=(9,4));ax.plot(iso_datetimes(frame.loc[test,"date"]),residual,lw=1)
ax.axhline(0,color="black",linestyle="--");ax.set(xlabel="Date in 2012",ylabel="Prediction − observation (rentals/day)",title="Chronological regression errors");plt.tight_layout();plt.show()
Computed figure from this code cell

5. Reconcile errors and export the model report

Check RMSE and the fitted training mean directly. Export every held-out row and the training-only scaler. A student who adds leaked target components should expect artificially improved errors, then reject that model for the intended workflow.

In [11] · Python
assert np.isclose(metrics["rmse"],np.linalg.norm(prediction-truth)/np.sqrt(len(truth)))
assert np.allclose(model["mean"],frame.loc[train,features].mean())
assert not {"casual","registered","cnt"}&set(features)
OUTPUT_DIR.mkdir(parents=True,exist_ok=True)
pd.DataFrame({"date":frame.loc[test,"date"],"observed_rentals":truth,"predicted_rentals":prediction,"error":residual}).to_csv(OUTPUT_DIR/"2012-predictions.csv",index=False)
(OUTPUT_DIR/"model.json").write_text(json.dumps({"features":features,"penalty":1.0,"training_year":2011,"test_year":2012,"mean":model["mean"].tolist(),"scale":model["scale"].tolist(),"coefficients":model["coefficients"].tolist(),"metrics":metrics},indent=2))
print("Training-only scale and metric checks passed; exports:",OUTPUT_DIR)
Training-only scale and metric checks passed; exports: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads/results/ds05-bike-regression

Exercises and reference explanation

  1. Why would casual and registered create target leakage?
  2. Would observed same-day weather be available for a real next-day prediction?

Reference explanation. Their sum equals the response. Same-day measured weather makes this a conditional regression; a next-day system would need genuine weather forecasts or past-only features.

Keep a copy before changing parameters. Compare old and new figures and record which inputs, calculations and conclusions changed.

Optional AI coding exercises

Prompt 1

Using NumPy/Pandas/Matplotlib, compare penalties 0, 1 and 10 on training-only chronological folds, then evaluate one final choice on 2012.

Prompt 2

Using the same packages, add monthly test-error summaries and counts of negative predictions without silently clipping them.

Human acceptance. The test year stays excluded from fitting and tuning, scaler statistics match training rows and error units remain rentals/day.

Review the edited source, then manually run affected cells and subsequent checks. Ask the assistant to explain the purpose, packages and data provenance of its changes, and reconcile that explanation with actual output.

Optional online extension

In [12] · Python
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
    import requests
    try:
        response=requests.get('https://archive.ics.uci.edu/dataset/275',timeout=20)
        response.raise_for_status()
        print("Source:",response.url)
        print(response.text[:700])
    except requests.RequestException as error:
        print("Online extension unavailable:",type(error).__name__,str(error)[:160])
else:
    print("Online extension skipped; embedded core data unchanged")
Online extension skipped; embedded core data unchanged

Sources, snapshots and next steps

  • UCI bike — CC BY 4.0; credit UCI and original dataset authors; retrieved 2026-10-09. SHA-256 b70182d0d0508e9abbb79306ce5c0cec34869000f8220175ac83d11dbe845401. Extract day.csv from original download; preserve all rows and declared units; no imputation. Fanaee-T, H. (2013). Bike Sharing. UCI Machine Learning Repository. DOI: 10.24432/C5W894.

Next steps. Transfer these methods to your own public or authorized data while retaining input checks, recorded parameters and result validation. Document the new experimental design and method limits; this example does not validate a new experiment. Full data licenses and generation instructions accompany the cookbook.