Chapter 4: Matrix multiplication as compositionยถ
1. The Geometric Meaningยถ
Key Questionยถ
When a transformation is applied, by what factor does it scale areas?
The Determinantยถ
For a matrix \(\begin{bmatrix} a & b \\ c & d \end{bmatrix}\), the determinant is: $\(\text{det}\begin{pmatrix}\begin{bmatrix} a & b \\ c & d \end{bmatrix}\end{pmatrix} = ad - bc\)$
Interpretationยถ
det = 2: Areas are doubled
det = 0.5: Areas are halved
det = 1: Areas preserved (rotation, reflection)
det = 0: Space is squished to lower dimension
det < 0: Orientation is flipped
Exampleยถ
A unit square (area = 1) transforms to a parallelogram. The determinant = area of that parallelogram!
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, Polygon, Circle
from matplotlib.collections import PatchCollection
import seaborn as sns
sns.set_style('whitegrid')
plt.rcParams['figure.figsize'] = (12, 10)
np.set_printoptions(precision=3, suppress=True)
def draw_grid(ax, transform=None, color='lightgray', alpha=0.3):
"""Draw coordinate grid"""
for x in range(-3, 4):
points = np.array([[x, y] for y in np.linspace(-3, 3, 50)])
if transform is not None:
points = points @ transform.T
ax.plot(points[:, 0], points[:, 1], color=color, alpha=alpha, linewidth=0.5)
for y in range(-3, 4):
points = np.array([[x, y] for x in np.linspace(-3, 3, 50)])
if transform is not None:
points = points @ transform.T
ax.plot(points[:, 0], points[:, 1], color=color, alpha=alpha, linewidth=0.5)
def draw_unit_square(ax, transform=None, **kwargs):
"""Draw unit square"""
square = np.array([[0, 0], [1, 0], [1, 1], [0, 1]])
if transform is not None:
square = square @ transform.T
patch = Polygon(square, closed=True, **kwargs)
ax.add_patch(patch)
return square
def setup_ax(ax, xlim=(-3, 3), ylim=(-3, 3), title=''):
ax.set_xlim(xlim)
ax.set_ylim(ylim)
ax.set_aspect('equal')
ax.axhline(y=0, color='k', linewidth=0.8)
ax.axvline(x=0, color='k', linewidth=0.8)
ax.grid(True, alpha=0.3)
if title:
ax.set_title(title, fontsize=14, fontweight='bold')
# Examples with different determinants
fig, axes = plt.subplots(2, 3, figsize=(18, 12))
transformations = [
('Stretch (det = 6)', np.array([[3, 0], [0, 2]])),
('Shear (det = 1)', np.array([[1, 1], [0, 1]])),
('Shrink (det = 0.25)', np.array([[0.5, 0], [0, 0.5]])),
('Rotation (det = 1)', np.array([[0, -1], [1, 0]])),
('Reflection (det = -1)', np.array([[-1, 0], [0, 1]])),
('Projection (det = 0)', np.array([[1, 0], [0, 0]]))
]
for idx, (name, matrix) in enumerate(transformations):
ax = axes[idx // 3, idx % 3]
det = np.linalg.det(matrix)
setup_ax(ax, xlim=(-2, 4), ylim=(-2, 4), title=name)
# Original unit square (light)
draw_unit_square(ax, fill=False, edgecolor='blue',
linestyle='--', linewidth=2, alpha=0.5)
ax.text(0.5, 0.5, '1', fontsize=16, ha='center', va='center',
color='blue', alpha=0.5, fontweight='bold')
# Transformed square
transformed_square = draw_unit_square(ax, matrix, fill=True,
facecolor='orange',
edgecolor='red',
alpha=0.6, linewidth=2)
# Calculate center and area
center = transformed_square.mean(axis=0)
area = abs(det)
# Show area
ax.text(center[0], center[1], f'{area:.2f}',
fontsize=16, ha='center', va='center',
color='darkred', fontweight='bold')
# Show determinant
det_text = f'det = {det:.2f}'
color = 'green' if det > 0 else ('red' if det < 0 else 'purple')
ax.text(0.98, 0.02, det_text, transform=ax.transAxes,
fontsize=13, ha='right', va='bottom',
bbox=dict(boxstyle='round', facecolor=color, alpha=0.3),
fontweight='bold')
plt.tight_layout()
plt.show()
print("Key Insight: The determinant tells you how much areas change!")
print("\nโข det > 1: Areas expand")
print("โข det = 1: Areas preserved (rotation/reflection)")
print("โข 0 < det < 1: Areas shrink")
print("โข det = 0: Squished to lower dimension")
print("โข det < 0: Orientation flipped")
2. Negative Determinants and Orientationยถ
What Does Negative Mean?ยถ
A negative determinant means the transformation flips orientation.
Orientation Testยถ
Imagine \(\hat{i}\) as your right hand and \(\hat{j}\) as your left hand:
det > 0: \(\hat{i}\) is still to the right of \(\hat{j}\) (same orientation)
det < 0: \(\hat{i}\) is now to the left of \(\hat{j}\) (flipped orientation)
The Signยถ
Magnitude: How much area scales
Sign: Whether orientation flips
# Visualize orientation flipping
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
def draw_basis_with_labels(ax, matrix, title):
setup_ax(ax, xlim=(-2, 3), ylim=(-2, 3), title=title)
# Original basis (faint)
i_orig = np.array([1, 0])
j_orig = np.array([0, 1])
ax.arrow(0, 0, i_orig[0], i_orig[1], head_width=0.1,
fc='lightgreen', ec='lightgreen', alpha=0.3, width=0.03)
ax.arrow(0, 0, j_orig[0], j_orig[1], head_width=0.1,
fc='lightcoral', ec='lightcoral', alpha=0.3, width=0.03)
# Transformed basis
i_new = matrix @ i_orig
j_new = matrix @ j_orig
ax.arrow(0, 0, i_new[0], i_new[1], head_width=0.15, head_length=0.15,
fc='green', ec='green', width=0.05, label='รฎ')
ax.arrow(0, 0, j_new[0], j_new[1], head_width=0.15, head_length=0.15,
fc='red', ec='red', width=0.05, label='ฤต')
# Label them
ax.text(i_new[0]*1.2, i_new[1]*1.2, 'รฎ', fontsize=18,
color='green', fontweight='bold')
ax.text(j_new[0]*1.2, j_new[1]*1.2, 'ฤต', fontsize=18,
color='red', fontweight='bold')
# Determinant
det = np.linalg.det(matrix)
sign_text = "Same orientation" if det > 0 else "FLIPPED orientation"
color = 'green' if det > 0 else 'red'
ax.text(0.5, 0.95, f'det = {det:.1f}', transform=ax.transAxes,
fontsize=14, ha='center', va='top', fontweight='bold',
bbox=dict(boxstyle='round', facecolor=color, alpha=0.3))
ax.text(0.5, 0.05, sign_text, transform=ax.transAxes,
fontsize=12, ha='center', va='bottom',
bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.5))
# Draw transformed unit square
draw_unit_square(ax, matrix, fill=True, facecolor=color,
edgecolor='black', alpha=0.2, linewidth=1.5)
# Positive determinant
draw_basis_with_labels(axes[0], np.array([[2, 1], [0, 1]]),
'Positive det: No flip')
# Negative determinant (reflection)
draw_basis_with_labels(axes[1], np.array([[-1, 0], [0, 1]]),
'Negative det: Flipped!')
# Negative determinant (different)
draw_basis_with_labels(axes[2], np.array([[1, 2], [2, 1]]),
'Negative det: Flipped!')
plt.tight_layout()
plt.show()
print("Orientation Check:")
print("Stand at the origin. Point right hand along รฎ, left hand along ฤต.")
print("โข If det > 0: Hands are still in the right/left configuration")
print("โข If det < 0: Your hands have crossed! Orientation flipped.")
3. Zero Determinant = Squishing Dimensionsยถ
Critical Conceptยถ
Geometric Meaningยถ
det = 0: The transformation squishes all of space onto a line (or point)
All 2D area becomes 0
The transformation is not invertible
Why?ยถ
If columns are linearly dependent, they point in the same direction (or one is zero). The parallelogram they form has zero area!
# Demonstrate zero determinant
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
# Case 1: Collinear columns
M1 = np.array([[2, 4], [1, 2]]) # Second column is 2ร first
setup_ax(axes[0], title='Collinear Columns\n(det = 0)')
draw_grid(axes[0], M1, color='orange', alpha=0.5)
i_hat = M1[:, 0]
j_hat = M1[:, 1]
axes[0].arrow(0, 0, i_hat[0], i_hat[1], head_width=0.2,
fc='green', ec='green', width=0.08, alpha=0.7)
axes[0].arrow(0, 0, j_hat[0], j_hat[1], head_width=0.2,
fc='red', ec='red', width=0.08, alpha=0.7)
axes[0].text(0.5, 0.9, f'det = {np.linalg.det(M1):.3f}',
transform=axes[0].transAxes, fontsize=14,
bbox=dict(boxstyle='round', facecolor='red', alpha=0.3))
axes[0].text(0.5, 0.1, 'ฤต = 2รฎ\n(on same line!)',
transform=axes[0].transAxes, fontsize=11, ha='center',
bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.5))
# Case 2: Projection onto x-axis
M2 = np.array([[1, 2], [0, 0]]) # Squishes to x-axis
setup_ax(axes[1], title='Projection to Line\n(det = 0)')
# Show many points collapsing to a line
n_points = 100
original_points = np.random.randn(2, n_points) * 1.5
transformed_points = M2 @ original_points
axes[1].scatter(original_points[0], original_points[1],
c='blue', alpha=0.3, s=20, label='Before')
axes[1].scatter(transformed_points[0], transformed_points[1],
c='red', alpha=0.6, s=30, label='After', marker='x')
# Draw the line everything collapses to
x_line = np.linspace(-4, 4, 100)
axes[1].plot(x_line, np.zeros_like(x_line), 'r-', linewidth=3,
alpha=0.5, label='All 2D โ this line')
axes[1].legend(loc='upper right')
axes[1].text(0.5, 0.9, f'det = {np.linalg.det(M2):.3f}',
transform=axes[1].transAxes, fontsize=14,
bbox=dict(boxstyle='round', facecolor='red', alpha=0.3))
# Case 3: Zero column
M3 = np.array([[1, 0], [0, 0]]) # One column is zero
setup_ax(axes[2], title='Zero Column\n(det = 0)')
draw_grid(axes[2], M3, color='purple', alpha=0.5)
axes[2].arrow(0, 0, 1, 0, head_width=0.2,
fc='green', ec='green', width=0.08, alpha=0.7)
axes[2].plot(0, 0, 'ro', markersize=15, label='ฤต = [0, 0]')
axes[2].text(0.5, 0.9, f'det = {np.linalg.det(M3):.3f}',
transform=axes[2].transAxes, fontsize=14,
bbox=dict(boxstyle='round', facecolor='red', alpha=0.3))
axes[2].legend()
plt.tight_layout()
plt.show()
print("Zero Determinant Means:")
print("โข Columns are linearly dependent")
print("โข Transformation squishes space to lower dimension")
print("โข Matrix is NOT invertible (no way to undo it)")
print("โข You lose information!")
4. Computing Determinantsยถ
2ร2 Formulaยถ
Why This Formula?ยถ
The parallelogram formed by \(\begin{bmatrix} a \\ c \end{bmatrix}\) and \(\begin{bmatrix} b \\ d \end{bmatrix}\) has area \(|ad - bc|\).
Geometric Derivationยถ
Parallelogram area = base ร height
Can also compute as: (bounding box) - (triangular corners)
Works out to \(ad - bc\)!
3ร3 and Higherยถ
Use cofactor expansion or row reduction (covered in advanced notebooks)
# Visualize the 2x2 formula geometrically
fig, axes = plt.subplots(1, 2, figsize=(16, 7))
# Example matrix
a, b, c, d = 3, 1, 1, 2
M = np.array([[a, c], [b, d]])
# Left: Show the parallelogram
setup_ax(axes[0], xlim=(-0.5, 4), ylim=(-0.5, 3),
title=f'Parallelogram Area\nMatrix = [[{a}, {c}], [{b}, {d}]]')
# Draw parallelogram
parallelogram = np.array([[0, 0], [a, b], [a+c, b+d], [c, d]])
patch = Polygon(parallelogram, closed=True, fill=True,
facecolor='cyan', edgecolor='blue',
alpha=0.5, linewidth=2)
axes[0].add_patch(patch)
# Draw vectors
axes[0].arrow(0, 0, a, b, head_width=0.15, head_length=0.15,
fc='green', ec='green', width=0.06, label=f'รฎ โ [{a}, {b}]')
axes[0].arrow(0, 0, c, d, head_width=0.15, head_length=0.15,
fc='red', ec='red', width=0.06, label=f'ฤต โ [{c}, {d}]')
# Show the formula
det = a*d - b*c
axes[0].text(0.5, 0.95, f'Area = ad - bc\n= {a}ร{d} - {b}ร{c}\n= {det}',
transform=axes[0].transAxes, fontsize=13, ha='center', va='top',
bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.7),
family='monospace')
axes[0].legend(loc='lower right')
# Right: Show the bounding box decomposition
setup_ax(axes[1], xlim=(-0.5, 4), ylim=(-0.5, 3),
title='Bounding Box Method')
# Draw bounding box
box = plt.Rectangle((0, 0), a+c, b+d, fill=False,
edgecolor='gray', linestyle='--', linewidth=2)
axes[1].add_patch(box)
# Draw parallelogram
patch2 = Polygon(parallelogram, closed=True, fill=True,
facecolor='cyan', edgecolor='blue',
alpha=0.5, linewidth=2)
axes[1].add_patch(patch2)
# Label corners that get subtracted
# Top-left triangle
tri1 = np.array([[0, b+d], [c, d], [c, b+d]])
axes[1].add_patch(Polygon(tri1, fill=True, facecolor='red', alpha=0.3))
# Bottom-right triangle
tri2 = np.array([[a, 0], [a+c, 0], [a, b]])
axes[1].add_patch(Polygon(tri2, fill=True, facecolor='red', alpha=0.3))
axes[1].text(0.5, 0.95,
f'Box area = (a+c)(b+d) = {(a+c)*(b+d)}\n'
f'Subtract corners: {b*c} + {b*c}\n'
f'Parallelogram = {(a+c)*(b+d)} - 2ร{b*c}\n'
f' = {(a+c)*(b+d) - 2*b*c} = ad - bc',
transform=axes[1].transAxes, fontsize=11, ha='center', va='top',
bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7),
family='monospace')
plt.tight_layout()
plt.show()
print("\nDeterminant Formula (2ร2):")
print(f"det([[{a}, {c}], [{b}, {d}]]) = {a}ร{d} - {b}ร{c} = {det}")
print(f"\nThis equals the AREA of the parallelogram!")
5. Determinant of Matrix Productsยถ
Beautiful Propertyยถ
Intuitionยถ
If transformation \(B\) scales areas by factor 3, and transformation \(A\) scales areas by factor 2, then doing both scales areas by \(3 \times 2 = 6\).
Why It Mattersยถ
Makes it easy to compute determinants of compositions
Connects to eigenvalues
Fundamental for many proofs
# Demonstrate det(AB) = det(A) ร det(B)
fig, axes = plt.subplots(1, 4, figsize=(20, 5))
# Two transformations
A = np.array([[2, 0], [0, 1.5]]) # Stretch
B = np.array([[1, 0.5], [0, 1]]) # Shear
det_A = np.linalg.det(A)
det_B = np.linalg.det(B)
det_AB = np.linalg.det(A @ B)
# Step 0: Original
setup_ax(axes[0], title='Original\n(area = 1)')
draw_unit_square(axes[0], fill=True, facecolor='lightblue',
edgecolor='blue', alpha=0.6, linewidth=2)
axes[0].text(0.5, 0.5, '1', fontsize=20, ha='center', va='center', fontweight='bold')
# Step 1: After B
setup_ax(axes[1], title=f'After B\n(area = {det_B:.2f})')
draw_unit_square(axes[1], B, fill=True, facecolor='lightgreen',
edgecolor='green', alpha=0.6, linewidth=2)
square_B = draw_unit_square(axes[1], B, fill=False, edgecolor='black', linewidth=0)
center_B = square_B.mean(axis=0)
axes[1].text(center_B[0], center_B[1], f'{det_B:.2f}',
fontsize=18, ha='center', va='center', fontweight='bold')
# Step 2: After A(B(ยท))
setup_ax(axes[2], title=f'After A, then B\n(area = {det_AB:.2f})')
draw_unit_square(axes[2], A @ B, fill=True, facecolor='lightyellow',
edgecolor='orange', alpha=0.6, linewidth=2)
square_AB = draw_unit_square(axes[2], A @ B, fill=False, edgecolor='black', linewidth=0)
center_AB = square_AB.mean(axis=0)
axes[2].text(center_AB[0], center_AB[1], f'{det_AB:.2f}',
fontsize=18, ha='center', va='center', fontweight='bold')
# Step 3: Verify the formula
axes[3].axis('off')
axes[3].text(0.5, 0.6,
'Determinant Product Rule\n\n'
f'det(A) = {det_A:.2f}\n'
f'det(B) = {det_B:.2f}\n'
f'det(A) ร det(B) = {det_A * det_B:.2f}\n\n'
f'det(AB) = {det_AB:.2f}\n\n'
'โ They match!',
transform=axes[3].transAxes, fontsize=14, ha='center', va='center',
bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.8),
family='monospace', fontweight='bold')
plt.tight_layout()
plt.show()
# Numerical verification
print("Verification:")
print(f"det(A) = {det_A}")
print(f"det(B) = {det_B}")
print(f"det(A) ร det(B) = {det_A * det_B}")
print(f"\ndet(AB) = {det_AB}")
print(f"\nDifference: {abs(det_AB - det_A * det_B):.10f}")
print("\nโ The formula works!")
6. Applications and Intuitionยถ
When to Use Determinantsยถ
Check invertibility: \(\det(A) \neq 0 \iff A\) is invertible
Compute areas/volumes: How much does transformation scale space?
Solve linear systems: Cramerโs rule (though not recommended for large systems)
Eigenvalues: Characteristic equation uses determinant
Change of variables: In integrals (Jacobian determinant)
Quick Testsยถ
\(\det(I) = 1\) (identity preserves area)
\(\det(A^T) = \det(A)\) (transpose doesnโt change determinant)
\(\det(A^{-1}) = 1/\det(A)\) (inverse scales by reciprocal)
\(\det(cA) = c^n \det(A)\) for \(n \times n\) matrix
# Practical examples
print("Determinant Applications\n" + "="*50)
# Example 1: Testing invertibility
matrices = {
'Invertible': np.array([[2, 1], [1, 3]]),
'Singular (not invertible)': np.array([[2, 4], [1, 2]]),
'Nearly singular': np.array([[1, 1], [1, 1.0001]])
}
print("\n1. Testing Invertibility:")
for name, M in matrices.items():
det = np.linalg.det(M)
invertible = "YES" if abs(det) > 1e-10 else "NO"
print(f" {name:30s}: det = {det:10.6f} โ Invertible? {invertible}")
# Example 2: Area scaling
print("\n2. Area Scaling:")
transformations = {
'Double in both directions': np.array([[2, 0], [0, 2]]),
'Rotation 45ยฐ': np.array([[np.cos(np.pi/4), -np.sin(np.pi/4)],
[np.sin(np.pi/4), np.cos(np.pi/4)]]),
'Shear': np.array([[1, 2], [0, 1]])
}
for name, M in transformations.items():
det = np.linalg.det(M)
print(f" {name:30s}: Areas scale by {det:.3f}")
# Example 3: Product rule
print("\n3. Product Rule:")
A = np.random.randn(3, 3)
B = np.random.randn(3, 3)
C = A @ B
det_A = np.linalg.det(A)
det_B = np.linalg.det(B)
det_C = np.linalg.det(C)
det_product = det_A * det_B
print(f" det(A) = {det_A:.6f}")
print(f" det(B) = {det_B:.6f}")
print(f" det(A) ร det(B) = {det_product:.6f}")
print(f" det(AB) = {det_C:.6f}")
print(f" Difference: {abs(det_C - det_product):.10f}")
# Example 4: Special matrices
print("\n4. Special Matrices:")
I = np.eye(3)
print(f" det(Identity) = {np.linalg.det(I)}")
M = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(f" det(Rank-deficient) = {np.linalg.det(M):.10f} (should be 0)")
theta = np.pi/3
rotation = np.array([[np.cos(theta), -np.sin(theta)],
[np.sin(theta), np.cos(theta)]])
print(f" det(Rotation) = {np.linalg.det(rotation):.10f} (should be 1)")
Summaryยถ
The Essence of Determinantsยถ
Geometric meaning: How much a transformation scales areas/volumes
Sign: Whether orientation is flipped
Zero determinant: Dimensions are squished (not invertible)
Product rule: \(\det(AB) = \det(A) \cdot \det(B)\)
Mental Pictureยถ
When you see \(\det(A)\), think:
โBy what factor does transformation \(A\) scale areas?โ
\(|\det(A)| = 3\): Areas triple
\(\det(A) = 1\): Areas preserved (rotation)
\(\det(A) = 0\): Squished to lower dimension
\(\det(A) < 0\): Orientation flipped
Key Factsยถ
\(\det(I) = 1\)
\(\det(A^T) = \det(A)\)
\(\det(A^{-1}) = 1/\det(A)\)
\(\det(AB) = \det(A)\det(B)\)
\(A\) invertible \(\iff\) \(\det(A) \neq 0\)
Next Stepsยถ
Understanding determinants is crucial for:
Inverse matrices (next topic!)
Eigenvalues (characteristic equation)
Cross products (in 3D)
Change of variables in calculus
Exercisesยถ
Compute \(\det\begin{pmatrix}\begin{bmatrix} 3 & 1 \\ 2 & 4 \end{bmatrix}\end{pmatrix}\) and interpret geometrically
Show that det(rotation matrix) = 1 for any angle
Find a matrix with det = -5
Prove that if two rows of a matrix are identical, det = 0
Verify \(\det(AB) = \det(A)\det(B)\) for random 3ร3 matrices