# Numpy

Numpy is a Python package for efficient numerical computations e.g. on vectors or matrices. The full documentation of Numpy is available here. To use Numpy, we first have to import it. Usually one imports `numpy as np`, where `np` is an alias (so we don’t have to type `numpy` whenever we access a functonality).

``````import numpy as np
``````

### Arrays

Now we have access to the Numpy functionalities. We start with creating a simple one-dimensional array. To do that, we use the function `np.array(e)` where `e` is a list of the elements, we want to have in the array:

``````e = [1, 2, 3]     # list of elements
v = np.array(e)   # or short: np.array([1,2,3])
print(v, type(v))
``````
``````[1 2 3] <class 'numpy.ndarray'>
``````

Recall the `IPython` feature `?` which can be used to show a documentation. We can use this to get some informations about the function `array()` and the class `ndarray`:

``````np.array?
``````
``````np.ndarray?
``````

Numpy arrays have attributes which describe their shape and the data type of the elements inside:

• `ndim` (the number of dimensions)
• `shape` (a tuple containing the size of each dimension)
• `size` (the number of elements)
• `dtype` (data type of the elements)

Attributes of objects are accessed via the dot-operator:

``````print('number of dimensions:  ', v.ndim)
print('sizes of dimensions:   ', v.shape)
print('number of elements:    ', v.size)
print('data type of elements: ', v.dtype)
``````
``````number of dimensions:   1
sizes of dimensions:    (3,)
number of elements:     3
data type of elements:  int32
``````

You may expected the data type to be `int` but Numpy has much more data types than Python, and devides e.g. integers into 8-bit, 32-bit and 64-bit integers. A list of Numpys data types is available here.

### Matrices

In the next cell we will create a 2D matrix. Again we will call `np.array(e)` but this time `e` is a list of lists, where each inner list will be one row in the matrix.

``````r0 = [1.0, 2.0]   # first row
r1 = [3.0, 4.0]   # second row
r2 = [5.0, 6.0]   # third row
e = [r0, r1, r2]  # a list, containing all rows as lists

M = np.array(e)   # or short: np.array([[1.0,2.0],[3.0,4.0],[5.0,6.0]])
print(M)
``````
``````[[1. 2.]
[3. 4.]
[5. 6.]]
``````
``````print('number of dimensions:  ', M.ndim)
print('sizes of dimensions:   ', M.shape)
print('number of elements:    ', M.size)
print('data type of elements: ', M.dtype)
``````
``````number of dimensions:   2
sizes of dimensions:    (3, 2)
number of elements:     6
data type of elements:  float64
``````

### Vectors

When using Numpy one should think of vectors as two dimensional arrays, where the first or second dimension has a length of 1. A simple way to create such a vector-like array is to place additional brackets:

``````vr = np.array([[1,2,3]])      # list containing one list with multiple values -> row-vector
vc = np.array([,,])  # list containing multiple lists with one value each -> column vector

print('vr:\n', vr)
print('vc:\n', vc)
``````
``````vr:
[[1 2 3]]
vc:
[

]
``````

Another way to create such a vector is to reshape a 1D array by using the `reshape()` method. We can reshape any array to change the number of dimensions or / and the length of each dimension with `x.reshape(n)` where `x` is the array to reshape and `n` is the new shape as a tuple. Note that `reshape()` will not modify the array but will instead return a new array. Reshaping arrays is valid as long as the number of elements in the old shape match the number of elements in the new shape.

``````v = np.array([1, 2, 3])   # one dimensional array
vc = v.reshape([3, 1])    # reshape v to a 3 x 1 column vector
vr = v.reshape([1, 3])    # reshape v to a 1 x 3 row vector

print('vr:\n', vr)
print('vc:\n', vc)
``````
``````vr:
[[1 2 3]]
vc:
[

]
``````

Note: Instead of declaring the length of each new dimension, Numpy is able to automatically detect the length of one dimension, denoted by `-1`:

``````M = np.array(((1, 2),(3,4)))
v = M.reshape((1, -1))         # new shape has 2 dims: first has a length of 1, second is fitted automatically
print(v)
``````
``````[[1 2 3 4]]
``````

