60 分钟真实数据快照已执行的网页阅读版

构建可解释的共享单车回归

用 2011 年拟合、2012 年评估,区分同日天气条件回归与预测,并检查误差与分布变化。

数据科学与机器学习 · 第 05 课🔋 离线核心分析⏱ 60 分钟内嵌真实数据📦 numpy · pandas · matplotlib · IPython

目标与运行方法

本课适合具备 Python 基础的本科生。先阅读每一步的方法与图注,再修改参数。AI 练习为可选部分,不需要 AI 账号即可完成核心课程。数据写在 notebook metadata 中,不需要另下载数据文件。请保持 notebook 已保存到当前工作文件夹。

学习任务是理解本课的研究问题、执行透明的计算、校验结果,并说明结论的边界。

环境与参数

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

读取内嵌快照

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']

方法函数

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

数据与模型边界

来源记录 2011–2012 年每天的租赁次数,不是独立人数。casual 与 registered 之和是目标,因此不能作为预测变量。来源归一化的日天气字段不会被静默改写为摄氏度。

分步分析

1. 审计日期、目标与来源归一化

日数据覆盖 2011 与 2012 年,目标是每日租赁次数,不是独立人数。温度、湿度和风速保留来源归一化方式,不根据仅适用于小时数据的说明推断日数据温度换算。

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. 声明划分与允许的预测变量

2011 年用于训练,2012 年用于测试。特征包括来源归一化天气、工作日标记及已过去天数。casual/registered 与目标相加关系明确,因此排除。同日实测天气使本课属于条件回归,而非未来天气预测。

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. 只用训练集拟合缩放与系数

岭回归最小化平方误差并惩罚标准化特征系数,截距不惩罚。使用增广最小二乘求解,避免显式求逆。penalty=1 在检查测试结果前声明。

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. 检查随时间变化的误差

残差为预测减去观测,单位为租赁次数/日。连续同方向误差可能提示分布变化或遗漏特征。测试 R² 可以为负,也不证明因果关系或部署准确性。

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. 核对误差并导出模型报告

直接核对 RMSE 与训练均值。导出全部测试记录和训练缩放器。若加入目标组成字段,会人为改善误差,但该模型不满足本课的预测信息边界。

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

自主练习与参考答案

  1. 为何 casual 与 registered 会形成目标泄漏?
  2. 真实下一日预测是否能使用同日实测天气?

参考解释。 两者之和就是目标。同日实测天气使本课成为条件回归;下一日系统需要真实天气预报或仅过去的特征。

先保留当前 notebook 副本,再修改参数。把新图与原图一起比较,并记录改变了哪些输入、哪些计算和哪些结论。

可选的 AI 编程练习

提示词 1

仅用 NumPy、Pandas、Matplotlib 在训练集时间折中比较惩罚 0、1、10,最终只选择一次方法评估 2012 年。

提示词 2

使用同样的包增加月度测试误差和负预测数量,不静默裁剪。

人工验收。 测试年不参与拟合或调参,缩放统计量来自训练行,误差单位为租赁次数/日。

检查修改后的源代码,再手动运行受影响单元及后续校验。要求助手解释修改的目的、使用的包和数据来源,并用实际输出核对解释。

可选联网扩展

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

来源、快照与下一步

  • 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.

下一步。 在保留本课的数据检查、参数记录和结果校验之后,将相同方法用于自己的公开或已获授权数据。记录研究设计与方法限制,不把示例结果当成新实验的验证。完整数据许可与生成说明见 cookbook 网页。