Structured Arrays & Random Walks
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
A regular ndarray is homogeneous — every element is the same plain type (all int64int64, all float64float64, 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 dictdicts 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 recordimport 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""U10" is a 10-character Unicode string, "i4""i4" a 32-bit integer, "f4""f4" a 32-bit float. Because people["age"]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())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
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 valuesimport 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
Random walks are a great way to see array-oriented thinking pay off. A random walk starts at 00 and, at every step, moves +1+1 or -1-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)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 walkwalk 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/-1-1 with np.wherenp.where, then call cumsumcumsum:
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())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
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) >= 10np.abs(walk) >= 10 gives a boolean array marking every step where that’s true; argmaxargmax on a boolean array returns the index of its first TrueTrue (since TrueTrue 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)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
The vectorized version scales effortlessly. Pass a (nwalks, nsteps)(nwalks, nsteps) tuple to rng.integersrng.integers and you get a 2D array of draws — one row per walk. cumsum(axis=1)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())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)any(axis=1) collapses each row down to a single boolean — “did this walk ever reach 30 or -30?” — and boolean .sum().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 +-30import 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)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())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())Next
Continue to: Introduction to Pandas to bring this same array-oriented thinking to labeled, tabular data.
🧪 Try It Yourself
Exercise 1 – Access a Structured Array’s Field
Exercise 2 – Build a Random Walk with where + cumsum
Exercise 3 – Find the First Crossing Time
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
