NumPy through computing returns
▸ Pretest — guess, even if you don't know
If a stock closes at $100 on day 1 and $102 on day 2, what's the 'return' from day 1 to day 2?
Why NumPy
You already know Python loops. Why do quants live in NumPy?
Because NumPy operates on arrays vectorized in C under the hood. A Python for loop over 10 million prices takes seconds. The equivalent NumPy operation takes milliseconds. The difference compounds across thousands of strategy backtests.
The other reason: most quantitative code is expressed naturally in array operations — applying an operation to a vector of prices, computing moments, fitting models. Loops obscure the math; NumPy mirrors it.
Returns — three conventions
For a price series :
- Dollar change: . Almost never useful directly.
- Simple return: . The natural percentage change.
- Log return: . The log of the price ratio. (We reserve the Greek letter ρ for correlation, which arrives in A2-06.)
Why bother with three? Each has different properties:
- Simple returns aggregate across assets: a portfolio's simple return is a weighted sum of constituent simple returns. So portfolio math is easier with simple.
- Log returns aggregate across time: the log return over 5 days is the sum of 5 daily log returns. (Simple returns don't sum across time — you have to multiply 1+r factors.)
- Log returns are approximately normal for many financial time series. Simple returns aren't — they're bounded below by −100% but unbounded above, which is asymmetric. Log returns can be any real number, which makes statistical machinery (regression, normal approximations) cleaner.
A useful rule of thumb: for small returns (< 5%), . They diverge at larger magnitudes:
| Simple return | Log return |
|---|---|
| +1% | +0.995% |
| +5% | +4.879% |
| +10% | +9.531% |
| +100% | +69.31% |
| −50% | −69.31% |
The asymmetry on the last two is informative: doubling and halving are mirror images in log space, but very different in percent space.
NumPy mechanics
The basic NumPy idiom for return computation:
import numpy as np
prices = np.array([100.0, 102.0, 101.5, 103.0, 102.8])
# Simple returns: shift and subtract
simple_returns = prices[1:] / prices[:-1] - 1
# array([ 0.02 , -0.00490196, 0.01477833, -0.00194175])
# Log returns: shift and log-difference
log_returns = np.log(prices[1:]) - np.log(prices[:-1])
# array([ 0.01980263, -0.00491402, 0.01467038, -0.0019436 ])
Three things to notice:
- No loop. The slicing
prices[1:] / prices[:-1]element-wise divides each price by the previous one. NumPy broadcasts this across the whole array. - The output is one shorter than the input. That's because the first price has no "previous." You'll see this a lot — most return-based operations lose one observation per lag.
- Dtype awareness matters. Price arrays should be
float64(NumPy's default for float input). Integer arrays work for/(true division returns float), but integer dtypes bite you elsewhere —//floor-division, overflow in cumulative products, and silent casts in in-place ops. Checkarr.dtypewhen results look off.
A small but important pattern: rolling windows
You'll want rolling computations constantly — rolling mean, rolling standard deviation, rolling Sharpe. The naive way:
# slow — Python loop
out = []
for i in range(window, len(returns)):
out.append(returns[i-window:i].mean())
The vectorized way uses cumulative sums for means, or specialized libraries (pandas.rolling, numpy.lib.stride_tricks.sliding_window_view) for general operations. We'll use pandas for most of these in C2. For now, just know that loops over time series are something to avoid.
When NOT to use NumPy
For anything with labels (named columns, time-indexed rows, mixed types), use pandas — built on NumPy but adds the metadata. We'll meet pandas in C2-01.
For multi-dimensional matrices with linear algebra (eigenvalues, matrix inverse, factorizations), NumPy is fine, but SciPy (scipy.linalg) has more numerically stable routines for serious work.
For massive datasets that don't fit in memory — Polars (or Dask, or DuckDB) become better tools. NumPy holds everything in RAM.
For Phase 1 of this curriculum, plain NumPy + pandas covers ~95% of what we need.
⧉ Review cardWhy use log returns instead of simple returns?
⧉ Review cardHow do you compute simple returns of a NumPy price array without a loop?
⧉ Review cardWhen do simple and log returns diverge meaningfully?
⧉ Review cardWhy is a Python for-loop over an array slow compared to NumPy?
Try it
The reading-on-mobile portion of this lesson ends here. The exercises below need a keyboard.
⚡ Try this first — wrong is fine
Before reading the next lesson, try this in your own Python environment (or on paper):
Given the price series [100, 103, 99, 102, 105, 101]:
- Compute simple returns (5 values)
- Compute log returns (5 values)
- Compute the 3-day rolling mean of simple returns (3 values)
- Without computing it, predict: which is higher — the arithmetic mean of simple returns, or the geometric mean? Why?
Now check your work directly in the browser — the exercise below runs real Python (with NumPy) right here:
Compute the simple returns and log returns of the given price series as NumPy arrays. No loops — use array slicing.
Predict before the next lesson
Tomorrow we'll meet pandas, which adds time-indexed data structures on top of NumPy. Predict:
- For an OHLC daily dataset of 10 years (≈2,520 rows), about how many MB of memory would you guess pandas uses? (Hint: each row has 4 floats + a date.)
- What's the most common pitfall you think we'll cover with
pandas.DataFrame.shiftfor returns?
Note your guesses. We'll check.
◈ Calibration check
How confident are you that you could write the NumPy returns code from scratch?
1 = guessing · 5 = could teach it
⏻ End of lesson
Mark it read to book its 4 review cards into your deck.
Sources & further reading
- bookVanderPlas (2016), Python Data Science Handbook — §2 (NumPy)
- bookHilpisch (2018), Python for Finance, 2e — §5
- webNumPy User Guide — Quickstart link