理解标准化 PCA
用 Iris 四个测量特征检查缩放、奇异值、主成分方向和重建误差。
目标与运行方法
本课适合具备 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 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='ds02-iris-pca'
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={'iris': 'cb2512f34911979eef3a42b7a9e6fbc6ca566fd2c0eaa7bd86daefd7ac1cf5ad'}
def snapshot(key):
return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['iris']
方法函数
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 standardized_pca(values,components=2):
"""Population-SD standardization followed by an SVD of centered observations."""
values=np.asarray(values,dtype=float)
if values.ndim!=2 or min(values.shape)<2 or not np.isfinite(values).all():
raise ValueError('PCA requires a finite observations × features matrix')
if not 1<=components<=min(values.shape):raise ValueError('Invalid number of components')
mean=values.mean(axis=0);scale=values.std(axis=0,ddof=0)
if np.any(scale==0):raise ValueError('Constant features must be removed or handled explicitly')
standardized=(values-mean)/scale
left,singular,loadings=np.linalg.svd(standardized,full_matrices=False)
# Fix signs for reproducible plots, without changing distances or explained variance.
for j in range(len(singular)):
pivot=np.argmax(np.abs(loadings[j]))
if loadings[j,pivot]<0:loadings[j]*=-1;left[:,j]*=-1
variance=singular**2/(len(values)-1)
return dict(mean=mean,scale=scale,scores=left[:,:components]*singular[:components],
loadings=loadings[:components],explained_ratio=variance/variance.sum(),standardized=standardized)
print("Method ready: standardized_pca")
Method ready: standardized_pca
数据与模型边界
这些是冻结的公开实测数据,不是新的总体随机样本。保留原始 UCI iris.data,包括来源说明的两处转录差异。舍入后的重复特征值本身不能证明两行属于同一株植物。
分步分析
1. 审计 PCA 输入
PCA 使用记录 × 特征矩阵,物种标签只用于解读图表。这里分析全部测量值,属于描述性分析,没有形成独立的预测评估。
iris=snapshot("iris")
frame=pd.DataFrame(iris["rows"],columns=iris["columns"])
features=list(frame.columns[:4])
audit=audit_numeric_table(frame,features)
assert len(frame)==150 and frame["species"].nunique()==3
assert (frame[features]>0).all().all()
print("Audit:",audit,"; measurement unit: cm")
print("Source variant:",iris["variant"])
display(frame.head(8))
Audit: {'rows': 150, 'features': 4, 'finite': True, 'duplicate_measurement_rows': 3} ; measurement unit: cm
Source variant: Original UCI iris.data; no silent correction of the two documented transcription discrepancies
| sepal_length_cm | sepal_width_cm | petal_length_cm | petal_width_cm | species | |
|---|---|---|---|---|---|
| 0 | 5.1 | 3.5 | 1.4 | 0.2 | Iris-setosa |
| 1 | 4.9 | 3.0 | 1.4 | 0.2 | Iris-setosa |
| 2 | 4.7 | 3.2 | 1.3 | 0.2 | Iris-setosa |
| 3 | 4.6 | 3.1 | 1.5 | 0.2 | Iris-setosa |
| 4 | 5.0 | 3.6 | 1.4 | 0.2 | Iris-setosa |
| 5 | 5.4 | 3.9 | 1.7 | 0.4 | Iris-setosa |
| 6 | 4.6 | 3.4 | 1.4 | 0.3 | Iris-setosa |
| 7 | 5.0 | 3.4 | 1.5 | 0.2 | Iris-setosa |
2. 标准化并分解
减去各特征均值,并除以总体标准差(ddof=0)。随后使用 SVD 求正交变化方向。方差分母为 n−1,共同因子在解释方差比例中抵消。
values=frame[features].to_numpy(float)
pca=standardized_pca(values,components=2)
display(pd.DataFrame({"feature":features,"mean_cm":pca["mean"],"SD_cm":pca["scale"]}))
fig,ax=plt.subplots(figsize=(7,4));ax.bar(np.arange(1,5),100*pca["explained_ratio"])
ax.set(xlabel="Principal component",ylabel="Explained variance (%)",title="Variance after feature standardization",xticks=[1,2,3,4]);plt.tight_layout();plt.show()
| feature | mean_cm | SD_cm | |
|---|---|---|---|
| 0 | sepal_length_cm | 5.843333 | 0.825301 |
| 1 | sepal_width_cm | 3.054000 | 0.432147 |
| 2 | petal_length_cm | 3.758667 | 1.758529 |
| 3 | petal_width_cm | 1.198667 | 0.760613 |
3. 解读主成分得分
点代表观测,颜色代表来源标签,得分无量纲。坐标轴给出标准化后变化的解释比例;可见分组不等于独立测试集的分类结果。
fig,ax=plt.subplots(figsize=(7,5))
for label in sorted(frame.species.unique()):
mask=frame.species.eq(label).to_numpy();ax.scatter(*pca["scores"][mask].T,label=label,s=26)
ax.set(xlabel=f"PC1 ({pca['explained_ratio'][0]:.1%})",ylabel=f"PC2 ({pca['explained_ratio'][1]:.1%})",title="Standardized Iris PCA")
ax.legend(fontsize=8);plt.tight_layout();plt.show()
4. 将方向与输入特征联系起来
载荷热图显示标准化特征的有符号系数。将整个主成分反号不会改变距离或方差。这些系数描述分解方向,不是因果效应。
loadings=pd.DataFrame(pca["loadings"].T,index=features,columns=["PC1","PC2"])
fig,ax=plt.subplots(figsize=(5,5));im=ax.imshow(loadings,cmap="coolwarm",vmin=-1,vmax=1)
ax.set_xticks([0,1],["PC1","PC2"]);ax.set_yticks(range(4),features)
for i in range(4):
for j in range(2):ax.text(j,i,f"{loadings.iloc[i,j]:.3f}",ha="center",va="center")
fig.colorbar(im,ax=ax,label="Signed coefficient");plt.tight_layout();plt.show()
5. 用独立分解核对并导出
用协方差矩阵特征值核对 SVD 的解释比例。四个主成分应恢复原始测量,再与有损的双主成分重建比较。
eigenvalues=np.linalg.eigvalsh(np.cov(pca["standardized"],rowvar=False))[::-1]
assert np.allclose(pca["explained_ratio"],eigenvalues/eigenvalues.sum())
assert np.allclose(pca["loadings"]@pca["loadings"].T,np.eye(2))
full=standardized_pca(values,4)
reconstructed=(full["scores"]@full["loadings"])*full["scale"]+full["mean"]
assert np.allclose(reconstructed,values)
two=(pca["scores"]@pca["loadings"])*pca["scale"]+pca["mean"]
print("Two-component RMSE (cm):",float(np.sqrt(np.mean((values-two)**2))))
OUTPUT_DIR.mkdir(parents=True,exist_ok=True)
pd.DataFrame(pca["scores"],columns=["PC1","PC2"]).assign(species=frame.species).to_csv(OUTPUT_DIR/"pca-scores.csv",index=False)
loadings.to_csv(OUTPUT_DIR/"pca-loadings.csv")
pd.DataFrame({"PC":np.arange(1,5),"explained_ratio":pca["explained_ratio"]}).to_csv(OUTPUT_DIR/"variance.csv",index=False)
print("Independent checks passed; exports:",OUTPUT_DIR)
Two-component RMSE (cm): 0.1885954928399019 Independent checks passed; exports: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads/results/ds02-iris-pca
自主练习与参考答案
- 用一个、两个和四个主成分重建测量。
- 不进行方差缩放再做 PCA,解释几何变化。
参考解释。 四个主成分恢复有限输入矩阵。较少成分一般增加重建误差;仅中心化会按原始特征分散程度加权。
先保留当前 notebook 副本,再修改参数。把新图与原图一起比较,并记录改变了哪些输入、哪些计算和哪些结论。
可选的 AI 编程练习
提示词 1
仅用 NumPy、Pandas、Matplotlib 增加 1–4 个成分的重建误差曲线,保留原分解校验。
提示词 2
使用同样的包比较仅中心化与标准化 PCA,并说明坐标和单位。
人工验收。 完整重建与输入一致,解释比例之和为一,载荷基正交归一。
检查修改后的源代码,再手动运行受影响单元及后续校验。要求助手解释修改的目的、使用的包和数据来源,并用实际输出核对解释。
可选联网扩展
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
import requests
try:
response=requests.get('https://archive.ics.uci.edu/dataset/53',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 iris — CC BY 4.0; credit UCI and original dataset authors; retrieved 2026-10-09. SHA-256
d11fe30213d36434a0879aab7cb00ce3c812eb7ba2495874438abff7b7b762e9. Extract iris.data from original download; preserve all rows and declared units; no imputation. Fisher, R. (1936). Iris. UCI Machine Learning Repository. DOI: 10.24432/C56C76.
下一步。 在保留本课的数据检查、参数记录和结果校验之后,将相同方法用于自己的公开或已获授权数据。记录研究设计与方法限制,不把示例结果当成新实验的验证。完整数据许可与生成说明见 cookbook 网页。