import random
import numpy as np
import pandas as pd
from typing import List
from numpy import random as rd
from timeit import default_timer

a = [1, 2, 3]
b = [2, 5, 3]
print(f'List a is of type {type(a)}.')
print(f'List b is of type {type(b)}.\n')
print(f'The sum of lists a and b is {a + b}. \n\n-> This is the concatenation of both lists.')

```
List a is of type <class 'list'>.
List b is of type <class 'list'>.
The sum of lists a and b is [1, 2, 3, 2, 5, 3].
-> This is the concatenation of both lists.
```

def sum_list(l1: List[int], l2: List[int]):
"""Sum two lists term-by-term
>>> sum_list([1,2,3], [2, 5, 3])
[3, 7, 6]
"""
if len(l1) != len(l2):
raise Exception('Lists need to be of equal size.')
return [x + y for x, y in zip(l1, l2)]
assert sum_list(a,b) == [3, 7, 6]

start = default_timer()
for _ in range(10000):
a = [random.random() for _ in range(100)]
b = [random.random() for _ in range(100)]
sum_list(a, b)
end = default_timer()
time_q3 = end - start
print('This function computes the time it takes to sum two lists of 100 random numbers 10,000 times.\n')
print(f'-> In this case, it takes {time_q3} seconds.')

```
This function computes the time it takes to sum two lists of 100 random numbers 10,000 times.
-> In this case, it takes 0.2956233959994279 seconds.
```

a = np.array([1, 2, 3])
b = np.array([2, 5, 3])
print(f'Matrix a is of type {type(a)}.')
print(f'Matrix b is of type {type(b)}.\n')
print(f'The sum of a and b is {a + b}.')
print(f'The sum of 3a and 2b is {3*a + 2*b}.\n')

```
Matrix a is of type <class 'numpy.ndarray'>
Matrix b is of type <class 'numpy.ndarray'>
The sum of a and b is [3 7 6].
The sum of 3a and 2b is [ 7 16 15].
```

start = default_timer()
for _ in range(10000):
random_array1 = np.random.random(size = 100)
random_array2 = np.random.random(size = 100)
result = random_array1 + random_array2
end = default_timer()
time_q5 = end - start
print(f'It takes {time_q5} seconds to perform 10,000 additions of randomly generated arrays of size 100.\n')
print(f'The difference between the time taken in this question and that in question 3 is {time_q5 - time_q3}.')

```
It takes 0.05401484598405659 seconds to perform 10,000 additions of randomly generated arrays of size 100.
The difference between the time taken in this question and that in question 3 is -0.2416085500153713.
```

# question 6.a
print('6.a')
a = np.array([
[1, 2, 3],
[4, 5, 6]
])
rows, cols = a.shape
print(f'{rows} rows and {cols} columns')
# question 6.b
print('\n6.b')
b = np.array([
[1, 2],
[3, 4],
[5, 6]
])
print(b)
print('\n6.c')
try:
print (a+b)
except Exception as e:
print(f'error: {e}')
print('\n-> Explanation: We cannot sum matrices of different sizes.')
print('\n6.d')
c = np.array([
[4, 1, -1],
[4, 0, 3]
])
print('a + c =')
print(a + c)
print('\nc + a =')
print(c + a)
print("\n-> Matrix addition indeed commutative.")

```
6.a
2 rows and 3 columns
6.b
[[1 2]
[3 4]
[5 6]]
6.c
error: operands could not be broadcast together with shapes (2,3) (3,2)
-> Explanation: We cannot sum matrices of different sizes.
6.d
a + c =
[[5 3 2]
[8 5 9]]
c + a =
[[5 3 2]
[8 5 9]]
-> Matrix addition indeed commutative.
```

mat1 = np.random.randint(10, size = (2,2))
mat2 = np.random.randint(10, size = (2,2))
print(f'Let mat1 be: \n\n{mat1}\n')
print(f'and mat2 be: \n\n{mat2}\n')

```
Let mat1 be:
[[4 2]
[4 9]]
and mat2 be:
[[6 8]
[0 9]]
```

print(f'Let mat1 and mat2 be the following two matrices: \n\n{mat1}\n\n{mat2}\n')
print(f'The result of mat1 * mat2 is: \n\n{mat1 * mat2}\n\n-> It is the result of element-wise multiplication.\n')
print(f'The result of mat1 @ mat2 is: \n\n{mat1 @ mat2}\n\n-> It is the result of standard matrix multiplicaiton.\n')

```
Let mat1 and mat2 be the following two matrices:
[[6 3]
[2 1]]
[[9 8]
[4 5]]
The result of mat1 * mat2 is:
[[54 24]
[ 8 5]]
-> It is the result of element-wise multiplication.
The result of mat1 @ mat2 is:
[[66 63]
[22 21]]
-> It is the result of standard matrix multiplicaiton.
```

print(f'Let mat1 and mat2 be the following two matrices: \n\n{mat1}\n\n{mat2}\n')
print(f'The product of mat1 and mat2 is: \n\n{mat1 @ mat2}\n')
print(f'The product of mat2 and mat1 is: \n\n{mat2 @ mat1}\n')
print('-> Because these products are unequal, matrix multiplication is not commutative.')

