import numpy as np
from math import *
from fractionalBrownianmotion import fBm

def ornstein_uhlenbeck(dt,n,drift,sigma,H):
    '''
    Generates a path of fractional Ornstein Uhlenbeck using a Euler scheme with timestep dt

    args:
        dt:    timestep for the Euler scheme (float)
        n:     sample size (array)
        drift:      drift parameter (float)
        sigma:      diffusion parameter (float)
        H:          hurst parameter (float)
    '''
    T = dt*n
    trials = 1
    B = fBm(T,n,H,trials)
    x = np.zeros((n+1,trials))
    for i in range(n):
        x[i+1,:] = x[i,:] - dt * x[i,:] * drift + sigma*(B[i+1,:]-B[i,:])
    return x[:,0]

def increments(x,q):
    '''
    Given a discrete sample path x, generates a q-dimensional sample which contains
    the original sample and its increments (to the qth order)

    args:
        x:    sample (array)
        q:     number of increments desired (float)

    returns: (n x q)-array
    '''
    n = len(x)-q
    X = np.zeros((n,q))
    for i in range(1,q):
        X[:,i] = x[i:i+n] - x[0:n]
    X[:,0] = x[0:n]
    return X

def true_sample(n,q,h,xi,sigma,H):
    '''
    Given an observation timestep h, generates the associated discrete sample path of fractional Ornstein Uhlenbeck.

    args:
        n:     sample size (float)
        q:     number of increments desired (float)
        h:     observation timestep
        xi:    drift parameter
        sigma: diffusion parameter
        H:     hurst parameter

    returns:  Observed process + its increments ->  (n x q)-array
    '''
    dt = 0.001
    x = ornstein_uhlenbeck(dt,int((n+q)*(h/dt)), xi, sigma, H)
    good_indices = [i for i in range(int((n+q)*(h/dt))) if i % int(1/h) == 0]
    x = np.array([x[i] for i in good_indices])
    X = increments(x,q)
    return X

def euler_sample(n,q,gamma,xi,sigma,H):
    x = ornstein_uhlenbeck(gamma,n+q, xi, sigma, H)
    X = increments(x,q)
    return X


# --------------
# Code starts here

import numpy as np

# Code starts here

# Adjacency matrix
adj_mat = np.array([[0,0,0,0,0,0,1/3,0],
                   [1/2,0,1/2,1/3,0,0,0,0],
                   [1/2,0,0,0,0,0,0,0],
                   [0,1,0,0,0,0,0,0],
                  [0,0,1/2,1/3,0,0,1/3,0],
                   [0,0,0,1/3,1/3,0,0,1/2],
                   [0,0,0,0,1/3,0,0,1/2],
                   [0,0,0,0,1/3,1,1/3,0]])

# Compute eigenvalues and eigencevectrs
eigenvalues,eigencevectors  = np.linalg.eig(adj_mat)

eigen_first = eigencevectors[:,0]
eigen_1 = abs(eigen_first)/(np.linalg.norm(eigen_first,1))
max_idx = -1
maxi = eigen_1[0]
print(eigen_1)
for i in range(1,len(eigen_1)):
    if eigen_1[i]>maxi:
        maxi= eigen_1[i]
        max_idx = i
page = max_idx + 1
print(page)

# Eigen vector corresponding to 1

# most important page

# Code ends here

# --------------
# Code starts here

# Initialize stationary vector I
init_I = np.array([1,0,0,0,0,0,0,0]
)

# Perform iterations for power method
for i in range(10):
    init_I = np.dot(adj_mat,init_I)
    init_I = (init_I)/ (np.linalg.norm(init_I,1))

print(init_I)
max_idx = -1
maxi = init_I[0]

for i in range(1,len(init_I)):
    if init_I[i]>maxi:
        maxi= init_I[i]
        max_idx = i
power_page = max_idx + 1
print(page)

# Code ends here

# --------------
# Code starts here

# New Adjancency matrix
# New Adjancency matrix
new_adj_mat = np.array([[0,0,0,0,0,0,0,0],
                   [1/2,0,1/2,1/3,0,0,0,0],
                  [1/2,0,0,0,0,0,0,0],
                   [0,1,0,0,0,0,0,0],
                   [0,0,1/2,1/3,0,0,1/2,0],
                   [0,0,0,1/3,1/3,0,0,1/2],
                   [0,0,0,0,1/3,0,0,1/2],
                   [0,0,0,0,1/3,1,1/2,0]])

# Initialize stationary vector I
new_init_I = np.array([1,0,0,0,0,0,0,0]
)

# Perform iterations for power method
for i in range(10):
    new_init_I = np.dot(new_adj_mat,new_init_I)
    new_init_I = (new_init_I)/ (np.linalg.norm(new_init_I,1))

print(new_init_I)
max_idx = -1
maxi = new_init_I[0]

for i in range(1,len(new_init_I)):
    if new_init_I[i]>maxi:
        maxi= new_init_I[i]
        max_idx = i
new_power_page = max_idx + 1

# Code ends here

# --------------
# Alpha value
alpha = 0.85

# Code starts here

# Modified adjancency matrix
G = (alpha*new_adj_mat) + (1-alpha)*(1 / len(new_adj_mat))*np.ones(new_adj_mat.shape)

# Initialize stationary vector I
final_init_I = np.array([1,0,0,0,0,0,0,0])

# Perform iterations for power method
for i in range(1000):
    final_init_I = np.dot(G,final_init_I)
    final_init_I = final_init_I/(np.linalg.norm(final_init_I,1))

print(final_init_I)

# Code ends here


# Simpson's rule -- odd intervals

import math
import numpy
import pylab

# function we wish to integrate
def fun(x):
    return numpy.sin(math.pi*x)

# analytic value of the integral of fun() above
def I_exact(a,b):
    return (1.0/math.pi)*(-math.cos(math.pi*b) + math.cos(math.pi*a))

# do a Simpson's integration by breaking up the domain [a,b] into N
# slabs.  Note: we consider the N odd case here:
def simp(a,b,f,N):

    xedge = numpy.linspace(a,b,N+1)

    integral = 0.0

    if N%2 == 0:
        M = N
        odd = 0
    else:
        M = N - 1
        odd = 1

    delta = (xedge[1] - xedge[0])

    n = 0
    while n < M:
        integral += (1.0/3.0)*delta*(f(xedge[n]) +
                                     4.0*f(xedge[n+1]) +
                                     f(xedge[n+2]))
        n += 2

    # if we had an odd # of bins, do the last one:
    if (odd):
        integral += (delta/12.0)*(-f(xedge[N-2]) +
                                   8.0*f(xedge[N-1]) +
                                   5.0*f(xedge[N]))

    return integral

a = 0.0
b = 1.0

N = [3, 7, 15, 31]

delta = []
error = []

for n in N:
    t = simp(a,b,fun,n)
    e = t - I_exact(a,b)
    dx = (b - a)/n

    print dx, t, e

    # for plotting
    delta.append(dx)
    error.append(e)

d = numpy.array(delta)
e = numpy.array(error)
pylab.scatter(d, e)

# plot a line representing delta**4 scaling
pylab.plot(d, e[0]*(d/d[0])**4)

ax = pylab.gca()
ax.set_xscale('log')
ax.set_yscale('log')

pylab.xlabel(r"$\delta$")
pylab.ylabel("absolute error")

pylab.savefig("simp-odd.eps")


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Thu Oct  7 11:04:44 2021

