NumPy Arrays¶
NumPy's array is the foundation of scientific computing in Python. Where Python lists are flexible but slow, NumPy arrays are typed, contiguous in memory, and vectorised — operations run in compiled C, not Python loops. This speed difference is the reason data scientists use NumPy.
Learning Objectives¶
- Create 1D and 2D NumPy arrays
- Perform vectorised arithmetic (no loops needed)
- Index and slice arrays
- Use universal functions (ufuncs)
- Apply statistical operations on arrays
Creating Arrays¶
import numpy as np
# From a Python list
revenues = np.array([42000, 38500, 51200, 49800, 55100])
print(revenues) # [42000 38500 51200 49800 55100]
print(revenues.dtype) # int64
print(revenues.shape) # (5,)
# Float array
prices = np.array([149.0, 35.99, 389.00, 49.99, 89.99])
print(prices.dtype) # float64
# Zeros and ones — initialise arrays
zeros = np.zeros(5) # [0. 0. 0. 0. 0.]
ones = np.ones((3, 4)) # 3×4 array of 1s
empty = np.empty((2, 3)) # uninitialized (fast, values are garbage)
# Ranges
months = np.arange(1, 13) # [1 2 3 4 5 6 7 8 9 10 11 12]
evenly = np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1. ]
# 2D array (matrix)
sales_matrix = np.array([
[100, 150, 200], # Q1 sales: 3 regions
[120, 130, 180], # Q2 sales: 3 regions
[90, 160, 210], # Q3 sales: 3 regions
])
print(sales_matrix.shape) # (3, 3) — 3 rows, 3 columns
Vectorised Operations — No Loops Needed¶
The key advantage of NumPy: operations apply element-wise without writing a loop.
revenues = np.array([42000, 38500, 51200, 49800, 55100])
# Scalar operations apply to every element
print(revenues * 1.1) # 10% increase: [46200. 42350. 56320. 54780. 60610.]
print(revenues / 1000) # in thousands: [42. 38.5 51.2 49.8 55.1]
print(revenues - 40000) # distance from baseline
# Element-wise between two arrays
costs = np.array([28000, 24000, 33000, 31000, 36000])
profits = revenues - costs
margins = profits / revenues * 100
print(margins.round(1)) # [33.3 37.7 35.5 37.7 34.7]
Speed comparison:
import time
data = list(range(1_000_000))
arr = np.array(data)
# Python loop
start = time.time()
result = [x * 2 for x in data]
print(f"Loop: {time.time() - start:.3f}s") # ~0.08s
# NumPy vectorised
start = time.time()
result = arr * 2
print(f"NumPy: {time.time() - start:.3f}s") # ~0.001s — 80x faster
Indexing and Slicing¶
revenues = np.array([42000, 38500, 51200, 49800, 55100])
# Single element
print(revenues[0]) # 42000
print(revenues[-1]) # 55100
# Slice [start:stop:step]
print(revenues[1:4]) # [38500 51200 49800]
print(revenues[::2]) # [42000 51200 55100]
# 2D indexing [row, column]
matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(matrix[1, 2]) # 6 (row 1, col 2)
print(matrix[0, :]) # [1 2 3] (entire first row)
print(matrix[:, 1]) # [2 5 8] (entire second column)
print(matrix[1:, 1:]) # bottom-right 2×2 submatrix
Boolean indexing¶
revenues = np.array([42000, 38500, 51200, 49800, 55100])
# Create boolean mask
above_50k = revenues >= 50000
print(above_50k) # [False False True False True]
# Use mask to filter
print(revenues[above_50k]) # [51200 55100]
# Combine conditions
print(revenues[(revenues >= 45000) & (revenues <= 55000)]) # [49800 55100]
Use & and | (not and and or) for NumPy boolean operations
revenues >= 45000 and revenues <= 55000 raises a ValueError on arrays. Always use & (AND) and | (OR), and wrap each condition in parentheses.
Statistical Functions¶
revenues = np.array([42000, 38500, 51200, 49800, 55100])
print(np.sum(revenues)) # 236600
print(np.mean(revenues)) # 47320.0
print(np.median(revenues)) # 49800.0
print(np.std(revenues)) # 5977.9 (population std)
print(np.std(revenues, ddof=1)) # 6685.0 (sample std)
print(np.var(revenues)) # 35734560.0
print(np.min(revenues)) # 38500
print(np.max(revenues)) # 55100
print(np.percentile(revenues, [25, 50, 75])) # [42000. 49800. 51200.]
# Index of min/max
print(np.argmin(revenues)) # 1 (February)
print(np.argmax(revenues)) # 4 (May)
# Cumulative functions
print(np.cumsum(revenues)) # [42000 80500 131700 181500 236600]
2D aggregation — axis parameter¶
sales = np.array([
[100, 150, 200], # Q1
[120, 130, 180], # Q2
[90, 160, 210], # Q3
])
print(np.sum(sales)) # 1340 — total of all elements
print(np.sum(sales, axis=0)) # [310 440 590] — sum each column (region totals)
print(np.sum(sales, axis=1)) # [450 430 460] — sum each row (quarterly totals)
print(np.mean(sales, axis=0)) # regional averages
Universal Functions (ufuncs)¶
ufuncs apply element-wise mathematical operations:
x = np.array([1, 4, 9, 16, 25])
print(np.sqrt(x)) # [1. 2. 3. 4. 5.]
print(np.log(x)) # natural log
print(np.log10(x)) # base-10 log
print(np.exp(x)) # e^x
print(np.abs(np.array([-3, 2, -1, 4]))) # [3 2 1 4]
print(np.round(np.array([1.234, 5.678, 9.012]), 2)) # [1.23 5.68 9.01]
Random Numbers — Essential for Simulation¶
rng = np.random.default_rng(seed=42) # seeded for reproducibility
# Simulate 1000 orders from a normal distribution
orders = rng.normal(loc=250, scale=80, size=1000) # mean=£250, std=£80
orders = np.clip(orders, 10, 1000) # clip to realistic range
print(f"Simulated: mean=£{orders.mean():.2f}, std=£{orders.std():.2f}")
# Random integers — simulate customer IDs
customer_ids = rng.integers(1, 101, size=1000) # 100 customers
# Uniform distribution — discount rates
discounts = rng.uniform(0, 0.3, size=1000) # 0% to 30%
Practice Exercises¶
# Dataset for exercises
np.random.seed(42)
daily_revenue = np.array([
12500, 13200, 11800, 14500, 16200, 8900, 9100,
15300, 14800, 13100, 12900, 17500, 15800, 8200,
16100, 14200, 13800, 15500, 18200, 9500, 10200,
17100, 16500, 14900, 15200, 19800, 16400, 8800,
])
Warm-up
1. Find the mean, median, and std of daily_revenue.
2. Find all days where revenue was above £15,000.
3. What percentage of days had revenue above £15,000?
Main
4. Calculate the 7-day moving average. (Hint: use a loop with np.mean(daily_revenue[i:i+7]) for each window, or look up np.convolve.)
5. Find the top 5 highest-revenue days and their indices (day numbers). Use np.argsort.
6. Reshape daily_revenue into a 4×7 matrix (4 weeks of 7 days). Calculate total revenue per week.
Stretch
7. Standardise daily_revenue to a z-score array (subtract mean, divide by std). Identify which days are "outliers" (|z| > 2).
8. Simulate 365 days of revenue using np.random.normal(mean, std) where mean and std are computed from daily_revenue. Calculate the 10th and 90th percentiles of the simulated distribution.
Interview Questions¶
[Beginner] What is the difference between a Python list and a NumPy array?
[Beginner] What does "vectorised operation" mean in NumPy?
[Mid-level] What does the axis parameter do in NumPy aggregation functions like np.sum?
[Mid-level] Why do you need to use & and | instead of and and or when combining NumPy boolean conditions?
[Senior] Explain the concept of broadcasting in NumPy. Give an example where it's useful.