A random walk animation with matplotlib
I have met with but one or two persons in the course of my life who understood the art of Walking
- Henry David Thoreau, Walking
If you start at a light pole, spin to face a random direction, and walk 1 meter. Then spin to face a new direction and walk 1 meter. Then you repeat that forever. What does that look like? Where do you end up? Back at the pole? If not, how far away are you?
This is the question of what a random walk looks like. We won't answer all of these questions, but let's look at how we can generate a random walk animation with matplotlib.
from typing import Tuple
import numpy as np
from numpy import random
from matplotlib import animation, rc
import matplotlib.pyplot as plt
First, a little magic for embedding matplotlib animations in a notebook. Thanks to this post for helping me figure this out.
rc('animation', html='html5')
We can start with a function that returns a random vector with unit length:
def random_unit_vector() -> Tuple[np.float, np.float]:
"""Gets a random point on the unit circle"""
angle = random.random() * 2 * np.pi
return np.cos(angle), np.sin(angle)
To create a random walk with $N$ steps, we simply start at $(0, 0)$,
add a random_unit_vector
, and continue to add a random_unit_vector
to the current position for $N$ iterations.
The get_random_walk
function does exactly that and returns all steps
from the random walk in a $2xN$ matrix. The values in row $0$ contain the $x$
coordinates and the values in row $1$ contain the $y$ coordinates.
def get_random_walk(num_steps: int) -> np.ndarray:
locations = np.zeros((2, num_steps))
for i in range(1, num_steps):
next_x, next_y = random_unit_vector()
locations[0, i] = locations[0, i-1] + next_x
locations[1, i] = locations[1, i-1] + next_y
return locations
Our plot will contain four elements:
- A bold red dot at the current step
- Faded red dots at all previous steps
- Text displaying the $(x, y)$ coordinates of the current step
- Text displaying the current distance from the origin
%%capture
fig = plt.figure()
ax = fig.add_subplot(
aspect="equal",
xlim=(-10, 10),
ylim=(-10, 10)
)
previous_steps, = plt.plot([], [], 'ro', alpha=0.2)
current_step, = plt.plot([], [], 'ro', alpha=1.0)
loc_text = ax.text(0.02, 0.95, "", transform=ax.transAxes)
distance_text = ax.text(0.02, 0.90, "", transform=ax.transAxes)
The update_plot
function is called at each step of the
animation and will return the four elements above. We start by generating
all of the points in our random walk. We'll only generate 50 steps to
keep the animation short, but we easily could have generated a lot more.
The FuncAnimation
object repeatedly calls update_plot
with the
current frame number. The frame number starts at $0$ and counts up to
num_steps
. At each step, we'll use the frame number to index into
the points generated by get_random_walk
.
num_steps = 50
all_steps = get_random_walk(num_steps)
def update_plot(step_num: int):
loc = all_steps[..., step_num]
previous_steps.set_data(all_steps[..., :step_num])
current_step.set_data(loc)
loc_text.set_text(
f"Location = ({loc[0]:0.2f}, {loc[1]:0.2f})"
)
distance_text.set_text(
f"Distance from origin = {np.linalg.norm(loc):0.2f}"
)
return previous_steps, current_step, loc_text, distance_text
The last step is to create the FuncAnimation
object which takes:
- The
plt.figure()
object - The function that updates the plot at each frame
- The number of frames to run the animation1
- The number of milliseconds to pause between frames
walk = animation.FuncAnimation(
fig,
update_plot,
num_steps,
interval=500,
)
walk
Notes¶
- The
frames
argument toFuncAnimation
confused me at first because I had seen it used in a few different ways and I hadn't read the documentation closely. That argument is meant to be a generator which yields values that get passed to theupdate_plot
function. When an integer is passed (like we have done), it's converted intorange(num_steps)
. OurFuncAnimation
object could have been created with:
walk = animation.FuncAnimation(
fig,
update_plot,
range(num_steps),
interval=500,
)
We also could have turned get_random_walk
into a generator
that yields a new step each time it's called. Rather than indexing
into previously generated data, the update_plot
function would
take a new step location and plot it along with the steps generated in
previous frames.