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

按时间顺序评估能耗回归

只用训练时段拟合天气/日历回归,与均值基线比较,并检查测试能耗误差。

工程 · 第 02 课🔋 离线核心分析⏱ 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='en02-energy-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={'energy': '81117ee0a0706bf416bedc50f23b455ee8dd771e588c91b6a420dc3b184f638f'}
def snapshot(key):
    return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['energy']

方法函数

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

数据与模型边界

建筑记录包含来源中单栋建筑的真实十分钟能耗及环境观测。机场天气字段含来源插值,不是同位置校准参考。梁、电路和 PID 例子是明确的数值设计模型;计算响应不能证明硬件性能或实际验收。

分步分析

1. 保留按时间顺序的测试时段

前 70% 时间记录用于拟合,末 30% 用于测试。随机分行会混合临近及季节相关观测。目标为区间 Wh,并使用同时段环境变量进行条件回归。

In [7] · Python
energy=snapshot("energy");frame=pd.DataFrame(energy["rows"],columns=energy["columns"])
frame["timestamp"]=frame["date"].astype(str);frame=frame.sort_values("timestamp").reset_index(drop=True)
assert len(frame)==19735 and frame.timestamp.is_unique
dates=iso_datetimes(frame.timestamp)
assert all((b-a).total_seconds()==600 for a,b in zip(dates,dates[1:]))
audit=audit_numeric_table(frame,["Appliances","lights","T1","RH_1"])
assert frame.Appliances.ge(0).all() and frame.lights.ge(0).all()
frame["appliance_average_power_W"]=frame.Appliances/(600/3600)
print("Observed rows:",len(frame),"; energy: Wh/10 min; derived interval-average power: W; source timezone unspecified")
display(frame[["timestamp","Appliances","appliance_average_power_W","T1","RH_1"]].head(8))
split=int(.7*len(frame));train=np.arange(len(frame))<split;test=~train
frame["hour_sin"]=np.sin(2*np.pi*(np.array([value.hour+value.minute/60 for value in dates]))/24)
frame["hour_cos"]=np.cos(2*np.pi*(np.array([value.hour+value.minute/60 for value in dates]))/24)
features=["T1","RH_1","T_out","RH_out","hour_sin","hour_cos"]
assert frame.loc[train,"timestamp"].max()<frame.loc[test,"timestamp"].min()
print("Training/test rows:",int(train.sum()),int(test.sum()))
Observed rows: 19735 ; energy: Wh/10 min; derived interval-average power: W; source timezone unspecified
timestamp Appliances appliance_average_power_W T1 RH_1
0 2016-01-11 17:00:00 60 360.0 19.890000 47.596667
1 2016-01-11 17:10:00 60 360.0 19.890000 46.693333
2 2016-01-11 17:20:00 50 300.0 19.890000 46.300000
3 2016-01-11 17:30:00 50 300.0 19.890000 46.066667
4 2016-01-11 17:40:00 60 360.0 19.890000 46.333333
5 2016-01-11 17:50:00 50 300.0 19.890000 46.026667
6 2016-01-11 18:00:00 60 360.0 19.890000 45.766667
7 2016-01-11 18:10:00 60 360.0 19.856667 45.560000
Training/test rows: 13814 5921

2. 仅用训练时段拟合缩放并比较基线

排除随机变量 rv1/rv2 及其他能量测量。岭回归缩放仅使用训练均值/标准差,均值基线也只用训练目标均值。简单模型即便表现较差,也应报告测试指标。

In [8] · Python
model=ridge_fit_predict(frame.loc[train,features].to_numpy(),frame.loc[train,"Appliances"].to_numpy(),frame.loc[test,features].to_numpy(),penalty=10)
truth=frame.loc[test,"Appliances"].to_numpy(float);prediction=model["predictions"]
baseline=np.full(len(truth),frame.loc[train,"Appliances"].mean())
metrics=pd.DataFrame({"ridge":regression_metrics(truth,prediction),"training-mean":regression_metrics(truth,baseline)}).T;display(metrics)
fig,ax=plt.subplots(figsize=(7,5));ax.scatter(truth,prediction,s=5,alpha=.25)
limit=[min(truth.min(),prediction.min()),max(truth.max(),prediction.max())];ax.plot(limit,limit,"k--")
ax.set(xlabel="Observed Wh/interval",ylabel="Predicted Wh/interval",title="Chronological held-out conditional predictions");plt.tight_layout();plt.show()
mae rmse r_squared
ridge 49.573429 87.729910 0.082222
training-mean 53.398043 91.646165 -0.001546
Computed figure from this code cell

