---
# System prepended metadata

title: COMPUTATIONAL METHODS FOR DATA SCIENCE HW1

---

# COMPUTATIONAL METHODS FOR DATA SCIENCE HW1
郭訓佑, R11946025

## 0
#### code
````python
# Q0: A Practice on the Randomization
import random

# read tsv file
def read_tsv(file_name) -> list:
    l = []
    with open(file_name, 'r') as f:
        for line in f:
            a = line.strip().split(' ')
            # remove empty string
            a = [x for x in a if x != '']
            l.append(a)
    return l

# Q1a
"""
Write a simple code (any programming language of your choice) to randomly pick 6
objects out of 12, where all 12 possible objects have the same probability to be picked
"""
# read tsv file
a = read_tsv("E:\\GitHub\\NTU_Computational_Methods_for_Data_Science_2023\\hw1\\staff.stat.sinica.edu.tw_fredphoa_HW_HW1_CAmaxTemp.txt")
# remove column 0,1, 14
a = [x[2:14] for x in a]
# check it is a 12 * 12 matrix
assert len(a) == 12, "the matrix must be a 12 * 12 matrix"
for i in range(12):
    assert len(a[1]) == 12, "the matrix must be a 12 * 12 matrix"
    
# randomly pick(not replace) 6 rows and 6 columns and sort them
# set seed
random.seed(0)
columns = random.sample(range(12), 6)
rows = random.sample(range(12), 6)
columns.sort()
rows.sort()

# Q0b
"""
Run the code on both the stations and the months, and state which stations and months
you obtain as the reduced data X.
"""

print("Pikced columns: ", columns)
print("Pikced rows: ", rows)
X = [[a[i][j] for j in columns] for i in rows]
# %%
# Q0c
"""
Do a quick check if X has at least four REAL eigenvalues. You can do this question by
using existing library or packages implemented in the software of your choice.
"""
# check if X has at least four REAL eigenvalues
import numpy as np
from numpy import linalg as LA
X_np = np.array(X, dtype=float)
eigenvalues, eigenvectors = LA.eig(X_np)
real_eigenvalues = []
for i in range(len(eigenvalues)):
    if np.isreal(eigenvalues[i]):
        real_eigenvalues.append(eigenvalues[i])
print("# of real eigenvalues: ", len(real_eigenvalues))
assert len(real_eigenvalues) >= 4, "X must have at least four REAL eigenvalues"
# %%
print("X: ")
print(X_np)
````
#### Ans:
Picked Stations:
23225BLUE_CANYON,CA
93193FRESNO,CA
23129LONG_BEACH,CA
93134LOS_ANGELES,CA
23232SACRAMENTO,CA
23190SANTA_BARBARA,CA
Picked Months:
1,4,5,7,8,12
```
Pikced columns:  [0, 3, 4, 6, 7, 11]
Pikced rows:  [2, 3, 4, 5, 7, 10]
# of real eigenvalues:  4

X: 
[[ 76.  82.  88.  95.  97.  75.]
 [ 78. 100. 107. 113. 112.  77.]
 [ 93. 105. 104. 109. 105.  92.]
 [ 95. 106. 102. 108. 105.  92.]
 [ 76.  95. 105. 114. 112.  74.]
 [ 86.  96. 101. 109. 105.  89.]]
```

