Structured Arrays & Random Walks
What you’ll learn
Section titled “What you’ll learn”- Build a structured array — an ndarray whose elements are records with named fields
- Access and nest structured fields, and mix them with regular NumPy math
- Turn a step-by-step Python loop into one vectorized random walk expression
- Simulate thousands of random walks at once and measure “first crossing time”
Structured arrays: rows with named fields
Section titled “Structured arrays: rows with named fields”A regular ndarray is homogeneous — every element is the same plain type (all int64, all float64, and so on). A structured array relaxes that: each element is a small record made of several named fields, possibly of different types, while the whole thing still lives in one contiguous, fast NumPy array.
Think of it as a lightweight table — closer to a list of dicts than a plain array, but still backed by NumPy’s fast memory layout instead of ordinary Python objects.
import numpy as np
dtype = [("name", "U10"), ("age", "i4"), ("height", "f4")]
people = np.array(
[("Bob", 25, 5.9), ("Sue", 30, 5.5)],
dtype=dtype,
)
print(people)
print(people["name"]) # every "name" field, as its own array
print(people["age"]) # every "age" field, as its own array
print(people[0]) # one whole recordEach field name maps to a type code, just like the dtypes you already know: "U10" is a 10-character Unicode string, "i4" a 32-bit integer, "f4" a 32-bit float. Because people["age"] is a normal ndarray, every NumPy method still works on it:
import numpy as np
dtype = [("name", "U10"), ("age", "i4"), ("height", "f4")]
people = np.array(
[("Bob", 25, 5.9), ("Sue", 30, 5.5)],
dtype=dtype,
)
print(people["age"].mean())Nested dtypes
Section titled “Nested dtypes”Fields can themselves be structured, letting you group related values — for example, a 2D point packed inside a labeled record:
import numpy as np
point_dtype = np.dtype([("x", "f8"), ("y", "f8")])
labeled_dtype = np.dtype([("point", point_dtype), ("label", "U5")])
data = np.array([((1.0, 2.0), "A"), ((3.0, 4.0), "B")], dtype=labeled_dtype)
print(data["point"]) # the (x, y) sub-array for every record
print(data["point"]["x"]) # just the x valuesflowchart LR A["structured array
(dtype = name, age, height)"] --> B["arr['name']
-> plain string ndarray"] A --> C["arr['age']
-> plain int ndarray"] A --> D["arr['height']
-> plain float ndarray"] A --> E["arr[0]
-> one full record"]
Structured arrays are handy for reading fixed-layout binary files (sensor logs, C structs) or when you want table-like data without bringing in pandas. Speaking of pandas: once your data has named, mixed-type columns like this, a DataFrame is usually a nicer tool for everyday analysis — structured arrays are the low-level building block underneath.
From a Python loop to an array expression: random walks
Section titled “From a Python loop to an array expression: random walks”Random walks are a great way to see array-oriented thinking pay off. A random walk starts at 0 and, at every step, moves +1 or -1 with equal probability. Here’s the obvious way to write it, one step at a time:
import random
position = 0
walk = [position]
nsteps = 1000
for _ in range(nsteps):
step = 1 if random.randint(0, 1) else -1
position += step
walk.append(position)Look closely and walk is nothing but the cumulative sum of the steps. That’s the insight that lets us drop the loop entirely: draw all the coin flips at once, turn them into +1/-1 with np.where, then call cumsum:
import numpy as np
nsteps = 1000
rng = np.random.default_rng(seed=12345)
draws = rng.integers(0, 2, size=nsteps) # 0s and 1s
steps = np.where(draws == 0, 1, -1) # turn coin flips into +1 / -1
walk = steps.cumsum() # running position at each step
print(walk.min(), walk.max())flowchart LR A["rng.integers(0, 2, size=1000)
(0/1 coin flips)"] --> B["np.where(draws == 0, 1, -1)
(+1 / -1 steps)"] B --> C["steps.cumsum()
(running position)"] C --> D["walk
(the random-walk path)"]
First crossing time
Section titled “First crossing time”A more interesting statistic than min/max is the first crossing time: how many steps did it take the walk to get at least 10 away from zero, in either direction? np.abs(walk) >= 10 gives a boolean array marking every step where that’s true; argmax on a boolean array returns the index of its first True (since True counts as the maximum value):
import numpy as np
nsteps = 1000
rng = np.random.default_rng(seed=12345)
draws = rng.integers(0, 2, size=nsteps)
steps = np.where(draws == 0, 1, -1)
walk = steps.cumsum()
crossed_10 = (np.abs(walk) >= 10).argmax()
print(crossed_10)Simulating many random walks at once
Section titled “Simulating many random walks at once”The vectorized version scales effortlessly. Pass a (nwalks, nsteps) tuple to rng.integers and you get a 2D array of draws — one row per walk. cumsum(axis=1) then runs every walk’s cumulative sum along its own row, all in one call:
import numpy as np
nwalks = 5000
nsteps = 1000
rng = np.random.default_rng(seed=12345)
draws = rng.integers(0, 2, size=(nwalks, nsteps))
steps = np.where(draws > 0, 1, -1)
walks = steps.cumsum(axis=1)
print(walks.max(), walks.min())Not every walk necessarily wanders as far as 30 steps from zero. any(axis=1) collapses each row down to a single boolean — “did this walk ever reach 30 or -30?” — and boolean .sum() counts how many walks did:
import numpy as np
nwalks = 5000
nsteps = 1000
rng = np.random.default_rng(seed=12345)
draws = rng.integers(0, 2, size=(nwalks, nsteps))
steps = np.where(draws > 0, 1, -1)
walks = steps.cumsum(axis=1)
hits30 = (np.abs(walks) >= 30).any(axis=1)
print(hits30.sum()) # how many of the 5,000 walks reached +-30Finally, filter down to just the walks that crossed, and run argmax(axis=1) on each remaining row to get every one’s individual crossing time:
import numpy as np
nwalks = 5000
nsteps = 1000
rng = np.random.default_rng(seed=12345)
draws = rng.integers(0, 2, size=(nwalks, nsteps))
steps = np.where(draws > 0, 1, -1)
walks = steps.cumsum(axis=1)
hits30 = (np.abs(walks) >= 30).any(axis=1)
crossing_times = (np.abs(walks[hits30]) >= 30).argmax(axis=1)
print(crossing_times.mean())Continue to: Introduction to Pandas to bring this same array-oriented thinking to labeled, tabular data.
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”Exercise 1 – Access a Structured Array’s Field
Section titled “Exercise 1 – Access a Structured Array’s Field”Exercise 2 – Build a Random Walk with where + cumsum
Section titled “Exercise 2 – Build a Random Walk with where + cumsum”Exercise 3 – Find the First Crossing Time
Section titled “Exercise 3 – Find the First Crossing Time”pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading