# 14. Numpy Array Operations and Linear Algebra#

This work is licensed under Creative Commons Attribution-ShareAlike 4.0 International

**Note:** We will see more tools for linear algebra in the section on Scipy Tools, which introduces the package *Scipy*.

As of version 3.5, Python can handle matrix multiplication on Numpy arrays, using the at sign “@”:

```
C = A @ B
```

Why this strange notation? Because

```
D = A * B
```

is instead “point-wise” multiplication of arrays, with `D[i,j] = A[i,j] * B[i,j]`

instead of

**Aside 1: Numpy matrix is now redundant!**

Previously, Numpy supported matrix calculations withe variables of type `matrix`

, a special sub-tpye of `array`

.
However, with the addition of “@” for matrix multiplication of arrays, that option is now un-needed, and I advise you to avoid it.

**Aside 2: Floating point numbers vs integers**

Though Python is generally good at understanding when an integer like `7`

is to be used as a floating point (real) number, it is sometimes best to make this distinction explicitly when working with module `numpy`

;
otherwise sometimes division done within numpy functions returns an integer answer, like `7/2 = 3`

.

Thus from now on, when I mean an integer to be used as a floating point number, I give it a decimal point: `7./2.`

will reliably be `3.5`

First we explore some basic features offered by Numpy; then we will look at some more advanced tools from package Scipy, and specifically its module `scipy.linalg`

for linear algebra.

```
import numpy as np
```

```
A = np.array([[1, 2, 3],[4, 5, 6]])
B = np.array([[1, 2], [3,4], [5, 6]])
C = np.array([[10, 9, 8],[7, 6, 5]])
print(f"A is\n{A}")
print(f"B is\n{B}")
print(f"C is\n{C}")
```

```
A is
[[1 2 3]
[4 5 6]]
B is
[[1 2]
[3 4]
[5 6]]
C is
[[10 9 8]
[ 7 6 5]]
```

```
print(f"The matrix product A times B is:\n {A @ B}")
print(f"The matrix product B times A is:\n {B @ A}")
```

```
The matrix product A times B is:
[[22 28]
[49 64]]
The matrix product B times A is:
[[ 9 12 15]
[19 26 33]
[29 40 51]]
```

```
print(f"The array product A * B fails:\n{A * B}")
```

```
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-4-3bb26336abf9> in <module>
----> 1 print(f"The array product A * B fails:\n{A * B}")
ValueError: operands could not be broadcast together with shapes (2,3) (3,2)
```

On the other hand:

```
print(f"Array product A times C is\n{A * C}\n")
```

```
Array product A times C is
[[10 18 24]
[28 30 30]]
```

```
print(f"Matrix product A times C fails:\n{A @ C}")
```

```
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-6-55b834c682c2> in <module>
----> 1 print(f"Matrix product A times C fails:\n{A @ C}")
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 2 is different from 3)
```

## 14.1. The matrix transpose \(A^T\) is given by `A.T`

#

```
print(f"A-transpose is\n{A.T}")
```

```
A-transpose is
[[1 4]
[2 5]
[3 6]]
```

## 14.2. *Slicing*: Extracting rows, columns, and other rectangular chunks from matrices#

This works with Python lists and Numpy arrays, and we have seen some of it before; I review it here because it will help with doing the row operations of linear algebra.

### 14.2.1. Index notation for slicing#

For an index with n possible values, from 0 to n-1:

`a:b`

means indices i in th usual semi-open interval \(a \leq i < b\) or \([a,b)\)`a:`

is short for`a:n`

, so indices \(a \leq i\), all the way to the maximum index value`:b`

is short for`0:b`

, so all indices \(i < b\)`:`

combines both of the above, so is short for`0:n`

; all possible indicesindex value

`-1`

refers the last entry; the same as index n-1, but without needing to know n.index value

`-m`

refers the “m-th last” entry; the same as index n-m, again without needing to know n.

```
print(f"A:\n{A}")
print(f"The column of index 1 (presented as a row vector): {A[:,1]}")
print(f"The row of index 1: {A[1,:]}")
print(f"The first 2 elements of the row of index 1: {A[1,:2]}")
print(f"Another way to say the above: {A[1,0:2]}")
print(f"The bottom-right element: {A[-1,-1]}")
print(f"The 2x2 sub-matrix in the bottom-right corner:\n{A[-2:,-2:]}")
```

```
A:
[[1 2 3]
[4 5 6]]
The column of index 1 (presented as a row vector): [2 5]
The row of index 1: [4 5 6]
The first 2 elements of the row of index 1: [4 5]
Another way to say the above: [4 5]
The bottom-right element: 6
The 2x2 sub-matrix in the bottom-right corner:
[[2 3]
[5 6]]
```

### 14.2.2. Synonyms for array names with the equal sign (not copying!)#

If we use the equal sign between two array names, that makes them synonyms, referring to the same values:

```
A = np.array([[1, 2, 3],[4, 5, 6]])
print(f"A is\n{A}")
Anickname = A
print(f"Anickname is\n{Anickname}")
Anickname[0,0] = 12
print(f"Anickname is now\n{Anickname}")
print(f"and so is A!:\n{A}")
```

```
A is
[[1 2 3]
[4 5 6]]
Anickname is
[[1 2 3]
[4 5 6]]
Anickname is now
[[12 2 3]
[ 4 5 6]]
and so is A!:
[[12 2 3]
[ 4 5 6]]
```

### 14.2.3. Copying arrays with method .copy()#

Thus if we want a separate new array or list with the same elements initially, we must make a copy with the method `.copy()`

, not the equal sign:

```
A = np.array([[1, 2, 3],[4, 5, 6]])
print(f"A is\n{A}")
Acopy = A.copy()
print(f"Acopy is\n{Acopy}")
Acopy[0,0] = 54
print(f"Acopy is now\n{Acopy}")
print(f"A is still\n{A}")
```

```
A is
[[1 2 3]
[4 5 6]]
Acopy is
[[1 2 3]
[4 5 6]]
Acopy is now
[[54 2 3]
[ 4 5 6]]
A is still
[[1 2 3]
[4 5 6]]
```

#### 14.2.3.1. Exercise A#

Create a Numpy array (not a Numpy *matrix*; those are now mostly obsolete!) containing the matrix

and one containing the vector

#### 14.2.3.2. Exercise B#

Create arrays \(c\) and \(d\) containing respectively the last row of \(A\) and the middle column of \(A\).

**Note:** Do this by manipulating the array `A`

with indexing and slicing operations, without entering any numerical values for array entries explicity.