## What is NumPy?

NumPy is a scientific library that provides multidimensional array object with fast routines on it. NumPy is short for Numerical Python.

When we talk about NumPy often we refer to the powerful **ndarray**, which is the multidimensional array (N-dimensional array).

A few comparisons between Python lists and ndarray.

ndarray | Python list |

Have fixed size at creation. | Is dynamic. You can add and remove elements. |

All elements have the same type. | Elements have type independent of each other. |

Can execute fast mathematical operations with simple syntax. | Need loops to make operations on each element. |

## Examples showing the difference: Fixed after creation

The Numpy is imported by default imported by **import numpy as np**. To create a ndarray, you can use the **array** call as defined below.

import numpy as np data = np.array([[1, 2, 3], [1, 2, 3]]) print(data)

Which will create an 2 dimensional array object with 2 times 3 elements.

[[1 2 3] [1 2 3]]

That will be a fixed sized **ndarray**. You cannot add new dimensions or elements to the the single arrays.

A Python list is a more flexible.

my_list = [] my_list.append(2) my_list.append(4) my_list.remove(2) print(my_list)

Which demonstrates the flexibility and power of Python lists. It is simple to add and remove elements. The above code will result in the following output.

[4]

## Examples showing the difference: One type

The type of a **ndarray** is stored in **dtype**. Interesting thing is that each element must have the same type.

import numpy as np data = np.random.randn(2, 3) print(data) print(data.dtype)

It will result in a random **ndarray** of type **float64**.

[[-0.85925182 -0.89247774 -2.40920842] [ 0.84647869 0.27631307 -0.80772023]] float64

An interesting way to demonstrate that only one type can be present in an ndarray, is by trying to create it with a mixture of **int**s and **float**s.

import numpy as np data = np.array([[1.0, 2, 3], [1, 2, 3]]) print(data) print(data.dtype)

As the first element is of type float they are all cast to float64.

[[1. 2. 3.] [1. 2. 3.]] float64

While the following list is valid.

my_list = [1.0, 2, 3] print(my_list)

Where the first element will be **float** the second and third element are **int**s.

[1.0, 2, 3]

## Examples showing the difference: No loops needed

Many operations can be made directly on the **ndarray**.

import numpy as np data = np.random.randn(2, 3) print(data) print(data*10) print(data + data)

Which will result in the following output.

[[ 1.18303358 -2.20017954 0.46294824] [-0.56508587 0.0990272 -1.8431866 ]] [[ 11.83033584 -22.00179538 4.62948243] [ -5.65085867 0.990272 -18.43186601]] [[ 2.36606717 -4.40035908 0.92589649] [-1.13017173 0.1980544 -3.6863732 ]]

Expected right? But easy to multiply and add out of the box.

Similar of the Python list would be.

my_list = [1, 2, 3] for i in range(len(my_list)): my_list[i] *= 10 for i in range(len(my_list)): my_list[i] += my_list[i]

And it is not even the same, as you write it directly to the old elements.

## Another way to compare differences

It might at first glance seem like **ndarrays** are inflexible with all the restrictions comparing the Python lists. Yes, that is true, but the benefit is the speed.

import time import numpy as np my_arr = np.arange(1000000) my_list = list(range(1000000)) start = time.time() for _ in range(10): my_arr2 = my_arr * 2 end = time.time() print(end - start) start = time.time() for _ in range(10): my_list2 = [x * 2 for x in my_list] end = time.time() print(end - start)

Which resulted in.

0.03456306457519531 0.9373760223388672

The advantage is that **ndarrays** are 10-100 times faster than Python lists, which makes a considerable impact on scientific calculations.