Universal Functions¶
Universal functions (ufuncs) operate element-wise on arrays with broadcasting support.
Mental Model
Ufuncs are the engine behind NumPy's speed: each one is a compiled C loop that takes array inputs, applies a scalar operation element-by-element, and handles broadcasting, type promotion, and output allocation automatically. When you write a + b on arrays, Python dispatches to np.add, which is a ufunc -- no Python loop ever runs.
The key elevation: ufuncs are the abstraction layer for ALL array computation. Arithmetic, comparisons, math functions, and rounding are all ufuncs. Their methods unify many seemingly different operations:
| Concept | UFunc expression |
|---|---|
np.sum(a) |
np.add.reduce(a) |
np.cumsum(a) |
np.add.accumulate(a) |
| Outer product | np.multiply.outer(a, b) |
| In-place update | np.add.at(a, idx, val) |
What are Ufuncs¶
1. Definition¶
A ufunc operates on ndarrays element-by-element, supporting broadcasting and type casting.
```python import numpy as np
def main(): a = np.array([1, 2, 3]) b = np.array([4, 5, 6])
# np.add is a ufunc
c = np.add(a, b)
print(f"a = {a}")
print(f"b = {b}")
print(f"np.add(a, b) = {c}")
print(f"Type: {type(np.add)}")
if name == "main": main() ```
2. Without Ufuncs¶
```python import numpy as np
def main(): a = [1, 2, 3] b = [4, 5, 6]
# Without ufuncs: manual loop
c = []
for i, j in zip(a, b):
c.append(i + j)
print(f"Result: {c}")
if name == "main": main() ```
3. With Ufuncs¶
```python import numpy as np
def main(): a = np.array([1, 2, 3]) b = np.array([4, 5, 6])
# With ufuncs: single expression
c = a + b
print(f"Result: {c}")
if name == "main": main() ```
Built-in Ufuncs¶
1. Math Ufuncs¶
```python import numpy as np
def main(): x = np.array([1, 4, 9, 16])
print(f"x = {x}")
print(f"np.sqrt(x) = {np.sqrt(x)}")
print(f"np.square(x) = {np.square(x)}")
print(f"np.abs(x) = {np.abs(x)}")
if name == "main": main() ```
2. Trigonometric Ufuncs¶
```python import numpy as np
def main(): x = np.array([0, np.pi/6, np.pi/4, np.pi/3, np.pi/2])
print(f"x (radians) = {x}")
print(f"np.sin(x) = {np.sin(x).round(4)}")
print(f"np.cos(x) = {np.cos(x).round(4)}")
if name == "main": main() ```
3. Comparison Ufuncs¶
```python import numpy as np
def main(): a = np.array([1, 2, 3]) b = np.array([3, 2, 1])
print(f"np.greater(a, b) = {np.greater(a, b)}")
print(f"np.maximum(a, b) = {np.maximum(a, b)}")
print(f"np.minimum(a, b) = {np.minimum(a, b)}")
if name == "main": main() ```
np.frompyfunc¶
1. Create Custom Ufunc¶
```python import numpy as np
def main(): # Python function def my_func(x): return x ** 2 + 1
# Convert to ufunc
my_ufunc = np.frompyfunc(my_func, 1, 1)
a = np.array([1, 2, 3, 4])
result = my_ufunc(a)
print(f"a = {a}")
print(f"my_ufunc(a) = {result}")
print(f"Result dtype: {result.dtype}")
if name == "main": main() ```
2. Signature¶
```python import numpy as np
def main(): # np.frompyfunc(func, nin, nout) # nin: number of input arguments # nout: number of output values
# Example: function with 2 inputs, 1 output
def combine(x, y):
return x * 10 + y
combine_ufunc = np.frompyfunc(combine, 2, 1)
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
result = combine_ufunc(a, b)
print(f"Result: {result}")
if name == "main": main() ```
3. String Conversion¶
```python import numpy as np
def main(): a = np.array([1, 2, 3])
# str() on array gives string representation of whole array
print(f"str(a) = {str(a)}")
# frompyfunc applies str to each element
str_ufunc = np.frompyfunc(str, 1, 1)
print(f"str_ufunc(a) = {str_ufunc(a)}")
if name == "main": main() ```
np.vectorize¶
1. Basic Usage¶
```python import numpy as np
def main(): def my_func(x): if x < 0: return 0 elif x > 10: return 10 else: return x
# Vectorize the function
vec_func = np.vectorize(my_func)
a = np.array([-5, 3, 8, 15, 2])
result = vec_func(a)
print(f"a = {a}")
print(f"vec_func(a) = {result}")
if name == "main": main() ```
2. Specify Output Type¶
```python import numpy as np
def main(): def classify(x): if x < 0: return "negative" elif x == 0: return "zero" else: return "positive"
vec_classify = np.vectorize(classify, otypes=[object])
a = np.array([-2, 0, 3])
result = vec_classify(a)
print(f"a = {a}")
print(f"Classification: {result}")
if name == "main": main() ```
3. Excluded Arguments¶
```python import numpy as np
def main(): def power_with_offset(x, n, offset): return x ** n + offset
# Exclude 'offset' from vectorization
vec_func = np.vectorize(power_with_offset, excluded=['offset'])
a = np.array([1, 2, 3, 4])
result = vec_func(a, 2, offset=10)
print(f"a = {a}")
print(f"Result: {result}")
if name == "main": main() ```
Performance Note¶
1. vectorize is Not Fast¶
```python import numpy as np import time
def main(): def square(x): return x ** 2
vec_square = np.vectorize(square)
a = np.random.randn(1_000_000)
# Vectorized (slow)
start = time.perf_counter()
result1 = vec_square(a)
vec_time = time.perf_counter() - start
# Native ufunc (fast)
start = time.perf_counter()
result2 = np.square(a)
native_time = time.perf_counter() - start
print(f"np.vectorize: {vec_time:.4f} sec")
print(f"np.square: {native_time:.6f} sec")
print(f"Speedup: {vec_time/native_time:.0f}x")
if name == "main": main() ```
2. When to Use¶
np.vectorize: Convenience, not performance- Use native ufuncs when available
- For performance: use NumPy operations or Numba
3. Documentation Quote¶
The
vectorizefunction is provided primarily for convenience, not for performance. The implementation is essentially a for loop.
Ufunc Attributes¶
1. nin and nout¶
```python import numpy as np
def main(): print(f"np.add.nin = {np.add.nin}") # 2 inputs print(f"np.add.nout = {np.add.nout}") # 1 output print() print(f"np.sqrt.nin = {np.sqrt.nin}") # 1 input print(f"np.sqrt.nout = {np.sqrt.nout}") # 1 output print() print(f"np.divmod.nin = {np.divmod.nin}") # 2 inputs print(f"np.divmod.nout = {np.divmod.nout}") # 2 outputs
if name == "main": main() ```
2. ntypes¶
```python import numpy as np
def main(): print(f"np.add.ntypes = {np.add.ntypes}") print() print("Supported type signatures:") for sig in np.add.types[:5]: print(f" {sig}") print(" ...")
if name == "main": main() ```
Ufunc Methods¶
1. reduce¶
```python import numpy as np
def main(): a = np.array([1, 2, 3, 4, 5])
# Reduce applies operation cumulatively
result = np.add.reduce(a) # Same as np.sum
print(f"a = {a}")
print(f"np.add.reduce(a) = {result}")
print(f"np.sum(a) = {np.sum(a)}")
if name == "main": main() ```
2. accumulate¶
```python import numpy as np
def main(): a = np.array([1, 2, 3, 4, 5])
# Accumulate keeps intermediate results
result = np.add.accumulate(a) # Same as np.cumsum
print(f"a = {a}")
print(f"np.add.accumulate(a) = {result}")
print(f"np.cumsum(a) = {np.cumsum(a)}")
if name == "main": main() ```
3. outer¶
```python import numpy as np
def main(): a = np.array([1, 2, 3]) b = np.array([10, 20])
# Outer applies operation to all pairs
result = np.multiply.outer(a, b)
print(f"a = {a}")
print(f"b = {b}")
print("np.multiply.outer(a, b) =")
print(result)
if name == "main": main() ```
4. at¶
```python import numpy as np
def main(): a = np.array([1, 2, 3, 4, 5]) indices = np.array([0, 2, 2]) # Note: index 2 appears twice
# Add 10 at specified indices (in-place)
np.add.at(a, indices, 10)
print(f"Result: {a}")
# Index 2 was incremented twice!
if name == "main": main() ```
Common Ufuncs¶
1. Math Operations¶
| Ufunc | Description |
|---|---|
np.add |
Addition |
np.subtract |
Subtraction |
np.multiply |
Multiplication |
np.divide |
Division |
np.power |
Power |
np.sqrt |
Square root |
np.abs |
Absolute value |
2. Trigonometric¶
| Ufunc | Description |
|---|---|
np.sin |
Sine |
np.cos |
Cosine |
np.tan |
Tangent |
np.arcsin |
Inverse sine |
np.arctan2 |
Two-argument arctangent |
3. Comparison¶
| Ufunc | Description |
|---|---|
np.greater |
Greater than |
np.less |
Less than |
np.equal |
Equal |
np.maximum |
Element-wise maximum |
np.minimum |
Element-wise minimum |
Exercises¶
Exercise 1. Use np.add.reduce to compute the sum of [1, 2, 3, 4, 5]. Compare with np.sum.
Solution to Exercise 1
```python import numpy as np
arr = np.array([1, 2, 3, 4, 5]) print(np.add.reduce(arr)) # 15 print(np.sum(arr)) # 15 ```
Exercise 2. Use np.add.accumulate to compute the cumulative sum of [1, 2, 3, 4, 5]. Compare with np.cumsum.
Solution to Exercise 2
```python import numpy as np
arr = np.array([1, 2, 3, 4, 5]) print(np.add.accumulate(arr)) # [ 1 3 6 10 15] print(np.cumsum(arr)) # [ 1 3 6 10 15] ```
Exercise 3. Use np.add.outer to create an addition table for digits 1 through 5. Print the 5x5 result.
Solution to Exercise 3
```python import numpy as np
digits = np.arange(1, 6) table = np.add.outer(digits, digits) print(table) ```
Exercise 4. Write a custom ufunc using np.frompyfunc that converts Celsius to Fahrenheit. Apply it to an array of temperatures.
Solution to Exercise 4
```python import numpy as np
celsius_to_fahrenheit = np.frompyfunc(lambda c: c * 9/5 + 32, 1, 1) temps_c = np.array([0, 20, 37, 100]) temps_f = celsius_to_fahrenheit(temps_c) print(temps_f) # [32.0, 68.0, 98.6, 212.0] ```