Chapter 13: Change of basis¶
1. What Are Coordinates Really?¶
Standard Basis¶
When we write \(v = \begin{bmatrix} 3 \\ 2 \end{bmatrix}\), we mean:
The numbers [3, 2] are scalars that tell us:
How much to scale \(\hat{i}\) (rightward unit vector)
How much to scale \(\hat{j}\) (upward unit vector)
Alternative Basis¶
But what if someone uses different basis vectors?
Say Jennifer uses:
\(b_1 = \begin{bmatrix} 2 \\ 1 \end{bmatrix}\) (from our perspective)
\(b_2 = \begin{bmatrix} -1 \\ 1 \end{bmatrix}\) (from our perspective)
To her, these are just [1, 0] and [0, 1]!
The same physical vector has different coordinates in her system.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch
import seaborn as sns
sns.set_style("whitegrid")
plt.rcParams['figure.figsize'] = (12, 8)
def draw_vector(ax, origin, vector, color='blue', label=None, width=2.5):
"""Draw a 2D vector."""
arrow = FancyArrowPatch(
origin, origin + vector,
mutation_scale=20, lw=width, arrowstyle='-|>', color=color, label=label
)
ax.add_patch(arrow)
def draw_grid(ax, basis1, basis2, extent=3, color='gray', alpha=0.3, label_prefix=''):
"""Draw grid lines based on basis vectors."""
for i in range(-extent, extent+1):
# Lines parallel to basis1
start = i * basis2 - extent * basis1
end = i * basis2 + extent * basis1
ax.plot([start[0], end[0]], [start[1], end[1]], color=color, alpha=alpha, linewidth=0.5)
# Lines parallel to basis2
start = i * basis1 - extent * basis2
end = i * basis1 + extent * basis2
ax.plot([start[0], end[0]], [start[1], end[1]], color=color, alpha=alpha, linewidth=0.5)
def visualize_different_bases():
"""Show the same vector in different coordinate systems."""
# Our standard basis
i_hat = np.array([1, 0])
j_hat = np.array([0, 1])
# Jennifer's basis (in our coordinates)
b1 = np.array([2, 1])
b2 = np.array([-1, 1])
# A vector in our coordinates
v_our = np.array([3, 2])
# Same vector in Jennifer's coordinates: [5/3, 1/3]
# Meaning: v = (5/3)*b1 + (1/3)*b2
v_jennifer = np.array([5/3, 1/3])
# Verify
v_reconstructed = v_jennifer[0] * b1 + v_jennifer[1] * b2
fig, axes = plt.subplots(1, 2, figsize=(16, 7))
# Left: Our coordinate system
ax1 = axes[0]
ax1.set_xlim(-2, 5)
ax1.set_ylim(-2, 4)
ax1.set_aspect('equal')
ax1.grid(True, alpha=0.3)
ax1.axhline(y=0, color='k', linewidth=1)
ax1.axvline(x=0, color='k', linewidth=1)
# Our grid
draw_grid(ax1, i_hat, j_hat, extent=5, color='blue', alpha=0.2)
# Our basis vectors
draw_vector(ax1, np.array([0, 0]), i_hat, color='blue', label='î (our basis)', width=2)
draw_vector(ax1, np.array([0, 0]), j_hat, color='cyan', label='ĵ (our basis)', width=2)
# The vector
draw_vector(ax1, np.array([0, 0]), v_our, color='red', label='v = [3, 2]', width=3)
ax1.set_title('Our Coordinate System\nv = 3î + 2ĵ = [3, 2]', fontsize=14, fontweight='bold')
ax1.legend(fontsize=11)
ax1.set_xlabel('x')
ax1.set_ylabel('y')
# Right: Jennifer's coordinate system
ax2 = axes[1]
ax2.set_xlim(-2, 5)
ax2.set_ylim(-2, 4)
ax2.set_aspect('equal')
ax2.axhline(y=0, color='k', linewidth=1)
ax2.axvline(x=0, color='k', linewidth=1)
# Jennifer's grid
draw_grid(ax2, b1, b2, extent=3, color='green', alpha=0.2)
# Jennifer's basis vectors
draw_vector(ax2, np.array([0, 0]), b1, color='green', label='b₁ (Jennifer)', width=2)
draw_vector(ax2, np.array([0, 0]), b2, color='lime', label='b₂ (Jennifer)', width=2)
# Same physical vector
draw_vector(ax2, np.array([0, 0]), v_our, color='red',
label=f'v = (5/3)b₁ + (1/3)b₂ = [{v_jennifer[0]:.2f}, {v_jennifer[1]:.2f}]', width=3)
ax2.set_title('Jennifer\'s Coordinate System\nSame vector, different coordinates!',
fontsize=14, fontweight='bold')
ax2.legend(fontsize=11)
ax2.set_xlabel('x')
ax2.set_ylabel('y')
plt.tight_layout()
plt.show()
print("The Same Vector in Two Languages:")
print(f"\nOur coordinates: {v_our}")
print(f"Jennifer's coordinates: [{v_jennifer[0]:.3f}, {v_jennifer[1]:.3f}]")
print(f"\nVerification: {v_jennifer[0]:.3f}×{b1} + {v_jennifer[1]:.3f}×{b2} = {v_reconstructed}")
print(f"Matches our vector: {np.allclose(v_reconstructed, v_our)}")
visualize_different_bases()
2. Change of Basis Matrix¶
From Jennifer’s Language to Ours¶
To translate Jennifer’s coordinates to our coordinates, use a change of basis matrix:
Columns = Jennifer’s basis vectors in our coordinates
Then:
Example¶
def demonstrate_change_of_basis_matrix():
"""Show how change of basis matrix works."""
# Jennifer's basis vectors (in our coordinates)
b1 = np.array([2, 1])
b2 = np.array([-1, 1])
# Change of basis matrix
P = np.column_stack([b1, b2])
print("Change of Basis Matrix P:")
print(P)
print("\nColumns = Jennifer's basis vectors in our coordinates")
print(f"Column 1 = b₁ = {b1}")
print(f"Column 2 = b₂ = {b2}")
# Test: Jennifer says [-1, 2]
v_jennifer = np.array([-1, 2])
# Convert to our coordinates
v_our = P @ v_jennifer
print(f"\n{'='*50}")
print("Translation Example:")
print(f"Jennifer says: v = {v_jennifer}")
print(f"Meaning: v = {v_jennifer[0]}×b₁ + {v_jennifer[1]}×b₂")
print(f"\nApply P:")
print(f"v_our = P × v_Jennifer = {P} × {v_jennifer} = {v_our}")
print(f"\nIn our system: v = {v_our}")
# Verify manually
v_manual = v_jennifer[0] * b1 + v_jennifer[1] * b2
print(f"\nManual check: {v_jennifer[0]}×{b1} + {v_jennifer[1]}×{b2} = {v_manual}")
print(f"Matches: {np.allclose(v_our, v_manual)} ✓")
demonstrate_change_of_basis_matrix()
3. Going the Other Way: Inverse¶
From Our Language to Jennifer’s¶
To go from our coordinates to Jennifer’s, use the inverse:
Why?¶
\(P\) transforms: Jennifer → us
\(P^{-1}\) transforms: us → Jennifer
\(P^{-1}P = I\) (does nothing)
def demonstrate_inverse_transform():
"""Show using inverse to go from our coords to Jennifer's."""
# Change of basis matrix
b1 = np.array([2, 1])
b2 = np.array([-1, 1])
P = np.column_stack([b1, b2])
# Compute inverse
P_inv = np.linalg.inv(P)
print("Change of Basis Matrix P:")
print(P)
print("\nInverse P⁻¹:")
print(P_inv)
# Our vector
v_our = np.array([3, 2])
# Convert to Jennifer's coordinates
v_jennifer = P_inv @ v_our
print(f"\n{'='*50}")
print("Translation Example (reverse direction):")
print(f"We say: v = {v_our}")
print(f"\nApply P⁻¹:")
print(f"v_Jennifer = P⁻¹ × v_our = {v_jennifer}")
print(f"\nJennifer would say: v = [{v_jennifer[0]:.4f}, {v_jennifer[1]:.4f}]")
# Verify by going back
v_back = P @ v_jennifer
print(f"\nVerification: P × v_Jennifer = {v_back}")
print(f"Matches original: {np.allclose(v_our, v_back)} ✓")
# Check P⁻¹P = I
identity = P_inv @ P
print(f"\nP⁻¹ × P = \n{identity}")
print(f"Is identity? {np.allclose(identity, np.eye(2))} ✓")
demonstrate_inverse_transform()
4. Transformations in Different Bases¶
The Challenge¶
Suppose you have a transformation matrix \(A\) in our coordinate system.
What does this same transformation look like in Jennifer’s coordinate system?
The Formula¶
Why This Works¶
Start with Jennifer’s coordinates: \(v_J\)
Convert to our system: \(P v_J\)
Apply transformation \(A\): \(A(Pv_J)\)
Convert back to Jennifer’s system: \(P^{-1}(APv_J)\)
So the entire operation is: \(P^{-1}AP\)
This is called similarity transformation or conjugation.
def demonstrate_transformation_in_different_basis():
"""Show how transformations look in different coordinate systems."""
# A 90-degree rotation (in our coordinates)
A_our = np.array([[0, -1],
[1, 0]])
# Jennifer's basis
b1 = np.array([2, 1])
b2 = np.array([-1, 1])
P = np.column_stack([b1, b2])
P_inv = np.linalg.inv(P)
# Transform A to Jennifer's coordinate system
A_jennifer = P_inv @ A_our @ P
print("Transformation in Our System:")
print(f"A (90° rotation) = \n{A_our}\n")
print("Same Transformation in Jennifer's System:")
print(f"A_Jennifer = P⁻¹AP = \n{A_jennifer}\n")
# Test with a vector
v_our = np.array([1, 0])
v_jennifer = P_inv @ v_our
print(f"{'='*60}")
print("Test: Apply rotation to î = [1, 0]")
print(f"\nIn our system:")
print(f" v = {v_our}")
print(f" Av = {A_our @ v_our}")
print(f"\nIn Jennifer's system:")
print(f" v = {v_jennifer}")
print(f" A_Jennifer × v = {A_jennifer @ v_jennifer}")
# Verify they represent the same transformation
result_our = A_our @ v_our
result_jennifer = A_jennifer @ v_jennifer
result_back = P @ result_jennifer
print(f"\nVerification:")
print(f"Our result converted to Jennifer's: {P_inv @ result_our}")
print(f"Jennifer's result: {result_jennifer}")
print(f"Match: {np.allclose(P_inv @ result_our, result_jennifer)} ✓")
demonstrate_transformation_in_different_basis()
5. Why This Matters: Eigenvalue Problems¶
The Power of Changing Basis¶
Sometimes a transformation looks complicated in one basis but simple in another!
Eigenvalue decomposition finds a basis where the transformation is diagonal:
where \(D\) is diagonal (very simple!).
Example: Powers of Matrices¶
Computing \(A^{100}\) directly is hard, but:
\(D^{100}\) is easy - just raise diagonal entries to 100!
def demonstrate_diagonal_simplification():
"""Show power of diagonal basis."""
# A transformation (Fibonacci matrix)
A = np.array([[1, 1],
[1, 0]])
# Eigenvalue decomposition
eigenvalues, eigenvectors = np.linalg.eig(A)
P = eigenvectors
D = np.diag(eigenvalues)
P_inv = np.linalg.inv(P)
print("Original Matrix A (Fibonacci):")
print(A)
print("\nEigenvalue Decomposition: A = PDP⁻¹")
print(f"\nP (eigenvector basis):\n{P}")
print(f"\nD (diagonal!):\n{D}")
# Compute A^10 two ways
print(f"\n{'='*60}")
print("Computing A¹⁰:")
# Direct way (slow)
A_10_direct = np.linalg.matrix_power(A, 10)
# Smart way using diagonal
D_10 = np.diag(eigenvalues ** 10)
A_10_smart = P @ D_10 @ P_inv
print(f"\nDirect computation A¹⁰:\n{A_10_direct.astype(int)}")
print(f"\nUsing eigenvalues D¹⁰:\n{A_10_smart.real.astype(int)}")
print(f"\nMatch: {np.allclose(A_10_direct, A_10_smart.real)} ✓")
print(f"\nD¹⁰ = \n{D_10.real}")
print("\nJust raise each diagonal entry to power 10!")
print("This is why eigenvalues are so powerful.")
demonstrate_diagonal_simplification()
6. Summary and Key Takeaways¶
Main Ideas¶
Coordinates are not absolute
Depend on choice of basis vectors
Same vector, different numbers
Change of basis matrix
Columns = new basis vectors in old coordinates
\(P\): Jennifer → us
\(P^{-1}\): us → Jennifer
Transformations change too
\(A_{\text{new}} = P^{-1}AP\)
Similarity transformation
Why it matters
Find simpler representations
Diagonal matrices (eigenvalues)
Computational efficiency
Formulas¶
Applications¶
🔢 Eigenvalue decomposition: Find natural axes
📊 PCA: Change to principal component basis
🎮 Computer graphics: Object-local vs world coordinates
🔬 Quantum mechanics: Different measurement bases
🤖 Robotics: Joint vs Cartesian coordinates
The Big Picture¶
There’s no “correct” coordinate system - just different perspectives on the same mathematical reality. Choosing the right basis can:
Simplify calculations
Reveal structure
Enable efficient algorithms
This is one of the most powerful ideas in linear algebra!
7. Practice Exercises¶
Exercise 1: Change of Basis¶
Given basis vectors \(b_1 = [1, 1]\) and \(b_2 = [-1, 1]\):
a) What is the change of basis matrix \(P\)?
b) Convert \(v = [4, 2]\) (our coords) to the new basis
c) Verify by reconstructing \(v\) from new coordinates
# Your code here
b1 = np.array([1, 1])
b2 = np.array([-1, 1])
P = np.column_stack([b1, b2])
print(f"a) Change of basis matrix P:\n{P}\n")
v_old = np.array([4, 2])
P_inv = np.linalg.inv(P)
v_new = P_inv @ v_old
print(f"b) v_new = P⁻¹ × v_old = {v_new}\n")
v_reconstructed = P @ v_new
print(f"c) Reconstruct: P × v_new = {v_reconstructed}")
print(f"Matches original: {np.allclose(v_old, v_reconstructed)} ✓")
Exercise 2: Transform a Transformation¶
The matrix \(A = \begin{bmatrix} 2 & 0 \\ 0 & 3 \end{bmatrix}\) represents scaling in the standard basis.
What does it look like in the basis \(b_1 = [1, 1], b_2 = [1, -1]\)?
# Your code here
A_old = np.array([[2, 0],
[0, 3]])
b1 = np.array([1, 1])
b2 = np.array([1, -1])
P = np.column_stack([b1, b2])
P_inv = np.linalg.inv(P)
A_new = P_inv @ A_old @ P
print(f"A in standard basis:\n{A_old}\n")
print(f"A in new basis:\n{A_new}")