@author: denisdumont
"""
###############################################################################
#Dice or Coin throws : Calulate "N_mean" means over "N_throw" throws of dice
#and verify the expected normal law distribution
###############################################################################

import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
from collections import Counter

N=10000 #Nombre de moyenne
n=1000 #Nombre de lancer
m=24 #Nombre de face du dé (2 pour une pièce)

#QUESTIONS 1 et 2
lancers=np.random.randint(1,m+1,n)
moyenne=np.mean(lancers)
print(moyenne)
print(Counter(lancers))

#QUESTION 3 et 4
N=10000
n=1000

list_moyenne=[]
for i in np.arange(1,N):
    lancers=np.random.randint(1,m+1,n)
    moyenne=np.mean(lancers)
    list_moyenne.append(moyenne)

prob,interv=np.histogram(list_moyenne,bins='auto',density=True)
#Milieu de chaque interval
mid_interv=[(interv[i]+interv[i+1])/2. for i in range(len(interv)-1)]
#Graphique
plt.figure(1)
plt.plot(mid_interv,prob,'o')
plt.xlabel('Value')
plt.ylabel('Probability')

#Definition de la fonction gaussienne
def gauss(x,a,mu,sigma):
    return a*np.exp(-1/2*((x-mu)/sigma)**2)

#Ajsutement et plot en graphique
popt,pcov=curve_fit(gauss,mid_interv,prob,p0=[1/(np.sqrt(np.var(np.arange(1,m+1)))*np.sqrt(2*np.pi)),np.mean(np.arange(1,m+1)),0.1])#Need to adjust p0 to help the fit,by default p0=[1,1,1]
plt.plot(mid_interv,gauss(mid_interv, *popt),'r-',label='Fit: a=%5.3f, mu=%5.3f, sigma=%5.3f' % tuple(popt))

#Graph prediction theoriques
Mean_theo= np.mean(np.arange(1,m+1)) #Calculation of the theoretical mean
Sigma_x=np.sqrt(np.var(np.arange(1,m+1)))
Sigma_theo=Sigma_x/np.sqrt(n) #Calculation of the theoretical standard deviation
x=np.linspace(Mean_theo-3*Sigma_theo,Mean_theo+3*Sigma_theo)
lab="Pred theo: a="+str(round(1/(Sigma_theo*np.sqrt(2*np.pi)),2))+str(round(Mean_theo,2))+", mu="+str(round(Mean_theo,2))+", sigma="+str(round(Sigma_theo,3))
plt.plot(x,gauss(x,1/(Sigma_theo*np.sqrt(2*np.pi)),Mean_theo,Sigma_theo),'g-.',label=lab)
plt.legend()
plt.show()

#QUESTION 5
list_n=[1000,10000,100000]
list_sigma=[]
for j in list_n:
    list_moyenne=[]
    for i in np.arange(1,N):
        lancers=np.random.randint(1,m+1,j)
        moyenne=np.mean(lancers)
        list_moyenne.append(moyenne)

    prob,interv=np.histogram(list_moyenne,bins='auto',density=True)
    #Milieu de chaque interval
    mid_interv=[(interv[i]+interv[i+1])/2. for i in range(len(interv)-1)]
    #Graphique
    plt.figure(2)
    plt.plot(mid_interv,prob,'o')
    plt.xlabel('Value')
    plt.ylabel('Probability')

    #Definition de la fonction gaussienne
    def gauss(x,a,mu,sigma):
        return a*np.exp(-1/2*((x-mu)/sigma)**2)

    #Ajsutement et plot en graphique
    popt,pcov=curve_fit(gauss,mid_interv,prob,p0=[1/(np.sqrt(np.var(np.arange(1,m+1)))*np.sqrt(2*np.pi)),np.mean(np.arange(1,m+1)),0.1])#Need to adjust p0 to help the fit,by default p0=[1,1,1]
    plt.plot(mid_interv,gauss(mid_interv, *popt),'r-',label='Fit: a=%5.3f, mu=%5.3f, sigma=%5.3f' % tuple(popt))
    list_sigma.append(abs(popt[2]))

plt.figure(3)
plt.loglog(list_n,list_sigma,'o',label='data')
plt.loglog(list_n,1/np.sqrt(np.array(list_n)),label='1/n**0.5')
plt.legend()
#Plus on augmente n plus la valeur moyenne ets précise + on observe bien une
#evolution en 1/n^0.5 de l'ecart type comme vu au cours theorique(cfr. figur 3)

#QUESTIONS 6 et 7
#mettre m=6 et m=24


# -*- coding: utf-8 -*-
"""
Coursework 2: Logistical regression
"""

#%%
"""
Imports and definitions
"""

import numpy as np
from pandas.io.parsers import read_csv
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
import scipy.optimize as opt

def carga_csv(file_name):
    """
    carga el fichero csv especificado y lo
    devuelve en un array de numpy
    """
    valores = read_csv(file_name,header=None).values
    #suponemos que siempre trabajaremos con float
    return valores.astype(float)

def sigmoid(z):
    """
    sigmoid function
    can be applied on numbers and on numpy arrays of any dimensions
    """
    return 1 / (1 + np.exp(-z))

def coste(theta, X, Y):
    """
    cost function
    computes J(theta) for a given dataset
    """
    m = np.shape(X)[0]
    H = sigmoid((np.dot(X,theta)))
    J = -1/m * ( np.log(H).transpose().dot(Y)
                 + np.log(1-H).transpose().dot(1-Y))
    return J

def gradiente(theta, X, Y):
    """
    gradient function
    computes the gradient of J at a given theta for a given dataset
    """
    m = np.shape(X)[0]
    H = sigmoid(np.dot(X,theta))
    G = 1/m * X.transpose().dot(H - Y)
    return G

def coste_reg(theta, X, Y, lamb):
    """
    cost function with regularization
    computes J(theta) for a given dataset
    with a regularization factor of lambda
    """
    m = np.shape(X)[0]
    H = sigmoid((np.dot(X,theta)))
    J = -1/m * ( np.log(H).transpose().dot(Y)
                 + np.log(1-H).transpose().dot(1-Y))
    reg = lamb/(2*m)*np.sum(theta[1:]**2)
    return J + reg

def gradiente_reg(theta, X, Y, lamb):
    """
    gradient function with regularization
    computes the gradient of J at a given theta for a given dataset
    with a regularization factor of lambda
    """
    m = np.shape(X)[0]
    n = np.shape(X)[1]
    H = sigmoid(np.dot(X,theta))
    G = 1/m * X.transpose().dot(H - Y)
    reg = np.ones(n,)
    reg[0] = 0
    reg = (lamb/m)*reg*theta
    return G + reg

def precision(theta,X,Y):
    """
    accuracy function
    computes the accuracy of the logistic model theta on X with true target variable Y
    """
    m = np.shape(X)[0]
    H = sigmoid(np.dot(X,theta))
    H[H >= 0.5] = 1
    H[H < 0.5] = 0
    return np.sum(H == Y)/m

def pinta_frontera_recta(X, Y, theta):
    """
    draws straight decision borders
    """
    plt.figure()
    x1_min, x1_max = X[:, 0].min(), X[:, 0].max()
    x2_min, x2_max = X[:, 1].min(), X[:, 1].max()
    xx1, xx2 = np.meshgrid(np.linspace(x1_min, x1_max),
               np.linspace(x2_min, x2_max))
    h = sigmoid(np.c_[np.ones((xx1.ravel().shape[0], 1)),
                      xx1.ravel(),
                      xx2.ravel()].dot(theta))
    h = h.reshape(xx1.shape)
    # el cuarto parámetro es el valor de z cuya frontera se
    # quiere pintar
    plt.contour(xx1, xx2, h, [0.5], linewidths=1, colors='b')
    # Add the data to the graph
    plt.scatter(X[pos, 0],X[pos, 1], marker='+', c='k')
    plt.scatter(X[not_pos, 0], X[not_pos, 1], marker='o', c='y')
    plt.xlabel('Exam 1 score')
    plt.ylabel('Exam 2 score')
    plt.show()

def pinta_frontera_curva(X, Y, theta, poly, lamb):
    """
    draws polynomial decision borders
    adds the value of the regularization parameter for wich it was computed
    """
    plt.figure()
    x1_min, x1_max = X[:, 0].min(), X[:, 0].max()
    x2_min, x2_max = X[:, 1].min(), X[:, 1].max()
    xx1, xx2 = np.meshgrid(np.linspace(x1_min, x1_max),
                           np.linspace(x2_min, x2_max))
    h = sigmoid(poly.fit_transform(np.c_[xx1.ravel(),
                            xx2.ravel()]).dot(theta))
    h = h.reshape(xx1.shape)
    # el cuarto parámetro es el valor de z cuya frontera se
    # quiere pintar
    plt.contour(xx1, xx2, h, [0.5], linewidths=1, colors='b')
    # add the data to the graph
    plt.scatter(X[pos, 0],X[pos, 1], marker='+', c='k')
    plt.scatter(X[not_pos, 0], X[not_pos, 1], marker='o', c='y')
    plt.title('lambda = ' + str(lamb))
    plt.xlabel('Exam 1 score')
    plt.ylabel('Exam 2 score')
    plt.show()

#%%
"""
1 - Logistical regression
"""

# loading the data
datos = carga_csv('ex2data1.csv')
X = datos[:, :-1]
Y = datos[:, -1]
m = np.shape(X)[0]
n = np.shape(X)[1]

# Visualizing the data
pos = np.where(Y == 1)
not_pos = np.where(Y == 0)
plt.scatter(X[pos, 0],X[pos, 1], marker='+', c='k', label = 'admited')
plt.scatter(X[not_pos, 0], X[not_pos, 1], marker='o', c='y', label = 'not admited')
plt.legend()
plt.xlabel('Exam 1 score')
plt.ylabel('Exam 2 score')

# Add the column of 1s
X = np.hstack([np.ones([m,1]),X])

theta = np.zeros((n+1,))
# Compute the best value for theta
result = opt.fmin_tnc(func=coste, x0=theta, fprime=gradiente, args=(X,Y), messages=0)
theta_opt = result[0]

# Display the border
pinta_frontera_recta(X[:,1:], Y, theta_opt)

# Display the accuracy of the model
p = precision(theta_opt,X,Y)
print('Model accuracy : {0:1.3g}'.format(p))

#%%
"""
2 - Regularized logistical regression
"""

datos = carga_csv('ex2data2.csv')
X = datos[:, :-1]
Y = datos[:, -1]
m = np.shape(X)[0]

# Visualizing the data
pos = np.where(Y == 1)
not_pos = np.where(Y == 0)
plt.scatter(X[pos, 0],X[pos, 1], marker='+', c='k', label = 'y = 1')
plt.scatter(X[not_pos, 0], X[not_pos, 1], marker='o', c='y', label = 'y = 0')
plt.legend()
plt.xlabel('Microchip test 1')
plt.ylabel('Microchip test 2')

# Adding new attributes
poly = PolynomialFeatures(6)
X_pol = poly.fit_transform(X)
n = np.shape(X_pol)[1]

theta = np.zeros((n,))
lamb_arr = [0,0.3,1,3,10,30]
# Compute the optimal value for theta using various values of lambda
for lamb in lamb_arr:
    # Compute the best value for theta
    result = opt.fmin_tnc(func=coste_reg, x0=theta, fprime=gradiente_reg,
                                        args=(X_pol,Y,lamb), messages = 0)
    theta_opt = result[0]
    # Display the border
    pinta_frontera_curva(X_pol[:,1:], Y, theta_opt,poly,lamb)
    # Display the accuracy of the model
    p = precision(theta_opt,X_pol,Y)
    print('With lambda = ' + str(lamb) + ' we get an accuracy of {0:1.3g}'.format(p))


# -*- coding: utf-8 -*-
"""
Created on Mon Dec 11 20:02:37 2017

@author: Anastasios Tzavellas
"""

import numpy as np
import scipy.integrate as integrate

def df(x, xValues, dValues):
    """Derivative using partial differences
    second order"""
    h = xValues[1] - xValues[0]
    theta = (x - xValues[0]) / h
    return (dValues[1] + 0.5 * (2*theta - 1)*dValues[2]) / h

def d2f(x, xValues, dValues):
    """Second Derivative using partial differences
    second order"""
    h = xValues[1] - xValues[0]
    return dValues[2] / np.power(h, 2)

def differenceTable(fxValues):
    """Calculates difference table and returns its
    top diagonal"""
    n = fxValues.size
    dValues = np.copy(fxValues)
    for i in range(1, n):
        for j in reversed(range(i, n)):
            dValues[j] = dValues[j] - dValues[j-1]
    return dValues

def dividedDifferenceTable(xValues, fxValues):
    """Calculates divided difference table and returs its
    top diagonal"""
    n = fxValues.size
    dValues = np.copy(fxValues)
    for i in range(1, n):
        for j in reversed(range(i, n)):
            dValues[j] = (dValues[j] - dValues[j-1]) / \
                         (xValues[j] - xValues[j-i])
    return dValues

def addCoefficient(xValues, fxValues, coefficients):
    """Adds extra Newton Polynomial Coefficient"""
    n = coefficients.size
    x = xValues[n]  # Get x of new point to interpolate
    y_x = fxValues[n]  # Get y of new point to interpolate
    p_x = NestedMultiplication(x, xValues, coefficients)  # evaluate p(x)
    product = 1.
    for i in range(n):
        product = product * (x - xValues[i])  # (x-x0)...(x-x_n-1)
    newCoefficient = (y_x - p_x) / product  # new coefficient
    return np.append(coefficients, newCoefficient)

def NestedMultiplication(x, xValues, coeff):
    """Evaluates Newton Polynomial at x in nested form
    given the interpolating points and its coefficents"""
    n = coeff.size
    y = coeff[n-1]
    for i in reversed(range(n - 1)):
        y = coeff[i] + (x - xValues[i]) * y
    return y

def lSquaresMonomial(f, order, a, b):
    """Solves least squares with monomial polynomials"""
    n = order + 1
    S = np.zeros((n, n))
    for i in range(n):
        for j in range(n):
            S[i, j] = integrate.quad(lambda x: np.power(x, i+j),
             a,
             b)[0]
    y = np.zeros(n)
    for i in range(n):
        y[i] = integrate.quad(lambda x: f(x) * np.power(x, i),
         a,
         b)[0]
        # Round to 0 if less than 1e-8
        y[i] = 0. if np.isclose(y[i], 0., atol=1e-8) else y[i]
    p = np.linalg.solve(S, y)
    return np.poly1d(np.flip(p, 0))

def lSquaresOrthogonal(f, w, phi, order, a, b):
    """Solves least squares system using orthogonal
    polynomials with weight w(x) and base phi(x)"""
    c = np.zeros(order)
    for i in range(order):
        c[i] = integrate.quad(lambda x: np.power(phi(x, i), 2),
         a,
         b)[0]
    A = np.zeros(order)
    for i in range(order):
        A[i] = integrate.quad(lambda x: f(x) * phi(x, i),
         a,
         b)[0] / c[i]
    legpoly = np.polynomial.legendre.leg2poly(A)
    return np.poly1d(np.flip(legpoly, 0))


'''This program simulates Sell off model described in "Mathematical modeling of
	allegiance to political parties", Iacopo Poli, 2015

    Copyright (C) 2015 - Iacopo Poli

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.'''

import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from paperlib import randomic
plt.ion()

N = 1000 # population sample
density = 35.5 # density per squared km of population
influence_area= 100 # squared kilometer of efficacy

#defining random constants

#fidelity rates
a1 = randomic(N, density, influence_area)
a2 = randomic(N, density, influence_area)
#rates of return
b1 = randomic(N, density, influence_area)
b2 = randomic(N, density, influence_area)
#rates of exchange
s1 = randomic(N, density, influence_area)
s2 = randomic(N, density, influence_area)

print 'a1:', a1
print 'a2:', a2
print 'b1:', b1
print 'b2:', b2
print 's1:', s1
print 's2:', s2

#defining system of equations
def f(y, t):
        S = y[0]
        L1 = y[1]
        L2 = y[2]
        # the model equations
        f0 = -a1*L1*S-a2*L2*S+b1*L1*S+b2*L2*S
        f1 = a1*L1*S-b1*L1*S+s1*L1*L2-s2*L1*L2
        f2 = a2*S*L2-b2*L2*S-s1*L1*L2+s2*L1*L2
        return [f0, f1, f2]

# initial conditions
S = N-2               # initial population
L1 = 1                  # initial loyal to party 1 population
L2 = 1                  # initial loyal to party 2 population
y0 = [S, L1, L2]       # initial condition vector
t  = np.linspace(0, 3., 100)   # time grid in months

# solve the ODEs
soln = odeint(f, y0, t)
S = soln[:, 0]
L1 = soln[:, 1]
L2 = soln[:, 2]

# plot results
plt.figure()
plt.plot(t, S, label='Susceptibles')
plt.plot(t, L1, label='Loyal 1')
plt.plot(t, L2, label='Loyal 2')
plt.xlabel('Months from start of the campaign')
plt.ylabel('Population')
plt.title('Campaign - No Init. Pop. of Loyals')
plt.legend(loc=0)
plt.pause(120)


import numpy as np

from ..validation import check_array, check_consistent_length

def mean_absolute_error(y_true, y_pred):
    """
    Returns the mean absolute error.

    Parameters
    ----------
    y_true : array_like
        The correct targets.
    y_pred : array_like
        The estimated targets.

    Returns
    -------
    float
        The mean absolute error.
    """
    y_true, y_pred = _validate_targets(y_true, y_pred)
    return np.average(np.abs(y_true - y_pred))

def median_absolute_error(y_true, y_pred):
    """
    Returns the median absolute error.

    Parameters
    ----------
    y_true : array_like
        The correct targets.
    y_pred : array_like
        The estimated targets.

    Returns
    -------
    float
        The median absolute error.
    """
    y_true, y_pred = _validate_targets(y_true, y_pred)
    return np.median(np.abs(y_true - y_pred))

def mean_squared_error(y_true, y_pred, root=False):
    """
    Returns the mean squared error (MSE) if root = False, else
    it returns the root mean squared error (RMSE)

    Parameters
    ----------
    y_true : array_like
        The correct targets.
    y_pred : array_like
        The estimated targets.
    root : bool, default=False
        If True, then the root mean squared error will be returned.

    Returns
    -------
    mse
        The (root) mean squared error.
    """
    y_true, y_pred = _validate_targets(y_true, y_pred)
    mse = np.average((y_true - y_pred) ** 2)
    if root:
        return np.sqrt(mse)
    return mse

def max_error(y_true, y_pred):
    """
    Returns the max absolute error

    Parameters
    ----------
    y_true : array_like
        The correct targets.
    y_pred : array_like
        The estimated targets.

    Returns
    -------
    float
        The max absolute error.
    """
    y_true, y_pred = _validate_targets(y_true, y_pred)
    return np.max(np.abs(y_true - y_pred))

def r2_score(y_true, y_pred):
    """
    Returns the :math:`R^2` score.
    Worst score is 0 and best possible score is 1.

    Parameters
    ----------
    y_true : array_like
        The correct targets.
    y_pred : array_like
        The estimated targets.

    Returns
    -------
    float
        The r2 score.

    Notes
    -----
    As the r-squared measure increases artificially with the number of
    regressors, it is important to compare it with the adjusted r-squared,
    which eliminates this problem by scaling it with a factor
    that is anti-proportionally to the number of regressors in the model.
    If the adjusted r-squared is significantly lower than the r-squared
    the model may have too many regressors which do not have a significant
    impact on the models predictions. Therefore, in a "good" model,
    both measures are close to each other.
    """
    y_true, y_pred = _validate_targets(y_true, y_pred)
    residual_sum_of_squares = np.sum((y_true - y_pred) ** 2)
    total_sum_of_squares = np.sum((y_true - np.average(y_true) ** 2))
    return 1 - residual_sum_of_squares / total_sum_of_squares

def adjusted_r2_score(y_true, y_pred, n_features):
    """
    Returns the :math:`R^2` score.

    The higher the value, the better. Because of the scaling factor, unlike
    the R2_score, this measure is *not* in the interval [0, 1].

    Parameters
    ----------
    y_true : array_like
        The correct targets.
    y_pred : array_like
        The estimated targets.
    n_features : int
        The number of features in the model.

    Returns
    -------
    float
        The adjusted r2 score.
    """
    y_true, y_pred = _validate_targets(y_true, y_pred)
    n_samples: int = len(y_true)
    return 1 - ((n_samples - 1) / (n_samples - n_features)) * (
        1 - r2_score(y_true, y_pred)
    )

def _validate_targets(y_true, y_pred):
    # TODO: Does not raise an error, if targets are not 1D, but it should
    y_true = check_array(y_true, ensure_2d=False)
    y_pred = check_array(y_pred, ensure_2d=False)
    check_consistent_length(y_true, y_pred)
    return y_true, y_pred


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Plot L1-norm
"""

import numpy as np
import matplotlib.pyplot as plt

from mpl_toolkits.mplot3d import axes3d
from matplotlib import cm

# Build datas ###############

x = np.arange(-5, 5, 0.1)
y = np.arange(-5, 5, 0.1)

xx,yy = np.meshgrid(x, y)
z = abs(xx) + abs(yy)

# Plot data #################

fig = plt.figure()

#ax = axes3d.Axes3D(fig)
#ax.plot_wireframe(xx, yy, z)

ax = fig.gca(projection='3d')

ax.plot_surface(xx, yy, z, rstride=5, cstride=5, alpha=0.3)
cset = ax.contourf(xx, yy, z, zdir='z', offset=0, cmap=cm.coolwarm)

ax.set_xlabel(r'$x_1$')
ax.set_ylabel(r'$x_2$')
ax.set_zlabel(r'$||x||_{1}$')

# SAVE FILES ######################
plt.savefig("norm_l1.png")

plt.show()


# coding: utf-8

#11,12,13
import numpy as np
from graphviz import Graph
import matplotlib.pyplot as plt

train_data = np.loadtxt('hw3_train.dat.txt')
test_data = np.loadtxt('hw3_test.dat.txt')
train_x = train_data[:,0:-1]
train_y = train_data[:,-1]
test_x = test_data[:,0:-1]
test_y = test_data[:,-1]

class node():
    def __init__(self,threshold,index,value=None):
        self.threshold = threshold
        self.index = index
        self.value = value
        self.leftNode = None
        self.rightNode = None

def gini_index(Y):
    length = len(Y)
    if length == 0:
        return 1
    else:
        return 1 - (np.sum(Y==1)/length)**2 - (np.sum(Y==-1)/length)**2

def decision_stump(train_x,train_y):
    error = train_y.shape[0]
    for i in range(len(train_x[0])):
        sort_array = np.sort(train_x[:,i])
        sort_array2 = []
        sort_array2 = np.append(sort_array[1:],0)
        threshold=[]
        threshold = np.append(-1*float("inf"),((sort_array + sort_array2)/2)[:-1])
        threshold = np.append(threshold,float("inf"))
        for theta in threshold:
            Y1 = train_y[train_x[:,i] < theta]
            Y2 = train_y[train_x[:,i] >= theta]
            impurity = Y1.shape[0]*gini_index(Y1)+Y2.shape[0]*gini_index(Y2)
            if error > impurity:
                error = impurity
                s = theta
                feature_index = i
    return (feature_index,s)

def stop_cut(X, Y):
    if gini_index(Y)==0 or X.shape[0]==1 or np.sum(X!=X[0])==0:
        return True
    else:
        return False

def decision_tree(train_x,train_y):
    if stop_cut(train_x,train_y):
        tree_node = node(None,None,train_y[0])
        return tree_node
    feature_index,s = decision_stump(train_x,train_y)
    pos1 = train_x[:,feature_index] < s
    pos2 = train_x[:,feature_index] >= s
    left_train_x = train_x[pos1,:]
    left_train_y = train_y[pos1]
    right_train_x = train_x[pos2,:]
    right_train_y = train_y[pos2]
    tree_node = node(s,feature_index)
    tree_node.leftNode = decision_tree(left_train_x,left_train_y)
    tree_node.rightNode = decision_tree(right_train_x,right_train_y)
    return tree_node