## 1
### a
Run the LU factorization on X to obtain a lower triangular matrix L and an upper triangular matrix U.
#### code:
```python
import numpy as np
import copy
np.set_printoptions(precision=3)
np.set_printoptions(suppress=True)

# Q1.a
"""
Run the LU factorization on X to obtain a lower triangular matrix L and an upper
triangular matrix U.
"""
def LU(X: np.array)-> tuple:
    # check if X is a square matrix
    assert len(X) == len(X[0]), "X must be a square matrix"
    n = len(X)
    # initialize L and U
    L = np.zeros((n, n))
    U = np.zeros((n, n))
    # calculate L and U
    for i in range(n):
        L[i][i] = 1
        for j in range(i, n):
            U[i][j] = X[i][j] - sum([L[i][k] * U[k][j] for k in range(i)])
        for j in range(i+1, n):
            L[j][i] = (X[j][i] - sum([L[j][k] * U[k][i] for k in range(i)])) / U[i][i]
    return L, U

L, U = LU(X_np)
print("Q1.a")
print("L: ")
print(L)
print("U: ")
print(U)

# %%
# check if LU decomposition is correct
print("Check if LU decomposition is correct")
print("L * U: ")
print(L.dot(U))
```
#### Ans:
```
L: 
[[ 1.     0.     0.     0.     0.     0.   ]
 [ 1.026  1.     0.     0.     0.     0.   ]
 [ 1.224  0.294  1.     0.     0.     0.   ]
 [ 1.25   0.221  1.36   1.     0.     0.   ]
 [ 1.     0.821 -0.385  0.917  1.     0.   ]
 [ 1.132  0.203  0.228  0.558  0.962  1.   ]]
U: 
[[ 76.     82.     88.     95.     97.     75.   ]
 [  0.     15.842  16.684  15.5    12.447   0.026]
 [  0.      0.     -8.59  -11.807 -17.357   0.216]
 [  0.      0.      0.      1.889   4.614  -2.05 ]
 [  0.      0.      0.      0.     -6.132   0.941]
 [  0.      0.      0.      0.      0.      4.315]]
 
 Check if LU decomposition is correct
L * U: 
[[ 76.  82.  88.  95.  97.  75.]
 [ 78. 100. 107. 113. 112.  77.]
 [ 93. 105. 104. 109. 105.  92.]
 [ 95. 106. 102. 108. 105.  92.]
 [ 76.  95. 105. 114. 112.  74.]
 [ 86.  96. 101. 109. 105.  89.]]
```

### b
Use Power Iteration method to find the largest eigenvalue-eigenvector pair of X.
#### code:
```python
def largest_eigen_pair(X: np.array, epsilon: float = 1e-6, max_iter=1000000)-> tuple:
    # power method
    assert len(X) == len(X[0]), "X must be a square matrix"
    n = len(X)
    # initialize x
    x = np.random.rand(n)
    v = x / np.linalg.norm(x)
    
    # eignevalue = v.T * X * v
    eigen = np.dot(v.T, np.dot(X, v))
    num_iter = 0
    while True:
        num_iter += 1
        # update v
        v = np.dot(X, v)
        v = v / np.linalg.norm(v)
        # update eigenvalue
        eigen_new = np.dot(v.T, np.dot(X, v))
        # check if converge
        if abs(eigen_new - eigen) < epsilon or num_iter > max_iter:
            if num_iter > max_iter:
                print("Not converge")
            break
        if num_iter % 100 == 0:
            print("Iteration: ", num_iter)
            
        eigen = eigen_new
        num_iter += 1
    return eigen, v

def QR(X: np.array) -> tuple:
    # QR decomposition using Gram-Schmidt process
    Q = np.zeros(X.shape)
    R = np.zeros((X.shape[1], X.shape[1]))
    for k in range(X.shape[1]):
        for i in range(k):
            R[i][k] = np.dot(Q[:, i].T, X[:, k])
        qk = copy.deepcopy(X[:, k])
        for i in range(k):
            qk = qk - R[i][k] * Q[:, i]
        R[k][k] = np.linalg.norm(qk)
        Q[:, k] = qk / R[k][k]
    return Q, R

largest_value, largest_vector = largest_eigen_pair(X_np)
print("Q1.b")
print("Largest eigenvalue: ")
print(largest_value)
print("Largest eigenvector: ")
print(largest_vector)
# %%
# check 
print("Check if largest eigenvalue and eigenvector is correct")
print("X * v: ")
print(X_np.dot(largest_vector))
print("lambda * v: ")
print(largest_value * largest_vector)
```
#### Ans:
```
Largest eigenvalue: 
581.7047038772532
Largest eigenvector: 
[0.361 0.414 0.427 0.427 0.406 0.412]

Check if largest eigenvalue and eigenvector is correct
X * v: 
[209.707 240.581 248.406 248.26  236.129 239.643]
lambda * v: 
[209.707 240.581 248.406 248.26  236.129 239.643]
```

