import numpy as np
a = np.array([1, 2, 3, 4, 5])
b = np.array([10, 20, 30, 40, 50])
floats = np.array([0.25, 0.5, 1.0, 2.0, 4.0])
angles = np.array([0, np.pi/6, np.pi/4, np.pi/2, np.pi]) # 0°,30°,45°,90°,180°
arr_2d = np.array([[1, 2, 3],
[4, 5, 6]])A ufunc (universal function) is a function that operates
element-wise on arrays — no Python loops needed.
Regular Python: NumPy ufunc:
result = [] result = np.sqrt(a)
for x in a: # → [1., 1.41, 1.73, 2., 2.23]
result.append(sqrt(x)) # vectorized — C speed
Key properties:
- Operates element-by-element on any shape array
- Supports broadcasting (different shaped arrays)
- Has extra methods: .reduce(), .accumulate(), .outer()
- Much faster than equivalent Python loops
# These are identical — operators call ufuncs under the hood
a + b == np.add(a, b) # → [11, 22, 33, 44, 55]
a - b == np.subtract(a, b) # → [-9,-18,-27,-36,-45]
a * b == np.multiply(a, b) # → [10, 40, 90,160,250]
a / b == np.divide(a, b) # → [0.1, 0.1, 0.1, 0.1, 0.1]
a ** 2 == np.power(a, 2) # → [ 1, 4, 9, 16, 25]
a % 3 == np.mod(a, 3) # → [ 1, 2, 0, 1, 2]
a // 2 == np.floor_divide(a, 2) # → [ 0, 1, 1, 2, 2]neg = np.array([-3, -1, 0, 2, -5])
np.abs(neg) # → [3, 1, 0, 2, 5]
np.absolute(neg) # → [3, 1, 0, 2, 5] same function
np.fabs(neg) # → [3., 1., 0., 2., 5.] always floatvals = np.array([1.2, 2.5, 3.7, -1.5, -2.8])
np.round(vals) # → [ 1., 2., 4., -2., -3.] banker's rounding
np.around(vals, decimals=1) # → [ 1.2, 2.5, 3.7, -1.5, -2.8]
np.floor(vals) # → [ 1., 2., 3., -2., -3.] always down
np.ceil(vals) # → [ 2., 3., 4., -1., -2.] always up
np.trunc(vals) # → [ 1., 2., 3., -1., -2.] toward zero
np.rint(vals) # → [ 1., 2., 4., -2., -3.] round to nearest intvals = np.array([-5, -1, 0, 3, 8])
np.sign(vals) # → [-1, -1, 0, 1, 1]
np.heaviside(vals, 0.5) # → [ 0., 0., 0.5, 1., 1.]
# x<0 → 0, x=0 → 0.5, x>0 → 1np.exp(a) # → [ 2.71, 7.38, 20.08, 54.6, 148.4]
np.exp2(a) # → [ 2., 4., 8., 16., 32.] 2^x
np.expm1(a) # → [ 1.71, 6.38, 19.08, ...] exp(x)-1
# more accurate than exp(x)-1 for small xnp.log(floats) # → [-1.386, -0.693, 0., 0.693, 1.386] base e
np.log2(floats) # → [-2., -1., 0., 1., 2. ] base 2
np.log10(floats) # → [-0.602, -0.301, 0., 0.301, 0.602] base 10
np.log1p(floats) # → log(1+x) — more accurate for small xnp.sqrt(a) # → [1., 1.414, 1.732, 2., 2.236]
np.cbrt(a) # → [1., 1.259, 1.442, 1.587, 1.709] cube root
np.square(a) # → [ 1, 4, 9, 16, 25]
np.power(a, 3) # → [ 1, 8, 27, 64, 125]
np.reciprocal(floats) # → [4., 2., 1., 0.5, 0.25] 1/xnp.sin(angles) # → [0., 0.5, 0.707, 1., 0.]
np.cos(angles) # → [1., 0.866, 0.707, 0., -1.]
np.tan(angles) # → [0., 0.577, 1., 1.633e16, -0.]
# Convert degrees → radians first
deg = np.array([0, 30, 45, 90, 180])
np.sin(np.deg2rad(deg)) # → [0., 0.5, 0.707, 1., 0.]
np.rad2deg(angles) # → [0., 30., 45., 90., 180.]vals = np.array([0., 0.5, 0.707, 1.])
np.arcsin(vals) # → [0., 0.523, 0.785, 1.570] radians
np.arccos(vals) # → [1.570, 1.047, 0.785, 0. ]
np.arctan(vals) # → [0., 0.463, 0.615, 0.785]
np.arctan2(a, b) # → angle of point (b,a) — handles quadrant correctlynp.sinh(a) # → [ 1.175, 3.626, 10.017, 27.28, 74.2 ]
np.cosh(a) # → [ 1.543, 3.762, 10.067, 27.31, 74.2 ]
np.tanh(a) # → [ 0.761, 0.964, 0.995, 0.999, 0.999]
np.arcsinh(a)
np.arccosh(a)
np.arctanh(np.array([0., 0.5, 0.9]))np.greater(a, 3) # → [False, False, False, True, True]
np.greater_equal(a, 3) # → [False, False, True, True, True]
np.less(a, 3) # → [ True, True, False, False, False]
np.less_equal(a, 3) # → [ True, True, True, False, False]
np.equal(a, 3) # → [False, False, True, False, False]
np.not_equal(a, 3) # → [ True, True, False, True, True]
# Operator shorthand — identical results
a > 3 # np.greater(a, 3)
a == 3 # np.equal(a, 3)a = np.array([1, 5, 3, 7, 2])
b = np.array([4, 2, 6, 1, 8])
np.maximum(a, b) # → [4, 5, 6, 7, 8] element-wise max
np.minimum(a, b) # → [1, 2, 3, 1, 2] element-wise min
np.fmax(a, b) # → same as maximum but ignores NaN
np.fmin(a, b) # → same as minimum but ignores NaNnp.maximum vs np.max:
np.max(a) → single scalar (max of whole array)
np.maximum(a,b)→ element-wise (compare two arrays, pick larger per element)
x = np.array([True, True, False, False])
y = np.array([True, False, True, False])
np.logical_and(x, y) # → [ True, False, False, False]
np.logical_or(x, y) # → [ True, True, True, False]
np.logical_xor(x, y) # → [False, True, True, False]
np.logical_not(x) # → [False, False, True, True]
# Practical use — combine boolean masks
mask = np.logical_and(a > 2, a < 5) # → [F, F, T, T, F]
a[mask] # → [3, 4]
# Equivalent with operators
a[(a > 2) & (a < 5)] # → [3, 4]np.any(a > 4) # → True (at least one element > 4)
np.all(a > 0) # → True (all elements > 0)
np.all(a > 3) # → False
# Per axis in 2D
np.any(arr_2d > 5, axis=0) # → [False, False, True]
np.all(arr_2d > 0, axis=1) # → [True, True]Every ufunc has built-in methods for reductions and combinations.
The four most useful are .reduce(), .accumulate(), .outer(), .reduceat().
# np.add.reduce = sum
np.add.reduce(a) # → 15 (1+2+3+4+5)
# np.multiply.reduce = product
np.multiply.reduce(a) # → 120 (1×2×3×4×5)
# np.maximum.reduce = max
np.maximum.reduce(a) # → 5
# np.logical_and.reduce = all()
np.logical_and.reduce(a > 0) # → True
# Along an axis in 2D
np.add.reduce(arr_2d, axis=0) # → [5, 7, 9] (sum down columns)
np.add.reduce(arr_2d, axis=1) # → [6, 15] (sum across rows)# Running sum (same as cumsum)
np.add.accumulate(a) # → [ 1, 3, 6, 10, 15]
# Running product (same as cumprod)
np.multiply.accumulate(a) # → [ 1, 2, 6, 24, 120]
# Running maximum
np.maximum.accumulate(a) # → [1, 2, 3, 4, 5]
# Useful for detecting new maximums
data = np.array([3, 1, 4, 5, 2, 6, 2])
np.maximum.accumulate(data) # → [3, 3, 4, 5, 5, 6, 6]
is_new_max = data == np.maximum.accumulate(data)
# → [True, False, True, True, False, True, False]# Multiplication table
np.multiply.outer(np.array([1,2,3]), np.array([1,2,3]))
# → [[1, 2, 3],
# [2, 4, 6],
# [3, 6, 9]]
# Add all combinations
np.add.outer(np.array([0, 10, 20]), np.array([1, 2, 3]))
# → [[ 1, 2, 3],
# [11, 12, 13],
# [21, 22, 23]]
# Distance matrix — all pairwise differences
points = np.array([1., 3., 6., 10.])
np.abs(np.subtract.outer(points, points))
# → [[ 0., 2., 5., 9.],
# [ 2., 0., 3., 7.],
# [ 5., 3., 0., 4.],
# [ 9., 7., 4., 0.]]# Sum within segments defined by indices
data = np.array([1, 2, 3, 4, 5, 6, 7, 8])
indices = [0, 3, 6] # segments: [0:3], [3:6], [6:]
np.add.reduceat(data, indices)
# → [6, 15, 15]
# 1+2+3, 4+5+6, 7+8# Scalar broadcast — applies to every element
np.multiply(a, 10) # → [10, 20, 30, 40, 50]
# 1D broadcast across 2D
row = np.array([1, 2, 3])
np.add(arr_2d, row)
# arr_2d is (2,3), row is (3,) → broadcasts to (2,3)
# → [[2, 4, 6],
# [5, 7, 9]]
# Column broadcast — reshape to (2,1)
col = np.array([[10], [20]])
np.add(arr_2d, col)
# arr_2d is (2,3), col is (2,1) → broadcasts to (2,3)
# → [[11, 12, 13],
# [24, 25, 26]]# Avoid creating a new array — write result into 'out'
result = np.zeros(5)
np.multiply(a, 10, out=result)
print(result) # → [10., 20., 30., 40., 50.]
# Useful for large arrays — saves memory allocation
big = np.zeros(1_000_000)
np.sqrt(np.arange(1_000_000), out=big)# Apply ufunc only where mask is True
mask = np.array([True, False, True, False, True])
result = np.zeros(5)
np.multiply(a, 10, out=result, where=mask)
# → [10., 0., 30., 0., 50.]
# applied where True, left as 0 where Falsedata = np.array([1., np.nan, 3., 4., np.nan])
# Regular ufuncs propagate NaN
np.add.reduce(data) # → nan
np.sum(data) # → nan
# NaN-safe alternatives
np.nansum(data) # → 8.0
np.nanprod(data) # → 12.0
np.nanmax(data) # → 4.0
np.nanmin(data) # → 1.0
np.nanmean(data) # → 2.666...
# Check for NaN
np.isnan(data) # → [False, True, False, False, True]
np.isfinite(data) # → [ True, False, True, True, False]
np.isinf(data) # → [False, False, False, False, False]| Category | ufunc | Operator |
|---|---|---|
| Add | np.add(a, b) |
a + b |
| Subtract | np.subtract(a, b) |
a - b |
| Multiply | np.multiply(a, b) |
a * b |
| Divide | np.divide(a, b) |
a / b |
| Power | np.power(a, n) |
a ** n |
| Modulo | np.mod(a, n) |
a % n |
| Floor divide | np.floor_divide(a, b) |
a // b |
| Absolute | np.abs(a) |
|
| Square root | np.sqrt(a) |
|
| Exponential | np.exp(a) |
|
| Log natural | np.log(a) |
|
| Log base 2 | np.log2(a) |
|
| Log base 10 | np.log10(a) |
|
| Sin / Cos / Tan | np.sin(a) etc |
|
| Element max | np.maximum(a, b) |
|
| Element min | np.minimum(a, b) |
|
| Greater than | np.greater(a, b) |
a > b |
| Equal | np.equal(a, b) |
a == b |
| Logical AND | np.logical_and(x, y) |
x & y |
| Logical OR | np.logical_or(x, y) |
x | y |
| Method | What it does | Example |
|---|---|---|
.reduce(a) |
Apply ufunc across array → scalar | np.add.reduce(a) → sum |
.accumulate(a) |
Running result at each step | np.add.accumulate(a) → cumsum |
.outer(a, b) |
All combinations of two arrays | multiplication table |
.reduceat(a, idx) |
Reduce within defined segments | group sums |
ufunc = a function that:
1. Operates element-wise on any shaped array
2. Is implemented in C — much faster than Python loops
3. Supports broadcasting — works across different shapes
4. Has methods: .reduce(), .accumulate(), .outer()
Operator vs ufunc:
a + b is literally np.add(a, b)
All Python array operators call ufuncs under the hood
Use explicit ufunc when you need .reduce(), .out=, or .where=
np.maximum vs np.max:
np.max(a) → one scalar from entire array
np.maximum(a,b) → element-wise comparison of two arrays
NaN propagation:
Any ufunc with NaN input → NaN output
Use np.nan* variants (nansum, nanmean) when NaN is possible
Use np.isnan() to find and handle NaN before computing
Broadcasting rule:
Shapes must be equal OR one of them must be 1
(3,) + (2,3) → broadcasts fine (3 matches 3)
(2,1) + (2,3) → broadcasts fine (1 stretches to 3)
(2,2) + (2,3) → ERROR (2 ≠ 3, neither is 1)
https://numpy.org/doc/stable/reference/ufuncs.html#available-ufuncs