Skip to content

Instantly share code, notes, and snippets.

@sreekarun
Last active April 5, 2026 17:03
Show Gist options
  • Select an option

  • Save sreekarun/34d031e533777ef70036e6d20b7210bc to your computer and use it in GitHub Desktop.

Select an option

Save sreekarun/34d031e533777ef70036e6d20b7210bc to your computer and use it in GitHub Desktop.
Numpy Universal Functions

NumPy Universal Functions (ufuncs)


SETUP

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]])

What is a ufunc?

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

PART 1 — ARITHMETIC UFUNCS


1. Basic Arithmetic — operator vs ufunc

# 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]

2. Absolute Value

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 float

3. Rounding ufuncs

vals = 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 int

4. Sign and Step

vals = 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 → 1

PART 2 — EXPONENTIAL AND LOG UFUNCS


5. Exponential

np.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 x

6. Logarithm

np.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 x

7. Power and Square Root

np.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/x

PART 3 — TRIGONOMETRIC UFUNCS


8. Basic Trig — input in radians

np.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.]

9. Inverse Trig

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 correctly

10. Hyperbolic Trig

np.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]))

PART 4 — COMPARISON UFUNCS


11. Element-wise Comparison — returns boolean array

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)

12. Element-wise Min and Max

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 NaN
np.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)

PART 5 — LOGICAL UFUNCS


13. Element-wise Logic

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]

14. Any and All

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]

PART 6 — UFUNC METHODS


Every ufunc has built-in methods for reductions and combinations. The four most useful are .reduce(), .accumulate(), .outer(), .reduceat().


15. .reduce() — apply ufunc cumulatively, return scalar

# 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)

16. .accumulate() — running result at each step

# 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]

17. .outer() — all combinations of two arrays

# 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.]]

18. .reduceat() — reduce over segments

# 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

PART 7 — BROADCASTING WITH UFUNCS


19. Broadcasting — operate on different shaped arrays

# 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]]

20. Out parameter — write result to existing array

# 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)

21. Where parameter — conditional application

# 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 False

PART 8 — NaN-SAFE UFUNCS


22. NaN behavior and workarounds

data = 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]

🧠 Quick Reference

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

🧠 Ufunc Methods Quick Reference

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

🧠 Mental Model

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)

References

https://numpy.org/doc/stable/reference/ufuncs.html#available-ufuncs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment