50 minReal data snapshotExecuted reading edition

Understand standardized PCA

Use all four Iris measurements to inspect scaling, singular values, component directions and reconstruction error.

Data science & machine learning · Lesson 02🔋 Offline core analysis⏱ 50 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 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

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={'iris': 'cb2512f34911979eef3a42b7a9e6fbc6ca566fd2c0eaa7bd86daefd7ac1cf5ad'}
def snapshot(key):
    return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['iris']

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

Data and model boundary

These are frozen public measurements, not a new random population sample. The original UCI iris.data is retained, including its two source-documented transcription discrepancies. Repeated rounded feature values do not by themselves establish that two rows are the same plant.

Steps

1. Audit the PCA input

PCA uses a rows × features matrix. Species labels are retained only for reading the final figure. This is a descriptive analysis of all measurements, so it does not create an independent predictive evaluation.

In [5] · Python
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. Standardize and decompose

Subtract each feature mean and divide by its population SD (ddof=0). SVD then determines orthogonal directions of variation. The variance denominator is n−1; its common factor cancels in explained-variance ratios.

In [6] · Python
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
Computed figure from this code cell

3. Read the component scores

Points are observations and colors are source labels. Scores are dimensionless. The axes report the percentage of standardized variation; visible grouping is not a held-out classification result.

In [7] · Python
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()
Computed figure from this code cell

4. Connect directions to input features

The signed loading heatmap shows coefficients of standardized features. Reversing an entire component changes neither distances nor variance. Coefficients describe this decomposition, not a causal feature effect.

In [8] · Python
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()
Computed figure from this code cell

5. Check an independent decomposition and export

Compare SVD ratios with eigenvalues of the covariance matrix. Recover the original measurements with all four components; compare that exact reconstruction with the lossy two-component reconstruction.

In [9] · Python
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

Exercises and reference explanation

  1. Reconstruct measurements using one, two and four components.
  2. Repeat PCA without variance scaling; explain why the geometry changes.

Reference explanation. Four components recover the finite input matrix. Fewer components generally increase reconstruction error; centering alone weights features according to their original spread.

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, add a reconstruction-error curve for 1–4 components, keeping the original decomposition checks.

Prompt 2

Using the same packages, compare centered-only and standardized PCA with clear axis/unit captions.

Human acceptance. Full reconstruction must match the input; explained ratios sum to one and every loading basis is orthonormal.

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 [10] · Python
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

Sources, snapshots and next steps

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

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.