Before defining what `ufunc`

are, we need to know why we need `ufunc`

at first place. In Python, `for`

loops are used to run operation on each item in the list. However, `for`

loops are very slow as it runs on each item in the list, one-by-one.

On the other hand, we can make this iteration process faster by applying **vectorized operations** using NumPy universal functions `ufunc`

, whose main purpose is to quickly execute repeated operations on values in NumPy arrays.

Vectorization is the process of converting an algorithm

fromoperating on a single value at a timetooperating on a set of values at one time.

## 1. UFUNC BASICS

### 1.1. Arithmetics

In the following examples, we will work with basic arithmetic operations like addition, subtraction, multiplication, division, square etc, on the NumPy array

```
import numpy as np
# creating an array first, using arange
arr = np.arange(1,11)
print(f"Array on which to perform ufunc: \n{arr}")
# addition
print(f"array + 1 is:\n{arr+1}")
# subtration
print(f"array - 1 is:\n{arr-1}")
# multiplication
print(f"array * 5 is:\n{arr*5}")
# division
print(f"array / 2 is:\n{arr/5}")
# negative * array
print(f"-array is:\n{-arr}")
# square
print(f"array sqaure is:\n{arr**2}")
# modulus operator
print(f"array % is:\n{arr%2}")
```

```
Array on which to perform ufunc:
[ 1 2 3 4 5 6 7 8 9 10]
array + 1 is:
[ 2 3 4 5 6 7 8 9 10 11]
array - 1 is:
[0 1 2 3 4 5 6 7 8 9]
array * 5 is:
[ 5 10 15 20 25 30 35 40 45 50]
array / 2 is:
[0.2 0.4 0.6 0.8 1. 1.2 1.4 1.6 1.8 2. ]
-array is:
[ -1 -2 -3 -4 -5 -6 -7 -8 -9 -10]
array sqaure is:
[ 1 4 9 16 25 36 49 64 81 100]
array % is:
[1 0 1 0 1 0 1 0 1 0]
```

We can use `np.abs`

function to find the *absolute value* of NumPy array elements;

```
absolute = np.array([-2,-1,0,1,2])
np.absolute(absolute)
```

```
array([2, 1, 0, 1, 2])
```

### 1.2. Trigonometric Functions

These trigonometric functions work on radians, so if we have an array of angles, we first need to convert it to radians by using `np.deg2rad(angles)`

function. Once we have all array value in radians, only then we can call trigonometric functions.

```
# creating array of angles
angles = np.array([0,30,45,60,90])
print(f"We have an array of following angles:\n{angles}")
rad = np.deg2rad(angles)
print(f"After converting degree into radian:\n{rad}")
# finding, sin, cos and tan of these angles, through radians
sin = np.sin(rad)
print(f"Sine of angles:\n{sin}")
cos = np.cos(rad)
print(f"Cosine of angles:\n{cos}")
tan = np.tan(rad)
print(f"Tangent of angles:\n{tan}")
```

```
We have an array of following angles:
[ 0 30 45 60 90]
After converting degree into radian:
[0. 0.52359878 0.78539816 1.04719755 1.57079633]
Sine of angles:
[0. 0.5 0.70710678 0.8660254 1. ]
Cosine of angles:
[1.00000000e+00 8.66025404e-01 7.07106781e-01 5.00000000e-01
6.12323400e-17]
Tangent of angles:
[0.00000000e+00 5.77350269e-01 1.00000000e+00 1.73205081e+00
1.63312394e+16]
```

Now, we will demonstrate the use of `np.arcsin`

, `np.arcos`

, and `np.arctan`

functions to return the trigonometric inverse of `sin`

, `cos`

, and `tan`

values of angle in the array

```
angle_sin = np.rad2deg(np.arcsin(sin))
print(f"We got back the angles, from arcsin and rad2degree:\n{angle_sin}")
angle_cos = np.rad2deg(np.arccos(cos))
print(f"We got back the angles, from arccos and rad2degree:\n{angle_cos}")
angle_tan = np.rad2deg(np.arctan(tan))
print(f"We got back the angles, from arctan and rad2degree:\n{angle_tan}")
```

```
We got back the angles, from arcsin and rad2degree:
[ 0. 30. 45. 60. 90.]
We got back the angles, from arccos and rad2degree:
[ 0. 30. 45. 60. 90.]
We got back the angles, from arctan and rad2degree:
[ 0. 30. 45. 60. 90.]
```

