n = 10 ** 7

# Implementation using python list
def std(x:list):
    x_mean = sum(x)/len(x)
    y = sum([(v - x_mean) ** 2 for v in x])/len(x)
    return y**0.5

%time std(range(n))

# Implementation using numpy array function
def std_np(x):
    x_mean = np.sum(x)/len(x)
    return (((x - x_mean) ** 2).mean())** 0.5

%time std_np(np.arange(n))

"""
Explanation: Compare speed python list vs numpy ops
Let's implement standard deviation function. And compare computation time for 10 million numbers.
End of explanation
"""


%time np.std(np.arange(int(1e7)))

%%time 
n_input = tf.placeholder(dtype=tf.float64)
x = tf.range(0, n_input)
x_mean = tf.reduce_mean(x)
x_std = tf.sqrt(tf.reduce_mean(tf.square(tf.subtract(x, x_mean))))
with tf.Session() as sess:
    print(sess.run([x_std], feed_dict={n_input: n}))

"""
Explanation: As we can see numpy function much fater than that implemtated on python list. There are a built-in function in numpy to compute the standard deviation. Verify the std computed in all three techniques give same result.
End of explanation
"""


np.random.seed(1)
W = np.array([2.3, - 5.7, 8.9]).T
b = 1.2
X = np.random.random((10, 3))
y = np.dot(X, W)
print("W: ", W)

print("y: ", y)

W_estimate = np.linalg.inv(X.T.dot(X)).dot(X.T).dot(y)
W_estimate

"""
Explanation: Matrix multiplication
Usecase - solve Normal Equations
http://mlwiki.org/index.php/Normal_Equation
End of explanation
"""


np.random.seed(1230)
X = np.random.ranf((5, 3))
X

n = X.shape[0]
X0 = X - np.mean(X, axis = 0)
(X0.T).dot(X0)/n

np.cov(X, ddof=0, rowvar=False)

np.var(X[:, 0])

np.cov(X[:,0], X[:,1], ddof=0)

"""
Explanation: Covariance Matrix
End of explanation
"""


cx = np.cov(X, rowvar=False)
cx

e, v = np.linalg.eig(cx)
e, v

Z = X - X.mean(axis=0)
Z

Z.mean(axis=0)

U, D, V = np.linalg.svd(Z)

U

D

V

"""
Explanation: Eigen Value Decomposition
Read about Eigen, SVD, PCA decomposition https://www.cc.gatech.edu/~dellaert/pubs/svd-note.pdf
End of explanation
"""


U.dot(U.T)

V.dot(V.T)

"""
Explanation: U and V are unitary matrix
End of explanation
"""


U[0].dot(U[1]), U[0].dot(U[2]), U[1].dot(U[2])

V[0].dot(V[1]), V[0].dot(V[2]), V[1].dot(V[2])

X_0 = np.zeros_like(X)

np.fill_diagonal(X_0, D)
X_0

U.dot(X_0).dot(V)

"""
Explanation: Vectors of U and V are orthogonal
End of explanation
"""


