# Numpy Universal functions *(ufuncs)*¶

In this lesson we'll learn about numpy "Universal Functions", or *ufuncs*. Ufuncs are functions that operate on arrays in an element-by-element basis, in a really efficient way.

In our previous lecture we introduced *Vectorized Operations*, which in turn, are one example of ufuncs. The important trait about ufuncs is that they're optimized internally using C code, which makes them REALLY fast and efficient.

We'll start by comparing the efficiency of regular "python loops" vs numpy vectorized operations (ufuncs).

## Hands on!¶

Let's start with an example: given an array of numbers, we want to compare the reciprocal of each element. Our first approach will be with a regular Python for-loop (example taken from Data Science Handbook):

```
import numpy as np
```

```
def compute_reciprocals(values):
output = np.empty(len(values))
for i in range(len(values)):
output[i] = 1.0 / values[i]
return output
```

```
values = np.random.randint(1, 999, size=10)
values
```

```
compute_reciprocals(values)
```

The numpy, vectorized operation counterpart is a lot easier to write:

```
1 / values
```

As you can see, it returns the same results. Numpy's vectorized operation version is a declarative one, compared to the for-loop based one, that is "imperative".

Now let's explore how much it takes to process a large array with our naive, loop based function:

```
big_array = np.random.randint(1, 999, size=1_000_000)
```

For loop version:

```
%time compute_reciprocals(big_array)
```

Numpy vectorized operation version:

```
%time (1 / big_array)
```

The vectorized operation is a lot faster (about 30 times faster); that is because it's implemented using a numpy ufunc, which is internally optimized as a C operation.

### Understanding NumPy ufuncs¶

Technically speaking, universal functions, are instances of the `numpy.ufunc`

class; many of which are implemented in C code.

They can be accessed from multiple interfaces; as we saw, you can use a regular operator with a ndarray (like `array + 3`

), or you can use function invocation `np.add`

:

```
%time np.divide(1, big_array)
```

```
%time (1 / big_array)
```

All the regular arithmetic operators applied to numpy arrays will be performed by ufuncs internally:

Operator | ufunc | Description |
---|---|---|

`+` |
`np.add` |
Addition (e.g., 1 + 1 = 2) |

`-` |
`np.subtract` |
Subtraction (e.g., 3 - 2 = 1) |

`-` |
`np.negative` |
Unary negation (e.g., -2) |

`*` |
`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) |

```
values + 10
```

```
np.add(values, 10)
```

### Other useful ufuncs¶

Aside from the regular operators described above, there are more ufuncs that are worth mentioning, for example:

#### Other basic arithmetic functions¶

```
np.abs(np.array([-5, -4, -3]))
```

```
values = np.arange(1, 6)
```

```
values
```

```
np.log(values)
```

```
np.log2(values)
```

```
np.log10(values)
```

```
np.exp(values)
```

```
np.exp2(values)
```

```
np.power(3, values)
```

#### Trigonometric functions¶

NumPy has standard trigonometric functions which return trigonometric ratios **for a given angle in radians**.

```
degrees = np.linspace(0, 360, 5)
degrees
```

Convert **degress** to **radians**:

```
radians = np.multiply(degrees, np.pi/180)
radians
```

Now calculate trigonometric functions using that radians:

```
np.sin(radians)
```

```
np.cos(radians)
```

```
np.tan(radians)
```