Python lists are great, but they are too slow for the massive datasets used in Machine Learning. Enter NumPy.
NumPy (Numerical Python) is a library that allows us to do math at the speed of AI. The secret is the NumPy Array, which is like a Python list but heavily optimized for speed.
First, we always import the library, usually as np.
Watch how the same 6 elements are stored and represented in different dimensions.
.ndim)2.shape)(2, 3).size)6In standard Python, if you want to add 1 to every item in a list, you need a for loop. In NumPy, you can do it instantly with "vectorized" operations. This is the magic behind fast AI!
See the scalar operation broadcasted across all vector elements simultaneously. No loops, purely parallel.
See how NumPy automatically expands (broadcasts) different shapes (3, 1) and (1, 3) to match each other.
NumPy makes analyzing data a breeze with built-in functions:
np.mean(arr) - Averagenp.max(arr) - Maximum valuenp.std(arr) - Standard DeviationA Python list stores references to separate Python objects, and a Python for loop processes them one interpreted step at a time. A NumPy array stores its numbers in one contiguous block of memory, all with the same type, and its operations run in compiled C code over that whole block. The same idea lets modern CPUs apply one instruction to several numbers at once. The practical result is that arithmetic on large arrays is typically one to two orders of magnitude faster than the equivalent loop. You can measure it yourself:
import numpy as np
import time
data = list(range(1_000_000))
arr = np.arange(1_000_000)
t0 = time.perf_counter()
squares_list = [x * x for x in data]
t1 = time.perf_counter()
squares_arr = arr * arr
t2 = time.perf_counter()
print(f"list: {t1 - t0:.4f}s numpy: {t2 - t1:.4f}s")The exact numbers depend on your machine, but the gap is large. The rule of thumb for data work: if you are about to write a loop over array elements, look for a vectorized operation instead.
Unlike a list, every element of an array has the same dtype. NumPy infers it from the data: a list of whole numbers gives an integer array (usually int64), and a single float in the list upgrades the whole array to float64. This matters because the dtype controls how assignments behave:
a = np.array([1, 2, 3])
print(a.dtype) # int64 on most systems
a[0] = 2.7 # the value is truncated to fit an integer
print(a) # [2 2 3]
b = np.array([1, 2, 3], dtype=float)
b[0] = 2.7
print(b) # [2.7 2. 3. ]
print(a.mean()) # 2.3333333333333335 (mean of ints is a float)Machine learning libraries usually want float32 or float64 arrays, so converting with arr.astype(float) is a common early step.
Indexing extends the list idea to several dimensions. For a 2D array you give the row and column separated by a comma, and a colon means "everything along this axis". This is how you pull out a column of features or a single sample:
m = np.array([[1, 2, 3],
[4, 5, 6]])
print(m[0, 2]) # 3 row 0, column 2
print(m[1]) # [4 5 6] the whole second row
print(m[:, 1]) # [2 5] the second column
print(m[:, :2]) # [[1 2] [4 5]] first two columnsComparing an array with a number produces an array of True and False values of the same shape. Using that array as an index keeps only the elements where the mask is True. The same mask can also be summed, because True counts as 1.
scores = np.array([0.9, 0.3, 0.7, 0.85])
mask = scores > 0.8
print(mask) # [ True False False True]
print(scores[mask]) # [0.9 0.85]
print(mask.sum()) # 2 how many scores exceed 0.8
scores[scores < 0.5] = 0.0 # assign through a mask
print(scores) # [0.9 0. 0.7 0.85]Combine conditions with & and |, and wrap each comparison in parentheses: (x > 0.2) & (x < 0.8). The words and and or do not work on whole arrays and raise a ValueError.
Statistics functions such as sum, mean, min and std accept an axis. For a 2D array, axis=0 collapses the rows and gives one result per column; axis=1 collapses the columns and gives one result per row. If your rows are samples and your columns are features, axis=0 gives per-feature statistics, which is exactly what you need for standardizing data.
m = np.array([[1, 2, 3],
[4, 5, 6]])
print(m.sum(axis=0)) # [5 7 9] column totals
print(m.sum(axis=1)) # [ 6 15] row totals
print(m.mean()) # 3.5 no axis: everythingThe broadcasting visualizer above showed the idea; here are the rules. NumPy compares the shapes of two arrays starting from the last dimension. Two dimensions are compatible when they are equal or when one of them is 1; a size-1 dimension is stretched to match. If neither holds, you get an error.
m = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
print(m + np.array([10, 20, 30]))
# shape (3,) is stretched across rows:
# [[11 22 33]
# [14 25 36]]
print(m + np.array([[10], [20]]))
# shape (2, 1) is stretched across columns:
# [[11 12 13]
# [24 25 26]]
m + np.array([10, 20])
# ValueError: operands could not be broadcast together
# with shapes (2,3) (2,)Basic slicing returns a view: a new array object that shares memory with the original. Changing the slice changes the original. Boolean-mask and index-list selections return copies instead. When you need an independent array, call .copy().
a = np.arange(5) # [0 1 2 3 4]
b = a[1:3] # a view
b[0] = 99
print(a) # [ 0 99 2 3 4] the original changed
c = a[1:3].copy() # an independent copy
c[0] = -1
print(a) # unchanged by cnp.arange(0, 10, 2) gives [0 2 4 6 8]: like range, but returns an array and accepts float steps.np.linspace(0, 1, 5) gives [0. 0.25 0.5 0.75 1.]: five evenly spaced points, endpoint included.np.ones((2, 3)) and np.zeros((2, 3)) create filled arrays of a given shape.np.random.default_rng(0).random((3, 3)) makes reproducible random numbers when you pass a seed.[1, 2] + [3] is [1, 2, 3], but np.array([1, 2]) + 3 adds 3 to each element.reshape(4, 2) on 6 elements fails; the product of the new shape must equal .size. A -1 lets NumPy infer one dimension.np.std divides by n by default (ddof=0); pandas divides by n-1. Pass ddof=1 to match.[row, col], slice with colons, and filter with boolean masks.axis=0 works down the rows (per column); axis=1 works across columns (per row)..copy() for independence.A very common preprocessing step in ML is standardizing features so they have a mean of 0. Let's do it with NumPy!
prices = np.array([100, 200, 300, 400, 500]).prices and store it in mean_price.std_price.normalized_prices by subtracting the mean from prices, and then dividing by the standard deviation. (Do it all in one vectorized line!).normalized_prices.