```
Let mat1 and mat2 be the following two matrices:
[[4 2]
[4 9]]
[[6 8]
[0 9]]
The product of mat1 and mat2 is:
[[ 24 50]
[ 24 113]]
The product of mat2 and mat1 is:
[[56 84]
[36 81]]
-> Because these products are unequal, matrix multiplication is not commutative.
```

a = np.array([
[1],
[1]
])
print(f'Let matrix a be: \n\n{a}\n')
print('We try to multiply it by itself. The result is therefore the product of a (2x1) matrix and a (2x1) matrix.')
print('-> This multiplication is not valid because row count and column count are not equal (i.e. 2 != 1).')
try:
print(a @ a)
except Exception as e:
print(f'\nerror: {e}')
print('-> We cannot multiply matrices of incompatible sizes.')

```
Let matrix a be:
[[1]
[1]]
We try to multiply it by itself. The result is therefore the product of a (2x1) matrix and a (2x1) matrix.
-> This multiplication is not valid because row count and column count are not equal (i.e. 2 != 1).
error: 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 1)
-> We cannot multiply matrices of incompatible sizes.
```

#Question 11
mat3 = np.random.randint(0, 20, size = (3, 4))
print(f'11 - Let mat3 be the following matrix: \n\n{mat3}\n')
print(f'The transpose of mat3 is: \n\n{np.transpose(mat3)}\n\n-> It is a 4 by 3 matrix.\n')
#Question 12
print(f'12 - The transpose of the transpose of mat3 (i.e. merely mat3) is: \n\n{np.transpose(np.transpose(mat3))}\n')
#Question 13
# (a)
mat4 = np.random.randint(0, 20, size = (3,4))
mat5 = np.random.randint(0, 20, size = (4,2))
print(f'13.a - Let mat4 and mat5 be the following matrices, respectively: \n\n{mat4}\n \n{mat5}\n')
# (b)
a = np.transpose(2 * mat3 + 3 * mat4)
b = 2 * np.transpose(mat3) + 3 * np.transpose(mat4)
print(f'13.b - Let matrix a be the result of (2mat3 + 3mat4)^T, that is a = \n\n{a}\n')
print(f'Let matrix b be the result of 2(mat3)^T + 3(mat4)^T, that is b = \n\n{b}\n')
print('The function M -> M^T is a linear mapping because it preserves the operations of addition and scalar multiplication.\n')
# (c)
c = np.transpose(np.dot(mat3, mat5))
d = np.dot(np.transpose(mat5), np.transpose(mat3))
print(f'13.c - Let matrix c be the result of (mat3mat5)^T, that is c = \n\n{c}\n')
print(f'Let matrix d be the result of (mat5)^T(mat3)^T, that is d = \n\n{d}\n')
print('-> The transpose of the product of two matrices is equal to the product of the transposes of the two matrices (but in the reverse order).')

```
11 - Let mat3 be the following matrix:
[[10 4 10 0]
[ 1 6 17 2]
[ 8 10 0 3]]
The transpose of mat3 is:
[[10 1 8]
[ 4 6 10]
[10 17 0]
[ 0 2 3]]
-> It is a 4 by 3 matrix.
12 - The transpose of the transpose of mat3 (i.e. merely mat3) is:
[[10 4 10 0]
[ 1 6 17 2]
[ 8 10 0 3]]
13.a - Let mat4 and mat5 be the following matrices, respectively:
[[ 1 8 2 4]
[16 14 4 11]
[ 2 16 1 10]]
[[ 8 13]
[ 0 11]
[11 17]
[ 1 3]]
13.b - Let matrix a be the result of (2mat3 + 3mat4)^T, that is a =
[[23 50 22]
[32 54 68]
[26 46 3]
[12 37 36]]
Let matrix b be the result of 2(mat3)^T + 3(mat4)^T, that is b =
[[23 50 22]
[32 54 68]
[26 46 3]
[12 37 36]]
The function M -> M^T is a linear mapping becuase it preserves the operations of addition and scalar multiplication.
13.c - Let matrix c be the result of (mat3mat5)^T, that is c =
[[190 197 67]
[344 374 223]]
Let matrix d be the result of (mat5)^T(mat3)^T, that is d =
[[190 197 67]
[344 374 223]]
-> The transpose of the product of two matrices is equal to the product of the transposes of the two matrices (but in the reverse order).
```

M = np.random.randint(0, 20, size = (2, 2))
N = np.random.randint(0, 20, size = (2, 2))
print('14.a - This formula is true.\n')
print(f'tr(M + N) = {np.trace(M + N)}')
print(f'tr(M) + tr(N) = {np.trace(M) + np.trace(N)}\n')
print('14.b - This formula is not true.\n')
print(f'tr(MN) = {np.trace(M @ N)}')
print(f'tr(M)tr(N) = {np.trace(M) * np.trace(N)}\n')
print('14.c - This formula is true.\n')
print(f'tr(MN) = {np.trace(M @ N)}')
print(f'tr(NM) = {np.trace(N @ M)}\n')
print('14.d - This formula is true.\n')
print(f'tr(M^T) = {np.trace(np.transpose(M))}')
print(f'tr(M) = {np.trace(M)}')

```
14.a - This formula is true.
tr(M + N) = 37
tr(M) + tr(N) = 37
14.b - This formula is not true.
tr(MN) = 244
tr(M)tr(N) = 210
14.c - This formula is true.
tr(MN) = 244
tr(NM) = 244
14.d - This formula is true.
tr(M^T) = 7
tr(M) = 7
```