Time a Python loop against the same calculation written as one array operation, and see where the gap comes from.
Because the array operation hands the whole job to compiled code that walks one block of memory, while the loop runs one Python step for every single element. Same answer, different amount of work.
In this lesson I time three pairs: summing two million numbers, turning a million prices into returns, and the same returns through pct_change(). Every timing comes from time.perf_counter, which reads a clock before and after and gives me the seconds in between.
Timings differ from machine to machine and from run to run, so the numbers printed below are the ones from the render, not values I typed in. Yours will differ.
Step 1. Sum two million numbers
np.arange builds the array. The loop adds one element at a time. arr.sum() does the same addition in one call.
import numpy as npfrom time import perf_counterarr = np.arange(1, 2_000_001, dtype=float) # 1.0, 2.0, ... 2000000.0print(arr.shape) # -> (2000000,)start = perf_counter() # clock beforetotal =0.0for x in arr: total = total + xloop_seconds = perf_counter() - start # clock after, minus beforestart = perf_counter()fast = arr.sum()array_seconds = perf_counter() - startprint(total == fast) # -> Trueprint(f"loop: {loop_seconds:.4f} seconds")print(f"arr.sum(): {array_seconds:.4f} seconds")print(f"ratio: {loop_seconds / array_seconds:.0f} times faster")
Both give the identical total. arr.sum() finishes in a couple of thousandths of a second and the loop takes about a tenth of a second, a gap of roughly a hundred times.
Step 2. The same for returns
A return is today’s price over yesterday’s, minus 1. The loop walks the positions. The array version divides the prices from position 1 onwards by the prices up to the second last, all at once.
The prices here are simulated, a random walk of one million steps, so the file stays small and the array stays big.
np.allclose compares the two answers element by element and allows for the last bits of floating point rounding. It came back True, so the fast version is the same calculation.
prices[1:] and prices[:-1] are the two arrays being divided: one shifted by a day against the other. A million prices give 999,999 returns, the same count the loop produced.
Step 3. The same in pandas
A pandas Series stores its values in a numpy array, so pct_change() runs the same kind of operation. The loop here reaches into the Series one position at a time with .iloc.
This gap is wider than in Step 2 on a tenth of the data. Every s.iloc[i] is a lookup through the Series, and the loop does 200,000 of them.
Step 4. What the two versions do
The loop asks Python to do the same paperwork for each element: fetch the value, wrap it in a Python object, look up what + means for that object, store the result. Two million elements, two million rounds of paperwork.
arr.sum() passes the whole array to compiled C code, which reads the numbers straight out of one contiguous block of memory, all of them the same type and the same width, and adds them with no Python involved.
print(arr.dtype) # -> float64print(arr.nbytes) # -> 16000000 <- 2 million values, 8 bytes each, one block
float64
16000000
Write the calculation as an operation on the array, not as a walk over it.
Your turn
Build a = np.arange(1, 1_000_001, dtype=float). Square every element twice: once with a loop that appends to a list, once as a * a. Time both, check they agree with np.allclose, and print the ratio.
TipShow answer
import numpy as npfrom time import perf_countera = np.arange(1, 1_000_001, dtype=float)start = perf_counter()squares = []for x in a: squares.append(x * x)loop_seconds = perf_counter() - startstart = perf_counter()squares_array = a * aarray_seconds = perf_counter() - startprint(np.allclose(squares, squares_array)) # -> Trueprint(f"loop: {loop_seconds:.4f} seconds")print(f"array: {array_seconds:.4f} seconds")print(f"ratio: {loop_seconds / array_seconds:.0f} times faster")
NoteWhat about 40 rows?
The array call has a fixed setup cost, so on a small table the two are both fast and the ratio means little. Here are the 40 AAPL closes from prices.csv, the simulated file used in these lessons, timed in microseconds.
Both finish in under twenty microseconds. The gap in Steps 1 to 3 comes from size: the loop pays its per element cost a million times over, the array operation does not.