The same returns, two places for the loop
Suppose one fictional asset has successive prices of 100, 110 and 99 USD, with no distributions or corporate actions. Its simple price return is
The two adjacent returns are and : +10% followed by −10%. They use different denominators. Their arithmetic sum is zero, but the asset finishes 1% below its starting price; consecutive simple returns compound rather than add.
import numpy as np
observations = [100.0, 110.0, 99.0] # successive prices, USD
reference = []
for i in range(1, len(observations)):
before, after = observations[i - 1], observations[i]
reference.append((after - before) / before)
prices = np.array(observations, dtype=np.float64)
assert prices.ndim == 1 and prices.size >= 2
assert np.isfinite(prices).all() and (prices > 0).all()
returns = (prices[1:] - prices[:-1]) / prices[:-1]
print(returns.shape, returns)
print(np.allclose(returns, reference, rtol=0, atol=1e-12))
# (2,) [ 0.1 -0.1]
# Truefor body. The array version pairs two slices: prices[1:] is [110, 99], and prices[:-1] is [100, 110]. Subtraction and division then operate element by element. For these ordinary float64 arrays, the arithmetic runs through NumPy's compiled numerical loops. NumPy calls such element-wise operations universal functions, or ufuncs. Its ufunc guide describes both the inner loops and their type selection.Shape is part of the calculation
(2,), and the result preserves their order. A shape describes dimensions, but does not attach financial meaning to them. If a larger table has dates in rows and assets in columns, adjacent-price pairing must happen along the date axis, independently for each asset. Reversing axes can produce plausible numbers for the wrong question.Observation order and data quality still belong to the author. The example assumes chronological, finite, strictly positive prices; its assertions check the numerical conditions, not the chronology. Missing dates, unadjusted splits or distributions can change what a computed price return means. A vectorised expression does not repair those inputs.
What the short expression costs
prices, so these slices share its data. The subtraction creates an intermediate difference array, and division creates the return array. This expression leaves prices unchanged. A compact line is therefore not necessarily a single pass or a calculation without extra storage. Large intermediate arrays can become the limiting cost, even when broadcasting avoids physically repeating a small input. NumPy's indexing and broadcasting guides explain these distinct memory behaviours.float64 makes this example's representation clear; it does not make decimal arithmetic exact. Fixed-width integer calculations can overflow, and object arrays do not inherit the same native numerical performance assumptions. Compare numerical results with a justified tolerance and check that a rewrite preserves the intended inputs, axis and units. See NumPy's data-type guide for precision and overflow limitations.Vectorisation often reduces Python overhead on substantial numerical arrays, but it offers no blanket speed guarantee. Conversion, allocation, memory traffic, array size and the chosen operation all matter. Keep a clear reference loop, check agreement, then measure the actual workload if performance motivates the rewrite. A time loop can also remain useful while the independent paths within each time step are processed as arrays.
np.vectorize does not compile a Python function into a fast numerical kernel. It provides an array-shaped calling interface, but its implementation essentially loops over calls to that function. NumPy explicitly presents it as a convenience facility in the vectorize reference. Its name alone is not evidence of a performance improvement.Continue with a quant example
Sources
The official pages below were checked in the NumPy v2.5 manual. The example was executed with Python 3.14.4 and NumPy 2.5.1; no benchmark timings are reported.
- NumPy: Universal functions basics — compiled inner loops and type selection.
- NumPy: Indexing on ndarrays — basic slicing and views.
- NumPy: Broadcasting — compatible shapes and memory trade-offs.
- NumPy: Data types — fixed-width overflow and floating-point precision.
- NumPy: vectorize — convenience wrapper and loop implementation.