### c
Use QR factorization to find all eigenvectors with REAL eigenvalues of X.
#### code:
````python
def real_eigen_pairs(X: np.array, max_iter=500000)-> tuple:
    # QR iteration
    """
    if matrix X is symmetric, then the eigenvalues are real and the eigenvectors is almost correct
    if matrix X is not symmetric, then the eigenvalues are complex and the eigenvectors is not correct
    """
    assert len(X) == len(X[0]), "X must be a square matrix"
    # check if X is symmetric
    if not np.allclose(X, X.T):
        print("X is not symmetric, the eigenvalues are complex and the eigenvectors is not correct")
    
    n = len(X)
    # initialize Q and R
    Q = np.identity(n)
    R = X
    num_iter = 0
    Q_eigen = np.identity(n)
    while True:
        num_iter += 1
        # update Q and R
        Q, R = QR(R.dot(Q))
        Q_eigen = Q_eigen.dot(Q)
        # check if converge
        if num_iter > max_iter:
            break
        if num_iter % 100 == 0:
            print("Iteration: ", num_iter)

    eigenvalues = []
    for i in range(n):
        eigenvalues.append(R[i][i])
    
    eigenvectors = []
    for i in range(n):
        eigenvectors.append(Q_eigen[:, i])
    return eigenvalues, eigenvectors

eigenvalues, eigenvectors = real_eigen_pairs(X_np)
print("Q1.c")
print("Eigenvalues: ")
print(eigenvalues)
print("Eigenvectors: ")
print(eigenvectors)
# %%
# check
print("Check if eigenvalues and eigenvectors are correct")
for i in range(len(eigenvalues)):
    print("X * v: ")
    print(X_np.dot(eigenvectors[i]))
    print("lambda * v: ")
    print(eigenvalues[i] * eigenvectors[i])
````
#### Ans:
I found some cases will break this method which the eigenvectors are difficult to converge. e.f. QR = RQ or some asymmetric.
```
Eigenvalues: 
[581.7047038564525, 9.393027168397762, 3.4800347037085526, 8.627255256768713, 3.915093807405027, 0.8047459721312528]
Eigenvectors: 
[array([0.361, 0.414, 0.427, 0.427, 0.406, 0.412]), array([-0.109,  0.585, -0.311, -0.185,  0.522, -0.492]), array([-0.533, -0.173,  0.063, -0.414,  0.476,  0.533]), array([-0.646, -0.101,  0.378,  0.548,  0.064, -0.355]), array([ 0.342, -0.663, -0.171,  0.189,  0.574, -0.219]), array([-0.201,  0.079, -0.738,  0.526, -0.044,  0.36 ])]
```

### d
Find the inverse of X.

#### code:
````python
# Q1.d
"""
Find the inverse of X.
"""
def inverse_LU_version(X: np.array)-> np.array:
    # find inverse of X using LU decomposition
    assert len(X) == len(X[0]), "X must be a square matrix"
    n = len(X)
    L, U = LU(X)
    # initialize inverse of X
    X_inv = np.zeros((n, n))
    # solve for each column of X_inv
    for i in range(n):
        b = np.zeros(n)
        b[i] = 1
        # solve Ly = b
        y = np.zeros(n)
        for j in range(n):
            y[j] = b[j] - sum([L[j][k] * y[k] for k in range(j)])
        # solve Ux = y
        x = np.zeros(n)
        for j in range(n-1, -1, -1):
            x[j] = (y[j] - sum([U[j][k] * x[k] for k in range(j+1, n)])) / U[j][j]
        X_inv[:, i] = x
    return X_inv
