Topic 3 UFunction
Topic 3 UFunction
def compute_reciprocals(values):
output = np.empty(len(values))
for i in range(len(values)):
output[i] = 1.0 / values[i]
return output
1.92 s ± 66.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
1.2 Bottleneck:
• the type-checking and function dispatches that CPython must do at each cycle of the loop.
• Each time the reciprocal is computed, Python first examines the object’s type and does a
dynamic lookup of the correct function to use for that type.
1
1.3 Introducing UFuncs
• NumPy provides a convenient interface into just this kind of statically typed, compiled rou-
tine. This is known as a vectorized operation.
• This can be accomplished by simply performing an operation on the array, which will then
be applied to each element.
• This vectorized approach is designed to push the loop into the compiled layer that underlies
NumPy, leading to much faster execution.
[3]: print(compute_reciprocals(values))
print(1.0 / values)
2.75 ms ± 456 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
• Vectorized operations in NumPy are implemented via ufuncs, whose main purpose is to quickly
execute repeated operations on values in NumPy arrays.
• Ufuncs are extremely flexible – before we saw an operation between a scalar and an array,
but we can also operate between two arrays:
[5]: np.arange(5) / np.arange(1, 6)
And ufunc operations are not limited to one-dimensional arrays–they can also act on multi-
dimensional arrays as well:
[6]: x = np.arange(9).reshape((3, 3))
2 ** x
• Computations using vectorization through ufuncs are nearly always more efficient than their
counterpart implemented using Python loops, especially as the arrays grow in size.
• Any time you see such a loop in a Python script, you should consider whether it can be
replaced with a vectorized expression.
2
- *binary ufuncs*, which operate on two inputs.
[7]: x = np.arange(4)
print("x =", x)
print("x + 5 =", x + 5)
print("x - 5 =", x - 5)
print("x * 2 =", x * 2)
print("x / 2 =", x / 2)
print("x // 2 =", x // 2) # floor division
x = [0 1 2 3]
x + 5 = [5 6 7 8]
x - 5 = [-5 -4 -3 -2]
x * 2 = [0 2 4 6]
x / 2 = [0. 0.5 1. 1.5]
x // 2 = [0 0 1 1]
There is also a unary ufunc for negation, and a ** operator for exponentiation, and a % operator
for modulus:
[8]: print("-x = ", -x)
print("x ** 2 = ", x ** 2)
print("x % 2 = ", x % 2)
-x = [ 0 -1 -2 -3]
x ** 2 = [0 1 4 9]
x % 2 = [0 1 0 1]
In addition, these can be strung together however you wish, and the standard order of operations
is respected:
[9]: -(0.5*x + 1) ** 2
Each of these arithmetic operations are simply convenient wrappers around specific functions built
into NumPy; for example, the + operator is a wrapper for the add function:
[10]: np.add(x, 2)
3
Operator Equivalent ufunc Description
* np.multiply Multiplication (e.g., 2 * 3 = 6)
/ np.divide Division (e.g., 3 / 2 = 1.5)
// np.floor_divide Floor division (e.g., 3 // 2 = 1)
** np.power Exponentiation (e.g., 2 ** 3 = 8)
% np.mod Modulus/remainder (e.g., 9 % 4 = 1)
The corresponding NumPy ufunc is np.absolute, which is also available under the alias np.abs:
[12]: np.absolute(x)
[13]: np.abs(x)
This ufunc can also handle complex data, in which the absolute value returns the magnitude:
[14]: x = np.array([3 - 4j, 4 - 3j, 2 + 0j, 0 + 1j])
np.abs(x)
4
theta = [0. 1.57079633 3.14159265]
sin(theta) = [0.0000000e+00 1.0000000e+00 1.2246468e-16]
cos(theta) = [ 1.000000e+00 6.123234e-17 -1.000000e+00]
tan(theta) = [ 0.00000000e+00 1.63312394e+16 -1.22464680e-16]
The values are computed to within machine precision, which is why values that should be zero do
not always hit exactly zero. Inverse trigonometric functions are also available:
[17]: x = [-1, 0, 1]
print("x = ", x)
print("arcsin(x) = ", np.arcsin(x))
print("arccos(x) = ", np.arccos(x))
print("arctan(x) = ", np.arctan(x))
x = [-1, 0, 1]
arcsin(x) = [-1.57079633 0. 1.57079633]
arccos(x) = [3.14159265 1.57079633 0. ]
arctan(x) = [-0.78539816 0. 0.78539816]
x = [1, 2, 3]
e^x = [ 2.71828183 7.3890561 20.08553692]
2^x = [2. 4. 8.]
3^x = [ 3 9 27]
The inverse of the exponentials, the logarithms, are also available. The basic np.log gives the
natural logarithm; if you prefer to compute the base-2 logarithm or the base-10 logarithm, these
are available as well:
[19]: x = [1, 2, 4, 10]
print("x =", x)
print("ln(x) =", np.log(x))
print("log2(x) =", np.log2(x))
print("log10(x) =", np.log10(x))
x = [1, 2, 4, 10]
ln(x) = [0. 0.69314718 1.38629436 2.30258509]
log2(x) = [0. 1. 2. 3.32192809]
log10(x) = [0. 0.30103 0.60205999 1. ]
There are also some specialized versions that are useful for maintaining precision with very small
input:
5
[20]: x = [0, 0.001, 0.01, 0.1]
print("exp(x) - 1 =", np.expm1(x))
print("log(1 + x) =", np.log1p(x))