Broadcasting in NumPy
Broadcasting is NumPy's rule for combining arrays with different but compatible shapes. A dimension of size one is treated as if it had the same size as the corresponding dimension of the other array, without copying the data into a larger array.
Vectorization lets one expression operate over an entire array. NumPy executes the underlying loop in optimized compiled code, which is often faster than an explicit Python loop.
import numpy as np
# Create two arrays
a = np.array([0, 1, 2])
b = np.array([2, 2, 2])
# Vectorization operation (element-wise)
result = a + b
print(result)
# Output: [2 3 4]In the example above, NumPy adds elements at corresponding positions. We describe the operation once, and NumPy handles the element positions for us.
Broadcasting Rules
Two arrays combine without an explicit loop when NumPy can line up their dimensions on its own. To decide whether two shapes are compatible, NumPy compares their dimensions from right to left and stops at the first pair that cannot be aligned.
Three rules cover every case. The table below states the condition each rule checks and the result NumPy produces for that condition.
| Rule | Condition on the shapes | What NumPy does |
|---|---|---|
| Rule | The two shapes have a different number of dimensions | NumPy adds dimensions of size on the left of the shorter shape |
| Rule | An aligned pair has equal sizes, or one side has size | Equal sizes already match. A size-one dimension is reused conceptually without materializing repeated copies |
| Rule | An aligned pair has different sizes and neither side has size | NumPy raises a ValueError |
Array with Scalar
Broadcasting also applies between an array and a single number. NumPy stretches the scalar across every element, so you do not need an explicit loop.
import numpy as np
# 1D array with scalar
a = np.arange(3) # [0, 1, 2]
b = 5
result = a + b
print(f"Array a: {a}")
print(f"Scalar b: {b}")
print(f"Result a + b: {result}")
# Shape explanation:
# a has shape (3,)
# b has shape () - scalar
# After broadcasting: b becomes [5, 5, 5]
# Output: [5 6 7]Two Dimensional Array with One Dimensional Array
When arrays have compatible but different shapes, NumPy can adjust them automatically through broadcasting. This applies one operation across every row or column without manually copying values.
import numpy as np
# 2D array with 1D array
a = np.ones((3, 3)) # 3x3 matrix filled with 1s
b = np.arange(3) # [0, 1, 2]
result = a + b
print("Array a (3x3):")
print(a)
print(f"Array b (1D): {b}")
print("Result a + b:")
print(result)
# Broadcasting occurs:
# a: shape (3, 3)
# b: shape (3,) -> expanded to (1, 3) -> (3, 3)
# b is added to each row of aFailed Broadcasting Case
For shapes (2, 3) and (2,), the last dimensions do not match: their sizes are 3 and 2, and neither is 1. NumPy raises ValueError before performing the addition.
import numpy as np
try:
# Arrays with incompatible shapes
a = np.arange(6).reshape(2, 3) # shape (2, 3)
b = np.arange(2) # shape (2,)
print(f"Array a shape: {a.shape}")
print(f"Array b shape: {b.shape}")
# This will produce an error
result = a + b
except ValueError as e:
print(f"Error: {e}")
print("Array shapes are incompatible for broadcasting")Array Arithmetic Operations
NumPy arithmetic operators work element by element unless an operation explicitly has different semantics, such as matrix multiplication.
When an array is combined with a scalar, broadcasting applies that scalar to every element. No manual indexing loop is required.
import numpy as np
a = np.array([0, 1, 2, 3, 4])
# Addition with scalar
print("Addition:")
print(f"a + 1 = {a + 1}")
# Output: [1 2 3 4 5]
# Multiplication with scalar
print("Multiplication:")
a *= 2
print(f"a *= 2: {a}")
# Output: [0 2 4 6 8]
# Power
print("Power:")
print(f"2**a = {2**a}")
# Output: [ 1 4 16 64 256]Operations Between Arrays
import numpy as np
a = np.array([0, 1, 2, 3, 4])
b = np.array([4, 3, 2, 1, 0])
# Element-wise subtraction
print("Subtraction:")
print(f"a - b = {a - b}")
# Output: [-4 -2 0 2 4]
# Element-wise multiplication
print("Element-wise multiplication:")
print(f"a * b = {a * b}")
# Output: [0 3 4 3 0]
# Matrix multiplication (dot product)
print("Matrix multiplication:")
print(f"a @ b = {a @ b}")
# Output: 10The * operator multiplies entries at the same position. For the two one-dimensional arrays above, @ returns their dot product: 0 + 3 + 4 + 3 + 0 equals 10. For two matrices, @ follows the row-by-column rule. Do not treat np.dot() as equivalent to @ for all array shapes. Their rules differ in higher dimensions.
Comparison and Logic
NumPy comparison operations produce boolean arrays. Use those arrays to filter data or combine several conditions.
import numpy as np
a = np.array([0, 1, 2, 3, 4])
b = np.array([0, 0, 2, 4, 4])
# Comparison operations
print("Greater than comparison:")
print(f"a > 2: {a > 2}")
# Output: [False False False True True]
print("Equal comparison:")
print(f"a == b: {a == b}")
# Output: [ True False True False True]
# Logical operations
print("Logical OR operation:")
print(f"(a > 2) | (a == b): {(a > 2) | (a == b)}")
# Output: [ True False True True True]For element-wise boolean logic, use ~ for NOT, & for AND, and | for OR, or use the corresponding np.logical_* functions. Put each comparison in parentheses because the operators have different precedence from comparisons. Python's scalar operators not, and, and or do not perform element-wise array logic.
Statistical Functions and Reductions
Reduction functions combine many elements into fewer values. Without axis, a reduction combines all elements into one value. With axis, it combines values along that axis while leaving the other axes in place. For a table of exam scores, this can produce an average for each subject or each student.
NumPy provides statistical functions for summaries such as totals, means, and standard deviations. Each function can process the entire array or a selected axis.
import numpy as np
# Create 2D array for example
data = np.array([[3, 0, -1, 1],
[2, -1, -2, 4],
[1, 7, 0, 4]])
print("Data array:")
print(data)
# Statistics on entire array
print(f"Total sum: {np.sum(data)}")
print(f"Mean: {np.mean(data):.2f}")
print(f"Minimum value: {np.min(data)}")
print(f"Maximum value: {np.max(data)}")
print(f"Standard deviation: {np.std(data):.2f}")
# Output:
# Total sum: 18
# Mean: 1.50
# Minimum value: -2
# Maximum value: 7
# Standard deviation: 2.50Operations with Axes
For a two-dimensional array, axis=0 collapses the row dimension and produces one value per column. axis=1 collapses the column dimension and produces one value per row.
The selected axis determines which dimension a statistical function reduces. For monthly sales data across several products, one axis can produce totals per product and the other totals per month.
import numpy as np
data = np.array([[3, 0, -1, 1],
[2, -1, -2, 4],
[1, 7, 0, 4]])
# Operations along axis=0 (for each column)
print("Maximum of each column (axis=0):")
print(f"max(axis=0): {np.max(data, axis=0)}")
# Output: [3 7 0 4]
print("Index of maximum in each column:")
print(f"argmax(axis=0): {np.argmax(data, axis=0)}")
# Output: [0 2 2 1]
# Operations along axis=1 (for each row)
print("Maximum of each row (axis=1):")
print(f"max(axis=1): {np.max(data, axis=1)}")
# Output: [3 4 7]
print("Index of maximum in each row:")
print(f"argmax(axis=1): {np.argmax(data, axis=1)}")
# Output: [0 3 1]Array Shape Manipulation
Shape operations reorganize how the same elements are indexed. A reshape keeps the number of elements unchanged while assigning them to different dimensions.
New NumPy arrays are commonly C-contiguous, meaning the last index changes fastest in memory. Views created by slicing or transposing can have different strides, so an array is not always stored in one simple row-major layout. The order and stride rules matter when reshaping or flattening.
import numpy as np
# Create 2D array
a = np.array([[0, 1], [2, 3]])
print("2D Array:")
print(a)
print(f"Shape: {a.shape}")
# See how it's stored in memory
print(f"Stored in memory as: {a.ravel()}")
# Output: [0 1 2 3]Flatten and Ravel
Both flatten() and ravel() return a one-dimensional array. flatten() always creates a copy, while ravel() returns a view when possible and a copy when the memory layout requires it.
import numpy as np
# Create diagonal array
a = np.diag([1, 2, 3])
print("Diagonal array:")
print(a)
# Flatten - creates independent copy
b_flatten = a.flatten()
print(f"Flatten result: {b_flatten}")
# Changing flatten values doesn't affect original array
b_flatten[0] = 9
print(f"After changing flatten: {b_flatten}")
print("Original array remains the same:")
print(a)
print()
# Ravel - tries to create a view instead of always copying data
b_ravel = a.ravel()
print(f"Ravel result: {b_ravel}")
# Changing ravel values affects original array
b_ravel[0] = 9
print(f"After changing ravel: {b_ravel}")
print("Original array changed:")
print(a)Reshape and Resize
reshape() changes the shape as long as the number of elements stays the same and returns a view when possible. The ndarray.resize() method used below changes the original array in place.
import numpy as np
# Create diagonal array and flatten
a = np.diag([1, 2, 3])
a_flat = a.flatten()
print(f"Flat array: {a_flat}")
# Reshape - change shape with same number of elements
b = a_flat.reshape(3, 3)
print("Reshape result to (3,3):")
print(b)
# Changing values in reshape
b[0, 0] = 9
print("After changing value:")
print(b)
print(f"Original flat array: {a_flat}") # Changes because reshape creates view
# Resize - change shape in-place (no return value)
a_flat.resize(3, 3)
print("After resize:")
print(a_flat) # Now a_flat is 2DTranspose
A transpose permutes an array's axes. For a two-dimensional array it swaps rows and columns. NumPy provides the transpose() method and the shorter .T attribute.
import numpy as np
# Create 2x4 array
a = np.linspace(1, 8, 8).reshape(2, 4)
print("Original array (2x4):")
print(a)
# Transpose using method
b = a.transpose()
print("Transpose result (4x2):")
print(b)
# Transpose using .T attribute (shorter)
c = a.T
print("Using .T:")
print(c)
# Verify that transpose is a view
print("Is transpose a view?", np.shares_memory(a, b))Data Standardization with Z Scores
Z-score standardization centers each feature at a mean of and scales it to a standard deviation of . It is defined only for features with nonzero standard deviation and puts features on comparable numerical scales.
The z-score formula is , where:
- is the feature matrix of size
- is the number of observations (rows)
- is the number of features (columns)
- is the mean vector for each column
- is the standard deviation vector for each column
import numpy as np
# Create sample data (5 observations, 3 features)
rng = np.random.default_rng(42)
X = rng.standard_normal((5, 3)) * 10 + 50 # Samples from a normal distribution with mean 50 and std 10
print("Original data:")
print(X)
print(f"Data shape: {X.shape}")
# Calculate mean and standard deviation for each column
mu = np.mean(X, axis=0) # Mean of each column
sigma = np.std(X, axis=0) # Standard deviation of each column
print(f"Mean of each feature: {mu}")
print(f"Standard deviation of each feature: {sigma}")
# Perform z-score standardization
Z = (X - mu) / sigma
print("Data after z-score standardization:")
print(Z)
# Verify standardization results
print("Standardization verification:")
print(f"New mean: {np.mean(Z, axis=0)}") # Should be close to 0
print(f"New standard deviation: {np.std(Z, axis=0)}") # Should be close to 1
# New standard deviation output: [1. 1. 1.]Standardization prevents units alone from creating large scale differences. It does not guarantee equal importance or influence: the model and the data still determine that. For example, height in centimeters and weight in kilograms become numerically comparable after standardization.