def draw_tree(root,node,leaf):
    if root.value ==None:
        node += 1
        graph.node(name='%d' %(node),label="%d;%5f" %(root.index+1,root.threshold))
        if root.leftNode != None:
            node,leaf = draw_tree(root.leftNode,node,leaf)
        if root.rightNode != None:
            node,leaf = draw_tree(root.rightNode,node,leaf)
    else:
        leaf += 1
        graph.node(name='l%d' %leaf,label="class = %d" %root.value)
    return node,leaf

def predict_tree(root,X):
    if root.value != None:
        return root.value
    threshold = root.threshold; index = root.index
    if X[index] < threshold:
        return predict_tree(root.leftNode,X)
    else:
        return predict_tree(root.rightNode,X)

def error(train_x,train_y,tree):
    predict = np.zeros(train_y.shape)
    for num in range(train_x.shape[0]):
        predict[num] = predict_tree(tree, train_x[num,:])
    return predict

tree = decision_tree(train_x,train_y)
graph = Graph('tree', format='svg')
internal_num,leaf = draw_tree(tree,0,0)

graph.edge('1','2')
graph.edge('2','3')
graph.edge('3','l1')
graph.edge('3','l2')
graph.edge('2','4')
graph.edge('4','5')
graph.edge('5','6')
graph.edge('6','l3')
graph.edge('6','l4')
graph.edge('5','7')
graph.edge('7','l5')
graph.edge('7','l6')
graph.edge('4','8')
graph.edge('8','9')
graph.edge('9','l7')
graph.edge('9','l8')
graph.edge('8','l9')
graph.edge('1','10')
graph.edge('10','l10')
graph.edge('10','l11')

predict_y = error(train_x,train_y,tree)
E_in = np.sum(predict_y!=train_y)/train_y.shape[0]
print("E in = %f" %E_in)
predict_y = error(test_x,test_y,tree)
E_out = np.sum(predict_y!=test_y)/test_y.shape[0]
print("E out = %f" %E_out)

#13
def purning_tree(X,Y,root,set_height,start_height,Error):
    if root.value == None and start_height < set_height:
        start_height += 1
        threshold = root.threshold; index = root.index
        X_left = X[X[:,index] < threshold,:]
        Y_left = Y[X[:,index] < threshold]
        X_right = X[X[:,index] >= threshold,:]
        Y_right = Y[X[:,index] >= threshold]
        Error = purning_tree(X_left,Y_left,root.leftNode,set_height,start_height,Error)
        Error = purning_tree(X_right,Y_right,root.rightNode,set_height,start_height,Error)
    elif root.value == None and start_height == set_height:
        if np.sum(Y) >= 0:
            error = np.sum(np.ones(np.shape(Y)) != Y)
            #print(error)
            return Error+error
        else:
            error = np.sum(-1*np.ones(np.shape(Y)) != Y)
            #print(error)
            return Error+error
    elif root.value != None and start_height <= set_height:
        error = np.sum(root.value * np.ones(np.shape(Y)) != Y)
        #print(error)
        return Error+error
    return Error

def purning_error(total_height,X,Y):
    error = []
    for height in range(total_height):
        error.append(purning_tree(X,Y,tree,height,0,0))
    Error = np.array(error)/Y.shape[0]
    return Error

total_height = 6
E_in = purning_error(total_height,train_x,train_y)
E_out = purning_error(total_height,test_x,test_y)

plt.plot(E_in,color='purple',linewidth=1,linestyle='-',label="E in")
plt.plot(E_out,color='red',linewidth=1,linestyle='-',label="E out")
plt.xlabel('Height')
plt.ylabel('Error')
plt.legend(loc='upper right')
plt.savefig('Q13.jpg')
plt.show()

#14,15
def bagging(X,Y,rate):
    pos = np.random.randint(0, X.shape[0],size = int(X.shape[0]*rate))
    return X[pos, :], Y[pos]

def random_forest(X, Y,iteration,rate):
    Treenode = []
    for i in range(iteration):
        bag_train_x, bag_train_y = bagging(X, Y,rate)
        node = decision_tree(bag_train_x, bag_train_y)
        Treenode.append(node)
    return Treenode

T = 30000
E_in = []
G_E_in = []
Gt = []
G_E_out = []
Gt_out = []
#build tree
treeArray = random_forest(train_x, train_y,T,0.8)

for i in range(T):
    predict_y = error(train_x,train_y,treeArray[i])
    G_predict_y = np.reshape(error(train_x,train_y,treeArray[i]),(-1,1))
    G_out_predict_y = np.reshape(error(test_x,test_y,treeArray[i]),(-1,1))
    if i==0:
        Gt = G_predict_y
        Gt_out = G_out_predict_y
    else:
        Gt = np.concatenate((Gt,G_predict_y),axis = 1)
        Gt_out = np.concatenate((Gt_out,G_out_predict_y),axis = 1)
    E_in.append(np.sum(predict_y!=train_y)/train_y.shape[0])
    G_E_in.append(np.sum(np.sign(np.sum(Gt,axis=1)) != train_y)/train_y.shape[0])
    G_E_out.append(np.sum(np.sign(np.sum(Gt_out,axis=1)) != test_y)/test_y.shape[0])

plt.hist(E_in,color='green')
plt.xlabel('Ein(gt)')
plt.ylabel('Count')
plt.savefig('Q14.jpg')
plt.show()

plt.plot(G_E_in)
plt.xlabel('T')
plt.ylabel('Ein(Gt)')
plt.savefig('Q15.jpg')
plt.show()

plt.plot(G_E_out)
plt.xlabel('T')
plt.ylabel('Eout(Gt)')
plt.savefig('Q16.jpg')
plt.show()


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat Jan 23 2021

