Principal Component Analysis (PCA) answers a simple question: if we are allowed to rotate our coordinate axes, which orientation describes the data most simply?
The idea in three steps:
So PCA is, in essence, a rotation into the data's own natural axes, ordered by how much variance each axis carries. Everything below makes this precise and shows it in code.
When we decompose $\mathbf{C} = \mathbf{Q}\mathbf{\Lambda}\mathbf{Q}^T$, we are essentially "un-sandwiching" the matrix to find its core properties:
Why the eigenvectors are the new coordinate system. A set of $n$ orthonormal vectors in $\mathbb{R}^n$ is, by definition, an orthonormal basis—so the eigenvector matrix $\mathbf{Q} = [\mathbf{v}_1\,\cdots\,\mathbf{v}_n]$ is not just a bundle of directions, it is a complete new set of axes. We met this already in eigen-decomposition, where $\mathbf{Q}$ turned out to be orthogonal ($\mathbf{Q}^{-1}=\mathbf{Q}^T$), i.e. a pure rotation. Re-expressing a point in this basis is exactly the passive rotation / change of basis from tensors and transformation: the coordinate of a centered point $\tilde{\mathbf{x}}$ along $\text{PC}_i$ is simply its projection $\tilde{\mathbf{x}}\cdot\mathbf{v}_i$ (stacked together, $\mathbf{Q}^T\tilde{\mathbf{x}}$). That is why the arrows below can be read as genuine axes and not merely as "interesting directions."
$\mathbf{\Lambda}$ is the covariance matrix in the new coordinates. A covariance matrix transforms under a change of basis by the law $\mathbf{C}' = \mathbf{A}\,\mathbf{C}\,\mathbf{A}^T$ (see tensors and transformation). Choosing the eigenvector basis, i.e. $\mathbf{A} = \mathbf{Q}^T$, gives $$\mathbf{C}' = \mathbf{Q}^T \mathbf{C}\, \mathbf{Q} = \mathbf{Q}^T (\mathbf{Q}\mathbf{\Lambda}\mathbf{Q}^T)\, \mathbf{Q} = \mathbf{\Lambda}.$$ So $\mathbf{\Lambda}$ is literally the covariance matrix of the data expressed in the principal-component basis: the off-diagonal entries are zero (the principal components are uncorrelated), and the diagonal entries are the variances $\lambda_i$ along each new axis. This is the precise statement behind "the variables become uncorrelated"—PCA is the rotation that diagonalizes the covariance.
Where do the entries of $\mathbf{Q}$ come from, and why do we call it a rotation? Compare with the ordinary 2D rotation by an angle $\theta$:
$$\mathbf{R}(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix} = \big[\; \mathbf{e}_1' \;\big|\; \mathbf{e}_2' \;\big].$$
The two columns of $\mathbf{R}(\theta)$ are exactly the rotated axes written in the old coordinates: the first column $\mathbf{e}_1'=(\cos\theta,\sin\theta)$ is the new first axis expressed in the original frame, and the second column $\mathbf{e}_2'=(-\sin\theta,\cos\theta)$ is the new second axis.
The eigen-equation $\mathbf{C}\mathbf{q}_i=\lambda_i\mathbf{q}_i$ produces the same kind of object. Each eigenvector $\mathbf{q}_i$ is a unit vector whose entries are the components of the $i$-th principal axis measured in the original $(x_1,x_2)$ coordinates. Stacking them as columns,
$$\mathbf{Q} = \big[\; \mathbf{q}_1 \;\big|\; \mathbf{q}_2 \;\big],$$
fills $\mathbf{Q}$ column-by-column with the new axes in the old system—precisely how $\mathbf{R}(\theta)$ is built. For a 2D covariance (symmetric, with the signs chosen so $\det\mathbf{Q}=+1$) this makes $\mathbf{Q}$ literally a rotation matrix: there is a single angle $\theta$—the tilt of the data ellipse—such that
$$\mathbf{q}_1 = \begin{bmatrix}\cos\theta\\ \sin\theta\end{bmatrix}, \qquad \mathbf{q}_2 = \begin{bmatrix}-\sin\theta\\ \cos\theta\end{bmatrix}, \qquad \mathbf{Q} = \mathbf{R}(\theta).$$
Reading off coordinates mirrors rotation too: the new coordinates of a centered point are $\mathbf{Q}^\top\tilde{\mathbf{x}} = \mathbf{R}(-\theta)\,\tilde{\mathbf{x}}$—we rotate the point by $-\theta$ (equivalently, rotate the axes by $+\theta$), and the $i$-th new coordinate is the projection $\tilde{\mathbf{x}}\cdot\mathbf{q}_i$.
The one subtlety versus a textbook rotation: eigenvectors are fixed only up to sign, so a solver may return a column pointing the opposite way (giving $\det\mathbf{Q}=-1$, a reflection). Flipping that column's sign restores a proper rotation and leaves $\mathbf{C}=\mathbf{Q}\mathbf{\Lambda}\mathbf{Q}^\top$ unchanged (see eigen-decomposition).
import numpy as np
# A symmetric covariance whose ellipse is tilted by 45 degrees.
C_demo = np.array([[3.0, 1.0],
[1.0, 3.0]])
# Eigen-decomposition (eigh returns ascending; reorder to descending variance).
w, V = np.linalg.eigh(C_demo)
order = w.argsort()[::-1]
w, V = w[order], V[:, order]
# Eigenvectors are defined only up to sign:
# - point v1 into the +x half-plane so the angle comes out clean,
# - then flip v2 if needed so Q is a proper rotation (det = +1), not a reflection.
if V[0, 0] < 0:
V[:, 0] *= -1
if np.linalg.det(V) < 0:
V[:, 1] *= -1
# The tilt angle theta read straight off the first eigenvector v1 = (cos, sin).
theta = np.arctan2(V[1, 0], V[0, 0])
R = np.array([[np.cos(theta), -np.sin(theta)],
[np.sin(theta), np.cos(theta)]])
print("Q = [v1 | v2] (new axes in the old coordinates):\n", np.round(V, 4))
print(f"\ntheta from v1 = {np.degrees(theta):.1f} deg -> R(theta):\n", np.round(R, 4))
print("\nQ equals the rotation matrix R(theta)? ", np.allclose(V, R))
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
import matplotlib.transforms as transforms
def draw_confidence_ellipse(mu, sigma, ax, n_std=2.0, facecolor='none', **kwargs):
"""
Utility to create a plot of the covariance confidence ellipse.
"""
# 1. Compute Eigen-decomposition of the covariance matrix
vals, vecs = np.linalg.eigh(sigma)
order = vals.argsort()[::-1]
vals, vecs = vals[order], vecs[:, order]
# 2. Calculate the angle of rotation
theta = np.degrees(np.arctan2(*vecs[:, 0][::-1]))
# 3. Width and height are standard deviations (sqrt of eigenvalues)
width, height = 2 * n_std * np.sqrt(vals)
# 4. Create the ellipse
ell = Ellipse(xy=mu, width=width, height=height, angle=theta,
facecolor=facecolor, **kwargs)
return ax.add_patch(ell)
# --- Parameters & Data ---
np.random.seed(42)
od = 0.8
Sigma = np.array([[2, od], [od, 0.6]])
mu = np.array([2., 1.])
nb_data = 100
X = np.random.multivariate_normal(mu, Sigma, nb_data)
# --- Statistics ---
mean_emp = X.mean(axis=0)
cov_mat = np.cov(X, rowvar=False)
# Use eigh (cov_mat is symmetric -> real eigenvalues, orthonormal eigenvectors)
# and sort the eigenpairs in descending order so column 0 is always PC1
# (the direction of largest variance). eigh/eig do not guarantee this order.
eig_vals, eig_vecs = np.linalg.eigh(cov_mat)
order = eig_vals.argsort()[::-1]
eig_vals, eig_vecs = eig_vals[order], eig_vecs[:, order]
# --- Visualization ---
fig, ax = plt.subplots(figsize=(7, 7))
# 1. Plot the sampled data
ax.scatter(X[:, 0], X[:, 1], c='b', marker='x', alpha=0.6, label="Sampled Data")
# 2. Plot the TRUE underlying distribution (The Ellipse)
# We draw the 2-sigma ellipse (approx 95% of data)
draw_confidence_ellipse(mu, Sigma, ax, n_std=2.0, edgecolor='black',
linestyle='--', label=r'True Dist. (2$\sigma$)')
# 3. Plot the Eigenvectors (Empirical)
colors = ['r', 'g']
for i in range(2):
std_dev = np.sqrt(eig_vals[i])
v = eig_vecs[:, i] * std_dev
ax.quiver(mean_emp[0], mean_emp[1], v[0], v[1],
color=colors[i], angles='xy', scale_units='xy', scale=1,
label=fr'PC {i+1} ($\sigma$)')
# Formatting
ax.axhline(0, color='grey', lw=0.5)
ax.axvline(0, color='grey', lw=0.5)
ax.set_aspect('equal')
ax.set_xlabel('$x_1$')
ax.set_ylabel('$x_2$')
ax.set_title('PCA and Underlying Gaussian Distribution')
ax.legend()
plt.grid(True, alpha=0.3)
plt.show()
The visualization summarizes the Eigen-decomposition of a Covariance Matrix:
The Takeaway
The plot shows that PCA has found a new coordinate system. Instead of using $x_1$ and $x_2$, we can describe the data more efficiently using the red and green axes. In dimensionality reduction, we would keep the red axis ($PC_1$) and discard the green one ($PC_2$) because it contains less variance.
To project the data onto the first Principal Component, we mathematically "collapse" every 2D point onto the line defined by the first eigenvector.
The Projection Step
If $\mathbf{q}_1$ is our first eigenvector (the red arrow), the 1D projection $z^{(k)}$ of a centered data point $\tilde{\mathbf{x}}^{(k)}$ is calculated via the dot product:
$$z^{(k)} = \tilde{\mathbf{x}}^{(k)} \cdot \mathbf{q}_1$$
This gives us a single coordinate representing the point's position along the axis of maximum variance.
# 1. Center the data (X is your original 100x2 array)
mean_emp = X.mean(axis=0)
X_centered = X - mean_emp
# 2. Get the first eigenvector (the direction of the red arrow)
# Ensure you use the eigenvector corresponding to the largest eigenvalue
v1 = eig_vecs[:, 0]
# 3. Project the 2D data onto the 1D line
# This results in 100 scalar values
z1 = X_centered @ v1
# 4. Reconstruct for visualization
# This maps the 1D values back into 2D space along the PC1 axis
X_reconstructed = np.outer(z1, v1) + mean_emp
# --- Visualization ---
plt.figure(figsize=(6, 6))
plt.scatter(X[:, 0], X[:, 1], c='b', marker='x', alpha=0.3, label="Original 2D Data")
plt.scatter(X_reconstructed[:, 0], X_reconstructed[:, 1], c='r', label="Projected 1D Data")
# Draw the axis line
plt.plot(X_reconstructed[:, 0], X_reconstructed[:, 1], color='r', alpha=0.2)
plt.gca().set_aspect('equal')
plt.legend()
plt.title("Projection onto the 1st Principal Component")
plt.show()
Why this matters
What is happening mathematically?
We are performing a change of basis.
The "magic" of PCA happens when we decide to ignore the eigenvectors with small eigenvalues.
If we have a 100-dimensional dataset, but the first 3 eigenvalues capture 95% of the total variance, we can "project" our data onto those 3 eigenvectors and discard the rest. We effectively simplify our "world" from 100 dimensions down to 3, while keeping almost all the important structural information.
A Scree Plot is a diagnostic tool used to visualize the importance of each Principal Component. It plots the eigenvalues (variance) against the component number.
In a tensor context, the scree plot tells us which "dimensions" of the Rank 2 tensor are physically significant and which are just noise. We look for the "elbow" of the plot—the point where the variance drops off sharply.
Python Code: Generating a Scree Plot This script calculates PCA on a synthetic dataset and visualizes how much "information" each eigenvalue captures.
import numpy as np
import matplotlib.pyplot as plt
# 1. Create synthetic data with 5 dimensions
# (Only 2 dimensions will have significant variance)
np.random.seed(42)
n_samples = 100
data = np.dot(np.random.randn(n_samples, 2), [[2, 0.5], [0.5, 1]]) # Main signal
noise = np.random.normal(0, 0.2, (n_samples, 3)) # Noise
X = np.hstack([data, noise])
# 2. PCA Steps
X_centered = X - np.mean(X, axis=0)
cov_matrix = np.cov(X_centered, rowvar=False)
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
# Sort eigenvalues in descending order
eigenvalues = eigenvalues[::-1]
total_variance = np.sum(eigenvalues)
explained_variance_ratio = eigenvalues / total_variance
# 3. Plotting the Scree Plot
plt.figure(figsize=(8, 5))
components = np.arange(1, len(eigenvalues) + 1)
# Bar plot for individual variance
plt.bar(components, explained_variance_ratio, alpha=0.7, color='blue', label='Individual Variance')
# Step plot for cumulative variance
plt.step(components, np.cumsum(explained_variance_ratio), where='mid', color='red', label='Cumulative Variance')
plt.xlabel('Principal Component Index')
plt.ylabel('Proportion of Explained Variance')
plt.title('Scree Plot: Finding the "Elbow"')
plt.xticks(components)
plt.legend()
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.show()
The Bars: Show how much of the "Total Variance" (the Trace) is captured by each eigenvector.
The Line: Shows the cumulative total. If the first two components reach 90%, it means you can describe the 5D dataset using only 2 dimensions with minimal information loss.
The "Elbow": The point where the curve flattens out. In a tensor interpretation, this is where we distinguish the Signal (the primary axes of the tensor) from the Noise (the tiny, insignificant fluctuations).
Dimensionality reduction is the ultimate goal of PCA. From a tensor perspective, we are identifying which dimensions of the space are physically meaningful (the signal) and which are just background fluctuations (the noise).
By looking at the Scree Plot, we can decide to keep only the top $k$ eigenvectors. We then form a projection matrix $\mathbf{Q}_k$ using these $k$ vectors. When we project our data:
$$\mathbf{\tilde X}_{reduced} = \mathbf{\tilde{X}} \mathbf{Q}_k$$
We are performing a passive rotation into a new coordinate system and then simply "ignoring" the axes that don't carry enough variance.
The Geometry of Truncation When we perform PCA, we aren't just "deleting" columns of data; we are choosing a new subspace that best represents the "energy" or "mass" of our data cloud.
Constructing the Projection Matrix ($\mathbf{Q}_k$)
After sorting our eigenvalues ($\lambda_1 \ge \lambda_2 \ge \dots \ge \lambda_n$), we select the first $k$ eigenvectors to form our projection matrix $\mathbf{Q}_k$. This matrix is an $n \times k$ operator.
The Projection Equation: $\mathbf{\tilde X}_{reduced} = \mathbf{\tilde X} \mathbf{Q}_k$
This multiplication is a Passive Transformation. For every data point (row) in $\mathbf{\tilde X}$:
By doing this, we are effectively looking at the data from the perspective of the "best" $k$ axes and ignoring the remaining $d-k$ axes.
Why it is "Truncating the Tensor"
Recall from eigen-decomposition that the sandwich product $\mathbf{C} = \mathbf{Q}\mathbf{\Lambda}\mathbf{Q}^T$ can be written equivalently as a sum of rank-1 projectors, one per principal axis. In this full form the covariance matrix (our Rank 2 tensor) is:
$$\mathbf{C} = \sum_{i=1}^{n} \lambda_i \mathbf{q}_i \mathbf{q}_i^T$$
When we reduce dimensions, we are approximating the tensor by cutting off the end of this sum, keeping only the $k$ terms with the largest eigenvalues:
$$\mathbf{C}_{approx} \approx \sum_{i=1}^{k} \lambda_i \mathbf{q}_i \mathbf{q}_i^T$$
We are claiming that the "physical relationship" described by the tensor is almost entirely contained within those first $k$ directions. The remaining variance (the sum of the ignored eigenvalues) is treated as isotropic noise—shaking that doesn't follow a meaningful pattern.
Summary of Information Loss
The reduction $\mathbf{\tilde X}_{reduced} = \mathbf{\tilde X}\,\mathbf{Q}_k$ maps each $n$-dimensional point to just $k$ coordinates. To compare it against the original we map it back into the full $n$-dimensional space using the same axes, giving the reconstruction
$$\mathbf{\tilde X}_{reconstructed} = \mathbf{\tilde X}_{reduced}\,\mathbf{Q}_k^T = \mathbf{\tilde X}\,\mathbf{Q}_k\mathbf{Q}_k^T.$$
Geometrically, $\mathbf{Q}_k\mathbf{Q}_k^T$ is the orthogonal projector onto the $k$-dimensional subspace: $\mathbf{\tilde X}_{reconstructed}$ is the shadow of each (centered) point on that subspace, still expressed in the original coordinates. (Adding back the mean, $\mathbf{\tilde X}_{reconstructed} + \bar{\mathbf{x}}^T$, returns it to the original, uncentered frame.)
The difference between $\mathbf{\tilde X}$ and $\mathbf{\tilde X}_{reconstructed}$ is the Reconstruction Error.
If the eigenvalues we discarded are near zero, the error is negligible. In tensor terms, we have successfully removed the "dimensions of least resistance," keeping only the axes where the data actually lives.
Measuring Information Retention
In the tensor perspective, the Trace of the covariance matrix represents the "Total Energy" or "Total Variance" of the system. Since the Trace is invariant under rotation, the sum of our eigenvalues must equal the total variance of the original data.When we reduce the dimensions from $n$ to $k$, we can calculate the percentage of information kept using this formula:$$\text{Explained Variance} = \frac{\sum_{i=1}^{k} \lambda_i}{\sum_{j=1}^{n} \lambda_j}$$
| Step in PCA | Tensor Interpretation |
|---|---|
| Calculate $\mathbf{C}$ | Defining the Rank 2 "relationship" between all variables. |
| Find Eigenvectors | Finding the "Natural Axes" (characteristic directions) of the data cloud. |
| Sort Eigenvalues | Ranking axes by how much "information" (variance) they carry. |
| Project Data | A Change of Basis (Passive Rotation) to the new coordinate system. |
| Dimension Reduction | Truncating the tensor to its most significant physical dimensions. |