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 is vectorized — it applies one operation to a whole array at once, in compiled C code, instead of looping element by element in the Python interpreter. A Python for loop over 10 million prices takes seconds. The equivalent NumPy operation takes milliseconds. The difference compounds across thousands of strategy backtests — a backtest being a simulation of a trading strategy on historical data.
The other reason: most quantitative code is expressed naturally in array operations — applying an operation to a vector of prices, computing moments (summary numbers of a distribution, like the mean and the spread), fitting models. Loops obscure the math; NumPy mirrors it.
Returns — three conventions
A return — the percentage change in a price from one period to the next — is the basic unit quants work with. It puts a cheap stock and an expensive stock on the same footing.
Write the price series as . Read as "P sub t": the price at time . So is the first price and ("P sub t minus one") is the price one step before — yesterday's price, if each step is a day.
-
Dollar change: .
In words: the change in price at time equals today's price minus yesterday's price. The triangle is the Greek capital letter delta — standard shorthand for "change in." Example: the price goes from 100 to 102, so the dollar change is 2. Almost never useful directly, because 2 dollars is a big move for a cheap stock and a rounding error for an expensive one.
-
Simple return: .
In words: the simple return "r sub t" equals the price change divided by yesterday's price. Equivalently: today's price divided by yesterday's price, minus 1. It's the natural percentage change. Example: from 100 to 102, , i.e. +2%.
-
Log return: .
In words: the log return equals the natural logarithm of (today's price divided by yesterday's price). is the natural logarithm — answers "e (about 2.718) raised to what power gives ?"; in Python it's
np.log. You never compute logs by hand. What matters is that logs turn multiplication into addition — the whole reason quants use them, as you'll see below. The wiggle over the is a tilde: reads "r-tilde sub t" and just marks this as the log version, a different quantity from plain . Example: from 100 to 102, , i.e. +1.98% — close to the simple return, but not identical. (We reserve the Greek letter ρ, "rho," for correlation, which arrives in A2-06.)
Why bother with three? Each has different properties:
- Simple returns aggregate across assets — "aggregate" here means "combine by simple addition." If your portfolio is half stock A and half stock B, the portfolio's simple return is 0.5 times A's return plus 0.5 times B's. So portfolio math is easier with simple returns.
- Log returns aggregate across time: the log return over 5 days is the sum of the 5 daily log returns. (Simple returns don't sum across time — to chain them you must multiply the factors together, day by day.)
- Log returns are approximately normal — close to the bell-curve distribution — for many financial time series. Simple returns aren't. A simple return can never go below −100% (you can't lose more than everything) but has no upper limit, so its distribution is lopsided. A log return can be any number, positive or negative. That symmetry makes statistical machinery (regression, normal approximations) cleaner.
A useful rule of thumb: for small returns (< 5%), . The symbol reads "is approximately equal to" — for small moves, the two conventions give nearly the same number. They drift apart as moves get larger:
| 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 rows is informative. A double (+100%) and a halving (−50%) are exact mirror images in log space: +69.31% and −69.31%. In percent space they look like unrelated numbers.
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.
prices[1:]is every price except the first;prices[:-1]is every price except the last. Dividing them lines up each price with the one before it, and NumPy performs all the divisions in one vectorized pass. - The output is one shorter than the input. That's because the first price has no "previous." You'll see this a lot — each lag (each step back in time an operation reaches) costs one observation off the front.
- 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
A rolling window computation slides a fixed-size window along the series and recomputes a statistic at every step — for example "the average of the last 20 days," refreshed each day. You'll want these constantly: rolling mean, rolling standard deviation, rolling Sharpe ratio (a running measure of return earned per unit of risk taken — defined properly later in the curriculum). 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 (the plain average), or the geometric mean (the compounding-aware average — what constant return per period would produce the same final result)? 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 (Open, High, Low, Close — the four prices recorded for each trading day) covering 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