Table Of Contents
- The Speed of C, The Simplicity of Python
- Understanding Universal Functions
- Creating Custom Universal Functions
- High-Performance ufunc Creation
- Advanced ufunc Operations
- Conditional and Logical ufuncs
- Performance Comparison
- Real-World Applications
- ufunc Methods and Attributes
- Performance Optimization Tips
- Master Advanced NumPy
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!