Navigation

Python

How to Use NumPy Universal Functions (ufuncs)

Unlock NumPy's secret weapon - universal functions that apply operations element-wise at C speed across any array shape.

Table Of Contents

The Speed of C, The Simplicity of Python

Universal functions (ufuncs) are NumPy's performance powerhouse. They vectorize operations, eliminating slow Python loops while maintaining readable code.

Understanding Universal Functions

import numpy as np

# Built-in ufuncs work on scalars and arrays seamlessly
x = np.array([1, 2, 3, 4, 5])

# Mathematical ufuncs
print(np.sqrt(x))           # [1.0 1.414 1.732 2.0 2.236]
print(np.exp(x))            # [2.718 7.389 20.086 54.598 148.413]
print(np.sin(x))            # [0.841 0.909 0.141 -0.757 -0.959]

# Works with any shape
matrix = np.array([[1, 4, 9], [16, 25, 36]])
print(np.sqrt(matrix))
# [[1. 2. 3.]
#  [4. 5. 6.]]

# Ufuncs broadcast automatically
scalar_result = np.add(x, 10)  # Adds 10 to each element
print(scalar_result)  # [11 12 13 14 15]

Creating Custom Universal Functions

# Define a Python function
def custom_function(x):
    """Custom mathematical function"""
    return x**3 + 2*x**2 - x + 1

# Vectorize it into a ufunc
custom_ufunc = np.vectorize(custom_function)

# Now it works on arrays at C speed
data = np.array([1, 2, 3, 4, 5])
result = custom_ufunc(data)
print(result)  # [3 19 58 129 246]

# Works with multiple inputs too
def distance_formula(x1, y1, x2, y2):
    return np.sqrt((x2-x1)**2 + (y2-y1)**2)

distance_ufunc = np.vectorize(distance_formula)

# Calculate distances between multiple points
x1_coords = np.array([0, 1, 2])
y1_coords = np.array([0, 1, 2])
x2_coords = np.array([3, 4, 5])
y2_coords = np.array([4, 5, 6])

distances = distance_ufunc(x1_coords, y1_coords, x2_coords, y2_coords)
print(distances)  # [5.0 5.0 5.0]

High-Performance ufunc Creation

# Using numba for even faster custom ufuncs
try:
    from numba import vectorize
    
    @vectorize(['float64(float64)'], target='cpu')
    def fast_sigmoid(x):
        return 1.0 / (1.0 + np.exp(-x))
    
    # Ultra-fast custom ufunc
    data = np.linspace(-10, 10, 1000000)
    result = fast_sigmoid(data)  # Blazing fast!
    
except ImportError:
    print("Install numba for maximum performance: pip install numba")

Advanced ufunc Operations

# Reduce operations
data = np.array([1, 2, 3, 4, 5])

# Apply ufunc along axis (reduction)
sum_result = np.add.reduce(data)        # Same as np.sum()
product_result = np.multiply.reduce(data)  # Same as np.prod()
print(f"Sum: {sum_result}")       # 15
print(f"Product: {product_result}")  # 120

# Accumulate (running total)
cumulative_sum = np.add.accumulate(data)
print(f"Cumulative sum: {cumulative_sum}")  # [1 3 6 10 15]

# Outer products
a = np.array([1, 2, 3])
b = np.array([10, 20])
outer_add = np.add.outer(a, b)
print(outer_add)
# [[11 21]
#  [12 22]
#  [13 23]]

Conditional and Logical ufuncs

# Logical ufuncs
arr1 = np.array([True, False, True, False])
arr2 = np.array([True, True, False, False])

# Logical operations
print(np.logical_and(arr1, arr2))  # [True False False False]
print(np.logical_or(arr1, arr2))   # [True True True False]
print(np.logical_not(arr1))        # [False True False True]

# Comparison ufuncs
x = np.array([1, 5, 3, 8, 2])
y = np.array([2, 4, 3, 6, 1])

print(np.greater(x, y))      # [False True False True True]
print(np.equal(x, y))        # [False False True False False]
print(np.maximum(x, y))      # [2 5 3 8 2] - element-wise max

Performance Comparison

import time

# Large array for performance testing
large_array = np.random.rand(1000000)

# Pure Python approach (slow)
start = time.time()
python_result = [x**2 + 2*x + 1 for x in large_array]
python_time = time.time() - start

# NumPy ufunc approach (fast)
start = time.time()
numpy_result = large_array**2 + 2*large_array + 1
numpy_time = time.time() - start

print(f"Python loop: {python_time:.4f}s")
print(f"NumPy ufunc: {numpy_time:.4f}s")
print(f"Speedup: {python_time/numpy_time:.1f}x faster")

# Custom ufunc approach
custom_poly = np.vectorize(lambda x: x**2 + 2*x + 1)
start = time.time()
custom_result = custom_poly(large_array)
custom_time = time.time() - start
print(f"Custom ufunc: {custom_time:.4f}s")

Real-World Applications

# Financial calculations
def black_scholes_call(S, K, T, r, sigma):
    """Simplified Black-Scholes call option pricing"""
    d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
    d2 = d1 - sigma*np.sqrt(T)
    
    from scipy.stats import norm
    call_price = S*norm.cdf(d1) - K*np.exp(-r*T)*norm.cdf(d2)
    return call_price

# Vectorize for portfolio calculations
bs_ufunc = np.vectorize(black_scholes_call)

# Price multiple options at once
stock_prices = np.array([100, 105, 110, 95, 115])
strike_price = 100
time_to_expiry = 0.25
risk_free_rate = 0.05
volatility = 0.2

option_prices = bs_ufunc(stock_prices, strike_price, time_to_expiry, 
                        risk_free_rate, volatility)
print(f"Option prices: {option_prices}")

ufunc Methods and Attributes

# Explore ufunc properties
add_ufunc = np.add

print(f"Number of inputs: {add_ufunc.nin}")     # 2
print(f"Number of outputs: {add_ufunc.nout}")   # 1
print(f"Data types: {add_ufunc.types}")         # Available type combinations

# Identity element for reductions
print(f"Add identity: {add_ufunc.identity}")    # 0
print(f"Multiply identity: {np.multiply.identity}")  # 1

# At method for advanced indexing
arr = np.array([1, 2, 3, 4, 5])
indices = np.array([0, 2, 4])
values = np.array([10, 30, 50])
np.add.at(arr, indices, values)  # Add values at specific indices
print(arr)  # [11 2 33 4 55]

Performance Optimization Tips

  • Chain ufuncs to avoid temporary arrays: np.sqrt(x**2 + y**2)
  • Use built-in ufuncs when possible (they're highly optimized)
  • Consider numba for compute-intensive custom functions
  • Use out parameter to avoid memory allocation

Master Advanced NumPy

Explore advanced array manipulation, learn scientific computing patterns, and dive into high-performance numerical methods.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Python