### 1.3. Statistics Functions

All of the following statistical functions can be applied, along axis of your choice using optional `axes`

kwarg

```
# creating array of random numbers
rand = np.random.RandomState(42)
ar_s = rand.randint(1000, size=(100,100))
print(f"Min value in the array: {np.min(ar_s)}")
print(f"Max value in the array: {np.max(ar_s)}")
print(f"Sum of all values in the array: {np.sum(ar_s)}")
print(f"Range of values, max-min, in the array: {np.ptp(ar_s)}")
print(f"70th percentile in the array: {np.percentile(ar_s, 75)}")
print(f"Mean value of the array: {np.mean(ar_s)}")
print(f"Median value of the array: {np.median(ar_s)}")
print(f"Standard Deviation of the array: {np.std(ar_s)}")
print(f"Variance of the array: {np.var(ar_s)}")
```

```
Min value in the array: 0
Max value in the array: 999
Sum of all values in the array: 5034706
Range of values, max-min, in the array: 999
70th percentile in the array: 757.0
Mean value of the array: 503.4706
Median value of the array: 505.5
Standard Deviation of the array: 289.70994725007284
Variance of the array: 83931.85353564
```

### 1.4. Log and Exponent

Let’s see how to calculate exponential of elements in a Numpy array:

```
# array
arr_el = np.array([1,2,3,4,5])
print(f"Array, we are working with: {arr_el}")
print(f"Exponent: {np.exp(arr_el)}")
print(f"E^2: {np.exp2(arr_el)}")
print(f"E^10: {np.power(10, arr_el)}")
```

```
Array, we are working with: [1 2 3 4 5]
Exponent: [ 2.71828183 7.3890561 20.08553692 54.59815003 148.4131591 ]
E^2: [ 2. 4. 8. 16. 32.]
E^10: [ 10 100 1000 10000 100000]
```

Now, let’s take log of each of these values -natural log, log with base 2 and 10. As we are applying these log on their corresponding exponent values, we will get our original array back in the output.

```
print(f"Natural Log, on values from E^: {np.log(np.exp(arr_el))}")
print(f"Log with base 2, on values from E^2: {np.log2(np.exp2(arr_el))}")
print(f"Log with base 10, on values from E^10: {np.log10(np.power(10, arr_el))}")
```

```
Natural Log, on values from E^: [1. 2. 3. 4. 5.]
Log with base 2, on values from E^2: [1. 2. 3. 4. 5.]
Log with base 10, on values from E^10: [1. 2. 3. 4. 5.]
```

## 2. BEYOND BASIC UFUNC

### 2.1. Specifying output

We can specify the output location, to save the array

```
input_arr = np.arange(1,6)
output_arr = np.empty(5)
# using np.multiply function and saving output in empty array
np.multiply(input_arr, 10, out=output_arr)
```

```
array([10., 20., 30., 40., 50.])
```

### 2.2. Reduce

In this section, we will discuss `.reduce`

method which repeatedly applies a given operation (let suppose, add or multiply) to all the elements of an array and give the final result. What? Keep reading to understand `reduce`

method and how it differs from its conceptual equivalents.

#### a. 1D

For taking sum, both `np.add.reduce`

and `np.sum`

produces the same result (for 1D array) but former is faster than later. The same logic goes for the results of `np.multiply.reduce`

and its equivalent `np.prod`

```
# creating array
r = np.arange(1,11)
print(f"1D array to start with:\n{r}")
print(f"Adding all elements and reduce: \n{np.add.reduce(r)}")
print(f"Same result as np.sum: \n{np.sum(r)}")
print(f"Multiply all elements and reduce: \n{np.multiply.reduce(r)}")
print(f"Same result as np.prod: \n{np.prod(r)}")
```

```
Array to start with:
[ 1 2 3 4 5 6 7 8 9 10]
Adding all elements and reduce:
55
Same result as np.sum:
55
Multiply all elements and reduce:
3628800
Same result as np.prod:
3628800
```

#### b. 2D

However, for 2D array, these above mentioned equivalence isn’t there, in terms of final results.

