Skip to main content
NumPy intermediate Lesson 7 of 12

NumPy Vectorization

Replace Python loops with vectorized NumPy operations to write faster, cleaner numerical code.

Real-World Scenario

A quant analyst calculates daily returns, rolling volatility, and position sizing across 500 stock tickers and 10 years of daily data — roughly 1.25 million data points. A naive Python loop takes 8 seconds per calculation. The vectorized version runs in under 50 milliseconds. Vectorization isn’t a micro-optimization; at data scale it’s the difference between interactive analysis and waiting.

The Core Idea: Eliminate Python Loops

Every Python for loop over array elements pays interpreter overhead: fetch the next object, unpack its type, dispatch the operation, re-box the result. At 1 million iterations, that overhead dominates. NumPy ufuncs (universal functions) are compiled C functions that operate on entire arrays — they pay that overhead exactly once.

import numpy as np
import time

n = 1_000_000
a = np.random.default_rng(0).standard_normal(n)
b = np.random.default_rng(1).standard_normal(n)

# Python loop — slow: interpreter overhead × n
start = time.perf_counter()
result_loop = [a[i] + b[i] for i in range(n)]
loop_time = time.perf_counter() - start

# Vectorized — fast: one C function call
start = time.perf_counter()
result_vec = a + b
vec_time = time.perf_counter() - start

print(f"Loop:       {loop_time:.3f}s")
print(f"Vectorized: {vec_time:.4f}s")
print(f"Speedup:    {loop_time / vec_time:.0f}x")
# Loop:       0.281s
# Vectorized: 0.002s
# Speedup:    140x

Example 1: Vectorizing a Mathematical Formula

Computing compound interest for 10,000 customers — each with a different principal, rate, and term.

import numpy as np

rng = np.random.default_rng(42)
n = 10_000

# Each customer's loan parameters
principal = rng.uniform(1_000, 100_000, n)   # $1k–$100k
annual_rate = rng.uniform(0.03, 0.15, n)      # 3%–15%
years = rng.integers(1, 30, n).astype(float)  # 1–30 year terms

# Compound interest formula: A = P(1 + r)^t
# Applies to all 10,000 customers simultaneously — no loop
final_balance = principal * (1 + annual_rate) ** years

print(f"Min balance: ${final_balance.min():,.2f}")
print(f"Max balance: ${final_balance.max():,.2f}")
print(f"Total portfolio: ${final_balance.sum():,.2f}")

Example 2: Financial Returns Calculation

Vectorized daily and log returns — the bread and butter of quantitative finance.

import numpy as np

# Simulated price series for 5 assets over 252 trading days
rng = np.random.default_rng(42)
prices = np.cumprod(1 + rng.normal(0.0003, 0.015, (252, 5)), axis=0) * 100

# Daily simple returns: (P_t - P_{t-1}) / P_{t-1}
# np.diff computes P[1:] - P[:-1] across axis 0
returns = np.diff(prices, axis=0) / prices[:-1]  # shape (251, 5)

# Log returns: more numerically stable for long holding periods
log_returns = np.log(prices[1:] / prices[:-1])   # shape (251, 5)

# Annualized volatility per asset — std of daily returns × sqrt(252 trading days)
annualized_vol = returns.std(axis=0) * np.sqrt(252)
print("Annualized volatility per asset:")
for i, vol in enumerate(annualized_vol):
    print(f"  Asset {i}: {vol:.1%}")

# Cumulative return — product of (1 + r) for each asset
cumulative_return = np.prod(1 + returns, axis=0) - 1
print("\nCumulative returns:", cumulative_return.round(3))

Example 3: np.where for Conditional Logic

Vectorized if-else without a loop.

import numpy as np

scores = np.array([88, 45, 72, 91, 55, 67, 83, 39, 76, 94])

# Single threshold
grades = np.where(scores >= 60, "pass", "fail")
print(grades)
# ['pass' 'fail' 'pass' 'pass' 'fail' 'pass' 'pass' 'fail' 'pass' 'pass']

# Multiple thresholds with np.select — vectorized if/elif/else
conditions = [
    scores >= 90,
    scores >= 80,
    scores >= 70,
    scores >= 60,
]
choices = ["A", "B", "C", "D"]
letter_grades = np.select(conditions, choices, default="F")
print(letter_grades)
# ['B' 'F' 'C' 'A' 'F' 'D' 'B' 'F' 'C' 'A']

Example 4: Rolling Calculations with Strides

Computing a rolling mean without a Python loop — using NumPy’s stride tricks to create a sliding window view.

import numpy as np

def rolling_mean(arr: np.ndarray, window: int) -> np.ndarray:
    """Vectorized rolling mean using stride_tricks."""
    shape = (arr.size - window + 1, window)
    strides = (arr.strides[0], arr.strides[0])
    # as_strided creates a 2-D view where each row is one window
    windows = np.lib.stride_tricks.as_strided(arr, shape=shape, strides=strides)
    return windows.mean(axis=1)

prices = np.array([100., 102., 98., 105., 103., 107., 110., 108., 112., 115.])

ma5 = rolling_mean(prices, window=5)
print(ma5.round(2))
# [101.6  103.   104.6  106.6  107.6  108.4]

np.apply_along_axis vs Pure Vectorization

np.apply_along_axis is cleaner than a Python loop over rows/columns, but still calls Python on each slice. Use true ufuncs when possible.

import numpy as np

data = np.random.default_rng(0).standard_normal((1000, 10))

# Slower: applies a Python function to each row
def normalize_row(row):
    return (row - row.mean()) / row.std()

normalized_loop = np.apply_along_axis(normalize_row, axis=1, arr=data)

# Faster: pure NumPy broadcasting — one C call for mean, one for std
mean = data.mean(axis=1, keepdims=True)   # (1000, 1)
std  = data.std(axis=1, keepdims=True)    # (1000, 1)
normalized_vec = (data - mean) / std      # broadcasts (1000, 10)

# Both produce the same result — the vectorized version is ~10x faster at scale
print(np.allclose(normalized_loop, normalized_vec))  # True

Common Mistakes

1. Using Python math functions on arrays:

import math
import numpy as np

arr = np.array([0.0, 1.0, 2.0, 3.0])

# Wrong — math.sqrt operates on scalars, raises TypeError on arrays
# result = math.sqrt(arr)

# Correct — np.sqrt is a ufunc: operates element-wise on the full array
result = np.sqrt(arr)  # [0. 1. 1.414 1.732]

2. Forgetting keepdims when broadcasting after a reduction:

import numpy as np
X = np.ones((5, 3))

# Without keepdims: mean shape is (3,) — broadcasts fine as a row
# but breaks for column normalization
row_mean = X.mean(axis=1)             # shape (5,) — cannot subtract from (5, 3)
row_mean_col = X.mean(axis=1, keepdims=True)  # shape (5, 1) — broadcasts correctly
result = X - row_mean_col             # (5, 3) - (5, 1) → (5, 3)

Frequently Asked Questions

What does vectorization mean in NumPy?
Vectorization means expressing an operation on an entire array at once rather than looping element-by-element in Python. NumPy dispatches the work to compiled C/Fortran routines that process all elements in a single function call, eliminating Python's per-iteration overhead.
When should I use np.vectorize?
np.vectorize is a convenience wrapper that applies a Python function element-wise. It does NOT produce compiled performance — it's essentially a fancy for-loop. Use it for readability when performance isn't critical. For performance, rewrite the function using NumPy ufuncs and array operations instead.