处理低采样率实测能耗信号
检查日尺度频谱、明确的非因果滤波及十分钟采样的限制。
目标与运行方法
本课适合具备 Python 基础的本科生。先阅读每一步的方法与图注,再修改参数。AI 练习为可选部分,不需要 AI 账号即可完成核心课程。数据写在 notebook metadata 中,不需要另下载数据文件。请保持 notebook 已保存到当前工作文件夹。
学习任务是理解本课的研究问题、执行透明的计算、校验结果,并说明结论的边界。
环境与参数
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 scipy.signal import welch, butter, sosfiltfilt, sosfreqz
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='en03-energy-filtering'
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
读取内嵌快照
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']
方法函数
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
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. 以日为单位设定采样尺度
十分钟观测为每日 144 样本,Nyquist 为每日 72 周期,不能解析 50/60 Hz 电网波形。本课分析区间平均能量/功率的慢时间变化。
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))
power=frame.appliance_average_power_W.to_numpy(float);sampling_per_day=144.0;cutoff_cycles_per_day=6.0
sos=butter(4,cutoff_cycles_per_day,btype="lowpass",fs=sampling_per_day,output="sos")
smoothed=sosfiltfilt(sos,power)
fig,ax=plt.subplots(figsize=(10,4));count=7*144
ax.plot(iso_datetimes(frame.timestamp.iloc[:count]),power[:count],label="Observed interval-average power",lw=.7,alpha=.7)
ax.plot(iso_datetimes(frame.timestamp.iloc[:count]),smoothed[:count],label="Acausal low-pass processing",lw=1.4)
ax.set(xlabel="Source-local time",ylabel="Average / processed power (W)",title="Measured source and offline processing");ax.legend();plt.tight_layout();plt.show()
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 |
2. 估计低采样率频谱
当 fs 采用样本/日,Welch 频率为周期/日,谱密度为 W² 每(周期/日)。日尺度峰描述此来源时段,不是高速电学谐波证据。
frequency,psd=welch(power-power.mean(),fs=sampling_per_day,nperseg=7*144,noverlap=3*144)
positive=frequency>0
fig,ax=plt.subplots(figsize=(8,5));ax.loglog(frequency[positive],psd[positive])
ax.axvline(cutoff_cycles_per_day,color="red",linestyle="--",label="Declared cutoff")
ax.set(xlabel="Frequency (cycles/day)",ylabel="Power PSD (W² per cycles/day)",title="Welch density at the actual observation cadence");ax.legend();plt.tight_layout();plt.show()
3. 解读双次滤波响应
前后向处理非因果,幅度为单次响应幅度的平方。它可用于离线探索,但不能原样当作实时控制滤波器。边界及振铃可能影响处理结果。
response_frequency,response=sosfreqz(sos,worN=2048,fs=sampling_per_day)
fig,ax=plt.subplots(figsize=(8,4));ax.plot(response_frequency,abs(response),label="Single pass")
ax.plot(response_frequency,abs(response)**2,label="Forward/backward magnitude")
ax.axvline(cutoff_cycles_per_day,color="gray",linestyle="--")
ax.set(xlabel="Frequency (cycles/day)",ylabel="Magnitude ratio",xlim=(0,20),title="Declared offline filter response");ax.legend();plt.tight_layout();plt.show()
4. 检查 FFT 归一化并保留原始数据
对 19735 个观测使用完整复数 FFT 核对 Parseval,避免实数 FFT 奇偶端点权重错误。分别保留测量与处理序列,并报告负处理值,不掩盖它们。
centered=power-power.mean();transform=np.fft.fft(centered)
assert np.isclose(np.sum(abs(transform)**2)/len(power)**2,np.mean(centered**2),rtol=1e-12)
assert np.isfinite(smoothed).all() and np.all(psd>=0)
OUTPUT_DIR.mkdir(parents=True,exist_ok=True)
pd.DataFrame({"timestamp":frame.timestamp,"observed_interval_average_W":power,"acausal_lowpass_W":smoothed}).to_csv(OUTPUT_DIR/"power-processing.csv",index=False)
pd.DataFrame({"cycles_per_day":frequency,"PSD_W_squared_per_cycles_per_day":psd}).to_csv(OUTPUT_DIR/"energy-spectrum.csv",index=False)
print("Cadence/FFT checks passed; negative processed values:",int(np.sum(smoothed<0)))
Cadence/FFT checks passed; negative processed values: 4
自主练习与参考答案
- 以周期/日计的 Nyquist 上限是多少?
- 为何单次幅度需要平方?
参考解释。 每日 144 样本对应 72 周期/日。前后向处理叠加两次滤波且非因果,幅度为单次平方。
先保留当前 notebook 副本,再修改参数。把新图与原图一起比较,并记录改变了哪些输入、哪些计算和哪些结论。
可选的 AI 编程练习
提示词 1
仅用 NumPy、SciPy、Matplotlib 比较每日三与六周期截止,保持采样尺度标签。
提示词 2
使用同样的包画首末周边界行为,分别保留测量与处理值。
人工验收。 奇数观测下 FFT 归一化通过,频谱单位明确,滤波标为离线/非因果。
检查修改后的源代码,再手动运行受影响单元及后续校验。要求助手解释修改的目的、使用的包和数据来源,并用实际输出核对解释。
可选联网扩展
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 网页。