NumPy Tutorial 01: Foundations and Performance

This chapter builds a practical mental model of NumPy arrays, performance, and numerical precision.

Download Notebook

Download this notebook

import numpy as np
from time import perf_counter

1. Why ndarray instead of Python list?

ndarray stores homogeneous numeric data in contiguous memory (or a predictable strided layout), which enables fast vectorized operations.

py_list = list(range(10))
np_array = np.arange(10)

print('Python list:', py_list[:5], '...')
print('NumPy array:', np_array[:5], '...')
print('Type:', type(np_array))
Python list: [0, 1, 2, 3, 4] ...
NumPy array: [0 1 2 3 4] ...
Type: <class 'numpy.ndarray'>

2. Vectorization benchmark

We compare pure Python loops and NumPy vectorization on the same computation: $y = x^2 + 3x + 1$.

n = 1_000_000
x_list = list(range(n))
x_np = np.arange(n, dtype=np.float64)

t0 = perf_counter()
y_list = [v * v + 3 * v + 1 for v in x_list]
t1 = perf_counter()

t2 = perf_counter()
y_np = x_np * x_np + 3 * x_np + 1
t3 = perf_counter()

print(f'Python loop time: {t1 - t0:.4f}s')
print(f'NumPy vectorized time: {t3 - t2:.4f}s')
print('First 5 values equal?', np.allclose(y_np[:5], y_list[:5]))
Python loop time: 0.1027s
NumPy vectorized time: 0.0050s
First 5 values equal? True

3. Dtype and numerical precision

Choosing dtype is a tradeoff between precision and memory.

a32 = np.array([1, 2, 3], dtype=np.float32)
a64 = np.array([1, 2, 3], dtype=np.float64)

print('float32 itemsize:', a32.itemsize, 'bytes')
print('float64 itemsize:', a64.itemsize, 'bytes')

big = np.arange(1_000_000, dtype=np.float32)
print('float32 total bytes:', big.nbytes)
print('float64 total bytes:', big.astype(np.float64).nbytes)
float32 itemsize: 4 bytes
float64 itemsize: 8 bytes
float32 total bytes: 4000000
float64 total bytes: 8000000

4. Reproducible random numbers

Use the modern random generator API (default_rng) for reproducible experiments.

rng1 = np.random.default_rng(42)
rng2 = np.random.default_rng(42)

s1 = rng1.normal(loc=0.0, scale=1.0, size=5)
s2 = rng2.normal(loc=0.0, scale=1.0, size=5)

print('Sample 1:', s1)
print('Sample 2:', s2)
print('Exactly same?', np.allclose(s1, s2))
Sample 1: [ 0.30471708 -1.03998411  0.7504512   0.94056472 -1.95103519]
Sample 2: [ 0.30471708 -1.03998411  0.7504512   0.94056472 -1.95103519]
Exactly same? True

5. Practice

  1. Create an array x = [0, 0.1, 0.2, ..., 9.9].

  2. Compute sin(x) + cos(x) without loops.

  3. Measure runtime for vectorized vs loop implementation.