### Other methods to create arrays

The `np.array()` method is not the only way to create a numpy array. The following blocks demonstrate some auxiliary functions to create arrays. Many of these functions take a tuple containing the length and number of dimensions as first argument.

``````v = np.arange(5, 15, 2)      # arg. like range(): (1-3 args: start | start,stop | start,stop,step)
print(v)
``````
``````[ 5  7  9 11 13]
``````
``````Z = np.zeros((2, 3))         # arg.: tuple of dimensions
print(Z)
``````
``````[[0. 0. 0.]
[0. 0. 0.]]
``````
``````O = np.ones((2, 4))          # arg.: tuple of dimensions
print(O)
``````
``````[[1. 1. 1. 1.]
[1. 1. 1. 1.]]
``````
``````E = np.random.random((2, 5)) # arg.: tuple of dimensions
print(E)
``````
``````[[0.53476819 0.38233119 0.10767903 0.6316657  0.10922429]
[0.73079033 0.36157971 0.65834157 0.08548247 0.83854358]]
``````
``````E = np.eye(2)                # arg.: single integer
print(E)
``````
``````[[1. 0.]
[0. 1.]]
``````

An overview of most ways to create Numpy arrays is given here.

Note: So far we used (nested) lists to create arrays with explicit values and tuples to pass the dimensions. It is also valid to pass the values as (nested) tuples or the dimensions as list. The way we used it so far is the common way.

### Explicid data types

Most of the above methods to create arrays have the optional keyword parameter `dtype`, which can be used to explicitly create an array of a specific data type. If the `dtype` argument is not used, Numpy will automatically try to detect a valid data type and cast all elements to this type. The following code shows the usage of explicid data types:

``````# creating a 2 x 3 array with ones and casting it to bool
B = np.ones((2, 3), dtype=np.bool_)
print(B)
``````
``````[[ True  True  True]
[ True  True  True]]
``````
``````# creating a 2 x 3 array with zeros and casting it to integer
B = np.zeros((2, 3), dtype=np.int64)
print(B)
``````
``````[[0 0 0]
[0 0 0]]
``````

#### Changing data types

To change the data type of an array we can use the method `astype()` as shown below:

``````M = np.ones((1, 5))
print(M, M.dtype)

N = M.astype(np.bool_) # convert each element in M to bool
print(N, N.dtype)
``````
``````[[1. 1. 1. 1. 1.]] float64
[[ True  True  True  True  True]] bool
``````

### Indexing and slicing

Accessing elements or slices of a Numpy array is very similar to Python lists or tuples. Differences occur, when we access elements in a multi dimensional array. In that case we denote the indices or slicing arguments for each axis seperated by commas:

``````M = np.array([[0, 1, 2], [3, 4, 5]]) # create a 2 x 3 array
print(M)
print('\nShape:',M.shape)
``````
``````[[0 1 2]
[3 4 5]]

Shape: (2, 3)
``````
``````elem = M[0, 1]       # get element at row 0 and column 1
print(elem)
``````
``````1
``````

Accessing slices (parts of an array) works in the same way:

``````s = M[0:1, 1:3]      # get slice from row 0 to 1 and column 1 to 3
print(s)
``````
``````[[1 2]]
``````

There are some special cases for slicing, that are demonstratet in the following cells:

``````s = M[:, 1:]         # get slice from every row and column from 1 (to end)
print(s)
``````
``````[[1 2]
[4 5]]
``````
``````s = M[:, 0]          # access 1st column of M (result will be a 1D array)
print(s)
``````
``````[0 3]
``````
``````s = M[:, 0:1:]       # access 1st column of M (result will be a column vector)
print(s)
``````
``````[
]
``````
``````s = M[:, ::2]        # get all rows and every second column
print(s)
``````
``````[[0 2]
[3 5]]
``````
``````s = M[::-1, ::-1]    # get all rows and all columns in reversed order (stepwith = -1)
print(s)
``````
``````[[5 4 3]
[2 1 0]]
``````
``````s = M[np.newaxis, :, :]     # adds another dimensions. Same as M[None,:,:]
print(s)
print('\nShape:',s.shape)
``````
``````[[[0 1 2]
[3 4 5]]]

