NumPy Arrays and Vectorization
NumPy is the default numerical foundation for machine learning in Python because it stores many numbers in one compact array and applies operations to the whole array at once. The outcome of this lesson is practical: you should be able to look at a dataset, reason about its shape and dtype, choose vectorized operations instead of row-by-row Python loops, and diagnose the common mistakes that produce wrong features or slow preprocessing.
In the Data Preparation section, arrays are where raw records become model-ready matrices. A model usually expects a two-dimensional feature matrix where rows are observations and columns are features. NumPy gives you the vocabulary and mechanics for building that matrix predictably before you hand it to scikit-learn, a neural network library, or your own estimator.
Arrays, Shape, Dtype, and Memory
A NumPy ndarray is not a Python list with extra methods. Internally, it is a view over a contiguous or strided block of memory, plus metadata: shape, dtype, strides, and an offset into the data buffer. The shape says how many elements exist along each axis. The dtype says how each element is represented, such as float64, int64, or bool. Strides say how many bytes NumPy must jump to move one step along each axis.
This design matters because NumPy can run tight loops in compiled code. When you write x * 2, Python does not multiply every element itself. It asks NumPy to execute a universal function, often called a ufunc, over the array buffer. The ufunc checks dtype and shape compatibility once, then iterates in optimized C loops. That is vectorization: expressing the operation over whole arrays so Python is the coordinator instead of the per-element worker.
Views are another important internal detail. Slicing often returns a new array object that points at the same memory with different shape or stride metadata. Copying creates a new buffer. Views are fast and memory-efficient, but mutating a view can change the original array. Machine learning preprocessing code should be explicit when it depends on a copy, especially before in-place scaling, masking, or imputation.
API Anatomy
The core array constructor is np.array, but production code often uses more specific constructors: np.asarray to accept existing arrays without unnecessary copying, np.zeros and np.ones to allocate known shapes, np.arange for ranges, and np.linspace for evenly spaced values. Inspect arrays with .shape, .ndim, .dtype, and sometimes .strides.
Indexing uses zero-based positions. A two-dimensional array is commonly addressed as X[row_index, column_index]. A colon selects a whole axis, so X[:, 0] means the first feature column for every observation. Reductions such as mean, sum, min, and std can operate on the whole array or along an axis. For a feature matrix, axis=0 usually summarizes each column, and axis=1 summarizes each row.
Broadcasting is NumPy’s rule system for combining arrays with different but compatible shapes. Starting from the trailing dimensions, two dimensions are compatible when they are equal or one of them is 1. The smaller array is conceptually stretched without physically copying data. This is why subtracting a vector of column means from a whole feature matrix works: shape (n_rows, n_features) can combine with shape (n_features,).
Example 1: Build a Feature Matrix
The first example converts a small table of house observations into a numeric feature matrix. Each row is one observation. Each column is one feature: square feet, bedrooms, and age. The target vector is kept separate because supervised models should receive inputs and labels as distinct arrays.
import numpy as np
rows = [
[850, 2, 18, 325000],
[1320, 3, 5, 510000],
[1690, 4, 12, 640000],
]
data = np.array(rows, dtype=np.float64)
X = data[:, :3]
y = data[:, 3]
print(X.shape)
print(y.shape)
print(X.dtype)
print(X[1, 0])
The expected output is (3, 3), then (3,), then float64, then 1320.0. The distinction between (3,) and (3, 1) matters: the first is a one-dimensional vector, while the second is a two-dimensional single-column matrix. Many APIs accept either, but broadcasting and concatenation behave differently.
Example 2: Vectorized Standardization
A common preprocessing step is standardization: subtract each feature’s mean and divide by its standard deviation. The loop version would visit every cell in Python. The vectorized version asks NumPy to compute column statistics and broadcast them across all rows.
import numpy as np
X = np.array([
[850.0, 2.0, 18.0],
[1320.0, 3.0, 5.0],
[1690.0, 4.0, 12.0],
])
means = X.mean(axis=0)
stds = X.std(axis=0)
X_scaled = (X - means) / stds
print(np.round(means, 2))
print(np.round(stds, 2))
print(np.round(X_scaled, 2))
The means are approximately [1286.67 3. 11.67]. The standard deviations are approximately [343.38 0.82 5.31]. The scaled matrix has each column centered around zero. This example also shows a design choice: NumPy’s default standard deviation uses the population formula. That is usually fine for feature scaling, while statistical estimation may require a different degrees-of-freedom setting.
Example 3: Boolean Masks and Feature Engineering
Vectorization is not limited to arithmetic. Comparisons produce boolean arrays that can filter rows or create indicator features. The next example identifies newer homes and computes price per square foot without writing a loop over observations.
import numpy as np
X = np.array([
[850.0, 2.0, 18.0],
[1320.0, 3.0, 5.0],
[1690.0, 4.0, 12.0],
])
y = np.array([325000.0, 510000.0, 640000.0])
newer_mask = X[:, 2] <= 10
price_per_sqft = y / X[:, 0]
newer_price_per_sqft = price_per_sqft[newer_mask]
print(newer_mask)
print(np.round(price_per_sqft, 2))
print(np.round(newer_price_per_sqft, 2))
The expected mask is [False True False]. The full price-per-square-foot vector is approximately [382.35 386.36 378.70], and the filtered result is [386.36]. Boolean masking keeps the code close to the mathematical idea: define a condition, compute a vector, then select the entries where the condition is true.
Example 4: Pairwise Distances with Broadcasting
Many machine learning algorithms compare observations. Nearest-neighbor methods, clustering, and anomaly detection all rely on distances. Broadcasting can compute distances from several query points to several training points without nested Python loops.
import numpy as np
train = np.array([[0.0, 0.0], [2.0, 0.0], [2.0, 2.0]])
query = np.array([[1.0, 0.0], [3.0, 2.0]])
deltas = query[:, np.newaxis, :] - train[np.newaxis, :, :]
distances = np.sqrt((deltas ** 2).sum(axis=2))
nearest = distances.argmin(axis=1)
print(np.round(distances, 2))
print(nearest)
The distance matrix has shape (2, 3): two query points by three training points. The expected nearest indices are [0 2]. The inserted axes are the key mechanism. query[:, np.newaxis, :] has shape (2, 1, 2), while train[np.newaxis, :, :] has shape (1, 3, 2). Broadcasting expands those middle dimensions so every query is compared with every training point.
Design Choices and Trade-offs
Choose dtype deliberately. float64 is NumPy’s common default and gives good numerical precision. float32 uses half the memory and is common in deep learning, but repeated operations can accumulate more rounding error. Integer arrays are compact for counts and categories, but integer division, missing values, and scaling usually require conversion to floating point.
Prefer vectorization when the operation can be expressed as array arithmetic, reductions, masking, linear algebra, or broadcasting. Do not force vectorization when it creates a huge temporary array that overwhelms memory. The pairwise distance example is elegant, but for millions of query and training rows it may allocate an impossible three-dimensional intermediate. In that case, process batches or use a library implementation designed for the algorithm.
Be careful with in-place operations such as X -= means. They reduce allocations and can be faster, but they mutate the existing array. That is risky if the original values are still needed for audit, visualization, or another branch of the pipeline. A clear copy at the boundary is often worth the memory cost.
Failure Modes and Troubleshooting
A common symptom is ValueError: operands could not be broadcast together. The cause is incompatible shapes, often a row vector used where a column vector was intended. Diagnose it by printing .shape for every operand immediately before the failing expression. Correct it with an explicit reshape, such as values[:, np.newaxis], only after deciding which axis represents observations.
Another symptom is silently wrong model input after slicing. The usual cause is mixing row and column assumptions, for example using X[0] when you meant the first feature column X[:, 0]. Diagnose by checking both shape and a few known values. Correct the code by naming intermediate arrays, such as square_feet = X[:, 0], and adding assertions for expected dimensions.
A performance failure looks like preprocessing that becomes dramatically slower as data grows. The cause is often a Python loop over rows, repeated np.append, or object dtype. Diagnose with arr.dtype, simple timing, and by looking for list-style accumulation. Correct it by allocating once, using array constructors, or applying vectorized expressions. If dtype is object, find the non-numeric value that forced mixed storage before fitting a model.
Numerical failures often appear as nan or inf values after scaling or division. The cause may be a zero standard deviation column, division by zero, or missing values. Diagnose with np.isfinite(X).all(), column-wise checks, and inspection of zero-variance columns. Correct by imputing missing values, removing constant columns, or guarding division with np.where.
Performance and Reliability Implications
Vectorized NumPy code is usually faster because it reduces Python interpreter overhead and uses contiguous memory efficiently. It is also more reliable when it expresses the intended matrix operation directly. However, vectorization can hide large temporary allocations. For large datasets, estimate shapes before computing. A temporary array with shape (100000, 100000) is not an implementation detail; it is a memory outage.
Reliability also depends on deterministic preprocessing. Store the means, standard deviations, selected columns, and dtype choices learned from training data, then reuse them for validation and production data. Recomputing scaling statistics on each evaluation split contaminates the comparison and makes results look better or worse for the wrong reason.
Hands-on Lab: Vectorize a Small Preprocessing Pipeline
Prerequisites: Python with NumPy installed, a terminal, and a basic editor. The lab builds a feature matrix, scales numeric columns, creates one boolean-derived feature, and verifies the result.
- Create a temporary script named
numpy_vectorization_lab.py. - Paste the lab code into the script.
- Run it with
python numpy_vectorization_lab.py. - Verify that the scaled feature means round to zero and that the final matrix has four columns.
- Cleanup by deleting the temporary script after you finish.
import numpy as np
raw = np.array([
[850.0, 2.0, 18.0, 325000.0],
[1320.0, 3.0, 5.0, 510000.0],
[1690.0, 4.0, 12.0, 640000.0],
[1010.0, 2.0, 3.0, 405000.0],
])
X_numeric = raw[:, :3]
y = raw[:, 3]
means = X_numeric.mean(axis=0)
stds = X_numeric.std(axis=0)
if np.any(stds == 0):
raise ValueError("cannot scale a constant feature")
X_scaled = (X_numeric - means) / stds
is_new = (X_numeric[:, 2] <= 10).astype(np.float64)[:, np.newaxis]
X_final = np.concatenate([X_scaled, is_new], axis=1)
print(np.round(X_final.mean(axis=0), 6))
print(X_final.shape)
print(np.round(y.mean(), 2))
The first three printed means should be 0.0 after rounding; the fourth mean is the share of homes with age less than or equal to ten. The shape should be (4, 4). If the script fails during concatenation, inspect is_new.shape; it must be two-dimensional so it can be appended as a column.
Assessment Exercises
- Given an array with shape
(100, 8), explain the difference betweenX[0],X[:, 0], andX[:, [0]], including the resulting shapes. - Rewrite a loop that computes
(value - mean) / stdfor every item in a column as a vectorized NumPy expression, and state what shape each operand should have. - A distance calculation uses broadcasting and suddenly exhausts memory. Identify the likely temporary array shape and propose a batching strategy.
- A model performs better on validation after you standardize validation data using its own mean and standard deviation. Explain why this is a flawed evaluation.
- Find and correct the bug in a preprocessing step where
np.concatenate([X_scaled, is_new], axis=1)raises an axis error becauseis_newhas shape(n,).
Summary
NumPy arrays make machine learning data preparation precise by tying every value to a shape, dtype, and memory layout. Vectorization moves repeated numerical work out of Python loops and into optimized array operations. The main skill is not memorizing functions; it is reasoning about axes, broadcasting, views, copies, and numeric representation so the feature matrix you build is fast, correct, and ready for modeling.
