Q&A
How to perform arithmetic operations in NumPy
Overview
NumPy's core type is the ndarray, and its superpower is vectorized arithmetic: an operation written once applies to every element, executed in optimized native code over contiguous memory. This replaces slow Python for loops and is the foundation of scientific computing and ML in Python.
graph LR
A["a = [1,2,3]"] --> C["a * 2"]
C --> D["[2,4,6] - element-wise, no loop"]
A --> E["a + b - element-wise"]
A --> F["broadcasting: different shapes align"]
style D fill:#1e1b4b,color:#fff
Element-wise Operations
Operators apply to each element; arrays of the same shape combine position by position:
import numpy as np
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
a + b # [11, 22, 33]
a * b # [10, 40, 90] (element-wise, NOT matrix multiply)
b - a # [9, 18, 27]
b / a # [10., 10., 10.]
a ** 2 # [1, 4, 9]
a % 2 # [1, 0, 1]
Contrast with Python lists, where [1,2,3] * 2 repeats the list - NumPy does math.
Scalar Operations (broadcasting a scalar)
A scalar applies to every element:
a + 100 # [101, 102, 103]
a * 2 # [2, 4, 6]
1 / a # [1.0, 0.5, 0.333...]
Broadcasting (different shapes)
NumPy "broadcasts" smaller arrays across larger ones when shapes are compatible - aligning dimensions from the right, stretching size-1 dimensions:
matrix = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
row = np.array([10, 20, 30]) # shape (3,)
matrix + row
# [[11, 22, 33],
# [14, 25, 36]] - row added to each matrix row
Broadcasting avoids manually tiling arrays and is a key NumPy idiom. Shapes are compatible when, compared from the trailing dimension, they're equal or one of them is 1.
Universal Functions (ufuncs)
NumPy provides fast element-wise math functions:
np.sqrt(a) # [1., 1.414, 1.732]
np.exp(a) # e^x for each element
np.log(b)
np.sin(a)
np.add(a, b) # same as a + b
np.maximum(a, b) # element-wise max
Aggregations
Reduce an array to summary values, optionally along an axis:
arr = np.array([[1, 2, 3], [4, 5, 6]])
arr.sum() # 21 (all elements)
arr.sum(axis=0) # [5, 7, 9] (column sums)
arr.sum(axis=1) # [6, 15] (row sums)
arr.mean() # 3.5
arr.max(), arr.min(), arr.std()
axis=0 collapses rows (operate down columns); axis=1 collapses columns (operate across rows).
Matrix Multiplication vs Element-wise
A crucial distinction: * is element-wise; @ (or np.dot) is true matrix multiplication:
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
A * B # element-wise: [[5,12],[21,32]]
A @ B # matrix product: [[19,22],[43,50]]
Why Vectorization Is Fast
A Python loop executes interpreted bytecode per element; NumPy executes one compiled C loop over a contiguous typed buffer, with no per-element Python overhead (and often SIMD acceleration). For large arrays this is often 10–100× faster - and the code is shorter.
Why This Matters in Interviews
Data-science/Python interviews use this to check NumPy fluency. Key points: arithmetic is element-wise/vectorized (no loops), the * (element-wise) vs @ (matrix multiply) distinction (a classic trap), broadcasting rules for differing shapes, aggregations with axis, ufuncs, and why vectorization beats Python loops (compiled C over contiguous memory). The axis semantics and broadcasting are the highest-value details.
Common Pitfalls
- Expecting
*to be matrix multiplication (it's element-wise; use@). - Confusing
axis=0(down columns) withaxis=1(across rows). - Shape-mismatch errors when broadcasting rules aren't satisfied.
- Writing Python loops over arrays instead of vectorizing (slow and un-idiomatic).
- Integer division/overflow surprises with integer dtypes.
References
- NumPy basics: https://numpy.org/doc/stable/user/absolute_beginners.html
- NumPy broadcasting: https://numpy.org/doc/stable/user/basics.broadcasting.html