# NumPy

## Contents

# NumPy¶

## Environment setup¶

```
import platform
print(f"Python version: {platform.python_version()}")
assert platform.python_version_tuple() >= ("3", "6")
import numpy as np
print(f"NumPy version: {np.__version__}")
```

## Creating tensors¶

NumPy provides several useful functions for initializing tensors with particular values.

### Filling a tensor with zeros¶

```
x = np.zeros(3)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[0. 0. 0.]
Dimensions: 1
Shape: (3,)
```

```
x = np.zeros((3,4))
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]
Dimensions: 2
Shape: (3, 4)
```

### Filling a tensor with random numbers¶

Values are sampled from a “normal” (Gaussian) distribution

```
x = np.random.randn(5,2)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[[ 0.50961507 -0.66325773]
[-0.26615276 -0.61404259]
[ 0.85148899 -1.00384867]
[ 0.52699103 1.76971159]
[-1.03823767 -0.36072364]]
Dimensions: 2
Shape: (5, 2)
```

## Tensor shape management¶

```
x = np.array([12, 3, 6]) # x is a 3 dimensions vector (1D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[12 3 6]
Dimensions: 1
Shape: (3,)
```

### Tensors with single-dimensional entries¶

```
x = np.array([[12, 3, 6, 14]]) # x is a one row matrix (2D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[[12 3 6 14]]
Dimensions: 2
Shape: (1, 4)
```

```
x = np.array([[12], [3], [6], [14]]) # x is a one column matrix (2D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[[12]
[ 3]
[ 6]
[14]]
Dimensions: 2
Shape: (4, 1)
```

### Removing single-dimensional entries from a tensor¶

```
x = np.array([[12, 3, 6, 14]])
x = np.squeeze(x) # x is now a vector (1D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[12 3 6 14]
Dimensions: 1
Shape: (4,)
```

```
x = np.array([[12], [3], [6], [14]])
x = np.squeeze(x) # x is now a vector (1D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[12 3 6 14]
Dimensions: 1
Shape: (4,)
```

### Reshaping a tensor¶

```
# Reshape a 3D tensor into a matrix
x = np.array([[[5, 6],
[7, 8]],
[[9, 10],
[11, 12]],
[[13, 14],
[15, 16]]])
print (f'Original dimensions: {x.ndim}')
print (f'Original shape: {x.shape}')
x = x.reshape(3, 2*2)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
Original dimensions: 3
Original shape: (3, 2, 2)
[[ 5 6 7 8]
[ 9 10 11 12]
[13 14 15 16]]
Dimensions: 2
Shape: (3, 4)
```

### Adding a dimension to a tensor¶

```
# Add a dimension to a vector, turning it into a row matrix
x = np.array([1, 2, 3])
print (f'Original dimensions: {x.ndim}')
print (f'Original shape: {x.shape}')
x = x[np.newaxis, :]
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
Original dimensions: 1
Original shape: (3,)
[[1 2 3]]
Dimensions: 2
Shape: (1, 3)
```

```
# Add a dimension to a vector, turning it into a column matrix
x = np.array([1, 2, 3])
print (f'Original dimensions: {x.ndim}')
print (f'Original shape: {x.shape}')
x = x[:, np.newaxis]
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
Original dimensions: 1
Original shape: (3,)
[[1]
[2]
[3]]
Dimensions: 2
Shape: (3, 1)
```

### Transposing a tensor¶

```
# Transpose a vector (no effect)
x = np.array([12, 3, 6, 14])
x = x.T # alternative syntax: x = np.transpose(x)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[12 3 6 14]
Dimensions: 1
Shape: (4,)
```

```
# Transpose a matrix
x = np.array([[5, 78, 2, 34],
[6, 79, 3, 35],
[7, 80, 4, 36]])
x = x.T
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[[ 5 6 7]
[78 79 80]
[ 2 3 4]
[34 35 36]]
Dimensions: 2
Shape: (4, 3)
```

## Tensor indexing and slicing¶

```
# Slice a vector
x = np.array([1, 2, 3, 4, 5, 6, 7])
print(x[:3])
print(x[3:])
```