```
# creating a 2D array and see how the result differs
r2d = np.arange(1,7).reshape(2,3)
print(f"2D array to start with:\n{r2d}")
print(f"Adding all elements and reduce: \n{np.add.reduce(r2d)}")
print(f"Result as np.sum: \n{np.sum(r2d)}")
print(f"Multiply all elements and reduce: \n{np.multiply.reduce(r2d)}")
print(f"Result as np.prod: \n{np.prod(r2d)}")
```

```
2D array to start with:
[[1 2 3]
[4 5 6]]
Adding all elements and reduce:
[5 7 9]
Result as np.sum:
21
Multiply all elements and reduce:
[ 4 10 18]
Result as np.prod:
720
```

Besides, we can use the optional `axis`

keyword argument for all of functions above to perform the operation on a given axis. For `.reduce`

method, the default value of `axis=0`

### 2.3. Accumulate

`np.accumulate`

function perform a given operation (let suppose, add or multiply) and passes its accumulated resulted to the next element in the array. Therefore, we can see how, we reached at the final result in the array.

#### a. 1D

However, for 1D arrays, `np.add.accumulate`

and `np.multiply.accumulate`

are logical equivalents to `np.cumsum`

and `np.cumprod`

```
print(f"1D array to start with:\n{r}")
# add and accumulate
print(f"Add and accumulate:\n{np.add.accumulate(r)}")
print(f"Same result as np.cumsum: \n{np.cumsum(r)}")
# multiply and accumulate
print(f"Multiply and accumulate:\n{np.multiply.accumulate(r)}")
print(f"Same result as np.cumprod: \n{np.cumprod(r)}")
```

```
1D array to start with:
[ 1 2 3 4 5 6 7 8 9 10]
Add and accumulate:
[ 1 3 6 10 15 21 28 36 45 55]
Same result as np.cumsum:
[ 1 3 6 10 15 21 28 36 45 55]
Multiply and accumulate:
[ 1 2 6 24 120 720 5040 40320 362880
3628800]
Same result as np.cumprod:
[ 1 2 6 24 120 720 5040 40320 362880
3628800]
```

#### b. 2D

However, for 2D array, these above mentioned equivalence isn’t there

```
print(f"2D array to start with:\n{r2d}")
# add and accumulate
print(f"Add and accumulate:\n{np.add.accumulate(r2d)}")
print(f"Result as np.cumsum: \n{np.cumsum(r2d)}")
# multiply and accumulate
print(f"Multiply and accumulate:\n{np.multiply.accumulate(r2d)}")
print(f"Result as np.cumprod: \n{np.cumprod(r2d)}")
```

```
2D array to start with:
[[1 2 3]
[4 5 6]]
Add and accumulate:
[[1 2 3]
[5 7 9]]
Result as np.cumsum:
[ 1 3 6 10 15 21]
Multiply and accumulate:
[[ 1 2 3]
[ 4 10 18]]
Result as np.cumprod:
[ 1 2 6 24 120 720]
```

Besides, we can use the optional `axis`

keyword argument for all of functions above to perform the operation on a given axis. For `.reduce`

method, the default value of `axis=0`

### 2.4. Outer products

`.outer`

method can be applied to any `ufunc`

to compute the output of *all pairs of two different inputs*. Examples, will make the concept clear:

```
a1 = np.arange(4)
print(f"Array to work on:\n{a1}")
print(f"add.outer on array:\n{np.add.outer(a1,a1)}")
print(f"multiply.outer on array:\n{np.multiply.outer(a1,a1)}")
```

```
Array to work on:
[0 1 2 3]
add.outer on array:
[[0 1 2 3]
[1 2 3 4]
[2 3 4 5]
[3 4 5 6]]
multiply.outer on array:
[[0 0 0 0]
[0 1 2 3]
[0 2 4 6]
[0 3 6 9]]
```

For example, `np.add.outer(a1,a1)`

has added each element of `a1`

, on every element in the row, once in each row:

- first row is
`[0,1,2,3]`

which is`0`

added to every element in row - second rows is
`[1,2,3,4]`

which is`1`

added to every element in the row - third rows is
`[2,3,4,5]`

which is`2`

added to every element in the row - fourth rows is
`[3,4,5,6]`

which is`3`

added to every element in the row

## 1 thought on “Part 5: NUMPY’S UNIVERSAL FUNCTIONS”