3. 检查时间相关误差

展示测试期首周的有限窗口,误差和目标单位都是 Wh/区间。少量特征可能遗漏占用及设备事件;残差轨迹不是独立样本置信计算。

In [9] · Python
timestamp=frame.loc[test,"timestamp"].to_numpy();residual=prediction-truth;count=min(7*144,len(truth))
fig,ax=plt.subplots(figsize=(10,4));ax.plot(iso_datetimes(timestamp[:count]),residual[:count],lw=.8)
ax.axhline(0,color="black",linestyle="--");ax.set(xlabel="Source-local test timestamp",ylabel="Prediction − observation (Wh/interval)",title="First test-week conditional errors");plt.tight_layout();plt.show()
Computed figure from this code cell

4. 按时钟小时比较平均误差

在相同测试日期按来源小时比较模型和基线绝对误差。模式可帮助设计新评估,不能在此测试结果上事后调参仍称其为未触碰测试。

In [10] · Python
hours=np.array([value.hour for value in iso_datetimes(frame.loc[test,"timestamp"])])
hourly=pd.DataFrame({"hour":hours,"ridge":abs(residual),"training-mean":abs(baseline-truth)}).groupby("hour").mean()
fig,ax=plt.subplots(figsize=(9,4));hourly.plot(ax=ax,marker="o")
ax.set(xlabel="Source-local clock hour",ylabel="Held-out MAE (Wh/interval)",title="Common-date error stratification");plt.tight_layout();plt.show()
Computed figure from this code cell

5. 核对预测信息边界并导出

核对训练统计量及直接 RMSE,不以裁剪掩盖负预测。导出时间划分、原目标及两种预测,并保留单栋建筑的观测限制。

In [11] · Python
assert np.allclose(model["mean"],frame.loc[train,features].mean())
assert np.isclose(metrics.loc["ridge","rmse"],np.sqrt(np.mean(residual**2)))
assert not {"Appliances","lights","rv1","rv2"}&set(features)
OUTPUT_DIR.mkdir(parents=True,exist_ok=True)
pd.DataFrame({"timestamp":timestamp,"observed_Wh":truth,"ridge_Wh":prediction,"training_mean_Wh":baseline}).to_csv(OUTPUT_DIR/"held-out-energy.csv",index=False)
metrics.to_csv(OUTPUT_DIR/"metrics-Wh-per-interval.csv");hourly.to_csv(OUTPUT_DIR/"hourly-test-MAE.csv")
print("Prediction-boundary checks passed; negative model predictions:",int(np.sum(prediction<0)))
Prediction-boundary checks passed; negative model predictions: 0

自主练习与参考答案

  1. 为何本例优先使用时间划分?
  2. 为何区分条件回归与预测?

参考解释。 临近季节观测可能在随机划分间泄漏信息。同时段环境值可供条件回归使用,但未来预测未必可知。

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

可选的 AI 编程练习

提示词 1

仅用 NumPy、Pandas、Matplotlib 增加仅训练数据的时钟小时均值基线,在同测试日期评估。

提示词 2

使用同样的包按观测高/低能耗分层报告误差,保持原拟合模型不变。

人工验收。 测试值不参与缩放或拟合,各方法使用同日期和 Wh/区间单位。

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

可选联网扩展

In [12] · Python
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
    import requests
    try:
        response=requests.get('https://archive.ics.uci.edu/dataset/374/appliances+energy+prediction',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 energy — CC BY 4.0; credit UCI and original dataset authors; retrieved 2026-10-09. SHA-256 2fccf354445d886e7917620b0195db1f3e3e34d5a067a93b844694a4c561255a. Extract energydata_complete.csv from original download; preserve all rows and declared units; no imputation. Candanedo, L. (2017). Appliances Energy Prediction. UCI Machine Learning Repository. DOI: 10.24432/C5VC8G.

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