```
[1 2 3]
[4 5 6 7]
```

```
# Slice a matrix
x = np.array([[5, 78, 2, 34],
[6, 79, 3, 35],
[7, 80, 4, 36]])
print(x[:2, :])
print(x[2:, :])
print(x[:, :2])
print(x[:, 2:])
```

```
[[ 5 78 2 34]
[ 6 79 3 35]]
[[ 7 80 4 36]]
[[ 5 78]
[ 6 79]
[ 7 80]]
[[ 2 34]
[ 3 35]
[ 4 36]]
```

## Operations between tensors¶

**Element-wise** operations are applied independently to each entry in the tensors being considered.

Other operations, like dot product, combine entries in the input tensors to produce a differently shaped result.

### Element-wise addition¶

```
# Element-wise addition between two vectors
x = np.array([2, 5, 4])
y = np.array([1, -1, 4])
z = x + y
print(z)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[3 4 8]
Dimensions: 1
Shape: (3,)
```

### Element-wise product¶

```
# Element-wise product between two matrices (shapes must be identical)
x = np.array([[1, 2, 3],
[3, 2, 1]])
y = np.array([[3, 0, 2],
[1, 4, -2]])
z = x * y
print(z)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[[ 3 0 6]
[ 3 8 -2]]
Dimensions: 2
Shape: (2, 3)
```

### Dot product¶

```
# Dot product between two matrices (shapes must be compatible)
x = np.array([[1, 2, 3],
[3, 2, 1]]) # x has shape (2, 3)
y = np.array([[3, 0],
[2, 1],
[4, -2]]) # y has shape (3, 2)
z = np.dot(x, y) # alternative syntax: z = x.dot(y)
print(z)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[[19 -4]
[17 0]]
Dimensions: 2
Shape: (2, 3)
```

## Broadcasting¶

Broadcasting is a powerful NumPy functionality.

If there is no ambiguity, the smaller tensor can be “broadcasted” implicitly to match the larger tensor’s shape before an operation is applied to them.

### Broadcasting between a vector and a scalar¶

```
x = np.array([12, 3, 6, 14])
x = x + 3
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[15 6 9 17]
Dimensions: 1
Shape: (4,)
```

### Broadcasting between a matrix and a scalar¶

```
x = np.array([[0, 1, 2],
[-2, 5, 3]])
x = x - 1
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[[-1 0 1]
[-3 4 2]]
Dimensions: 2
Shape: (2, 3)
```

### Broadcasting between a matrix and a vector¶

```
x = np.array([[0, 1, 2],
[-2, 5, 3]])
y = np.array([1, 2, 3])
z = x + y
print(z)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[[ 1 3 5]
[-1 7 6]]
Dimensions: 2
Shape: (2, 3)
```

## Summing tensors¶

### Summing on all axes¶

```
x = np.array([[0, 1, 2],
[-2, 5, 3]])
x = np.sum(x) # x is now a scalar (0D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
9
Dimensions: 0
Shape: ()
```

### Summing on a specific axis¶

```
# Sums a matrix on its first axis (rows)
x = np.array([[0, 1, 2],
[-2, 5, 3]])
x = np.sum(x, axis=0) # x is now a vector (1D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[-2 6 5]
Dimensions: 1
Shape: (3,)
```

```
# Sums a matrix on its second axis (columns)
x = np.array([[0, 1, 2],
[-2, 5, 3]])
x = np.sum(x, axis=1) # x is now a vector (1D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[3 6]
Dimensions: 1
Shape: (2,)
```

### Keeping tensor dimensions while summing¶

```
# Sums a matrix on its second axis (columns), keeping the same dimensions
x = np.array([[0, 1, 2],
[-2, 5, 3]])
x = np.sum(x, axis=0, keepdims=True) # x is still a matrix (2D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
```

```
[[-2 6 5]]
Dimensions: 2
Shape: (1, 3)
```

## TODO¶

Explain fancy indexing (https://jakevdp.github.io/PythonDataScienceHandbook/02.07-fancy-indexing.html)