@authors: Dr. M. S. Ramkarthik and Dr. Pranay Barkataki
"""

import numpy as np
import math

class QuantumMechanics:

    def __init__(self):
        """It is a quantum mechanics class, and it primarily intrinsic
            functions of uses numpy and scipy
        """

    def inner_product(self,vec1,vec2):
        """
        Here we compute the inner product
        Attributes:
            vec1: it is a column vector.
            vec2: it is the second vector.
        Returns:
            inn: inner product between vec1 and vec2.
        """
        inn=complex(0.0,0.0)
        assert len(vec1) ==  len(vec2),\
        "Dimension of two vectors not equal to each other"
        for i in range(0,len(vec1)):
            inn=inn+(np.conjugate(vec1[i])*vec2[i])

        return inn

    def norm_vec(self,vec):
        """
        Here we calculate norm of a vector
        Attributes:
            vec: column vector
        Returns:
            norm: it contains the norm of the column vector vec
        """
        norm=0.0
        for i in range(0,len(vec)):
            norm=norm+abs(np.conjugate(vec[i])*vec[i])
        return np.sqrt(norm)

    def normalization_vec(self,vec):
        """
        Here normalize a given vector
        Attributes:
            vec: unnormalized column vector
        Returns:
            vec: normalized column vector
        """
        norm=0.0
        for i in range(0,len(vec)):
            norm=norm+abs(np.conjugate(vec[i])*vec[i])
        vec=vec/np.sqrt(norm)
        return vec

    def outer_product_rvec(self,vec1,vec2):
        """
        Here we calculate the outer product
        Attributes:
            vec1: it is a column real vector
            vec2: it is another real vector
        Returns:
            matrix: outer product of vec1 and vec2
        """
        matrix = np.zeros([len(vec1),len(vec2)],dtype='float64')
        for i in range(0,len(vec1)):
            for j in range(0,len(vec2)):
                matrix[i,j]=vec1[i]*vec2[j]
        return matrix

    def outer_product_cvec(self,vec1,vec2):
        """
        Here we calculate the outer product
        Attributes:
            vec1: it is a column complex vector
            vec2: it is another complex vector
        Returns:
            matrix: outer product of vec1 and vec2
        """
        matrix = np.zeros([len(vec1),len(vec2)],dtype=np.complex_)
        for i in range(0,len(vec1)):
            for j in range(0,len(vec2)):
                matrix[i,j]=vec1[i]*np.conjugate(vec2[j])
        return matrix

    def tensor_product_matrix(self,A,B):
        """
        Here we calculate the tensor product of two matrix A and B
        Attributes:
            A: it is either a 1D or 2D array
            B: it is either a 1D or 2D array
        Returns: tensor product of A and B
        """
        if len(A.shape)==1:
            A=A.reshape(A.shape[0],1)
        if len(B.shape)==1:
            B=B.reshape(B.shape[0],1)
        return np.kron(A,B)

    def commutation(self,A,B):
        """
        Here we calculate commutation between matrices A and B
        Attributes:
            A: it is a square matrix
            B: it is another square matrix
        Returns: AB-BA matrix
        """
        return np.matmul(A,B)-np.matmul(B,A)

    def anti_commutation(self,A,B):
        """
        Here we calculate anti commutation between matrices A and B
        Attributes:
            A: input a square matrix
            B: input another square matrix
        Returns: AB+BA matrix
        """
        return np.matmul(A,B)+np.matmul(B,A)

    def rstate_shift(self,vec,shift=1,shift_direction='right'):
        """
        Shifting of a real quantum state
        Inputs:
            vec: real quantum state.
            shift: degree of the state.
            shift_direction: It shows the direction of the shift,
                             by default it is right
        Return:
            state2: shifted state of vec
        """
        assert vec.shape[0]%2==0,"Not a qubit quantum state"
        N=int(math.log(vec.shape[0],2))
        basis=np.zeros([N],dtype=int)
        state2=np.zeros([2**N],dtype='float64')
        for i in range(0,vec.shape[0]):
            if vec[i] != 0.0:
                basis=self.decimal_binary(i,N)
                basis=self.binary_shift(basis,shift=shift,\
                                        shift_direction=shift_direction)
                j=int(self.binary_decimal(basis))
                state2[j]=vec[i]

        return state2

    def cstate_shift(self,vec,shift=1,shift_direction='right'):
        """
        Shifting of a complex quantum state
        Inputs:
            vec: complex quantum state.
            shift: degree of the state.
            shift_direction: It shows the direction of the shift, by default
                            it is right
        Return:
            state2: shifted state of vec
        """
        assert vec.shape[0]%2==0,"Not a qubit quantum state"
        N=int(math.log(vec.shape[0],2))
        basis=np.zeros([N],dtype=int)
        state2=np.zeros([2**N],dtype=np.complex_)
        for i in range(0,vec.shape[0]):
            if vec[i] != complex(0.0,0.0):
                basis=self.decimal_binary(i,N)
                basis=self.binary_shift(basis,shift=shift,\
                                        shift_direction=shift_direction)
                j=self.binary_decimal(basis)
                state2[j]=vec[i]

        return state2

    def decimal_binary(self,i,N):
        """
        Decimal to binary conversion
        Inputs:
            i: input decimal number to changed to binary number
            N: number of the binary string
        Returns:
            bnum: binary number equivalent to i, it is column matrix.
        """
        bnum=np.zeros([N],dtype=int)
        for j in range(0,N):
            bnum[bnum.shape[0]-1-j]=i%2
            i=int(i/2)
        return bnum

    def binary_decimal(self,vec):
        """
        Binary to decimal conversion
        Input:
            vec:  array containing binary matrix elements i.e. 0 or 1
        Returns:
            dec: decimal equivalent number of binary array vec
        """
        dec=0
        for i in range(0,vec.shape[0]):
            dec=dec+int((2**(vec.shape[0]-1-i))*vec[i])
        dec=int(dec)
        return dec

    def binary_shift(self,vec,shift=1,shift_direction='right'):
        """
        Shifting of string of binary number
        Input:
            vec: array containing binary matrix elements i.e. 0 or 1
            shift: degree of the shift
            shift_direction: its value is either left or right
        Output:
            shift_vec: Shifted vec
        """
        assert shift_direction == 'left' or shift_direction == 'right',\
        'Not proper shift direction'
        assert shift >= 1 and shift < vec.shape[0],\
        "degree of shift is not proper"
        shift_vec=np.zeros([vec.shape[0]],dtype='int_')
        if shift_direction =='right':
            for i in range(0,vec.shape[0]):
                ishift=i+shift
                if ishift >= vec.shape[0]:
                    ishift=ishift-vec.shape[0]
                shift_vec[ishift]=vec[i]
        if shift_direction == 'left':
            for i in range(0,vec.shape[0]):
                ishift=i-shift
                if ishift < 0:
                    ishift=ishift+vec.shape[0]
                shift_vec[ishift]=vec[i]

        return shift_vec


# =============================================================================
# Measuring the performance of a buy and hold strategy
# Author : Mayank Rasu

# Please report bug/issues in the Q&A section
# =============================================================================

# Import necesary libraries
import pandas_datareader.data as pdr
import numpy as np
import datetime

# Download historical data for required stocks
ticker = "^GSPC"
SnP = pdr.get_data_yahoo(ticker,datetime.date.today()-datetime.timedelta(1825),datetime.date.today())

def CAGR(DF):
    "function to calculate the Cumulative Annual Growth Rate of a trading strategy"
    df = DF.copy()
    df["daily_ret"] = DF["Adj Close"].pct_change()
    df["cum_return"] = (1 + df["daily_ret"]).cumprod()
    n = len(df)/252
    CAGR = (df["cum_return"][-1])**(1/n) - 1
    return CAGR

def volatility(DF):
    "function to calculate annualized volatility of a trading strategy"
    df = DF.copy()
    df["daily_ret"] = DF["Adj Close"].pct_change()
    vol = df["daily_ret"].std() * np.sqrt(252)
    return vol

def sharpe(DF,rf):
    "function to calculate sharpe ratio ; rf is the risk free rate"
    df = DF.copy()
    sr = (CAGR(df) - rf)/volatility(df)
    return sr

def sortino(DF,rf):
    "function to calculate sortino ratio ; rf is the risk free rate"
    df = DF.copy()
    df["daily_ret"] = DF["Adj Close"].pct_change()
    neg_vol = df[df["daily_ret"]<0]["daily_ret"].std() * np.sqrt(252)
    sr = (CAGR(df) - rf)/neg_vol
    return sr

def max_dd(DF):
    "function to calculate max drawdown"
    df = DF.copy()
    df["daily_ret"] = DF["Adj Close"].pct_change()
    df["cum_return"] = (1 + df["daily_ret"]).cumprod()
    df["cum_roll_max"] = df["cum_return"].cummax()
    df["drawdown"] = df["cum_roll_max"] - df["cum_return"]
    df["drawdown_pct"] = df["drawdown"]/df["cum_roll_max"]
    max_dd = df["drawdown_pct"].max()
    return max_dd

def calmar(DF):
    "function to calculate calmar ratio"
    df = DF.copy()
    clmr = CAGR(df)/max_dd(df)
    return clmr


# 9.2.2 多次元ガウス分布のEMアルゴリズム

#%%

# 9.2.2項で利用するライブラリ
import numpy as np
from scipy.stats import multivariate_normal # 多次元ガウス分布
import matplotlib.pyplot as plt

#%%

## 真の分布の設定

# 次元数を設定:(固定)
D = 2

# クラスタ数を指定
K = 3

# K個の真の平均を指定
mu_truth_kd = np.array(
    [[5.0, 35.0],
     [-20.0, -10.0],
     [30.0, -20.0]]
)

# K個の真の共分散行列を指定
sigma2_truth_kdd = np.array(
    [[[250.0, 65.0], [65.0, 270.0]],
     [[125.0, -45.0], [-45.0, 175.0]],
     [[210.0, -15.0], [-15.0, 250.0]]]
)

# 真の混合係数を指定
pi_truth_k = np.array([0.45, 0.25, 0.3])

#%%

# 作図用のx軸のxの値を作成
x_1_line = np.linspace(
    np.min(mu_truth_kd[:, 0] - 3 * np.sqrt(sigma2_truth_kdd[:, 0, 0])),
    np.max(mu_truth_kd[:, 0] + 3 * np.sqrt(sigma2_truth_kdd[:, 0, 0])),
    num=300
)

# 作図用のy軸のxの値を作成
x_2_line = np.linspace(
    np.min(mu_truth_kd[:, 1] - 3 * np.sqrt(sigma2_truth_kdd[:, 1, 1])),
    np.max(mu_truth_kd[:, 1] + 3 * np.sqrt(sigma2_truth_kdd[:, 1, 1])),
    num=300
)

# 作図用の格子状の点を作成
x_1_grid, x_2_grid = np.meshgrid(x_1_line, x_2_line)

# 作図用のxの点を作成
x_point_arr = np.stack([x_1_grid.flatten(), x_2_grid.flatten()], axis=1)

# 作図用に各次元の要素数を保存
x_dim = x_1_grid.shape
print(x_dim)

# 真の分布を計算
model_dens = 0
for k in range(K):
    # クラスタkの分布の確率密度を計算
    tmp_dens = multivariate_normal.pdf(
        x=x_point_arr, mean=mu_truth_kd[k], cov=sigma2_truth_kdd[k]
    )

    # K個の分布を線形結合
    model_dens += pi_truth_k[k] * tmp_dens

#%%

# 真の分布を作図
plt.figure(figsize=(12, 9))
plt.contour(x_1_grid, x_2_grid, model_dens.reshape(x_dim)) # 真の分布
plt.suptitle('Mixture of Gaussians', fontsize=20)
plt.title('K=' + str(K), loc='left')
plt.xlabel('$x_1$')
plt.ylabel('$x_2$')
plt.colorbar() # 等高線の色
plt.show()

#%%

# (観測)データ数を指定
N = 250

# 潜在変数を生成
z_truth_nk = np.random.multinomial(n=1, pvals=pi_truth_k, size=N)

# クラスタ番号を抽出
_, z_truth_n = np.where(z_truth_nk == 1)

# (観測)データを生成
x_nd = np.array([
    np.random.multivariate_normal(
        mean=mu_truth_kd[k], cov=sigma2_truth_kdd[k], size=1
    ).flatten() for k in z_truth_n
])

#%%

# 観測データの散布図を作成
plt.figure(figsize=(12, 9))
for k in range(K):
    k_idx, = np.where(z_truth_n == k) # クラスタkのデータのインデック
    plt.scatter(x=x_nd[k_idx, 0], y=x_nd[k_idx, 1], label='cluster:' + str(k + 1)) # 観測データ
plt.contour(x_1_grid, x_2_grid, model_dens.reshape(x_dim), linestyles='--') # 真の分布
plt.suptitle('Mixture of Gaussians', fontsize=20)
plt.title('$N=' + str(N) + ', K=' + str(K) +
          ', \pi=(' + ', '.join([str(pi) for pi in pi_truth_k]) + ')$', loc='left')
plt.xlabel('$x_1$')
plt.ylabel('$x_2$')
plt.colorbar() # 等高線の色
plt.show()

#%%

## 初期値の設定

# 平均パラメータの初期値を生成
mu_kd = np.array([
    np.random.uniform(
        low=np.min(x_nd[:, d]), high=np.max(x_nd[:, d]), size=K
    ) for d in range(D)
]).T

# 共分散行列の初期値を指定
sigma2_kdd = np.array([np.identity(D) * 1000 for _ in range(K)])

# 混合係数の初期値を生成
pi_k = np.random.rand(3)
pi_k /= np.sum(pi_k) # 正規化

# 初期値による対数尤度を計算:式(9.14)
term_dens_nk = np.array(
    [multivariate_normal.pdf(x=x_nd, mean=mu_kd[k], cov=sigma2_kdd[k]) for k in range(K)]
).T
L = np.sum(np.log(np.sum(term_dens_nk, axis=1)))

#%%

# 初期値による混合分布を計算
init_dens = 0
for k in range(K):
    # クラスタkの分布の確率密度を計算
    tmp_dens = multivariate_normal.pdf(
        x=x_point_arr, mean=mu_kd[k], cov=sigma2_kdd[k]
    )

    # K個の分布線形結合
    init_dens += pi_k[k] * tmp_dens

# 初期値による分布を作図
plt.figure(figsize=(12, 9))
plt.contour(x_1_grid, x_2_grid, model_dens.reshape(x_dim),
            alpha=0.5, linestyles='dashed') # 真の分布
#plt.contour(x_1_grid, x_2_grid, init_dens.reshape(x_dim)) # 推定値による分布:(等高線)
plt.contourf(x_1_grid, x_2_grid, init_dens.reshape(x_dim), alpha=0.6) # 推定値による分布:(塗りつぶし)
plt.suptitle('Mixture of Gaussians', fontsize=20)
plt.title('iter:' + str(0) + ', K=' + str(K), loc='left')
plt.xlabel('$x_1$')
plt.ylabel('$x_2$')
plt.colorbar() # 等高線の色
plt.show()

#%%

## 推論処理

# 試行回数を指定
MaxIter = 100

# 推移の確認用の受け皿を作成
trace_L_i = [L]
trace_gamma_ink = [np.tile(np.nan, reps=(N, K))]
trace_mu_ikd = [mu_kd.copy()]
trace_sigma2_ikdd = [sigma2_kdd.copy()]
trace_pi_ik = [pi_k.copy()]

# 最尤推定
for i in range(MaxIter):

    # 負担率を計算:式(9.13)
    gamma_nk = np.array(
        [multivariate_normal.pdf(x=x_nd, mean=mu_kd[k], cov=sigma2_kdd[k]) for k in range(K)]
    ).T
    gamma_nk /= np.sum(gamma_nk, axis=1, keepdims=True) # 正規化

    # 各クラスタとなるデータ数の期待値を計算:式(9.18)
    N_k = np.sum(gamma_nk, axis=0)

    for k in range(K):
        # 平均パラメータの最尤解を計算:式(9.17)
        mu_kd[k] = np.dot(gamma_nk[:, k], x_nd) / N_k[k]

        # 共分散行列の最尤解を計算:式(9.19)
        term_x_nd = x_nd - mu_kd[k]
        sigma2_kdd[k] = np.dot(gamma_nk[:, k] * term_x_nd.T, term_x_nd) / N_k[k]

    # 混合係数の最尤解を計算:式(9.22)
    pi_k = N_k / N

    # 対数尤度を計算:式(9.14)
    tmp_dens_nk = np.array(
        [multivariate_normal.pdf(x=x_nd, mean=mu_kd[k], cov=sigma2_kdd[k]) for k in range(K)]
    ).T
    L = np.sum(np.log(np.sum(tmp_dens_nk, axis=1)))

    # i回目の結果を記録
    trace_L_i.append(L)
    trace_gamma_ink.append(gamma_nk.copy())
    trace_mu_ikd.append(mu_kd.copy())
    trace_sigma2_ikdd.append(sigma2_kdd.copy())
    trace_pi_ik.append(pi_k.copy())

    # 動作確認
    print(str(i + 1) + ' (' + str(np.round((i + 1) / MaxIter * 100, 1)) + '%)')

#%%

## 推論結果の確認

# K個のカラーマップを指定
colormap_list = ['Blues', 'Oranges', 'Greens']

# 負担率が最大のクラスタ番号を抽出
z_n = np.argmax(gamma_nk, axis=1)

# 割り当てられたクラスタとなる負担率(確率)を抽出
prob_z_n = gamma_nk[np.arange(N), z_n]

# 最後の更新値による混合分布を計算
res_dens = 0
for k in range(K):
    # クラスタkの分布の確率密度を計算
    tmp_dens = multivariate_normal.pdf(
        x=x_point_arr, mean=mu_kd[k], cov=sigma2_kdd[k]
    )

    # K個の分布を線形結合
    res_dens += pi_k[k] * tmp_dens

# 最後の更新値による分布を作図
plt.figure(figsize=(12, 9))
plt.contour(x_1_grid, x_2_grid, model_dens.reshape(x_dim),
            alpha=0.5, linestyles='dashed') # 真の分布
plt.scatter(x=mu_truth_kd[:, 0], y=mu_truth_kd[:, 1],
            color='red', s=100, marker='x') # 真の平均
plt.contourf(x_1_grid, x_2_grid, res_dens.reshape(x_dim), alpha=0.5) # 推定値による分布:(塗りつぶし)
for k in range(K):
    k_idx, = np.where(z_n == k) # クラスタkのデータのインデックス
    cm = plt.get_cmap(colormap_list[k]) # クラスタkのカラーマップを設定
    plt.scatter(x=x_nd[k_idx, 0], y=x_nd[k_idx, 1],
                c=[cm(p) for p in prob_z_n[k_idx]], label='cluster:' + str(k + 1)) # 負担率によるクラスタ
#plt.contour(x_1_grid, x_2_grid, res_dens.reshape(x_dim)) # 推定値による分布:(等高線)
#plt.colorbar() # 等高線の値:(等高線用)
plt.suptitle('Mixture of Gaussians:Maximum Likelihood', fontsize=20)
plt.title('$iter:' + str(MaxIter) +
          ', L=' + str(np.round(L, 1)) +
          ', N=' + str(N) +
          ', \pi=[' + ', '.join([str(pi) for pi in np.round(pi_k, 3)]) + ']$',
          loc='left')
plt.xlabel('$x_1$')
plt.ylabel('$x_2$')
plt.legend()
plt.show()

#%%

## 対数尤度の推移を作図
plt.figure(figsize=(12, 9))
plt.plot(np.arange(MaxIter + 1), np.array(trace_L_i))
plt.xlabel('iteration')
plt.ylabel('value')
plt.suptitle('Maximum Likelihood', fontsize=20)
plt.title('Log Likelihood', loc='left')
plt.grid() # グリッド線
plt.show()

#%%

## パラメータの更新値の推移の確認

# muの推移を作図
plt.figure(figsize=(12, 9))
plt.hlines(y=mu_truth_kd, xmin=0, xmax=MaxIter + 1, label='true val',
           color='red', linestyles='--') # 真の値
for k in range(K):
    for d in range(D):
        plt.plot(np.arange(MaxIter+1), np.array(trace_mu_ikd)[:, k, d],
                 label='k=' + str(k + 1) + ', d=' + str(d + 1))
plt.xlabel('iteration')
plt.ylabel('value')
plt.suptitle('Maximum Likelihood', fontsize=20)
plt.title('$\mu$', loc='left')
plt.legend() # 凡例
plt.grid() # グリッド線
plt.show()

#%%

# lambdaの推移を作図
plt.figure(figsize=(12, 9))
plt.hlines(y=sigma2_truth_kdd, xmin=0, xmax=MaxIter + 1, label='true val',
           color='red', linestyles='--') # 真の値
for k in range(K):
    for d1 in range(D):
        for d2 in range(D):
            plt.plot(np.arange(MaxIter + 1), np.array(trace_sigma2_ikdd)[:, k, d1, d2],
                     alpha=0.5, label='k=' + str(k + 1) + ', d=' + str(d1 + 1) + ', d''=' + str(d2 + 1))
plt.xlabel('iteration')
plt.ylabel('value')
plt.suptitle('Maximum Likelihood', fontsize=20)
plt.title('$\Sigma$', loc='left')
plt.legend() # 凡例
plt.grid() # グリッド線
plt.show()

#%%

# piの推移を作図
plt.figure(figsize=(12, 9))
plt.hlines(y=pi_truth_k, xmin=0, xmax=MaxIter + 1, label='true val',
           color='red', linestyles='--') # 真の値
for k in range(K):
    plt.plot(np.arange(MaxIter + 1), np.array(trace_pi_ik)[:, k], label='k=' + str(k + 1))
plt.xlabel('iteration')
plt.ylabel('value')
plt.suptitle('Maximum Likelihood', fontsize=20)
plt.title('$\pi$', loc='left')
plt.legend() # 凡例
plt.grid() # グリッド線
plt.show()

#%%

## アニメーションによる確認

# 追加ライブラリ
import matplotlib.animation as animation

#%%

## 分布の推移の確認

# K個のカラーマップを指定
colormap_list = ['Blues', 'Oranges', 'Greens']

# 画像サイズを指定
fig = plt.figure(figsize=(9, 9))

# 作図処理を関数として定義
def update_model(i):
    # i回目の更新値による混合分布を計算
    res_dens = 0
    for k in range(K):
        # クラスタkの分布の確率密度を計算
        tmp_dens = multivariate_normal.pdf(
            x=x_point_arr, mean=trace_mu_ikd[i][k], cov=trace_sigma2_ikdd[i][k]
        )

        # K個の分布を線形結合
        res_dens += trace_pi_ik[i][k] * tmp_dens

    # 前フレームのグラフを初期化
    plt.cla()

    # i回目の負担率を取得
    gamma_nk = trace_gamma_ink[i]

    # i回目のサンプルによる分布を作図
    plt.contour(x_1_grid, x_2_grid, model_dens.reshape(x_dim),
                alpha=0.5, linestyles='dashed') # 真の分布
    #plt.contour(x_1_grid, x_2_grid, res_dens.reshape(x_dim)) # 推定値による分布:(等高線)
    plt.contourf(x_1_grid, x_2_grid, res_dens.reshape(x_dim), alpha=0.5) # 推定値による分布:(塗りつぶし)
    plt.scatter(x=mu_truth_kd[:, 0], y=mu_truth_kd[:, 1],
                color='red', s=100, marker='x') # 真の平均
    for k in range(K):
        k_idx, = np.where(np.argmax(gamma_nk, axis=1) == k)
        cm = plt.get_cmap(colormap_list[k]) # クラスタkのカラーマップを設定
        plt.scatter(x=x_nd[k_idx, 0], y=x_nd[k_idx, 1],
                    c=[cm(p) for p in gamma_nk[k_idx, k]], label='cluster:' + str(k + 1)) # 負担率によるクラスタ
        plt.suptitle('Mixture of Gaussians:Maximum Likelihood', fontsize=20)
    plt.title('$iter:' + str(i) +
              ', L=' + str(np.round(trace_L_i[i], 1)) +
              ', N=' + str(N) +
              ', \pi=(' + ', '.join([str(pi) for pi in np.round(trace_pi_ik[i], 3)]) + ')$',
              loc='left')
    plt.xlabel('$x_1$')
    plt.ylabel('$x_2$')
    plt.legend()

# gif画像を作成
model_anime = animation.FuncAnimation(fig, update_model, frames=MaxIter + 1, interval=100)
model_anime.save("ch9_2_2_Model.gif")

#%%

print('end')


import math
import numpy as np

#can be applied to nowcasts and forecasts with lead time values

def mean_absolute_bias(LeadDay,df,model,observed):
    """
    Args:
        LeadDay: specifically applies to forecast models with lead times
            if no lead day is available input None
            LeadDay is desired Lead day (int)
        df: data frame where Observed and Model data is found
        model & observed : column name (string)

    Return:
        mean bias
    Function:
         subtract model values from observed,
        add all errors and divide by number of
        values
    """
    count =0
    avgdiff=0

    if LeadDay is None:
         for a, j in zip(df[observed], df[model]):
            avgdiff += abs((a)-(j))
            count+=1
    else:
         for a, j in zip(df[LeadDay][observed], df[LeadDay][model]):
            avgdiff += abs((a)-(j))
            count+=1

    return avgdiff/count

def total_mean_bias(LeadDays,df,model,observed):
    """
    Args:
        Lead day: list of desired lead days
        df: data frame where Observed and Model data is found
        model & observed : column name (string)
    Return:
        averaging total mean absolute bias for all lead days
    Function:
        returns average  mean absolute bias for forecasts
    """
    return np.mean([mean_absolute_bias(LeadDay,df,model,observed) for LeadDay in LeadDays])

def root_mean_squared_error(LeadDay,df,model,observed):
    """
    Args:
        LeadDay: specifically applies to forecast models with lead times
         if no lead day is available input None
         LeadDay is desired Lead day (int)
        df: data frame where Observed and Model data is found
        model & observed : column name (string)

    Return: Root Mean Squared Error
    Function:
        subtract model values from observed
        square result and sum all squares
        divide by number of values
    """
    count = 0
    total = 0

    if LeadDay is None:
        for a, j in zip(df[observed], df[model]):
            diffsquared = ((j) - (a))**2
            total += diffsquared
            count +=1
    else:
        for a, j in zip(df[LeadDay][observed], df[LeadDay][model]):
            diffsquared = ((j) - (a))**2
            total += diffsquared
            count +=1
    avg = total/count

    return math.sqrt(avg)

def percent_gross_error(LeadDay,df,model,observed,threshold):
    """
    Arg:
        LeadDay: specifically applies to forecast models with lead times
         if no lead time is available input None
         LeadDay is desired Lead day (int)
        df: data frame where Observed and Model data is found
        model & observed : column name (string)
        threshold: is difference greater than a certain value
        can be in any variable desired
    Return: Percent gross error
    Function:
        calculate the number of instances
        the abs diff btw obs and forecast
        is greater than a certain threshold,
        divide this number
        by total number of occasions
    """
    count =0
    count2=0
    if LeadDay is None:
          for a, j in zip(df[observed], df[model]):
            if abs((a)-(j)) >= threshold:
                count+=1
            count2+=1
    else:
          for a, j in zip(df[LeadDay][observed], df[LeadDay][model]):
            if abs((a)-(j)) >= threshold:
                count+=1
            count2+=1

    return count/count2

def mean_absolute_percentage_error(LeadDay,df,model,observed):
    """
    Arg:
        LeadDay: specifically applies to forecast models with lead times
         if no lead day is available input None
         LeadDay is desired Lead day (int)
        df: data frame where Observed and Model data is found
        model & observed : column name (string)

    Return: Mean absolute percentage error error
    Function:
        absolute value (actual minus forecast )
        / forecast, multiply by 100, sum all
        values and divide by number of values
    """
    count =0
    percent_error=0
    if LeadDay is None :
         for a, j in zip(df[observed], df[model]):
            val = abs((a)-(j))/a
            percent_error += val*100
            count+=1
    else:
         for a, j in zip(df[LeadDay][observed], df[LeadDay][model]):
            val = abs((a)-(j))/a
            percent_error += val*100
            count+=1

    return percent_error/count

def statistical_analysis(LeadDay,df,model,observation,threshhold):
    """
    Arg:
        LeadDay: specifically applies to forecast models with lead times
         LeadDay is desired Lead day (int)
         if no lead day is available input None
        df: data frame where Observed and Model data is found
        model & observed : column name (string)
        threshold: for percent gross error (int)

    Return: mean bias, root mean squared error,
        percent gross error, mean absolute percentage error
    """
    print(mean_absolute_bias(LeadDay,df,model,observation),'is the mean bias for lead time of',str(LeadDay),'days')
    print(root_mean_squared_error(LeadDay,df,model,observation), 'is the root mean squared error for lead time of',str(LeadDay),'days')
    print(percent_gross_error(LeadDay,df,model,observation,threshhold), 'is the percent gross error for lead time of',str(LeadDay), 'days')
    print(mean_absolute_percentage_error(LeadDay,df,model,observation), 'is the mean absolute percentage error',str(LeadDay), 'days')


#Import the required libraries
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.preprocessing import MinMaxScaler

#Main function to apply PCA algorithm
def my_pca(data, img_w, img_h, train_labels, test_data, test_labels, training_phase, N, goal_variance, show_plot, show_comparison):

    data = np.transpose(data) # "vectorized" images are in columns (N_features x N_images)

    #3 I apply PCA and project the train images into a space of dimension N

    # 3a. I calculate the mean and centre the data
    m = np.mean(data, axis=1)     #for every pixel (i,j) mean between all the images pixels (row vector of nRows x nColumns elements)
    Xc = data - m[:, np.newaxis]  #centralized coordinates over "m" (m 1D vector --> m[:,np.newaxis] column vector)

    # 3b. I calculate the covariance matrix
    C = np.cov(Xc)

    # 3c. Extracting eigenvectors (U) and eigenvalues (lambdas) of the covariance matrix
    lambdas, U = np.linalg.eigh(C)

    # 3d. I order the eigenvalues from largest to smallest
    best_eig_idxs = np.argsort(lambdas)[::-1]  #ascending sort that is then reversed to achieve descending sorting
                                               #argsort returns indeces
    best_eig = lambdas[best_eig_idxs]  #get the "highest" eigenvalues
    best_U = U[:, best_eig_idxs]       #get the associated eigenvectors to their eigenvalues

    # 3e. I check the amount of variance in the data that each eigenvalue carries and set N equal to the number of eigenvectors sufficient to have at least 80%
    # of the total variance.
    d = lambdas.shape[0]  #number of eigenvalues

    if training_phase:
        y = np.cumsum(best_eig) / np.sum(best_eig)  # normalize eigenvalues between 0 and 1 (0= the most significative)
        if show_plot:
            fig, axs = plt.subplots(2)
            axs[0].plot(np.arange(1, d + 1), best_eig)  # draw the line (xlimit = [1,256])
            axs[0].scatter(np.arange(1, d + 1), best_eig)  # scatter the eigenvalues points
            axs[1].plot(np.arange(1, d + 1), y)  # draw the line (xlimit = [1,256])
            axs[1].scatter(np.arange(1, d + 1), y)  # scatter the values
            plt.show()

        N = np.where(y >= goal_variance)[0][0]  #get the first ("0") element that satisfies y>=...
        if show_comparison:
            print("[myPCA] From PCA (implemented step by step) we get that the number of components to have",
                   str(np.round(goal_variance, 2)), "explained variance is", N)

    # 3f. I project the data using the N largest eigenvectors
    first_N_eigenvectors = best_U[:, :N]   #get the first N more significant eigenvectors (previously ordered)
    projectedImages = first_N_eigenvectors.T.dot(Xc)

    if training_phase:
        # Computing the accuracy achieved by the PCA classifier
        accuracy = test_my_pca(first_N_eigenvectors, projectedImages, train_labels, test_data, test_labels, m, show_updates=show_comparison)
        return np.transpose(projectedImages), N, accuracy
    else:
        return np.transpose(projectedImages)

#Function that prints the accuracy of the PCA feature reduction
def test_my_pca(first_N_eigenvectors, projected_images, train_labels, test_data, test_labels, m, show_updates):
    # projected_images: "vectorized" images in columns, with reduced number of features (from PCA)
    #                   (nReducedFeaturePCA x nImages)

    # 4. Calculating the Theta threshold
    from scipy.spatial.distance import cdist
    theta = np.max(cdist(projected_images.T, projected_images.T, 'euclidean'))

    # 5. Centre my test data
    test_data = np.transpose(test_data)  #to have "vectorized" images along columns

    # centralized (pixel intensities of) TEst images (m 1D array --> m[:, np.newaxis] columns vector np.shape=(nElements,1) )
    x_te = test_data - m[:, np.newaxis] # subtract from each column of test_data the column m[:, np.newaxis]

    # test_data images transformed into the reduced feature space (reduced by PCA)
    omega_te = first_N_eigenvectors.T.dot(x_te)
    # x_te: original test images centralized (nOriginalPixels x nImages)
    # first_N_eigenvectors: (nOriginalPixels,nReducedFeatures) (notice. the pixels are features, so they are synonim in this case)
    # omega_te: (nReducedFeatures,nImages) set of reduced test images

    # 6. Calculating the set of epsilon distances
    epsilon = []
    for i in range(test_data.shape[1]): # iterate over all test images
        tmp_test = omega_te[:, i]       # i-th reduced test image
        # tmp_test[:, np.newaxis]: column vector reduced image (i-th reduced TEST image) (nReducedFeatures,1)
        # projected_images: (nReducedFeatures,nTrainImages)
        # tmp_test[:, np.newaxis] - projected_images: projected_images: (nReducedFeatures, nTrainImages), where the j-th column more similar to tmp_test will be similar or equalt to null vector (nReducedFeatures x 1)
        # epsilon[j] = np.linalg.norm(tmp_test[:, np.newaxis] - projected_images, ord=2, axis=0): (nTestImages,) 1D array, where the k-th element is the norm of the above matrix k-th column
        #            '-> the smaller norm in this row, will correspond to the more similar test image in train set "projected_images"
        epsilon.append(np.linalg.norm(tmp_test[:, np.newaxis] - projected_images, ord=2, axis=0)) # ord=2 Euclidean norm/2-norm of COLUMNS
        # v = np.array([x1 x2 ... xN], [y1 y2 ... yN]) -> norm(v,axis=0) = array([sqrt(x1^2+y1^2), sqrt(x2^2+y2^2), ... , sqrt(xN^2+yN^2)])
        #              '-> matrix columns are the vectors of which you compute the norm
        # vs
        # v = np.array([x1 y1], [x2 y2], ... [xN yN]]) -> norm(v,axis=1) = array([sqrt(x1^2+y1^2), sqrt(x2^2+y2^2), ... , sqrt(xN^2+yN^2)])
        #              '-> matrix rows are the vectors of which you compute the norm

    epsilon = np.array(epsilon)
    # epsilon: (nTestImages,nTrainImages) where in each row, the smaller l-th value (of norm) indicates which is the image of
    #           the train set "projected_images" more similar to the test image omega_te[:,l]!

    # 7. I reconstruct the images (transf. from reducedFeatures space to entire resolution space)
    g = first_N_eigenvectors.dot(omega_te)
    # omega_te: (nReducedFeatures,nImages) set of reduced test images
    # first_N_eigenvectors: (nOriginalPixels, nReducedFeatures)
    # g: (nOriginalPixels,nImages) reduced test images projected in the feature space of entire dimension (dimension of
    #                              original input images)

    """
    #  Make an imshow of the original image against the reconstructed one (only the first 5 images)!
    r = 32; c = 32 # modify it manually!
    fig, axs = plt.subplots(5, 2)
    for i in range(5):
        axs[i, 0].imshow(x_te[:, i].reshape((r, c)), cmap='gray')
        axs[i, 1].imshow(g[:, i].reshape((r, c)), cmap='gray')
    plt.show()
    """

    # 8. Calculation xi for classification
    xi = np.linalg.norm(g - x_te, ord=2, axis=0)

    # 9. In which of the 3 cases are we for each test image? Is the corresponding image of the same class? Check the first 5 images
    ''' # It does not work in our case, because Xc variable is defined out of scope of this function
    fig, axs = plt.subplots(5, 2)
    for i in range(5):
        if xi[i] >= theta:
            print(str(i + 1) + ": It's not one of the classes!")
        elif xi[i] < theta and any(epsilon[i, :] > theta):
            print(str(i + 1) + ": It's a new image of one of the classes!")
        elif xi[i] < theta and np.min(epsilon[i, :]) < theta:
            print(str(i + 1) + ": It's a familiar image! I'll show you!")
            matched_indx = np.argmin(epsilon[i, :])
            axs[i, 0].imshow(x_te[:, i].reshape((r, c)), cmap='gray')
            axs[i, 1].imshow(Xc[:, matched_indx].reshape((r, c)), cmap='gray')
            if i == 0:
                axs[i, 0].set_title('Unknown class!')
                axs[i, 1].set_title('Known class!')
    plt.show()
    '''

    # 10. Calculate the accuracy of the classifier and test how the result changes when N (+/- eigenvectors) is changed
    # Set the prediction equal to -1 in case of classification as no one of the classes or new image of one of the 6 classes,
    # equal to the train sample label with lower epsilon if a match is found
    predicted = []
    for i in range(test_data.shape[1]):
        if xi[i] < theta and np.min(epsilon[i, :]) < theta:
            predicted.append(train_labels[np.argmin(epsilon[i, :])])
        else:
            predicted.append(-1)

    predicted = np.array(predicted)

    accuracy = np.sum(predicted == test_labels)
    if show_updates:
        print('[myPCA] Classifier accuracy: ' + "{0:.2f}".format(accuracy / len(test_labels) * 100) + '%\n')
    return accuracy / len(test_labels) * 100

def my_pca_tuning(increment_range, increment, imageSize, trainImages, trainLabels, testImages, testLabels, show_updates):
    startingVariance = increment_range[0]
    finalVariance = increment_range[1]
    actualVariance = startingVariance

    bestGoalVariance = 0
    bestAccuracy = 0
    while actualVariance < finalVariance:
        if show_updates:
            print("[PCA tuning] Considering variance=" + str(np.round(actualVariance, 2)))

        _, N, accuracy = my_pca(trainImages, imageSize, imageSize, trainLabels, testImages, testLabels,
                                training_phase=True, N=None, goal_variance=actualVariance, show_plot=False,
                                show_comparison=show_updates)  #reducing feature space
        if accuracy > bestAccuracy:
            bestAccuracy = accuracy
            bestGoalVariance = actualVariance
            N_best = N

        actualVariance += increment

    return bestGoalVariance, bestAccuracy, N_best

def sklearn_pca(features, goalVariance):

    scaler = MinMaxScaler()
    scaledFeatures = scaler.fit_transform(features)

    pca = PCA(n_components=goalVariance)
    pca.fit_transform(scaledFeatures)

    print("[sklearn PCA] PCA components considering the set variance ratio (" + str(np.round(goalVariance, 2)) + "): "
          + str(pca.explained_variance_ratio_.shape[0]))


# -*- coding: utf-8 -*-

import math
import numpy as np

def direct_ft(x):
    """
    direct discrete FT
    """
    N = x.size
    n = np.arange(N)
    k = n.reshape((N, 1))
    M = np.exp(-2j * np.pi * k * n / N)
    return np.dot(M, x)

def recursive_ft(x):
    """
    recursive discrete FT
    calls dft on lowest level

    x.size needs to be power of 2
    """
    N = x.size
    if N <= 2:
        return direct_ft(x)
    else:
        X_even = recursive_ft(x[::2])
        X_odd = recursive_ft(x[1::2])
        W = np.exp(-2j * np.pi * np.arange(N//2) / N)
        # X_i = E_i + W_i * O_i
        # X_i+N/2 = E_i - W_i * O_i
        return np.concatenate([X_even + W * X_odd,
                               X_even - W * X_odd])

def bitrev_ft(x):
    return _bitrev_ft(x.astype(complex))

def _bitrev_ft(x):
    """
    discret fast fourier transform

    x needs to be complex (e.g.: x=x.astype(complex))
       and x.size a power of 2

    iterative and in-place implementation of the recursive approach:
        the elements are reordered and grouped into "recursion layers",
        mulitplied with the corresponding factors and summed over
    """
    def reverse(x, s):
        """
        bit reversal of input x with wordlength s
        """
        y = 0
        for i in range(s):
            y = (y << 1) | (x & 1)
            x >>= 1
        return y

    n           = x.size
    layers      = int(math.log(n, 2))
    x           = x[[reverse(i, layers) for i in range(n)]]     # bit reversed
    exptable    = np.exp([-2j * np.pi * i / n for i in range(n // 2)])

    # outer loop
    # goes through log(n, 2) "recursion" layers
    # on the l-th layer (l=log(s,2)) the fft is divided into (n-1) / s
    #  segments, where s is the size of each segment on the given layer
    s = 2
    while s <= n:
        half_s      = s // 2
        tablestep   = n // s
        # middle loop
        # goes through the fft's segments at layer log(s, 2)
        for i in range(0, n, s):
            # inner loop
            # goes through the segment's s elements and performs the
            #  "butterfly" operation
            k = 0
            for j in range(i, i+half_s):
                tmp             = x[j + half_s] * exptable[k]
                # x_right = x_left - x_right * exp(-i*pi*element / (2**layer))
                x[j + half_s]   = x[j] - tmp
                # x_left = x_left + x_right * exp(-i*pi*element / (2**layer))
                x[j]    += tmp
                k       += tablestep
        s *= 2
    return x


import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

def target_function(x, y):
    return x ** 2 + np.sin(y) ** 2

def derivative_function(theta):
    x = theta[0]
    y = theta[1]
    return np.array([2 * x, 2 * np.sin(y) * np.cos(y)])

def show_3d_surface(x, y, z):
    fig = plt.figure()
    ax = Axes3D(fig)

    v = np.linspace(-3, 3, 100)
    len_v = len(v)
    print(len_v)
    X, Y = np.meshgrid(v, v)
    R = np.zeros((len_v, len_v))
    for i in range(len_v):
        for j in range(len_v):
            R[i, j] = target_function(X[i, j], Y[i, j])
    ax.plot_surface(X, Y, R, cmap='rainbow')
    plt.plot(x, y, z, color="red",linewidth=3)
    plt.show()

if __name__ == "__main__":
    theta = np.array([3, 1])
    eta = 0.1
    error = 1e-2
    X = []
    Y = []
    Z = []
    while True:
        print(theta)
        x = theta[0]
        y = theta[1]
        z = target_function(x, y)
        X.append(x)
        Y.append(y)
        Z.append(z)
        print("x=%f, y=%f, z=%f" % (x, y, z))
        d_theta = derivative_function(theta)
        print('   ', theta)
        theta = theta - eta * d_theta
        if z < error:
            break
    show_3d_surface(X, Y, Z)


import numpy as np

# From https://jakevdp.github.io/blog/2018/09/13/waiting-time-paradox/

N = 1000000 # Number of trials
tau = 10 # Time between busses
rand = np.random.RandomState(42) # Random seed

# Calculate arrivals
bus_arrival_times = N * tau * np.sort(rand.rand(N))

intervals = np.diff(bus_arrival_times)
print("Mean: ", intervals.mean())

def simulate_wait_times(arrival_times,
                        rseed=8675309,  # Passenger Seed
                        n_passengers=1000000):
    rand = np.random.RandomState(rseed)

    arrival_times = np.asarray(arrival_times)
    passenger_times = arrival_times.max() * rand.rand(n_passengers)

    # find the index of the next bus for each simulated passenger
    i = np.searchsorted(arrival_times, passenger_times, side='right')

    return arrival_times[i] - passenger_times

wait_times = simulate_wait_times(bus_arrival_times)
print("Mean wait time: ", wait_times.mean())


from argparse import ArgumentParser
from random import uniform

import matplotlib.pyplot as plt
import numpy as np

def orientation(p1, p2, p3):
    return (p3[1] - p1[1]) * (p2[0] - p1[0]) - (p2[1] - p1[1]) * (p3[0] - p1[0])

def counterclockwise(p1, p2, p3):
    return orientation(p1, p2, p3) > 0

def between(p1, p2, p3):
    if orientation(p1, p2, p3) != 0:
        return False

    if min(p1[0], p3[0]) > p2[0] or p2[0] > max(p1[0], p3[0]):
        return False

    if min(p1[1], p3[1]) > p2[1] or p2[1] > max(p1[1], p3[1]):
        return False

    return True

def jarvis(points):
    if len(points) < 3:
        return []

    points = sorted(points)

    convex_hull = [points[0]]

    current = None
    while True:
        current = points[0]
        for point in points[1:]:
            if current == convex_hull[-1] or \
                between(convex_hull[-1], current, point) or \
                    counterclockwise(convex_hull[-1], current, point):
                current = point

        if current == convex_hull[0]:
            break

        convex_hull.append(current)

    return convex_hull

if __name__ == "__main__":
    argparser = ArgumentParser(
        description=(
            "Visualizing the Convex Hull "
            "of a given set of points "
            "with the help of the Jarvis March Algorithm"
        )
    )

    argparser.add_argument(
        "-n", "--number",
        type=int, required=True,
        help="The number of points to be randomly generated"
    )

    argparser.add_argument(
        "-x", "--xrange",
        nargs='+', default=[-50, +50],
        type=int, required=False,
        help="The horizontal axis' range of values"
    )

    argparser.add_argument(
        "-y", "--yrange",
        nargs='+', default=[-50, +50],
        type=int, required=False,
        help="The vertical axis' range of values"
    )

    argv = argparser.parse_args()

    assert len(argv.xrange) == 2 and argv.xrange[0] < argv.xrange[1]
    assert len(argv.yrange) == 2 and argv.yrange[0] < argv.yrange[1]

    pxs = [uniform(argv.xrange[0], argv.xrange[1]) for _ in range(argv.number)]
    pys = [uniform(argv.yrange[0], argv.yrange[1]) for _ in range(argv.number)]

    points = [(x, y) for x, y in zip(pxs, pys)]

    convex_hull = jarvis(points)

    cxs = [point[0] for point in convex_hull]
    cys = [point[1] for point in convex_hull]

    plt.plot(pxs, pys, 'k.', markersize=12)
    plt.plot(cxs + [convex_hull[0][0]], cys + [convex_hull[0][1]], 'b-')
    plt.plot(cxs, cys, 'm.', markersize=12)

    plt.title(f"The {len(convex_hull)} points of the Convex Hull of {argv.number} randomly generated points")
    plt.grid()
    plt.xlim([argv.xrange[0], argv.xrange[1]])
    plt.xlim([argv.yrange[0], argv.yrange[1]])
    plt.axis('equal')
    plt.show()