Exploring Linear Transformations with Python
Every undergraduate math student knows that matrices are linear transformations. That’s all well and good, but what is a transformation, and what does it mean for one to be linear?
Essentially, a transformation is a function that takes in stuff and transforms it into new stuff
More specifically, a transformation is a function . That is, it’s a function that maps a set onto itself. For example, when you multiply a matrix by a vector, you get another vector.
Here’s an example. Note that in Python, the @
operator means matrix multiplication, where the *
operator means elementwise (Hadamard) product.
In [1]: import numpy as np
In [2]: A = np.matrix([[2, 1], [1, 1]])
In [3]: v = np.array([3, 4]) # A row vector
In [4]: A @ v.T # we right multiply by column vectors
Out[4]:
matrix([[2],
[7]])
In [5]: v @ A # we left multiply by row vectors
Out[5]:
matrix([[10, 1]])
Now what about a linear transformation? In a word, a linear transformation , where and are vector spaces, is a transformation that preserves the vector space operations of addition and scalar multiplications. If you want more theory than that, go pick up a linear algebra textbook. It’ll explain things quite a bit better than I can.
Otherwise, a good way to think about linear transformations is that things that started parallel end up parallel, and things that started perpendicular end up perpendicular. These are harder to visualize in higher dimensions, but here we’ll be sticking with 2D.
There are several different kinds of 2D linear transformations that we can describe using matrices.

Shear transformations

Scale transformations

Reflection transformations

Projection transformations
 Rotation transformations
 And others!
Each of these transformations can be fully represented as a matrix. Further, every (square) matrix encodes a linear transformation. Interestingly, if you want to apply two transformations, you can multiply the transformation matrices, and the result will be the same as applying one transformation and then the other. Note that this process is not commutative.
While the above pictures are nice, I wanted to animate the transformation to get a more intuitive feel for how they work. My Python code to do so is heavily influenced by @dododas’s work here.
Here’s a function to animate a transformation:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import animation, rc
# Display matplotlib animations as HTML5 videos in a Jupyter Notebook
rc('animation', html='html5')
def animate_transform(A, grid=None, num_steps=50, repeat=False):
"""
Animates the effect a transform has on a given grid.
If no grid is given, one will be generated.
You can expect a small delay while the function generates the animation.
"""
def colorizer(x, y):
"""Map xy coordinates to a unique rgb color"""
r = min(1, 1  y/3)
g = min(1, 1 + y/3)
b = 1/4 + x/16
return r, g, b
def stepwise_transform(A, grid, num_steps):
"""
Returns a list of transformed grids,
stepping slowly from the given `grid` to
the grid `A @ grid`.
"""
# create empty array of the right size
transgrid = np.zeros((num_steps + 1, ) + np.shape(grid))
# compute intermediate transforms
for i in range(num_steps + 1):
intermediate = np.eye(2) + i / num_steps * (A  np.eye(2))
# apply intermediate matrix transformation
transgrid[i] = intermediate @ grid
return transgrid
if grid is None:
# Create a grid of points in xy space
xvals = np.linspace(4, 4, 9)
yvals = np.linspace(3, 3, 7)
grid = np.column_stack([[x, y] for x in xvals for y in yvals])
# Map grid coordinates to colors. Done only for xygrid, not all grids.
colors = list(map(colorizer, grid[0], grid[1]))
intermediate_transforms = stepwise_transform(A, grid, num_steps)
fig = plt.figure(figsize=(6, 6))
xmin = min(min(grid[0]), min(intermediate_transforms[1][0]))
xmax = max(max(grid[0]), max(intermediate_transforms[1][0]))
ymin = min(min(grid[1]), min(intermediate_transforms[1][1]))
ymax = max(max(grid[1]), max(intermediate_transforms[1][1]))
ax = plt.axes(xlim=(xmin, xmax), ylim=(ymin, ymax))
scatter = ax.scatter([], [], c=colors)
# Prevent `%matplotlib inline` from displaying the above scatter plot.
plt.close()
def update(i):
"""Draws the ith intermediate transform"""
scatter.set_offsets(intermediate_transforms[i].T)
return scatter,
return animation.FuncAnimation(fig,
update,
interval=50,
frames=num_steps,
blit=True,
repeat=repeat)
Note that this code is meant to be ran in a Jupyter Notebook.
Here are some examples.

A shear transform:
A = np.column_stack([[1, 0], [2, 1]]) anim = animate_transform(A, repeat=True) anim.save('shear.mp4') anim

A projection onto the axis:
A = np.column_stack([[1, 0], [0, 0]]) anim = animate_transform(A, repeat=True) anim.save('projection.mp4') anim

A rotation by radians:
A rotation matrix , that rotates space by radians is given by
theta = np.pi / 6 # 30 degree counterclockwise rotation A = np.column_stack([[np.cos(theta), np.sin(theta)], [np.sin(theta), np.cos(theta)]]) anim = animate_transform(A, repeat=True) anim.save('rotation.mp4') anim
 A permutation matrix:
A = np.column_stack([[0, 1], [1, 0]]) anim = animate_transform(A, repeat=True) anim.save('permutation.mp4') anim
Note that this is different than a matrix transpose. Interestingly, you cannot represent the transpose as a matrix multiplication.
 A scale matrix:
A = np.column_stack([[2, 0], [0, 5]]) anim = animate_transform(A, repeat=True) anim.save('scale.mp4') anim
Now we can visualize combining different transformations!
theta = np.pi / 6 # 30 degree counterclockwise rotation
A = np.column_stack([[np.cos(theta), np.sin(theta)], [np.sin(theta), np.cos(theta)]])
B = np.column_stack([[2, 0], [0, 1]]) # A scale by 2 units in the x direction
anim = animate_transform(A @ B, repeat=True)
anim.save('combination.mp4')
anim
The notebook all of this is taken from can be found here