X_inv = inverse_LU_version(X_np)
print("Q1.d")
print("Inverse of X: ")
print(X_inv)
# %%
# check
print("Check if inverse of X is correct")
print("X * X_inv: ")
print(X_np.dot(X_inv))
````
#### Ans:
I use LU decomposition to do invertion.
```
Q1.d
Inverse of X: 
[[ 0.102 -0.302  0.397 -0.172  0.256 -0.269]
 [-0.114  0.204 -0.391  0.316 -0.146  0.118]
 [ 0.14  -0.097  0.822 -0.623  0.063 -0.292]
 [-0.19  -0.208 -0.241  0.217  0.24   0.165]
 [ 0.113  0.208 -0.303  0.161 -0.197  0.036]
 [-0.034  0.19  -0.241  0.075 -0.223  0.232]]
 
 Check if inverse of X is correct
X * X_inv: 
[[ 1.  0.  0.  0. -0.  0.]
 [-0.  1.  0.  0.  0.  0.]
 [ 0.  0.  1.  0.  0. -0.]
 [-0.  0. -0.  1.  0.  0.]
 [-0.  0.  0.  0.  1.  0.]
 [-0.  0.  0.  0.  0.  1.]]
```

## 2
### a
Center the data and compute the variance-covariance matrix.
#### code:
````python
def cov_matrix(X: np.array)-> np.array:
    # Center the data and compute the variance-covariance matrix.
    # center X
    X = copy.deepcopy(X - np.mean(X, axis=0))
    # calculate variance-covariance matrix of X
    n = len(X[0])
    # initialize variance-covariance matrix
    var_cov = np.zeros((n, n))
    # calculate variance-covariance matrix
    for i in range(n):
        for j in range(i, n):
            var_cov[i][j] = np.dot(X[:, i].T, X[:, j]) / (n-1)
            var_cov[j][i] = var_cov[i][j]
    return var_cov

X_mean = X_np - np.mean(X_np, axis=0)
var_cov = cov_matrix(X_np)
print("Q2.a")
print("Centered data: ")
print(X_mean)
print("Variance-covariance matrix: ")
print(var_cov)
````
#### Ans:
```
Q2.a
Centered data: 
[[ -8.    -15.333 -13.167 -13.     -9.     -8.167]
 [ -6.      2.667   5.833   5.      6.     -6.167]
 [  9.      7.667   2.833   1.     -1.      8.833]
 [ 11.      8.667   0.833   0.     -1.      8.833]
 [ -8.     -2.333   3.833   6.      6.     -9.167]
 [  2.     -1.333  -0.167   1.     -1.      5.833]]
Variance-covariance matrix: 
[[74.    57.4   14.8    7.4   -6.8   72.8  ]
 [57.4   76.667 47.533 41.    25.    53.333]
 [14.8   47.533 46.167 45.2   34.6   13.567]
 [ 7.4   41.    45.2   46.4   36.2    7.   ]
 [-6.8   25.    34.6   36.2   31.2   -8.4  ]
 [72.8   53.333 13.567  7.    -8.4   75.767]]
```

### b
Find the top three principal components using power iteration. Calculate the cumulative
percentage of the total eigenvalues that these three principal components cover.
#### code:
````python
def deflated_power_method(X: np.array, n: int,epsilon: float = 1e-6, max_iter=1000000)-> tuple:
    # power method with deflation to find the first n largest eigenvalues and eigenvectors
    assert len(X) == len(X[0]), "X must be a square matrix"
    # initialize eigenvalues and eigenvectors
    eigenvalues = []
    eigenvectors = []
    for i in range(n):
        eigen, vec = largest_eigen_pair(X, epsilon, max_iter)
        eigenvalues.append(eigen)
        eigenvectors.append(vec)
        # deflation
        X = X - eigen * np.outer(vec, vec)
    return eigenvalues, eigenvectors

