Broadcasting & Operations
Definition
Broadcasting is NumPy’s set of rules for performing element-wise operations on arrays of different (but compatible) shapes, without explicitly copying data. Element-wise operations themselves are implemented as ufuncs (universal functions) — compiled C loops that apply an operation across every element.
Element-Wise Arithmetic
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
a + b # [11, 22, 33]
a - b # [-9, -18, -27]
a * b # [10, 40, 90]
a / b # [0.1, 0.1, 0.1]
a ** 2 # [1, 4, 9]
a % 2 # [1, 0, 1]All arithmetic is element-wise by default
a * bmultiplies element-by-element (NOT matrix multiplication). For matrix multiplication, usea @ bornp.matmul(a, b)— see 06-Linear-Algebra.
Broadcasting Rules
Two shapes are compatible for broadcasting if, comparing dimensions from the right:
- They are equal, OR
- One of them is 1, OR
- One of them doesn’t exist (treated as 1)
a = np.array([[1, 2, 3], [4, 5, 6]]) # shape (2, 3)
b = np.array([10, 20, 30]) # shape (3,)
a + b
# b is broadcast across each row:
# [[11, 22, 33],
# [14, 25, 36]]
c = np.array([[100], [200]]) # shape (2, 1)
a + c
# c is broadcast across each column:
# [[101, 102, 103],
# [204, 205, 206]]graph LR A["shape (2,3)"] --> C["Result (2,3)"] B["shape (3,) broadcast to (1,3) then (2,3)"] --> C
Mental model
Broadcasting “stretches” the smaller array’s size-1 dimensions to match, without actually copying memory — it’s a virtual expansion, so it’s memory-efficient.
Scalar Broadcasting
arr * 2 # every element doubled
arr + 100
arr > 5 # element-wise boolean arrayCommon ufuncs
np.sqrt(arr)
np.exp(arr)
np.log(arr) / np.log10(arr) / np.log2(arr)
np.abs(arr)
np.round(arr, 2)
np.floor(arr) / np.ceil(arr)
np.sin(arr) / np.cos(arr) / np.tan(arr)
np.power(arr, 3)
np.mod(arr, 2) # equivalent to arr % 2
np.maximum(a, b) # element-wise max between two arrays
np.minimum(a, b)
np.clip(arr, a_min=0, a_max=100) # cap values to a rangeComparison Operators (Return Boolean Arrays)
a == b
a != b
a > b
a >= b
np.array_equal(a, b) # True if entire arrays match exactly
np.allclose(a, b, atol=1e-8) # True if arrays match within tolerance (for floats)In-Place Operations (Memory-Efficient)
arr += 5 # modifies arr in place, no new array allocated
arr *= 2
np.add(a, b, out=result_arr) # write result into a pre-allocated arrayUse
out=or augmented assignment (+=) for large arraysThese avoid allocating a new array for the result, which matters for memory-bound loops over big datasets.
Custom ufuncs
def my_func(x):
return x ** 2 + 1
vectorized = np.vectorize(my_func)
vectorized(arr) # applies element-wise, but still Python-loop speed under the hood
np.vectorizeis a convenience wrapper, not a performance toolIt’s implemented as a Python loop internally — for real speed, express the logic using actual NumPy ufuncs/broadcasting instead. See 11-Performance-Vectorization.