Bridge To Data And ML
NumPy Primer
Python's built-in list (Module 1, Chapter 6) is flexible but slow for numeric computation — each element is a full Python object, and operations loop one item a
Jr Codex Python Notes
Level: Advanced Prerequisites: Module 5: Advanced Python Time to complete: ~30 minutes
Table of Contents
- Why NumPy?
- Creating Arrays
- Array Shape & Dimensions
- Vectorized Operations
- Indexing & Slicing
- Boolean Masking
- Broadcasting
- Useful Array Methods
- Summary & Next Steps
1. Why NumPy?
Python's built-in list (Module 1, Chapter 6) is flexible but slow for numeric computation — each element is a full Python object, and operations loop one item at a time. NumPy (Numerical Python) provides the ndarray, a fixed-type array stored contiguously in memory, with operations implemented in optimized C — often 10-100x faster than an equivalent Python loop.
import numpy as np
# Plain Python list — a loop is required for element-wise math
prices = [10, 20, 30, 40]
doubled = [p * 2 for p in prices] # Module 2's comprehension pattern
print(doubled) # [20, 40, 60, 80]
# NumPy array — the operation applies to EVERY element at once, no explicit loop
prices_np = np.array([10, 20, 30, 40])
doubled_np = prices_np * 2
print(doubled_np) # [20 40 60 80]This "apply an operation to every element without writing a loop" style is called vectorization — it's the core idea behind everything in this chapter, and the foundation of virtually every numeric library used later in this curriculum (pandas, scikit-learn, PyTorch).
2. Creating Arrays
import numpy as np
a = np.array([1, 2, 3, 4, 5]) # from a list
print(a) # [1 2 3 4 5]
print(type(a)) # <class 'numpy.ndarray'>
zeros = np.zeros(5) # [0. 0. 0. 0. 0.]
ones = np.ones(5) # [1. 1. 1. 1. 1.]
range_arr = np.arange(0, 10, 2) # [0 2 4 6 8] — like Python's range(), but returns an array
linspace = np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1.] — 5 evenly spaced points
matrix = np.array([[1, 2, 3], [4, 5, 6]]) # a 2D array, from a nested list (Module 1, Ch.6)
print(matrix)
# [[1 2 3]
# [4 5 6]]
random_arr = np.random.rand(3) # 3 random floats between 0 and 1Key difference from Python lists: every element in a NumPy array shares the same data type (dtype) — this uniformity is exactly what makes the C-level optimizations possible.
a = np.array([1, 2, 3])
print(a.dtype) # int64 (or int32 on some systems)
b = np.array([1, 2, 3.5])
print(b.dtype) # float64 — NumPy "upcasts" ALL elements to float if any one is a float
print(b) # [1. 2. 3.5]3. Array Shape & Dimensions
a = np.array([1, 2, 3, 4, 5])
print(a.shape) # (5,) — a 1D array with 5 elements
print(a.ndim) # 1
matrix = np.array([[1, 2, 3], [4, 5, 6]])
print(matrix.shape) # (2, 3) — 2 rows, 3 columns
print(matrix.ndim) # 2
print(matrix.size) # 6 — total number of elements (2 * 3)
reshaped = a.reshape(5, 1) # reshape into a column: 5 rows, 1 column
print(reshaped)
# [[1]
# [2]
# [3]
# [4]
# [5]]
flattened = matrix.flatten() # collapse back down to 1D
print(flattened) # [1 2 3 4 5 6]Shape Notation
─────────────────────────────────────────
(5,) → 1D array, 5 elements
(2, 3) → 2D array (matrix), 2 rows × 3 columns
(2, 3, 4) → 3D array, e.g. two 3x4 grids stacked
─────────────────────────────────────────
4. Vectorized Operations
a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])
print(a + b) # [11 22 33 44] — element-wise addition, no loop needed
print(a * b) # [10 40 90 160] — element-wise multiplication
print(a * 2) # [2 4 6 8] — every element multiplied by a scalar
print(a ** 2) # [1 4 9 16] — every element squared
print(a > 2) # [False False True True] — element-wise comparison, produces a bool array
print(np.sqrt(a)) # [1. 1.41 1.73 2.] — apply sqrt to every element
print(np.sum(a)) # 10 — sum of all elements
print(np.mean(a)) # 2.5 — average
print(np.max(a)) # 4
print(np.std(a)) # standard deviationCompare to what the equivalent would require with a plain Python list:
# Plain Python — every operation needs an explicit loop or comprehension
a_list = [1, 2, 3, 4]
b_list = [10, 20, 30, 40]
added = [x + y for x, y in zip(a_list, b_list)] # Module 1 Ch.11's zip()
squared = [x ** 2 for x in a_list]
total = sum(a_list)NumPy replaces all of that looping with a single, optimized operation per line.
5. Indexing & Slicing
Familiar syntax from Module 1's strings/lists, extended to multiple dimensions:
a = np.array([10, 20, 30, 40, 50])
print(a[0]) # 10
print(a[-1]) # 50
print(a[1:4]) # [20 30 40]
matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(matrix[0]) # [1 2 3] — entire first row
print(matrix[0, 1]) # 2 — row 0, column 1 (NumPy's own comma syntax)
print(matrix[:, 1]) # [2 5 8] — entire SECOND column (all rows, column index 1)
print(matrix[0:2, 1:3]) # [[2 3] [5 6]] — a sub-matrix (rows 0-1, columns 1-2)matrix[:, 1] — Reading the Syntax
─────────────────────────────────────────
matrix[ ROWS , COLUMNS ]
: 1
↑ ↑
"all rows" "column index 1"
─────────────────────────────────────────
6. Boolean Masking
A distinctly NumPy idiom, used constantly in real data work — filter an array using a condition, without writing an explicit loop:
a = np.array([10, 15, 20, 25, 30, 35])
mask = a > 20
print(mask) # [False False False True True True]
filtered = a[mask] # only keeps elements where mask is True
print(filtered) # [25 30 35]
# Usually written in one line:
print(a[a > 20]) # [25 30 35]
# Combine conditions with & (and) / | (or) — NOT Python's `and`/`or` (Module 1, Ch.3)!
print(a[(a > 15) & (a < 30)]) # [20 25]
# Modify only the matching elements
a[a > 20] = 0
print(a) # [10 15 20 0 0 0]Important: use & and |, never and/or, when combining conditions on arrays — Python's and/or don't work element-wise and will raise an error on arrays with more than one element.
7. Broadcasting
Broadcasting is NumPy's rule for applying operations between arrays of different shapes, by conceptually "stretching" the smaller one to match — this is what made a * 2 work earlier (a single scalar "broadcast" across the whole array).
matrix = np.array([[1, 2, 3], [4, 5, 6]]) # shape (2, 3)
row = np.array([10, 20, 30]) # shape (3,)
result = matrix + row
print(result)
# [[11 22 33]
# [14 25 36]]Broadcasting Visualized
─────────────────────────────────────────
matrix (2,3) row (3,) "stretched" to match
[1 2 3] + [10 20 30] [1 2 3] [10 20 30]
[4 5 6] (conceptually) [4 5 6] + [10 20 30]
= [[11 22 33], [14 25 36]]
─────────────────────────────────────────
Broadcasting only works when shapes are compatible (matching, or one of them is size 1 along a dimension) — mismatched shapes raise a ValueError. This exact mechanism is why you can write data - data.mean() in one line rather than looping — a pattern you'll see constantly once you reach the Machine Learning notes' data preprocessing chapter.
8. Useful Array Methods
a = np.array([5, 2, 8, 1, 9, 3])
print(np.sort(a)) # [1 2 3 5 8 9] — like Module 1's sorted(), but for arrays
print(np.argsort(a)) # [3 1 5 0 2 4] — INDICES that would sort the array
print(np.unique(a)) # [1 2 3 5 8 9] — like Module 1's set(), but sorted and array-typed
print(a.argmax()) # 4 — index of the maximum value
print(a.argmin()) # 3 — index of the minimum value
matrix = np.array([[1, 2], [3, 4], [5, 6]])
print(matrix.sum(axis=0)) # [9 12] — sum DOWN each column
print(matrix.sum(axis=1)) # [3 7 11] — sum ACROSS each rowaxis Parameter — the #1 Source of Confusion for Beginners
─────────────────────────────────────────
axis=0 → operate DOWN the rows (collapse rows, one result per COLUMN)
axis=1 → operate ACROSS the columns (collapse columns, one result per ROW)
─────────────────────────────────────────
9. Summary & Next Steps
Key Takeaways
- NumPy's
ndarraystores same-typed data contiguously and applies operations in optimized C — vastly faster than looping over a Pythonlistfor numeric work. - Vectorized operations (
a + b,a * 2,np.sqrt(a)) apply element-wise without an explicit loop. - Boolean masking (
a[a > 20]) is the idiomatic way to filter arrays — combine conditions with&/|, neverand/or. - Broadcasting lets operations work between differently-shaped arrays by conceptually stretching the smaller one — the mechanism behind writing
data - data.mean()in a single line.
Concept Check
- Why is
np.array([1, 2, 3]) * 2faster than[x * 2 for x in [1, 2, 3]]at scale? - Why must you use
&instead ofandwhen combining two boolean array conditions? - What does
axis=0mean when calling.sum(axis=0)on a 2D array?
Next Chapter
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index