def find_topn_principal_components(X: np.array, n: int)-> np.array:
    # find the top n principal components of X
    # calculate covariance matrix of X
    C = cov_matrix(X)
    # find the top n eigenvalues and eigenvectors
    eigenvalues, eigenvectors = deflated_power_method(C, n)
    # sort eigenvalues and eigenvectors
    eigenvalues = np.array(eigenvalues)
    eigenvectors = np.array(eigenvectors)
    idx = eigenvalues.argsort()[::-1]
    eigenvalues = eigenvalues[idx]
    eigenvectors = eigenvectors[idx]
    # return the top n principal components and eigenvalues
    return eigenvectors, eigenvalues

# find the top 3 principal components of X
PCn, eigvals = find_topn_principal_components(X_np, 3)
cov_X = cov_matrix(X_np)
all_eigenvalues, _ = deflated_power_method(cov_X, 10)
print("Q2.b")
print("Top 3 principal components: ")
print(PCn)
print("Cumulative percentage of the total eigenvalues that these three principal components cover: ", sum(eigvals) / sum(all_eigenvalues))
````
#### Ans:
```
Q2.b
Top 3 principal components: 
[[ 0.487  0.57   0.329  0.283  0.148  0.477]
 [-0.412  0.111  0.426  0.477  0.469 -0.435]
 [ 0.176  0.632 -0.083 -0.443 -0.029 -0.605]]
Cumulative percentage:  0.9973567855222676
```

### c
 Plot the data on a 3D space with three principal component axes. Provide the coordinates of the recast data
 
#### code:
````python
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# plot original data (first three columns) with three principal components as axes
fig = plt.figure()
ax = Axes3D(fig)
# plot three principal component times eigenvalues as axes
ax.plot([0, PCn[0][0] * np.sqrt(eigvals[0])*2], [0, PCn[0][1] * np.sqrt(eigvals[0])*2], [0, PCn[0][2] * np.sqrt(eigvals[0])*2], color='r')
ax.plot([0, PCn[1][0] * np.sqrt(eigvals[1])*2], [0, PCn[1][1] * np.sqrt(eigvals[1])*2], [0, PCn[1][2] * np.sqrt(eigvals[1])*2], color='g')
ax.plot([0, PCn[2][0] * np.sqrt(eigvals[2])*2], [0, PCn[2][1] * np.sqrt(eigvals[2])*2], [0, PCn[2][2] * np.sqrt(eigvals[2])*2], color='b')
# center X
X_mean = copy.deepcopy(X_np - np.mean(X_np, axis=0))
# plot data
ax.scatter(X_mean[:, 0], X_mean[:, 1], X_mean[:, 2])
plt.show()
# %%
def transform_data(X: np.array, PCs: np.array, eigenvalues: np.array)-> np.array:
    # center X
    X_mean = copy.deepcopy(X - np.mean(X, axis=0))
    # transform X to the new coordinate system
    return np.dot(X_mean, PCs.T)