Shape: (1, 2, 3)
``````

### Editing

Like with lists, we can use this indexing / slicing syntax to edit single elements and slices:

``````N = np.ones((2, 3))
N[0, 0] = 0          # set the element at first row and first column to 0
print(N)
``````
``````[[0. 1. 1.]
[1. 1. 1.]]
``````
``````N[:, -1] = 2         # set every element in the last column to 2
print(N)
``````
``````[[0. 1. 2.]
[1. 1. 2.]]
``````
``````N[-1, :] = (3, 4, 5) # set the elements in the last row to (3, 4, 5)
print(N)
``````
``````[[0. 1. 2.]
[3. 4. 5.]]
``````

### Attention! References!

When a slice or a channel of an array is accessed usually a reference (a view) is returned. This implies, that changing a part of `A` will also change `A`! We can avoid this by creating a copy before changing values:

``````N = np.ones([2, 4])
row1 = N[0, :]           # first row of N (refernce)
row2 = np.copy(N[1, :])  # copy of second row

row1 *= 0     # changes N
row2 *= 0     # does not change N
print(N)
``````
``````[[0. 0. 0. 0.]
[1. 1. 1. 1.]]
``````

### Array operations

#### Arithmetic operations

All arithmetic Python operations with scalars can be directly applied to matrices in an element wise fashion:

``````M = np.ones((2,2))
M = M + 1          # element wise addition
M = M * 2          # element wise multiplication
M = M ** 3         # element wise exponentiation
# and so on...
print(M)
``````
``````[[64. 64.]
[64. 64.]]
``````

Arithmetic operations can also be applied to arrays with the same shape (element wise)

``````M = np.ones((2,2))      # array with ones
N = np.ones((2,2)) * 2  # array with twos
S = M + N               # element wise sum
print(S)
``````
``````[[3. 3.]
[3. 3.]]
``````

Numpy also provides methods for matrix multiplications, the cross product and to transpose arrays:

``````v = np.arange(3).reshape(-1, 1)  # create a column vector with values (0,1,2)
print(v)
``````
``````[

]
``````
``````vt = v.T         # vt will be the transposed of v
print(vt)
``````
``````[[0 1 2]]
``````
``````vvt = v.dot(vt)  # matrix multiplication of v and vt
print(vvt)
``````
``````[[0 0 0]
[0 1 2]
[0 2 4]]
``````
``````v = np.array([9, 2, 0])
w = np.array([5, 5, 0])
c = np.cross(v, w)       # cross product of v and w (length of last dimension must be 2 or 3)
print(c)
``````
``````[ 0  0 35]
``````

#### Comparisions

Comparing an array against a scalar or against a second array with the same shape will result in a boolean array that contains the results of a element wise comparision.

``````M = np.arange(9).reshape((3, 3))
print(M,'\n')
print(M > 4)
``````
``````[[0 1 2]
[3 4 5]
[6 7 8]]

[[False False False]
[False False  True]
[ True  True  True]]
``````
``````M = np.array([1, 2])
N = np.array([2, 2])

print('M =', M, ', N =',N)
print('M == N:', M == N)
print('M > N: ', M > N)
print('M <= N:', M <= N)
``````
``````M = [1 2] , N = [2 2]
M == N: [False  True]
M > N:  [False False]
M <= N: [ True  True]
``````

The resulting bool arrays can then be passed to

• `np.any(B)` (returns true, if at least one element in B is true)
• `np.all(B)` (returns true, if all elements in B are true)

For example can we use an element wise comparision and `np.all()` to check if two matrices are identical:

``````M = np.array((1,2,3))
N = np.array((1,2,3))
b = np.all(M==N)
print(b)

# cleaner way:
print(np.array_equal(M,N))
``````
``````True
True
``````

#### More Numpy features

The functionalities listed above are only a very small part of Numpy. There are also additional modules like linalg which contains e.g. functions to compute eigenvectors, invert matrices or to solve equations. To get an overview of all functions I recommend to start at the documentation.