# plot transformed data (first three columns) with three principal components as axes
PCn_transformed = transform_data(X_np, PCn, eigvals)
fig = plt.figure()
ax = Axes3D(fig)
# plot eigenvalues as axes
ax.plot([0, np.sqrt(eigvals[0])], [0, 0], [0, 0], color='r')
ax.plot([0, 0], [0, np.sqrt(eigvals[1])], [0, 0], color='g')
ax.plot([0, 0], [0, 0],[0, np.sqrt(eigvals[2])], color='b')
# plot data
ax.scatter(PCn_transformed[:, 0], PCn_transformed[:, 1], PCn_transformed[:, 2])
plt.show()
````
#### Ans:
Original data with three principal compnents
![](https://hackmd.io/_uploads/Hkuw9ejlp.png)
After transformed
![](https://hackmd.io/_uploads/rk7u9xoxp.png)

### d
Find all principal components with their eigenvalues using SVD.
#### code:
````python
"""
Find all principal components with their eigenvalues using SVD.
"""
def SVD(X: np.array)-> tuple:
    # Step1: find eigenvalues and eigenvectors
    # I use numpy.linalg.eig to find eigenvalues and eigenvectors because power method can not work on non square matrix and QR decomposition can not grarantee the eigenvectors are correct
    eigenvalues, eigenvectors = np.linalg.eig(np.dot(X.T, X))
    # Step2: obtain S, which is the diagonal matrix whose diagonal entries are the square roots of the eigenvalues of X
    idx = np.argsort(eigenvalues)[::-1]
    e = eigenvalues[idx]
    s = np.sqrt(e)
    S = np.diag(s)
    S1 = np.diag(s)
    for i in range(len(S1)):
        if S1[i][i] != 0:
            S1[i][i] = 1 / S1[i][i]
    # print(s)
    # Step3: obtain V, which is the matrix whose columns are the eigenvectors of X.T * X
    V = eigenvectors[:, idx]
    # Step4: obtain U, which is the matrix whose columns are the eigenvectors of X * X.T
    U = X.dot(V.dot(S1))
    return U, S, V

def find_all_principal_components_svd(X: np.array)-> np.array:
    #  Find all principal components with their eigenvalues using SVD.
    # center X
    X_mean = copy.deepcopy(X - np.mean(X, axis=0))
    # calculate SVD of X
    U, s, V = SVD(X_mean)
    s = s ** 2 / (len(X) - 1)
    return V, s

PCn, eigvals = find_all_principal_components_svd(X_np)

print("Q2.d")
print("Principal components: ")
print(PCn.T)
print("Eigenvalues: ")
print(eigvals)
# %%
U, s, V = SVD(X_np)
print("Principal components: ")
print(V)
print("Eigenvalues: ")
print(s)
````
#### Ans:
```
Q2.d
Principal components: 
[[ 0.487  0.57   0.329  0.283  0.148  0.477]
 [ 0.412 -0.111 -0.426 -0.477 -0.469  0.435]
 [ 0.176  0.632 -0.083 -0.443 -0.029 -0.605]
 [-0.599  0.235  0.462 -0.45  -0.221  0.348]
 [-0.297  0.231 -0.573 -0.147  0.642  0.311]
 [-0.339  0.394 -0.403  0.521 -0.544  0.012]]
Eigenvalues: 
[[224.681   0.      0.      0.      0.      0.   ]
 [  0.    119.155   0.      0.      0.      0.   ]
 [  0.      0.      5.439   0.      0.      0.   ]
 [  0.      0.      0.      0.801   0.      0.   ]
 [  0.      0.      0.      0.      0.125   0.   ]
 [  0.      0.      0.      0.      0.      0.   ]]
```

### e
SVD provides an extra information on U that PCA does not usually have. Is it any interpretation of this U matrix? If yes, please state it
U is the eigenvectors of X * X.T
#### Ans:
C is the covariance matrix, and each matrix can be decomposed through SVD --> $X=USV^T$. Therefore, $U$ will be deleted since it is an orthogonal set. So in this case of PCA it will be eliminated and has no additional use.
$$
C=\frac{X^TX}{n-1}=\frac{VSU^TUSV^T}{n-1}=V\frac{S^2}{n-1}V^T
$$
But if $X$ is regarded as a group of data and if $XX^T$ is needed, $U$ will be its eigenvectors. because:
$$
XX^T = USV^TVSU^T=US^2U^T
$$



### f
Conduct a rank-3 approximation (SVD version) of X
#### code:
````python
PCn, eigvals = find_all_principal_components_svd(X_np)
# get top 3 principal components
PCn = PCn[:3]
# get top 3 eigenvalues
eigvals = eigvals[:3]
# transform data
PCn_transformed = transform_data(X_np, PCn, eigvals)
print("Q2.f")
print("Top 3 principal components: ")
print(PCn_transformed)
print("Top 3 eigenvalues: ")
print(eigvals)
````
#### Ans:
```
Q2.f
Top 3 principal components: 
[[  0.697 -19.525   7.44 ]
 [ -3.488   0.106  -2.241]
 [  4.748   9.546  -3.065]
 [  6.381   9.075  -3.129]
 [ -6.457  -2.685   1.068]
 [ -1.881   3.483  -0.074]]
Top 3 eigenvalues: 
[[224.681   0.      0.      0.      0.      0.   ]
 [  0.    119.155   0.      0.      0.      0.   ]
 [  0.      0.      5.439   0.      0.      0.   ]]
```

## 3
#### code: 
````python
rows = range(12)
columns = [1, 5, 9]
X = [[a[i][j] for j in columns] for i in rows]
X_np = np.array(X, dtype=float)
print("Matrix of Q3:")
print(X_np)
print()
````
#### results:
```
Matrix of Q3:
[[ 87. 114. 103.]
 [ 81. 109.  97.]
 [ 73.  92.  88.]
 [ 83. 110. 102.]
 [ 92. 109. 111.]
 [ 95. 112. 108.]
 [ 83. 117. 106.]
 [ 78. 115. 104.]
 [ 90. 101. 107.]
 [ 81. 103. 102.]
 [ 87. 109. 103.]
 [ 89. 110. 108.]]

```

### a
Explain why the monthly data is unlikely to be Gaussian.
#### code: 
````python
def guassianaity_of_a_column(X: np.array)-> float:
    # check Guassianaity of X (each column of X) by Kurtosis
    # calculate mean and variance of X
    mean = np.mean(X)
    var = np.var(X)
    # calculate kurtosis
    kurtosis = np.mean((X - mean) ** 4) / var ** 2 - 3
    return kurtosis
print("Q3.a")
for i in range(len(X[0])):
    print("Kurtosis of row {}: {}".format(i, guassianaity_of_a_column(X_np[:,i])))
````
#### Ans:
All Kurtosis of each row is not close to 0. Forthermore, monthly temperature data typically exhibit seasonal patterns these variations lead to non-Gaussian behavior, as temperatures tend to cluster around certain ranges during specific months (e.g., colder in winter, warmer in summer). For the long term view, climate change and other long-term trends impact temperature data. Non-stationarity (changing statistical properties over time) violates the assumptions of Gaussianity.
```
Q3.a
Kurtosis of row 0: -0.6224382359992942
Kurtosis of row 1: 0.7578802109307694
Kurtosis of row 2: 1.4866569765083826
```

### b
Run the three preprocessing steps of ICA on X
#### code
````python
def ICA_preprocess(X: np.array)-> np.array:
    # Run the three preprocessing steps of ICA on X
    # Step 1: Center the data
    X_mean = copy.deepcopy(X - np.mean(X, axis=0))
    # print(X_mean)
    # Step 2: Decorrelate the data
    # calculate covariance matrix of X
    C = np.cov(X_mean.T)
    # find the top n eigenvalues and eigenvectors
    eigenvalues, eigenvectors = deflated_power_method(C, len(C))
    eigenvalues = np.array(eigenvalues)
    eigenvectors = np.array(eigenvectors)
    # sort eigenvalues
    indx = eigenvalues.argsort()
    eigenvalues = eigenvalues[indx]
    eigenvectors = eigenvectors[indx]
    # project X_mean to PCA space
    X_PCA = np.dot(eigenvectors, X_mean.T)
    # Step 3: Whiten the data
    l = np.sqrt(eigenvalues)
    l = 1 / l
    # diagonal matrix
    L = np.diag(l)
    # whiten data
    X_whiten = np.dot(L, X_PCA)
    return X_whiten.T

# X_111 = np.array([[1, 1, 2, 0, 5, 4, 5, 3],
#                   [3, 2, 3, 3, 4, 5, 5, 4]]).T
# X_whiten = ICA_preprocess(X_111)
W_whiten = ICA_preprocess(X_np)
print("Q3.b")
print("Whiten data: ")
print(W_whiten)
```` 
#### Ans:
```
Whiten data: 
[[ 1.283  0.591  0.444]
 [ 1.227  0.745 -0.594]
 [ 0.338 -0.512 -2.669]
 [ 0.104  0.486 -0.096]
 [-0.771 -0.978  0.946]
 [ 1.446 -0.759  1.112]
 [-0.526  1.352  0.583]
 [-1.394  1.683  0.041]
 [-0.795 -1.771  0.089]
 [-1.228 -0.311 -0.638]
 [ 0.718 -0.134  0.142]
 [-0.404 -0.393  0.639]]
```

### c
Provide a graphical illustration on the transformation, like the one in Lecture 03-2.
#### code
````python
# plot original data
fig = plt.figure()
ax = Axes3D(fig)
ax.scatter(X_np[:, 0], X_np[:, 1], X_np[:, 2])
plt.show()

# plot transformed data
fig = plt.figure()
ax = Axes3D(fig)
ax.scatter(W_whiten[:, 0], W_whiten[:, 1], W_whiten[:, 2])
plt.show()
```` 
#### Ans:
Before:
![](https://hackmd.io/_uploads/B1Iy3gogp.png)
After:
![](https://hackmd.io/_uploads/BJve3xjx6.png)

### d
Run the Fast ICA on Kurtosis Maximization to find the three independent components.
#### code
````python
def fast_ICA(X: np.array,  alpha=1, thresh=1e-8, iterations=5000):
    m, n = X.shape
    # Initialize random weights
    W = np.random.rand(m, m)

    for c in range(m):
            w = W[c, :].copy().reshape(m, 1)
            w = w / np.sqrt((w ** 2).sum())

            i = 0
            lim = 100
            while ((lim > thresh) & (i < iterations)):
                # Dot product of weight and X
                ws = np.dot(w.T, X)
                # Pass w*s into contrast function g
                wg = np.tanh(ws * alpha).T
                # Pass w*s into g prime 
                wg_ = (1 - np.square(np.tanh(ws))) * alpha
                # Update weights
                w_new = (X * wg.T).mean(axis=1) - wg_.mean() * w.squeeze()
                # Decorrelate weights              
                w_new = w_new - np.dot(np.dot(w_new, W[:c].T), W[:c])
                w_new = w_new / np.sqrt((w_new ** 2).sum())
                # Calculate limit condition
                lim = np.abs(np.abs((w_new * w).sum()) - 1)
                # Update weights
                w = w_new
                # Update counter
                i += 1

            W[c, :] = w.T
    return W

X_whiten = ICA_preprocess(X_np).T
W = fast_ICA(X_whiten)
# transform data
X_ICA = np.dot(W, X_whiten)
print("Q3.d")
print("W")
print(W)
print("independent components: ")
print(X_ICA)
# %%
# check independent components are independent
print("Check independent components are independent")
print("Covariance matrix of independent components: ")
print(np.cov(X_ICA))
```` 
##### Ans:
```
Q3.d
W
[[ 0.141 -0.166 -0.976]
 [ 0.698 -0.682  0.217]
 [ 0.702  0.712 -0.019]]
independent components: 
[[-0.35   0.63   2.737  0.028 -0.871 -0.756 -0.868 -0.516  0.094  0.5
  -0.014 -0.616]
 [ 0.589  0.219  0.007 -0.279  0.334  1.769 -1.164 -2.113  0.673 -0.783
   0.624  0.125]
 [ 1.313  1.402 -0.076  0.421 -1.255  0.453  0.583  0.219 -1.82  -1.071
   0.406 -0.575]]
   
Check independent components are independent
Covariance matrix of independent components: 
[[ 1.  0.  0.]
 [ 0.  1. -0.]
 [ 0. -0.  1.]]
```

## B
determinant is equal to the area of the parallelogram. Please prove it.
#### Ans:
![](https://hackmd.io/_uploads/BJOgFZoep.png)
