import numpy as np

def compute_95CI_ols(x,y):
    '''returns the max likliehood estimators and 95% confidence intervals
    that result from ordinary least squares regression applied to the
    1-dimensional numpy arrays, x and y.'''
    x=x.flatten();y=y.flatten()
    n=x.shape[0]
    assert(n==y.shape[0])
    assert(n>2)
    if not n>=8:
        print('Warning: CI not valid for less than 8 data points!')
    xbar=np.mean(x);ybar=np.mean(y)
    #compute sums of squares
    SSxx=np.sum((x-xbar)**2)
    SSxy=np.dot((x-xbar),(y-ybar))
    SSyy=np.sum((y-ybar)**2)
    #best linear unbiased estimator of slope
    m=SSxy/SSxx
    #best linear unbiased estimator of intercept
    b=ybar-m*xbar
    #values of fit
    yhat=b+m*x
    #standard error of fit, s_{y,x}^2=ssE
    SSE=np.sum((y-yhat)**2)
    ssE=SSE/(n-2)
    #standard deviation of slope
    sm = np.sqrt(ssE/SSxx)
    #standard deviation of intercept
    sb = np.sqrt(ssE*(1/n+xbar**2/SSxx))
    #compute 95% CI for parameters
    Delta_m = 1.96*sm
    Delta_b = 1.96*sb
    #compute Rsquared
    Rsquared=(SSyy-SSE)/SSyy
    #format results as a human readable dict
    dict_output={
        'm':m,
        'Delta_m':Delta_m,
        'b':b,
        'Delta_b':Delta_b,
        'Rsquared':Rsquared
    }
    return dict_output


import torch.nn.functional as F
import numpy as np
import torch
from util import randomize_in_place

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def graph1(a_np, b_np, c_np):
    """
    Computes the graph
        - x = a * c
        - y = a + b
        - f = x / y

    Computes also df/da using
        - Pytorchs's automatic differentiation (auto_grad)
        - user's implementation of the gradient (user_grad)

    :param a_np: input variable a
    :type a_np: np.ndarray(shape=(1,), dtype=float64)
    :param b_np: input variable b
    :type b_np: np.ndarray(shape=(1,), dtype=float64)
    :param c_np: input variable c
    :type c_np: np.ndarray(shape=(1,), dtype=float64)
    :return: f, auto_grad, user_grad
    :rtype: torch.DoubleTensor(shape=[1]),
            torch.DoubleTensor(shape=[1]),
            numpy.float64
    """
    # YOUR CODE HERE:
    a = torch.from_numpy(a_np)
    b = torch.from_numpy(b_np)
    c = torch.from_numpy(c_np)
    a.requires_grad = True
    x = a * c
    y = a + b
    f = x / y
    f.backward()
    auto_grad = a.grad
    """
    df/da = df/dx * dx/da + df/dy * dy/da
    df/dx = 1/y, dx/da = c,  df/dy = - x/y², dy/da = 1
    """
    user_grad = ((c / y) - (x / (y * y))).detach().numpy()
    # END YOUR CODE
    return f, auto_grad, user_grad

def graph2(W_np, x_np, b_np):
    """
    Computes the graph
        - u = Wx + b
        - g = sigmoid(u)
        - f = sum(g)

    Computes also df/dW using
        - pytorchs's automatic differentiation (auto_grad)
        - user's own manual differentiation (user_grad)

    F.sigmoid may be useful here

    :param W_np: input variable W
    :type W_np: np.ndarray(shape=(d,d), dtype=float64)
    :param x_np: input variable x
    :type x_np: np.ndarray(shape=(d,1), dtype=float64)
    :param b_np: input variable b
    :type b_np: np.ndarray(shape=(d,1), dtype=float64)
    :return: f, auto_grad, user_grad
    :rtype: torch.DoubleTensor(shape=[1]),
            torch.DoubleTensor(shape=[d, d]),
            np.ndarray(shape=(d,d), dtype=float64)
    """
    # YOUR CODE HERE:
    W = torch.from_numpy(W_np)
    W.requires_grad = True
    x = torch.from_numpy(x_np)
    b = torch.from_numpy(b_np)
    u = torch.matmul(W, x) + b
    g = F.sigmoid(u)
    f = torch.sum(g)
    f.backward()
    auto_grad = W.grad
    """
    df_du = sigmoid(u) * (1 - sigmoid(u))
    du_dW = x^t
    df_dW = df_du * du_dW
    """
    xt = torch.transpose(x, 0, 1)
    sigU = F.sigmoid(u)
    user_grad = (torch.matmul((sigU * (1 - sigU)), xt)).detach().numpy()
    # END YOUR CODE
    return f, auto_grad, user_grad

def SGD_with_momentum(X,
                      y,
                      inital_w,
                      iterations,
                      batch_size,
                      learning_rate,
                      momentum):
    """
    Performs batch gradient descent optimization using momentum.

    :param X: design matrix
    :type X: np.ndarray(shape=(N, d))
    :param y: regression targets
    :type y: np.ndarray(shape=(N, 1))
    :param inital_w: initial weights
    :type inital_w: np.array(shape=(d, 1))
    :param iterations: number of iterations
    :type iterations: int
    :param batch_size: size of the minibatch
    :type batch_size: int
    :param learning_rate: learning rate
    :type learning_rate: float
    :param momentum: accelerate parameter
    :type momentum: float
    :return: weights, weights history, cost history
    :rtype: np.array(shape=(d, 1)), list, list
    """
    # YOUR CODE HERE:
    z = torch.autograd.Variable(torch.zeros(inital_w.shape).double() , requires_grad = True)
    W = torch.autograd.Variable(torch.from_numpy(inital_w), requires_grad = True)
    x = torch.autograd.Variable(torch.from_numpy(X), requires_grad = False)
    Y = torch.autograd.Variable(torch.from_numpy(y), requires_grad = False)
    cost_history = []
    weights_history = []
    for i in range(iterations) :
        temp = torch.randperm(x.shape[0])
        x = x[temp]
        Y = Y[temp]
        xW = torch.matmul(x[:batch_size], W)
        xWY = xW - Y[:batch_size]
        J = torch.matmul(torch.transpose(xWY, 0, 1), xWY)  / batch_size
        J.backward()
        z.data = momentum * z + W.grad
        W.grad.zero_()
        W.data -= learning_rate * z
        cost_history.append(J)
        weights_history.append(W.data)
    w_np = W.detach().numpy()
    # END YOUR CODE

    return w_np, weights_history, cost_history


import os
import dill
import numpy as np
from math import pi, exp, atan, sqrt, acos

def angle(x, y):
	"""calculates the angle of (x,y) with respect to (0,0)

	:param x: x choordinate
	:param y: y choordinate
	:returns: the angle"""
	at = atan(y/x)
	if(x < 0): return at+pi
	elif(y < 0): return at+2*pi
	return at

def angle_between_vectors(v, w):
	"""calculates the angle between two vectors (any dimensional)

	:param v: vector 1
	:param w: vector 2
	:returns: the angle"""
	scalar = sum(v[i]*w[i] for i in range(len(v)))
	if(scalar/(L2_norm(v)*L2_norm(w)) > 1): return 0
	if(scalar/(L2_norm(v)*L2_norm(w)) < -1): return pi
	return acos(scalar/(L2_norm(v)*L2_norm(w)))

def L1_norm(seq):
	"""calculates the L1 norm of a sequence or vector

	:param seq: the sequence or vector
	:returns: the L1 norm"""
	norm = 0
	for i in range(len(seq)):
		norm += abs(seq[i])
	return norm

def L2_norm(seq):
	"""calculates the L2 norm of a sequence or vector

	:param seq: the sequence or vector
	:returns: the L2 norm"""
	norm = 0
	for i in range(len(seq)):
		norm += seq[i]**2
	return sqrt(norm)

def Linf_norm(seq):
	"""calculates the Linf norm of a sequence or vector

	:param seq: the sequence or vector
	:returns: the Linf norm"""
	largest = 0
	for i in range(len(seq)):
		if(abs(seq[i]) > largest): largest = abs(seq[i])
	return largest

def linear_interpolation(listx, listy, argument):
	"""calculates the linear interpolation of [listx,listy] at argument

	:param listx: x choordinates (should be ordered in ascending order)
	:param listy: y choordinates
	:param argument: where to evaluate the linear interpolation
	:returns: value of the linear interpolation at argument"""
	if(argument in listx):
		return listy[listx.index(argument)]
	if(argument < listx[0]):
		return listy[0] + (listy[0]-listy[1])/(listx[1]-listx[0])*(listx[0]-argument)
	if(argument > listx[-1]):
		return listy[-1] + (listy[-1]-listy[-2])/(listx[-1]-listx[-2])*(argument-listx[-1])
	index = 0
	while((listx[index] < argument and listx[index+1] > argument) != 1): index += 1
	return listy[index] + (listy[index+1]-listy[index])/(listx[index+1]-listx[index])*(argument-listx[index])

def gaussian(x, std=1):
	"""returns a Gaussian distribution

	:param x: variable
	:param std: standard deviation (default 1)
	:returns: Gaussian PDF"""
	return 1/(std*sqrt(2*pi))*exp(-x**2/(2*std**2))

def gaussian_der(x, std=1):
	"""returns a Gaussian derivative distrbution (x*PDF_normal)

	:param x: variable
	:param std: the standard deviation
	:returns: positive half of the Gaussian derivative PDF"""
	if(x < 0): return 0
	return (2-pi/2)/std**2*x*exp(-(2-pi/2)*x**2/(2*std**2))

def kernel_convolve(seq, kernel, kernel_width):
	"""convolves a sequence with a kernel

	:param seq: sequence to be convolved
	:param kernel: the kernel, a probability distribuion fucntion (does not have to be normalized)
	:param kernel_width: the width of the kernel (std for Gaussian)
	:returns: convolved sequence"""
	ker = np.array([kernel(i/kernel_width) for i in range(-round(3*kernel_width), round(3*kernel_width))])
	ker = ker/sum(ker)
	return np.convolve(seq, ker ,mode='same')

def save_object_dill(obj, filename, save_path="objects_save"):
	"""saves an object with dill (also makes the 'objects_save/' directory if it wasn't there)

	:param obj: object to save
	:param filename: filename without any extension
	:param save_path: path where to save object, staring from the directory of the simulation (default "objects_save")"""
	# directory
	if not os.path.exists(save_path):
		os.makedirs(save_path)
	# pickling
	f = open(save_path+"/"+filename+'.pickle', 'wb')
	dill.dump(obj, f)
	f.close()

def load_object_dill(filename, load_path="objects_save"):
	"""loads an object with dill (from 'objects_save/' directory)

	:param filename: filename without any extension
	:param load_path: path where to load object from, staring from the directory of the simulation (default "objects_save")
	:returns: saved object"""
	# unpickling
	f = open(load_path+"/"+filename+'.pickle', 'rb')
	obj = dill.load(f)
	f.close()
	return obj


import numpy as np
import sympy as sym
import matplotlib.pyplot as plt

def f(x):
    """
    Define a function (you can change to whatever function you want
    """
    return (x - np.pi)**2 + 4 * np.sin(x)

def visualize(function, minimum, iter_points, limits, npoints=100):
    """
    Quick plot of the function
    """
    x, y = function[0], function[1]
    lam_x = sym.lambdify(x, y)
    x_vals = np.linspace(limits[0], limits[1], num=npoints)
    y_vals = lam_x(x_vals)

    # Some plot configurations
    plt.rc('font', family='serif')
    plt.rc('font', size=11)
    plt.rc('axes', labelsize=11)

    fig, ax = plt.subplots()
    ax.plot(x_vals, y_vals, 'r-')
    ax.plot(iter_points[0], iter_points[1], 'og')
    ax.plot(minimum[0], minimum[1], 'sr')
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    plt.show()

def bisection(function, limits, tol=1e-3, max_iter=100):
    """
    :param function: symbolic expression
    :param limits: limits of the function
    :param tol: tolerance to converge
    :return:
    """
    xm_vector = []
    fxm_vector = []
    b = [limits[0], limits[1]]
    if b[0] > b[1]:
        raise Exception('Bracket values must be in ascending order')

    yprime = function.diff(x)
    dfb0 = float(yprime.subs(x, b[0]))
    dfb1 = float(yprime.subs(x, b[1]))

    if not ((np.sign(dfb0) < 0) and (np.sign(dfb1) > 0)):
        raise Exception('Minimum may not be contained in bracket')

    b_size = b[1] - b[0]
    k = 0

    while (b_size > tol) and (k < max_iter):
        k += 1
        xm = (b[1] + b[0])/2
        fxm = float(y.subs(x, xm))
        dfxm = float(yprime.subs(x, xm))

        if dfxm == 0:
            break
        elif dfxm > 0:
            b[1] = xm
            xm_vector.append(xm)
            fxm_vector.append(fxm)
        elif dfxm < 0:
            b[0] = xm
            xm_vector.append(xm)
            fxm_vector.append(fxm)

        b_size = b[1] - b[0]
        minimum = [xm, fxm]
        iter_points = [xm_vector, fxm_vector]

    return minimum, iter_points

#%%
# You are supposed to run the previous box before
x = sym.symbols('x')
y = (x - sym.pi)**2 + 4 * sym.sin(x)
minimum, iter_points = bisection(y, [-10, 10])

function = [x, y]
visualize(function, minimum, iter_points, [-10, 10])


#Ejercicio 0
#Lean el capitulo 5 del Landau (ver el programa del curso).

import numpy as np
import matplotlib.pylab as plt

#Ejercicio 1
# Usando los generadores de numeros aleatorios de numpy (https://docs.scipy.org/doc/numpy-1.15.1/reference/routines.random.html):
# a) Genere 1000 numeros aleatorios que sigan una distribucion uniforme y esten entre -10 y 10. Haga un histograma y guardelo sin mostrarlo en un archivo llamado uniforme.pdf

uniforme=(np.random.random(1000)*20)-10

plt.figure()
plt.hist(uniforme,bins=50)
plt.xlabel("x")
plt.ylabel("y")
plt.title("Distribución Uniforme")
plt.grid()
plt.savefig("uniforme.png")
plt.close()

# a) Genere 1000 numeros aleatorios que sigan una distribucion gausiana centrada en 17 y de sigma 5. Haga un histograma y guardelo sin mostrarlo en un archivo llamado gausiana.pdf

gaussiana=np.random.normal(17,5.0,1000)

plt.figure()
plt.hist(gaussiana,bins=50)
plt.xlabel("x")
plt.ylabel("y")
plt.title("Distribución Gausiana")
plt.grid()
plt.savefig("gausiana.png")
plt.close()

# Ejercicio 2
# Escriba un programa en Python que:
# Genere puntos aleatorios distribuidos uniformemente dentro de un cuadrado de lado 30.5. Grafique sus puntos y guarde la grafica sin mostrarla en un archivo llamado cuadrado.pdf.

xcuad=np.random.random(3000)*30
ycuad=np.random.random(3000)*5

plt.figure()
plt.scatter(xcuad,ycuad)
plt.xlabel("x")
plt.ylabel("y")
plt.title("Cuadrado 30x5")
plt.grid()
plt.savefig("cuadrado.png")
plt.close()

# Genere puntos aleatorios distribuidos uniformemente dentro de circulo de radio 23. Grafique sus puntos y guarde la grafica sin mostrarla en un archivo llamado circulo.pdf.

xr=(np.random.random(3000)*46)-23
yr=(np.random.random(3000)*46)-23
xcirc=[]
ycirc=[]

for i in range(len(xr)):
    s=np.sqrt((xr[i]**2)+(yr[i]**2))
    if(s<=23):
        xcirc.append(xr[i])
        ycirc.append(yr[i])

plt.figure()
plt.scatter(xcirc,ycirc)
plt.xlabel("x")
plt.ylabel("y")
plt.title("Circulo de rad 23")
plt.grid()
plt.savefig("circulo.png")
plt.close()

# Ejercicio 3
# Lean sobre caminatas aleatorias.

# Ejercicio 4
# Tome los puntos distribuidos aleatoriamente dentro del cuadrado y haga que cada punto siga una caminata aleatoria de 100 pasos.
# La magnitud de los pasos de esta caminata debe seguir una distribucion gaussiana centrada en el punto y de sigma igual a 0.25
# Implemente condiciones de frontera periodicas: si un punto se "sale" de cuadrado por un lado, "entra" por el otro

def RW(xini,yini,N=100,sigma=0.25):
    x=np.zeros(N)
    y=np.zeros(N)
    x[0]=xini
    y[0]=yini

    i=0
    while(i<100):
        paso=np.random.normal(scale=sigma)
        angulo=np.random.random()*2*np.pi

        xpaso=paso*np.cos(angulo)
        ypaso=paso*np.sin(angulo)
        xpaso+=x[i]
        ypaso+=y[i]

        if(xpaso<30 and xpaso>0):
            if(ypaso<5 and ypaso>0):
                x[i]=xpaso
                y[i]=ypaso

        i+=1

    return x,y

# Grafique la distribucion final de puntos y guarde dicha grafica sin mostrarla en un archivo llamado DistCaminata.pdf
# Grafique la caminata de UNO de sus puntos y guarde dicha grafica sin mostrarla en un archivo llamado puntoCaminata.pdf

xipunto=xcuad[np.random.randint(len(xcuad))]
yipunto=ycuad[np.random.randint(len(xcuad))]

xpunto,ypunto=RW(xipunto,yipunto)
print(xpunto)
print(ypunto)

plt.figure()
plt.plot(xpunto,ypunto)
plt.xlabel("x")
plt.ylabel("y")
plt.title("Caminata de uno")
plt.grid()
plt.savefig("puntoCaminata.png")
plt.close()

# Repita el proceso para sigma = 0.00025 y sigma= 2.5. Grafique la caminata de UNO de sus puntos para los distintos sigmas y guardela sin mostrarla en sigmaCaminata.pdf

# Repita el proceso para condiciones abiertas: si un punto se "sale" del cuadrado deja de ser considerado en la simulacion.

# Si le queda tiempo puede:

##################################################################################################################################################################
############################################################ Ejercicio  ##########################################################################
##################################################################################################################################################################

#difusion: una gota de crema en un Cafe.
#
#Condiciones iniciales:
#Cafe: 10000 particulas distribuidas uniformemente dentro de un circulo de radio igual a raiz de 230
#Crema: 100 particulas distribuidas uniformemente dentro de un circulo de radio igual a raiz de 2
#
#Nota: si su codigo se esta demorando mucho en correr, puede usar 1000 particulas de cafe en vez de 10000.
#
# 1) Haga una grafica de las condiciones iniciales donde los dos tipos de particulas tengan distintos colores. Guarde dicha grafica sin mostrarla en CafeLecheIni.pdf
#
#2) Todas las particulas deben hacer una caminata aleatoria de 1000 pasos. Los pasos en las coordenadas x y deben seguir una distribucion gausiana de sigma 2.5. Si va a usar coordenadas polares elija un sigma apropiado.
#
#3) Condiciones de frontera: implemente unas condiciones tales que si la particulas "sale" del circulo, usted vuelva a dar el paso. Si no puede implementar solo las condiciones antes descritas, debe al menos escribir comentarios explicando que hace cada linea de codigo de las condiciones propuestas (comentado abajo)
#
# 4) Haga una grafica de las posiciones finales de las particulas despues de la caminata donde los dos tipos de particulas tengan distintos colores. Guarde dicha grafica sin mostrarla en CafeLecheFin.pdf
#

import numpy as np
import matplotlib.pylab as plt

#Una posible implementacion de condiciones de frontera. Trate de hacer la suya propia sin usar esta.
#Si usa esta (obtiene menos puntos) debe comentar cada una de las lineas explicando en palabras que hace el codigo. Debe tambien naturalmente usar los nombres de variables que uso en el resto de su codigo propio.
#indexcafe=np.where((xcafenuevo*xcafenuevo+ycafenuevo*ycafenuevo)>230)
#indexcrema=np.where((xcremanuevo*xcremanuevo+ycremanuevo*ycremanuevo)>230)
#while(len(indexcafe[0])>1):
#	xcafenuevo[indexcafe]=xcafe[indexcafe] + np.random.normal(0,sigma)
#	ycafenuevo[indexcafe]=ycafe[indexcafe] + np.random.normal(0,sigma)
#	indexcafe=np.where((xcafenuevo*xcafenuevo+ycafenuevo*ycafenuevo)>=230)
#while(len(indexcrema[0])>1):
#	xcremanuevo[indexcrema]=xcrema[indexcrema] + np.random.normal(0,sigma)
#	ycremanuevo[indexcrema]=ycrema[indexcrema] + np.random.normal(0,sigma)
#	indexcrema=np.where((xcremanuevo*xcremanuevo+ycremanuevo*ycremanuevo)>=230)


'''
Recursion - Fibonacci Sequence

'''
import functools
import math
from numpy import matrix

def fibonacci_oneliner(n):
    '''
    Generate Fibonacci Sequence from F_0 to F_n

    returns list[F_0, F_1, ..., F_n]
    Adapted from Mayer2020 using one python line of code.
    '''
    fibs = functools.reduce(lambda x, _: x + [x[-2] + x[-1]],
                            [0] * (n - 1), [0, 1])
    return fibs

@functools.lru_cache(None)  # Comment this line to have a non-memoized version
def fibonacci_recursive(n):
    '''Recursive F_n
    '''
    if n < 0:
        raise ValueError('n must be non negative integer.')
    elif n > 1:
        return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)
    else:
        return n

# Static constants for Binet's and rounding implementations
__denom = 5 ** 0.5
__golden = (1 + __denom) / 2  # Golden Ratio
__psi = (1 - __denom) / 2

def fibonacci_binet(n):
    r'''
    Closed-form expression: Binet's Formula
    F_n = \frac{}{\sqrt{5}}

    Golden ratio $\phi = \frac{1 + \sqrt{5}}{2}$
    \phi^2 = \phi + 1
    \psi = (1 - \sqrt{5}) / 2
    \psi^2 =  \psi + 1

    Rounding errors above 71 (i.e. n < 72)
    '''
    if n < 0:
        raise ValueError('n must be non negative integer.')
    elif n > 71:
        raise ValueError('This function has rounding errors above n=71.')
    else:
        return int((__golden ** n - __psi ** n) / __denom)

def fibonacci_rounding(n):
    """
    Fast Fibonacci using rounding

    It is limited to n < 71"""
    if n < 0:
        raise ValueError('Fibs for negative values are not defined.')
    elif n > 70:
        raise ValueError('This function has rounding errors above n=70.')
    return round(math.pow(__golden, n) / __denom)

def fibonacci_matrix(n):
    '''
    Fibonacci number F_n using matrix powers

    From bebidek2018 <https://stackoverflow.com/questions/3323001/what-is-the-maximum-recursion-depth-in-python-and-how-to-increase-it>
    '''
    return (matrix('0 1; 1 1', dtype='object') ** n).item(1)

class Solution:
    def fib(self, N: int) -> int:
        if N <= 1:
            return N
        F = [0] * (N + 1)
        F[1] = 1
        for n in range(2, N + 1):
            F[n] = F[n-1] + F[n-2]
        return F[N]

    def fib2(self, N: int) -> int:
        if N <= 1:
            return N
        F = [0, 1, 1]
        for n in range(3, N + 1):
            F[0], F[1], F[2] = F[1], F[2], F[-1] + F[-2]
        return F[-1]

if __name__ == '__main__':
    n = 45  # n > 1 # 70 is the limit for fast_fib
    fibs = fibonacci_oneliner(n)

    # Some known values to verify
    if n >= 70:
        assert fibs[70] == 190392490709135
        assert fibs[71] == 308061521170129

    # lr = lb = []
    if n < 72:
        lb = ([fibonacci_binet(ni) for ni in range(n + 1)])
        print(f'lb\n{lb}')
        assert fibs == lb
    if n < 71:
        lf = ([fibonacci_rounding(ni) for ni in range(n + 1)])
        print(lf)
        assert fibs == lf, "Disc"

    if n < 5000:
        lr = ([fibonacci_recursive(ni) for ni in range(n + 1)])
        assert fibs == lr
        # print(f'lr\n{lr}')
    lm = [fibonacci_matrix(ni) for ni in range(n + 1)]
    assert lm == fibs

    # From Wolfram|Alpha: Fibo[100] = 354224848179261915075
    fibo100 = 354224848179261915075
    print(f'{fibo100=}')
    assert fibonacci_recursive(100) == fibo100


#
#	root-regula-falsi.py
#	Finding root using regula falsi method
#
#	Sparisoma Viridi | https://butiran.github.io/
#
#	Execute: py root-regula-falsi.js
#
#	20210210
#	0527 Typo node --> py, use newton-raphson.py as template.
#   0827 Continue at campus.
#   1001 Continue after archiving lab modules.
#   1028 Try compile again after install Python 3.9.1 64 bit.
#

# Import necessary libraries
import numpy as np

# Define a test function
def test_function(x):
	y3 = 0.01 * x * x * x
	y2 = -0.2252 * x * x
	y1 = 0.4136 * x
	y0 = 1.808
	y = y3 + y2 + y1 + y0
	return y

# Define input
f = test_function
x1 = 3
x2 = 6
eps = 1E-10
n = 0
maxstep = 40

# Define default message and parameter
xroot = "not found"
SHOW_PROGRESS = True

# Do iteration
Nstep = 0
x = []
x.append(x1)
x.append(x2)
froot = np.abs(f(x[n]))

while froot > eps and n < maxstep - 1:
	x.append(x[0] - ((x[n+1] - x[0])/(f(x[n+1]) - f(x[0]))) * f(x[0]))

	froot = np.abs(f(x[n+2]))
	if froot < eps:
		xroot = x[n+2]

	if SHOW_PROGRESS:
		if n == 0:
			fn = f(x[n])
			print("n\tx\tf(x)")
			print(n, x[n], f(x[n]), sep="\t")
			print(n, x[n+1], f(x[n+1]), sep="\t")
		print(n+1, x[n+2], f(x[n+2]), sep="\t")

	n += 1

Nstep = n+2

if SHOW_PROGRESS:
	print()

# Display result
print("f(x)  0.01x^3 - 0.2192x^2 + 0.3056x + 1.568");
print("x1    ", x1, sep="")
print("x2    ", x2, sep="")
print("ε     ", eps, sep="")
print("Nstep ", Nstep, sep="")
print("xroot ", xroot, sep="")


import numpy as np

__all__ = ['tstatistic',
           'nantstatistic',
           'diffmean']

def diffmean(a, b, axis=0):
    """Difference of means statistic.

    Parameters
    ----------
    a,b : ndarray with shapes equal along all dims except axis
        Input data for the calculation.
    axis : int
        Specify the axis along which the statistic will be computed.

    Returns
    -------
    dm : ndarray with one less dimension"""
    return a.mean(axis=axis) - b.mean(axis=axis)

def tstatistic(a, b, axis=0, equal_var=True):
    """Computes a two-sample t-statistic on a and b along the specific axis
    Code is lifted from scipy.stats.ttest_ind except that there is no
    calculation of the associated p-value

    Parameters
    ----------
    a,b : ndarray with shapes equal along all dims except axis
        Input data for the calculation.
    axis : int
        Specify the axis along which the statistic will be computed.
    equal_var : bool
        Specify if the statistic will use a pooled estimate of the variance (True)
        or if it will make no assumption about equal variance (False).

    Returns
    -------
    t : ndarray with one less dimension"""
    v1 = np.var(a, axis, ddof=1)
    v2 = np.var(b, axis, ddof=1)
    n1 = a.shape[axis]
    n2 = b.shape[axis]

    if equal_var:
        df = n1 + n2 - 2
        svar = ((n1 - 1) * v1 + (n2 - 1) * v2) / float(df)
        denom = np.sqrt(svar * (1.0 / n1 + 1.0 / n2))
    else:
        vn1 = v1 / n1
        vn2 = v2 / n2
        df = ((vn1 + vn2)**2) / ((vn1**2) / (n1 - 1) + (vn2**2) / (n2 - 1))

        # If df is undefined, variances are zero (assumes n1 > 0 & n2 > 0).
        # Hence it doesn't matter what df is as long as it's not NaN.
        df = np.where(np.isnan(df), 1, df)
        denom = np.sqrt(vn1 + vn2)

    d = np.mean(a, axis) - np.mean(b, axis)
    t = np.divide(d, denom)
    return t

def nantstatistic(a, b, axis = 0, equal_var = True):
    """Computes a two-sample t-statistic on a and b along the specific axis
    Uses nan* functions which can be slightly slower.
    Code is lifted from scipy.stats.ttest_ind except that there is no
    calculation of the associated p-value

    Parameters
    ----------
    a,b : ndarray with shapes equal along all dims except axis
        Input data for the calculation.
    axis : int
        Specify the axis along which the statistic will be computed.
    equal_var : bool
        Specify if the statistic will use a pooled estimate of the variance (True)
        or if it will make no assumption about equal variance (False).

    Returns
    -------
    t : ndarray with one less dimension
    """

    v1 = np.nanvar(a, axis, ddof=1)
    v2 = np.nanvar(b, axis, ddof=1)
    n1 = a.shape[axis]
    n2 = b.shape[axis]

    if equal_var:
        df = n1 + n2 - 2
        svar = ((n1 - 1) * v1 + (n2 - 1) * v2) / np.float(df)
        denom = np.sqrt(svar * (1.0 / n1 + 1.0 / n2))
    else:
        vn1 = v1 / n1
        vn2 = v2 / n2
        df = ((vn1 + vn2)**2) / ((vn1**2) / (n1 - 1) + (vn2**2) / (n2 - 1))

        # If df is undefined, variances are zero (assumes n1 > 0 & n2 > 0).
        # Hence it doesn't matter what df is as long as it's not NaN.
        df = np.where(np.isnan(df), 1, df)
        denom = np.sqrt(vn1 + vn2)

    d = np.nanmean(a, axis) - np.nanmean(b, axis)
    t = np.divide(d, denom)
    return t

'''TODO: implement in numba so they can be used in a numbized permutation test
"""Attempt to import numba and define numba compiled versions of these functions"""
import os
import sys
try:
    import numba as nb
    print 'mytstats: Successfully imported numba version %s' % (nb.__version__)
    NB_SUCCESS = True
except OSError:
    try:
        """On Windows it is neccessary to be on the same drive as the LLVM DLL
        in order to import numba without generating a "Windows Error 161: The specified path is invalid."""
        curDir = os.getcwd()
        targetDir = os.path.splitdrive(sys.executable)[0]
        os.chdir(targetDir)
        import numba as nb
        print 'mytstats: Successfully imported numba version %s' % (nb.__version__)
        NB_SUCCESS = True
    except OSError:
        NB_SUCCESS = False
        print 'mytstats: Could not load numba\n(may be a path issue try starting python in C:\\)'
    finally:
        os.chdir(curDir)
except ImportError:
    NB_SUCCESS = False
    print 'mytstats: Could not load numba'

"""TODO: (1) Test numba functions (this code is just copied from above)
             if the function can just be decorated then do that,
             but i think it may need to be modified

         (2) Add numba permutation function that utlizes these statistics
             (this is where the speed-up will happen)"""

if NB_SUCCESS and False:
    __all__.extend(['nb_tstatistic', 'nb_nantstatistic'])

    @nb.jit(nb.float64[:](nb.float64[:],nb.float64[:], nb.int32, nb.boolean), nopython = True)
    def nb_tstatistic(a, b, axis, equal_var):
        v1 = np.var(a, axis, ddof=1)
        v2 = np.var(b, axis, ddof=1)
        n1 = a.shape[axis]
        n2 = b.shape[axis]

        if equal_var:
            df = n1 + n2 - 2
            svar = ((n1 - 1) * v1 + (n2 - 1) * v2) / np.float(df)
            denom = np.sqrt(svar * (1.0 / n1 + 1.0 / n2))
        else:
            vn1 = v1 / n1
            vn2 = v2 / n2
            df = ((vn1 + vn2)**2) / ((vn1**2) / (n1 - 1) + (vn2**2) / (n2 - 1))

            # If df is undefined, variances are zero (assumes n1 > 0 & n2 > 0).
            # Hence it doesn't matter what df is as long as it's not NaN.
            df = np.where(np.isnan(df), 1, df)
            denom = np.sqrt(vn1 + vn2)

        d = np.mean(a, axis) - np.mean(b, axis)
        t = np.divide(d, denom)
        return t

    @nb.jit(nb.float64[:](nb.float64[:],nb.float64[:], nb.int, nb.boolean), nopython = True)
    def nb_nantstatistic(a, b, axis, equal_var):
        v1 = np.nanvar(a, axis, ddof=1)
        v2 = np.nanvar(b, axis, ddof=1)
        n1 = a.shape[axis]
        n2 = b.shape[axis]

        if equal_var:
            df = n1 + n2 - 2
            svar = ((n1 - 1) * v1 + (n2 - 1) * v2) / float(df)
            denom = np.sqrt(svar * (1.0 / n1 + 1.0 / n2))
        else:
            vn1 = v1 / n1
            vn2 = v2 / n2
            df = ((vn1 + vn2)**2) / ((vn1**2) / (n1 - 1) + (vn2**2) / (n2 - 1))

            # If df is undefined, variances are zero (assumes n1 > 0 & n2 > 0).
            # Hence it doesn't matter what df is as long as it's not NaN.
            df = np.where(np.isnan(df), 1, df)
            denom = np.sqrt(vn1 + vn2)

        d = np.nanmean(a, axis) - np.nanmean(b, axis)
        t = np.divide(d, denom)
        return t

'''


import math
import numpy as np
from collections import Counter
#-------------------------------------------------------------------------

#-----------------------------------------------
#    Utility functions
#-----------------------------------------------

#--------------------------
def entropy(Y):
    '''
        Compute the entropy of a list of values.
        Input:
            Y: a list of values, a numpy array of int/float/string values.
        Output:
            e: the entropy of the list of values, a float scalar
    '''

    n = len(Y) # total number of values
    c = Counter(Y) # create a counter on the list
    e = 0.
    for k,v in c.items():
        p = v/n
        e -=  p*math.log(p,2)
    return e
#--------------------------
def conditional_entropy(Y,X):
    '''
        Compute the conditional entropy of y given x.
        Input:
            Y: a list of values, a numpy array of int/float/string values.
            X: a list of values, a numpy array of int/float/string values.
        Output:
            ce: the conditional entropy of y given x, a float scalar
    '''
    n = len(Y) # total number of values
    c = Counter(X) # create a counter on the list
    ce = 0.
    for k,v in c.items():
        ce += entropy(Y[X==k])*v/n
    return ce
#--------------------------
def information_gain(Y,X):
    '''
        Compute the information gain of y after splitting over attribute x
        Input:
            X: a list of values, a numpy array of int/float/string values.
            Y: a list of values, a numpy array of int/float/string values.
        Output:
            g: the information gain of y after splitting over x, a float scalar
    '''
    g = entropy(Y) - conditional_entropy(Y,X)
    return g

#-----------------------------------------------
#     A Decision Tree Node
#-----------------------------------------------
class Node:
    '''
        The class of a Decision Tree Node

        Properties
            isleaf: whether or not this node is a leaf node, a boolean scalar
            p: the label to be predicted on the node (i.e., most common label in the node).
            i: the index of the attribute being tested in the node, an integer scalar
            th: the threshold on the attribute, a float scalar.
            C1: the first child node for values smaller than threshold
            C2: the second child node for values larger than threshold
    '''
    #--------------------------
    def __init__(self,X,Y):
        '''
        Create a decision tree node
        Inputs:
            X: the data instances in the node, a numpy matrix of shape p by n.
               Each element can be int/float.
               Here n is the number data instances in the node, p is the number of attributes.
               Each row of X represents one attribute, each column of X represents a data instance.
            Y: the class labels in the node, a numpy array of length n.
               Each element can be int/float/string.
        '''
        assert len(X)>0
        assert len(Y)>0
        self.X = X
        self.Y = Y
        # compute the most common label in the node for prediction
        self.p = Counter(Y).most_common(1)[0][0]
        # test whether or not if the node is a leaf node
        self.isleaf = True
        for f in X:
            if not len(np.unique(f))==1:
                self.isleaf = False
                break
        if (len(np.unique(Y))==1):
            self.isleaf = True

    #--------------------------
    def cutting_points(self,i):
        '''
            Find all possible cutting points in the i-th attribute.
            (1) sort unique attribute values in X, like, x1, x2, ..., xn
            (2) consider splitting points of form (xi + x(i+1))/2
            (3) only consider splitting between instances of different classes
            Input:
                i: the index of the attribute to be used, an integer scalar
            Output:
                cp: the list of  potential cutting points, a float numpy vector.
        '''
        x = self.X[i] # the i-th attribute
        cp = []
        z = sorted(np.unique(x)) # unique values in the attribute
        for i in range(len(z)-1):
            idx = np.logical_or(x==z[i], x==z[i+1])
            ys = self.Y[idx]
            if len(np.unique(ys))>1:
                cp.append((z[i] + z[i+1])/2.) # add a candidate cutting point
        return cp

    #--------------------------
    def best_threshold(self,i):
        '''
            Find the best threshold among all possible cutting points in the i-th attribute.
            Input:
                i: the index of the attribute to be used, an integer scalar
            Output:
                th: the best threshold, a float scalar.
                g: the information gain by using the best threshold, a float scalar.
        '''
        X = self.X[i] # the i-th attribute
        cp = self.cutting_points(i)
        assert len(cp)>0
        g= -math.inf
        for c in cp:
            x = X>=c
            g_new = information_gain(self.Y,x)
            if g_new>g:
                g = g_new
                th = c
        return th,g
    #--------------------------
    def best_attribute(self):
        '''
            Find the best attribute to split the node. The attributes have continuous values (int/float).
            Here we use information gain to evaluate the attributes.
            Output:
                i: the index of the attribute to split, an integer scalar
                th: the threshold of the attribute to split, a float scalar
        '''
        g= -math.inf
        for k in range(self.X.shape[0]):
            if len(np.unique(self.X[k]))>1:
                tk,gk = self.best_threshold(k)
                if gk>g:
                    g = gk
                    self.i = k
                    self.th = tk

    #--------------------------
    def add_children_nodes(self):
        '''
            build the children nodes C1 and C2 with the best attribute and threshold in the node
            Output:
                C1: the child node for values smaller than threshold
                C2: the child node for values larger than (or equal to) threshold
        '''
        self.best_attribute()
        i,th = self.i, self.th
        x = self.X[i]
        self.C1 = Node(self.X[:,x<th], self.Y[x<th])
        self.C2 = Node(self.X[:,x>=th], self.Y[x>=th])

    #--------------------------
    def build_tree(self):
        '''
            Recursively build a subtree from the current tree node.
        '''
        if self.isleaf==False:
            self.add_children_nodes()
            self.C1.build_tree()
            self.C2.build_tree()
    #--------------------------
    def predict_1(self,x):
        '''
            Using the decision tree starting from the current node to predict the label on one test instance
            Input:
                x: the attribute vector, a numpy vector of shape p.
                   Each attribute value can be int/float
            Output:
                p: the label prediction on the test instance, a scalar, can be int/float/string.
        '''
        if self.isleaf:
            return self.p
        if x[self.i] <= self.th:
            return self.C1.predict_1(x)
        else:
            return self.C2.predict_1(x)

#-----------------------------------------------
#     A Decision Tree
#-----------------------------------------------
class DecisionTree:
    '''
        The Class of Decision Tree
        Properties
            root: the root node of the decision tree
    '''
    #--------------------------
    def __init__(self,X,Y):
        '''
        Create a decision tree
        Inputs:
            X: the data instances in the node, a numpy matrix of shape p by n.
               Each element can be int/float.
               Here n is the number data instances in the node, p is the number of attributes.
               Each row of X represents one attribute, each column of X represents a data instance.
            Y: the class labels in the node, a numpy array of length n.
               Each element can be int/float/string.
        '''
        self.root = Node(X,Y)  # create root node
        self.root.build_tree() # build the tree

    #--------------------------
    def predict_1(self,x):
        '''
            Using the decision tree to predict the label on one test instance
            Input:
                x: the attribute vector, a numpy vector of shape p.
                   Each attribute value can be int/float
            Output:
                p: the label prediction on the test instance, a scalar, can be int/float/string.
        '''
        return self.root.predict_1(x)

    #--------------------------
    def predict(self,X):
        '''
            Using the decision tree to predict the label on all test instances
            Input:
                X: the feature matrix of all test instances, a numpy matrix of shape p by n.
                   Each element can be int/float.
                   Here n is the number data instances in the dataset, p is the number of attributes.
            Output:
                P: the predicted class labels on all test instances, a numpy array of length n.
                   Each element can be int/float/string.
        '''
        return np.array([self.predict_1(x) for x in X.T])


import numpy as np

#Questions on NumPy Statistics
# Compute the median of the flattened NumPy array
#median Compute the median along the specified axis.
np.median(np.array([1, 2, 3, 4, 5, 6, 7]))

# Find Mean of a List of Numpy Array
#mean Compute the arithmetic mean along the specified axis.
np.mean(np.array([1, 2, 3]))

# Calculate the mean of array ignoring the NaN value
#nanmean Compute the arithmetic mean along the specified axis, ignoring NaNs.
np.nanmean(np.array([[20, 15, 37], [47, 13, np.nan]]))

# Get the mean value from given matrix
#matrix.mean Return mean value from given matrix
np.matrix('[64, 1; 12, 3]').mean()

# Compute the variance of the NumPy array
# var Return Variance of the array (a scalar value if axis is none)
# or array with variance values along specified axis.
np.var([20, 2, 7, 1, 34]  , dtype = np.float32)

# Compute the standard deviation of the NumPy array
# std Return tandard Deviation of the array (a scalar value if axis is none)
# or array with standard deviation values along specified axis.
np.std([20, 2, 7, 1, 34]  , dtype = np.float32)

# Compute pearson product-moment correlation coefficients of two given NumPy arrays
# corrcoef Pearson product-moment correlation coefficients
np.corrcoef( np.array([0, 1, 2]), np.array([3, 4, 5]))

# Calculate the mean across dimension in a 2D NumPy array
np.mean(np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), axis=1)

# Calculate the average, variance and standard deviation in Python using NumPy
# average Return Default is False. If True, the tuple is returned, otherwise
# only the average is returned
np.average([2, 4, 4, 4, 5, 5, 7, 9] )
np.var([20, 2, 7, 1, 34]  , dtype = np.float32)
np.std([20, 2, 7, 1, 34]  , dtype = np.float32)

# Describe a NumPy Array in Python
# amin takes a NumPy array as an argument and returns the minimum
# amax takes a NumPy array as an argument and returns maximum.
# ptp takes a NumPy array as an argument and returns the range of the data.
arr = np.array([4, 5, 8, 5, 6, 4, 9, 2, 4, 3, 6])
# measures of central tendency
mean = np.mean(arr)
median = np.median(arr)
  # measures of dispersion
np.amin(arr)
np.amax(arr)
np.ptp(arr)
np.var(arr)
np.std(arr)

NumpyTutorial/Questions on NumPy Statistics.py


import cupy

# Collection of activation functions
# Reference: https://en.wikipedia.org/wiki/Activation_function

class Sigmoid():
    def __call__(self, x):
        return 1 / (1 + cupy.exp(-x))

    def gradient(self, x):
        return self.__call__(x) * (1 - self.__call__(x))

class Softmax():
    def __call__(self, x):
        e_x = cupy.exp(x - cupy.max(x, axis=-1, keepdims=True))
        return e_x / cupy.sum(e_x, axis=-1, keepdims=True)

    def gradient(self, x):
        p = self.__call__(x)
        return p * (1 - p)

class TanH():
    def __call__(self, x):
        return 2 / (1 + cupy.exp(-2*x)) - 1

    def gradient(self, x):
        return 1 - cupy.power(self.__call__(x), 2)

class ReLU():
    def __call__(self, x):
        return cupy.where(x >= 0, x, 0)

    def gradient(self, x):
        return cupy.where(x >= 0, 1, 0)

class LeakyReLU():
    def __init__(self, alpha=0.2):
        self.alpha = alpha

    def __call__(self, x):
        return cupy.where(x >= 0, x, self.alpha * x)

    def gradient(self, x):
        return cupy.where(x >= 0, 1, self.alpha)

class ELU():
    def __init__(self, alpha=0.1):
        self.alpha = alpha

    def __call__(self, x):
        return cupy.where(x >= 0.0, x, self.alpha * (cupy.exp(x) - 1))

    def gradient(self, x):
        return cupy.where(x >= 0.0, 1, self.__call__(x) + self.alpha)

class SELU():
    # Reference : https://arxiv.org/abs/1706.02515,
    # https://github.com/bioinf-jku/SNNs/blob/master/SelfNormalizingNetworks_MLP_MNIST.ipynb
    def __init__(self):
        self.alpha = 1.6732632423543772848170429916717
        self.scale = 1.0507009873554804934193349852946

    def __call__(self, x):
        return self.scale * cupy.where(x >= 0.0, x, self.alpha*(cupy.exp(x)-1))

    def gradient(self, x):
        return self.scale * cupy.where(x >= 0.0, 1, self.alpha * cupy.exp(x))

class SoftPlus():
    def __call__(self, x):
        return cupy.log(1 + cupy.exp(x))

    def gradient(self, x):
        return 1 / (1 + cupy.exp(-x))


# -*- coding: utf-8 -*-
"""
Linear regression example using least squares from COMP551 lecture 2
@author: Demetrios Koziris
"""

import numpy as np
from numpy.linalg import inv
import matplotlib.pyplot as plt

"""
Least-Squares solution
"""
def least_squares(order=1, save_plots=False):
    #print('Least-Squares:\n')

    x = [0.86, 0.09, -0.85, 0.87, -0.44, -0.43, -1.1, 0.40, -0.96, 0.17]
    y = [2.49, 0.83, -0.25, 3.10, 0.87, 0.02, -0.12, 1.81, -0.83, 0.43]

    # input matrix of feature vectors
    X = np.array([np.power(x,p) for p in range(order+1)]).T
    # target vector
    Y = np.array(y).T

    Xt_X = X.T @ X
    # print(f'X^tX = \n{Xt_X}\n')
    Xt_Y = X.T @ Y
    # print(f'X^tY = \n{Xt_Y}\n')
    w =  inv(Xt_X) @ Xt_Y
    # print(f'W = (X^tX)^-1(X^tY) = \n{w}\n')

    def fit_function(x):
        return sum([weight*(x**p) for p,weight in enumerate(w)])

    def fit_function_label():
        terms = [f'{weight:.2f}x^{p}' for p,weight in enumerate(w)]
        return '$y = ' + ' + '.join(terms[::-1]) + '$'

    plot_title = f'Least-Squares Linear Regression Order {order}'
    x_interval = np.linspace(-1.6, 1.6, 1000)
    plt.plot(x_interval, fit_function(x_interval), 'r', label=fit_function_label())
    plt.scatter(x, y, marker='x')
    plt.xlim(-1.6, 1.6);
    plt.ylim(-2, 5);
    plt.title(plot_title)
    plt.legend(loc=2)
    if (save_plots):
        plt.savefig(plot_title.lower().replace(' ','_'), bbox_inches="tight")
    plt.show()

for i in range(10):
    least_squares(i, True)


import numpy as np
from sklearn import linear_model

from MetReg.base.base_model import BaseModel
import numpy as np
np.random.seed(1)

class BaseLinearRegressor(BaseModel):
    """implementation of base linear regression.

    Ordinary least squares Linear Regression.LinearRegression fits a linear
    model with coefficients w = (w1, …, wp) to minimize the residual sum of
    squares between the observed targets in the dataset, and the targets
    predicted by the linear approximation.

    Args:
        fit_intercept (bool, optional):
            Whether to calculate the intercept for this model. If set to False,
            no intercept will be used in calculations (i.e. data is expected
            to be centered). Defaults to True.
        normalize (bool, optional):
            This parameter is ignored when `fit_intercept` is set to False. If
            True, the regressors X will be normalized before regression by
            subtracting the mean and dividing by the l2-norm. Defaults to False.

    Attributes:
        coef_:
            Estimated coefficients for the linear regression problem. If
            multiple targets are passed during the fit (y 2D), this is a 2D
            array of shape (n_targets, n_features), while if only one target
            is passed, this is a 1D array of length n_features.
    """

    def __init__(self,
                 fit_intercept=True,
                 normalize=True,
                 **kwargs):
        self.fit_intercept = fit_intercept
        self.normalize = normalize
        self.regressor = None

    def fit(self, X, y):
        self.regressor = linear_model.LinearRegression(
            fit_intercept=self.fit_intercept,
            normalize=self.normalize)

        self.regressor.fit(X, y)
        return self

class RidgeRegressor(BaseLinearRegressor, BaseModel):
    """Linear least squares with l2 regularization.

    Minimizes the objective function::
    ||y - Xw||^2_2 + alpha * ||w||^2_2

    This model solves a regression model where the loss function is
    the linear least squares function and regularization is given by
    the l2-norm. Also known as Ridge Regression or Tikhonov regularization.
    This estimator has built-in support for multi-variate regression
    (i.e., when y is a 2d-array of shape (n_samples, n_targets)).

    cv: TODO(lewlee): implement of `grid_search` edition.
        Ridge regression with built-in cross-validation.

    Args:
        cv (bool, optional): [description]. Defaults to True.
        cv_alphas (ndarray, optional):
            Array of alpha values to try.
        cv_num_folds (int, optional):
            cross-validation generator or an iterable, Determines the
            cross-validation splitting strategy.

            Possible inputs for cv are:
            - None, to use the efficient Leave-One-Out cross-validation
            - integer, to specify the number of folds.
        alpha (float, optional):
            Regularization strength; must be a positive float. Regularization
            improves the conditioning of the problem and reduces the variance
            of the estimates. Larger values specify stronger regularization.
            Alpha corresponds to ``1 / (2C)`` in other linear models. If an
            array is passed, penalties are assumed to be specific to the
            targets. Hence they must correspond in number. Defaults to 1.0.
        max_iter ([type], optional):
            Maximum number of iterations for conjugate gradient solver.
            For 'sparse_cg' and 'lsqr' solvers, the default value is determined
            by scipy.sparse.linalg. For 'sag' solver, the default value is 1000.
            Defaults to None.
        tol ([type], optional):
            Precision of the solution. Defaults to 1e-3.
        solver (str, optional):
            - 'auto' chooses the solver automatically based on the type of data.
            - 'svd' uses a Singular Value Decomposition of X to compute the
                Ridge coefficients. More stable for singular matrices than
                'cholesky'.
            - 'cholesky' uses the standard scipy.linalg.solve function to
                obtain a closed-form solution.. Defaults to 'auto'.
            - 'lsqr' uses the dedicated regularized least-squares routine
                scipy.sparse.linalg.lsqr. It is the fastest and uses an
                iterative procedure.
        fit_intercept (bool, optional): see class `LR`. Defaults to True.
        normalize (bool, optional): see class `LR`. Defaults to False.

    Attributes:
        coef_ :
            ndarray of shape (n_features,) or (n_targets, n_features)
            Weight vector(s).
        intercept_ :
            float or ndarray of shape (n_targets,) Independent term in
            decision function.
        n_iter_ : None or ndarray of shape (n_targets,)
            Actual number of iterations for each target. Available only for
            lsqr solvers. Other solvers will return None.
    """

    def __init__(self,
                 max_iter=100,
                 tol=1e-3,
                 fit_intercept=True,
                 normalize=True,
                 alpha=4,
                 solver='auto',
                 cv=False,
                 cv_alphas=[0.1, 1.0, 10.0],
                 cv_num_folds=5,
                 **kwargs):
        # common setting
        self.max_iter = max_iter
        self.tol = tol
        self.fit_intercept = fit_intercept
        self.normalize = normalize

        # default setting
        self.alpha = alpha
        self.solver = solver

        # cross-validation setting
        self.cv = cv
        self.cv_alphas = cv_alphas
        self.cv_num_folds = cv_num_folds

        self.regressor = None

    def fit(self, X, y):
        if self.cv:
            self.regressor = linear_model.RidgeCV(
                alphas=self.cv_alphas,
                cv=self.cv_num_folds,
                fit_intercept=self.fit_intercept,
                normalize=self.normalize,)
        else:
            self.regressor = linear_model.Ridge(
                alpha=self.alpha,
                fit_intercept=self.fit_intercept,
                normalize=self.normalize,
                max_iter=self.max_iter,
                tol=self.tol,
                solver=self.solver)

        self.regressor.fit(X, y)
        return self

class LassoRegressor(BaseLinearRegressor, BaseModel):
    """Linear Model trained with L1 prior as regularizer.

    The optimization objective for Lasso is:
        (1 / (2 * n_samples)) * ||y - Xw||^2_2 + alpha * ||w||_1

    The Lasso is a linear model that estimates sparse coefficients. It is
    useful in some contexts due to its tendency to prefer solutions with fewer
    non-zero coefficients, effectively reducing the number of features upon
    which the given solution is dependent.

    """

    def __init__(self,
                 max_iter=1000,
                 tol=1e-4,
                 fit_intercept=True,
                 normalize=True,
                 alpha=0.01,
                 cv=False,
                 cv_num_folds=5,
                 **kwargs):
        # common setting
        self.max_iter = max_iter
        self.tol = tol
        self.fit_intercept = fit_intercept
        self.normalize = normalize

        # default setting
        self.alpha = alpha

        # cross-validation setting
        self.cv = cv
        self.cv_num_folds = cv_num_folds
        self.regressor = None

    def fit(self, X, y):
        if self.cv:
            self.regressor = linear_model.LassoCV(
                cv=self.cv_num_folds,
                fit_intercept=self.fit_intercept,
                normalize=self.normalize,
                max_iter=self.max_iter,
                tol=self.tol,
            )
        else:
            self.regressor = linear_model.Lasso(
                alpha=self.alpha,
                fit_intercept=self.fit_intercept,
                normalize=self.normalize,
                max_iter=self.max_iter,
                tol=self.tol,
            )

        self.regressor.fit(X, y)
        return self

class ElasticRegressor(BaseLinearRegressor, BaseModel):
    """Linear regression with combined L1 and L2 priors as regularizer.

    Minimizes the objective function::
            1 / (2 * n_samples) * ||y - Xw||^2_2
            + alpha * l1_ratio * ||w||_1
            + 0.5 * alpha * (1 - l1_ratio) * ||w||^2_2

    Notes:: l1_ratio parmameters means the coefficient of L1 regularization,
            the coefficient of L2 regularization is (1-l1_ratio).
    """

    def __init__(self,
                 max_iter=1000,
                 tol=1e-4,
                 fit_intercept=True,
                 normalize=True,
                 alpha=0.01,
                 l1_ratio=0.2,
                 cv=False,
                 cv_num_folds=5,
                 **kwargs):
        # common setting
        self.max_iter = max_iter
        self.tol = tol
        self.fit_intercept = fit_intercept,
        self.normalize = normalize

        # default setting
        self.alpha = alpha
        self.l1_ratio = l1_ratio

        # cross-validation setting
        self.cv = cv
        self.cv_num_folds = cv_num_folds
        self.regressor = None

    def fit(self, X, y):
        if self.cv:
            self.regressor = linear_model.ElasticNetCV(
                l1_ratio=self.l1_ratio,
                cv=self.cv_num_folds,
                fit_intercept=self.fit_intercept,
                normalize=self.normalize,
                max_iter=self.max_iter,
                tol=self.tol,
            )
        else:
            self.regressor = linear_model.ElasticNet(
                alpha=self.alpha,
                l1_ratio=self.l1_ratio,
                fit_intercept=self.fit_intercept,
                normalize=self.normalize,
                max_iter=self.max_iter,
                tol=self.tol,
            )

        self.regressor.fit(X, y)
        return self

class ExpandLinearRegressor(BaseLinearRegressor, BaseModel):
    """expand edition of linear model from sklearn library."""

    def estimate(self, algorithm=None):
        """
        1. automatic relevance determination,
        Compared to the OLS (ordinary least squares) estimator, the coefficient
        weights are slightly shifted toward zeros, which stabilises them.
        2. stochastic gradient descent method
        NOTE: could used for different penalty
        3. online passive-aggressive
        They are similar to the Perceptron in that they do not require a
        learning rate. However, contrary to the Perceptron, they include a
        regularization parameter C.
        """
        if algorithm == 1:
            reg = linear_model.ARDRegression()
        elif algorithm == 2:
            reg = linear_model.SGDRegressor()
        elif algorithm == 3:
            reg = linear_model.PassiveAggressiveRegressor()
        return reg

    def robust(self, algorithm=None):
        """
        Robust regression aims to fit a regression model in the presence of
        corrupt data: either outliers, or error in the model.
        1. RANSAC
        2. Theil-Sen
        3. Huber
        """
        if algorithm == 1:
            reg = linear_model.RANSACRegressor()
        elif algorithm == 2:
            reg = linear_model.TheilSenRegressor()
        elif algorithm == 3:
            reg = linear_model.HuberRegressor()
        return reg

if __name__ == "__main__":

    a = LassoRegressor()
    a.fit([1, 2, 3], [2, 3, 1])
    a.predict([1, 3, 4])


# --------------

# Code starts here
#The adjacency matrix adj_mat is provided to you.
import numpy as np
# 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]])

#For this matrix perform the eigen vector decomposition using .linalg.eig() method of numpy. This function returns a tuple and save them as eigenvalues and eigenvectors
eigenvalues,eigenvectors=np.linalg.eig(adj_mat)

#The following is a single step divided into small steps:
#Find the eigen vector corresponding to 1 from eigenvectors (first column of eigenvectors), that is abs(eigenvectors[:,0])
#Normalize this by dividing with np.linalg.norm(eigenvectors[:,0],1). Save it as eigen_1
eigen_1= abs(eigenvectors[:,0])/np.linalg.norm(eigenvectors[:,0],1)

#Next save the most important page number by finding the index with highest value within eigen_1. This can be done by using the .where() method
#from numpy Save it as page and print it out.
page=np.where(eigen_1==eigen_1.max())[0][0]+1
print(page)
# Code ends here

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

# Initialize stationary vector I
#The hyperlink matrix adj_mat is already defined for you. Initialize a stationary matrix init_I which has 1 at the first position and 0s in the rest 7 blocks of the numpy ndarray.
init_I=np.array([1,0,0,0,0,0,0,0])

#Use a for loop over 10 iterations where you update adj_mat according to the rule I^{k+1} = HI^k
# this can be done by .dot(adj_mat, init_I) . Also normalize init_I at every iteration using np.linalg.norm(init_I, 1)
# Perform iterations for power method
for i in range(10):
  init_I=np.dot(adj_mat, init_I)/np.linalg.norm(init_I, 1)

#Save the page number with highest importance as power_page. This can be found by .where() as done in the previous task.
power_page=np.where(init_I==init_I.max())[0][0]+1
print(power_page)

# Code ends here

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

#Problem with power method
#The new adjacency matrix this time for the new webpage connection structure shown in the above image. It is provided as new_adj_mat
# 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 a stationary matrix new_init_I in the same manner as you did for the previous task
new_init_I=np.array([1,0,0,0,0,0,0,0])

#Use a for loop to iterate 10 times and update in the similar manner as you did for the previous task i.e. first take dot product np.dot(new_adj_mat, new_init_I)
#and then normalize as done in previous task.
# Perform iterations for power method
for i in range(10):
  new_init_I=np.dot(new_adj_mat, new_init_I)/np.linalg.norm(new_init_I, 1)

#Print out new_init_I to check out its result. Observe how you get pagerank value for 3rd webpage as zero. Is it not possible as it has incoming connections.
print(new_init_I)

# Code ends here

# --------------
#Code Starts here
# Alpha value
alpha = 0.85

# Code starts here
# Modified adjancency matrix
#Initialize new hyperlink matrix G with the help of the mathematical formula given above. In the formula n can be taken as len(new_adj_mat))
#and 1 as np.ones(new_adj_mat.shape.Save it as G
n=len(new_adj_mat)
S=np.ones(new_adj_mat.shape)/n
I=np.ones(new_adj_mat.shape)
#G=αS+(1−α)*(1/n)*I
G=alpha*S+(I-alpha)*(1/n)

#Initialize stationary vector as final_init_I consisting of 1 at its beginning and rest all zeros in a 1D NumPy array
final_init_I=np.array([1,0,0,0,0,0,0,0])

#Perform 1000 iterations using a for loop to update the stationary vector in the same manner as for the Power Method. Also, do not forget to normalize it.
for i in range(1000):
  final_init_I=np.dot(G, final_init_I)/np.linalg.norm(final_init_I, 1)

#Print out final_init_I
print(final_init_I)

# Code ends here


# a simple cubic spline example.
#
# generate some random data in 10 intervals -- note the data changes
# each time this is run.
#
# Our form of the spline polynomial comes from Pang, Ch. 2
#
# solve the matrix system for the splines
#
# plot the splines
#
# M. Zingale (2013-02-10)

import numpy
import pylab
import math
from scipy import linalg   # scipy modules need to be imported separately
from scipy import interpolate   # scipy modules need to be imported separately

# plot a spline
def plot_spline(x0, x1, f0, f1, ppp0, ppp1):

    # lots of points for a smooth plot
    x = numpy.linspace(x0, x1, 100)

    dx = x1-x0

    alpha = ppp1/(6.0*dx)
    beta = -ppp0/(6.0*dx)

    gamma = (-ppp1*dx*dx/6.0 + f1)/dx
    eta = (ppp0*dx*dx/6.0 - f0)/dx

    p = alpha*(x-x0)**3 + beta*(x-x1)**3 + gamma*(x-x0) + eta*(x-x1)

    pylab.plot(x, p)

# number of intervals
n = 20

xmin = 0.0
xmax = 1.0

# coordinates of the data locations
x = numpy.linspace(xmin, xmax, n+1)
dx = x[1] - x[0]

# random data
f = numpy.random.rand(n+1)

# we are solving for n-1 unknowns

# setup the righthand side of our matrix equation
b = numpy.zeros(n+1)

# b_i = (6/dx) * (f_{i-1} - 2 f_i + f_{i+1})
# here we do this with slice notation to fill the
# inner n-1 slots of b
b[1:n] = (6.0/dx)*(f[0:n-1] - 2.0*f[1:n] + f[2:n+1])

# we only care about the inner n-1 quantities
b = b[1:n]

# the matrix A is tridiagonal.  Create 3 arrays which will represent
# the diagonal (d), the upper diagonal (u), and the lower diagnonal
# (l).  l and u will have 1 less element.  For u, we will pad this at
# the beginning and for l we will pad at the end.
#
# see http://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.solve_banded.html#scipy.linalg.solve_banded
# for the description of a banded matrix

u = numpy.zeros(n-1)
d = numpy.zeros(n-1)
l = numpy.zeros(n-1)

d[:] = 4.0*dx

u[:] = dx
u[0] = 0.0

l[:] = dx
l[n-2] = 0.0

# create a banded matrix -- this doesn't store every element -- just
# the diagonal and one above and below
A = numpy.matrix([u,d,l])

# solve Ax = b using the scipy banded solver -- the (1,1) here means
# that there is one diagonal above the main diagonal, and one below.
xsol = linalg.solve_banded((1,1), A, b)

# x now hold all the second derivatives for points 1 to n-1.  Natural
# boundary conditions set p'' = 0 at i = 0 and n
# ppp will be our array of second derivatives
ppp = numpy.insert(xsol, 0, 0)  # insert before the first element
ppp = numpy.insert(ppp, n, 0)   # insert at the end

# now plot -- data points first
pylab.scatter(x, f, marker="x", color="r")

# plot the splines
i = 0
while i < n:

    # working on interval [i,i+1]
    ppp_i = ppp[i]
    ppp_ip1 = ppp[i+1]

    f_i = f[i]
    f_ip1 = f[i+1]

    x_i = x[i]
    x_ip1 = x[i+1]

    plot_spline(x_i, x_ip1, f_i, f_ip1, ppp_i, ppp_ip1)

    i += 1

pylab.savefig("spline.png")

# note: we could have done this all through scipy -- here is their
# spline, but it doesn't seem to support natural boundary conditions

#s = interpolate.InterpolatedUnivariateSpline(x, f, k=3)
#xx = numpy.linspace(xmin, xmax, 1000)
#pylab.plot(xx, s(xx), color="k", ls=":")

# old way from scipy -- this raises a NotImplementedError for natural
#spl1 = interpolate.splmake(x, f, order=3, kind="natural")
#xx = numpy.linspace(xmin, xmax, 1000)
#yy = interpolate.spleval(spl1, xx)
#pylab.plot(xx, yy, color="k", ls=":")

#pylab.savefig("spline-scipy.png")


import time
from IPython import display
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
from sklearn.datasets import load_iris
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_blobs

import warnings
warnings.filterwarnings("ignore")

def get_data_iris():
	X, Y = load_iris(True)
	X = pd.DataFrame(X, columns = ['SEPAL_LENGTH', 'SEPAL_WIDTH', 'PETAL_LENGTH', 'PETAL_WIDTH'])
	Y = pd.Series(Y, name = 'SPECIES')
	return X, Y

def plot_pair_plots(X, Y):
	data = pd.concat((X,Y),axis=1)
	sns.pairplot(data,hue="SPECIES")

def get_sepal_vs_petal_width():
    X, y = get_data_iris()
    df = pd.concat([X[['SEPAL_WIDTH', 'PETAL_WIDTH', ]], y], axis=1)
    df = df.loc[df.SPECIES.isin([1, 2])]
    df.SPECIES = df.SPECIES.map({1: 0, 2: 1})
    return df

def super_simple_classifier_plot(X, Y):
	data = pd.concat((X,Y),axis=1)
	data.loc[data["SPECIES"]!=0, "SPECIES"] = 1
	sns.lmplot("SEPAL_LENGTH", "PETAL_WIDTH",data=data, hue="SPECIES", fit_reg=False)
	plt.plot(np.linspace(4,8,1000), [0.8]*1000, 'r-')

def linear_separation_plot(X, Y):
	data = pd.concat((X,Y),axis=1)
	xx, yy = np.mgrid[4:8:.01, 0:2.8:.01]
	grid = np.c_[xx.ravel(), yy.ravel()]
	for species in [2,1,0]:
		labels = data.loc[data.SPECIES != species,'SPECIES'].unique()
		clf = LogisticRegression(random_state=0).fit(data.loc[data.SPECIES != species,['SEPAL_LENGTH','PETAL_WIDTH']],
			data.loc[data.SPECIES != species,'SPECIES'])
		probs = clf.predict_proba(grid)[:, 1].reshape(xx.shape)
		f, ax = plt.subplots(figsize=(8, 6))
		contour = ax.contourf(xx, yy, probs, 25, cmap="RdBu",
		                      vmin=0, vmax=1)
		ax_c = f.colorbar(contour)

		ax_c.set_label("$P(Species = %0.1i)$"%labels[1])
		ax_c.set_ticks([0, .25, .5, .75, 1])

		ax.scatter(data.loc[data.SPECIES != species,'SEPAL_LENGTH'], data.loc[data.SPECIES != species,'PETAL_WIDTH'],
			c=data.loc[data.SPECIES != species,'SPECIES'],
			s=50,cmap="RdBu", vmin=-.2, vmax=1.2,edgecolor="white", linewidth=1)
		ax.set(aspect="equal",
		       xlim=(4, 8), ylim=(0, 2.8),
		       xlabel="SEPAL_LENGTH", ylabel="PETAL_WIDTH", title="Logistic Regression Classification Between Species %0.1i and %0.1i" % (labels[0], labels[1]))
		plt.show()

def predict_probability_point(X, Y, point):
	data = pd.concat((X,Y),axis=1)
	a, b = point
	clf = LogisticRegression(random_state=0).fit(data.loc[data.SPECIES != 2,['SEPAL_LENGTH','PETAL_WIDTH']],
			data.loc[data.SPECIES != 2,'SPECIES'])
	probs = clf.predict_proba(np.array([[a,b]]))[:, 1]
	print("The probability of point (%0.1f,%0.1f) belonging to Species 1 is %0.2f" % (a, b, probs))

def final_classification_plot(X, Y, threshold_values):
    data = pd.concat((X,Y),axis=1)
    data = data.loc[data['SPECIES']!=2]
    xx, yy = np.mgrid[4:8:.01, 0:2.8:.01]
    grid = np.c_[xx.ravel(), yy.ravel()]
    for threshold in threshold_values:
        clf = LogisticRegression(random_state=0).fit(data.loc[:,['SEPAL_LENGTH','PETAL_WIDTH']],
                                                     data.loc[:,'SPECIES'])
        probs = clf.predict_proba(grid)[:, 1]
        probs[probs>=threshold] = 1
        probs[probs<threshold] = 0
        probs = probs.reshape(xx.shape)
        f, ax = plt.subplots(figsize=(8, 6))
        contour = ax.contourf(xx, yy, probs, 25, cmap="RdBu",
                      vmin=0, vmax=1)
        ax.scatter(data['SEPAL_LENGTH'], data['PETAL_WIDTH'],
                   c=data['SPECIES'],
                   s=50,cmap="RdBu", vmin=-.2, vmax=1.2,edgecolor="white", linewidth=1)
        ax.set(aspect="equal",
               xlim=(4, 8), ylim=(0, 2.8),
               xlabel="SEPAL_LENGTH", ylabel="PETAL_WIDTH", title="Logistic Regression Classification Using threshold = %0.2f" % (threshold))
        plt.show()

def get_split(x, coef1, coef2, intercept):
    y = (0.5 - (coef1 * x) - intercept) / coef2
    return y

def get_separation_line(x_range, linear_reg):
    line_x = x_range

    line_y = [get_split(i,
                        linear_reg.coef_[0],
                        linear_reg.coef_[1],
                        linear_reg.intercept_)
              for i in line_x]

    return line_x, line_y

def get_sepal_vs_petal_width():
    X, y = get_data_iris()
    df = pd.concat([X[['SEPAL_WIDTH', 'PETAL_WIDTH', ]], y], axis=1)
    df = df.loc[df.SPECIES.isin([1, 2])]
    df.SPECIES = df.SPECIES.map({1: 0, 2: 1})

    return df

def plot_line(df, x, y, c, linear_reg):
    ax = df.plot(kind='scatter', x=x,
                 y=y, c=c,
                 cmap='seismic', s=45, figsize=(12, 8), sharex=False, );

    line_x, line_y = get_separation_line(x_range=np.linspace(1, 2.5, 100),
                                         linear_reg=linear_reg)

    plt.plot(line_x, line_y, c="k", ls='--', alpha=.5,
             label=['Prediction = 0.5'])
    plt.legend()
    plt.ylim([df.SEPAL_WIDTH.min() - .5, df.SEPAL_WIDTH.max() + .5])
    plt.show()

def plot_line_and_annot(df, lin_reg, x='PETAL_WIDTH', y='SEPAL_WIDTH',
                        c='predictions_linreg', ):
    ax = df.plot(kind='scatter', x=x, y=y, c=c, cmap='seismic',
                 s=45, figsize=(12, 8), sharex=False, );

    line_x, line_y = get_separation_line(x_range=np.linspace(1, 2.5, 100),
                                         linear_reg=lin_reg)

    plt.plot(line_x, line_y, c="k", ls='--', alpha=.5,
             label=['Prediction = 0.5'])
    plt.legend()
    plt.ylim([df.SEPAL_WIDTH.min() - .5, df.SEPAL_WIDTH.max() + .5])

    props = dict(boxstyle='round', facecolor='wheat', alpha=0.2)

    ax.text(.52, 0.7, '50%', transform=ax.transAxes, fontsize=14,
            verticalalignment='top', bbox=props)

    ax.text(.42, 0.5, '50%', transform=ax.transAxes, fontsize=14,
            verticalalignment='top', bbox=props)

    ax.text(.32, 0.3, '50%', transform=ax.transAxes, fontsize=14,
            verticalalignment='top', bbox=props)

    text = '~100%\nsure\nit is blue'

    ax.text(.07, 0.6, text, transform=ax.transAxes, fontsize=14,
            verticalalignment='top', horizontalalignment='center', bbox=props)

    text = '~100%\nsure\nit is red'

    ax.text(.93, 0.4, text, transform=ax.transAxes, fontsize=14,
            verticalalignment='top', horizontalalignment='center', bbox=props)

    text = '~98%\nsure\nit is red'

    ax.text(.60, 0.32, text, transform=ax.transAxes, fontsize=14,
            verticalalignment='top', horizontalalignment='center', bbox=props)

    text = '~98%\nsure\nit is blue'

    ax.text(.40, 0.82, text, transform=ax.transAxes, fontsize=14,
            verticalalignment='top', horizontalalignment='center', bbox=props)

    plt.show()

def draw_logit_curve(df):
    pca = PCA(n_components=1)
    X = pca.fit_transform(df[df.columns[0:2]])
    lr = LogisticRegression()
    lr.fit(X, df['SPECIES'])
    margin = 5
    space = np.linspace(-margin, margin, 100)
    pd.Series(lr.predict_proba(space.reshape(-1, 1))[:, 1], index=space).plot()
    plt.xlabel('Distance to line')
    plt.ylabel('Probability of being red')
    plt.show()

def get_new_version_of_dataset(df_full):
    df_new = df_full[['SEPAL_WIDTH', 'PETAL_WIDTH', 'SPECIES']].copy()
    df_new = df_new.loc[df_new['SPECIES'].isin([1, 2])]

    df_new['SPECIES'] = df_new['SPECIES'].replace({1: 0, 2: 1})
    df_new['STEM_TENSILE_STRENGTH'] = (df_new['PETAL_WIDTH'] + 1) * 10e8

    return df_new

def gradient_descent_classification_plot(X, Y, iter):
	data = pd.concat((X,Y),axis=1)
	xx, yy = np.mgrid[4:8:.01, 0:2.8:.01]
	grid = np.c_[xx.ravel(), yy.ravel()]
	species = 2
	labels = data.loc[data.SPECIES != species,'SPECIES'].unique()
	clf = LogisticRegression(max_iter=1, warm_start = True, solver = 'sag',random_state=0).fit(data.loc[data.SPECIES != species,['SEPAL_LENGTH','PETAL_WIDTH']],
			data.loc[data.SPECIES != species,'SPECIES'])
	probs = clf.predict_proba(grid)[:, 1].reshape(xx.shape)
	f, ax = plt.subplots(figsize=(8, 6))

	for i in range(iter):
		display.clear_output(wait=True)
		time.sleep(0.1)
		contour = ax.contourf(xx, yy, probs, 25, cmap="RdBu",
		vmin=0, vmax=1)

		ax.scatter(data.loc[data.SPECIES != species,'SEPAL_LENGTH'], data.loc[data.SPECIES != species,'PETAL_WIDTH'],
		c=data.loc[data.SPECIES != species,'SPECIES'],
		s=50,cmap="RdBu", vmin=-.2, vmax=1.2,edgecolor="white", linewidth=1)
		ax.set(aspect="equal",
		       xlim=(4, 8), ylim=(0, 2.8),
		       xlabel="SEPAL_LENGTH", ylabel="PETAL_WIDTH", title="Logistic Regression Classification Between Species %0.1i and %0.1i \n (iteration %0.1i)" % (labels[0], labels[1], i+1))
		if(i==0):
			ax_c = f.colorbar(contour)
			ax_c.set_label("$P(Species = %0.1i)$"%labels[1])
			ax_c.set_ticks([0, .25, .5, .75, 1])
		display.display(plt.gcf());
		clf.fit(data.loc[data.SPECIES != species,['SEPAL_LENGTH','PETAL_WIDTH']],
		data.loc[data.SPECIES != species,'SPECIES'])
		probs = clf.predict_proba(grid)[:, 1].reshape(xx.shape)
	plt.clf();

def plot_maximum_log_likelihood():
	x = np.linspace(0,1,1000)
	cost_one = []
	cost_zero = []
	for i in x:
		cost_one.append(logloss(1,i))
		cost_zero.append(logloss(0,i))
	f, axs = plt.subplots(nrows=1,ncols=2)
	ax = axs[0]
	ax.set(xlabel="$\hat{p}$", ylabel="Maximum Log-Likelihood", title="Maximum Log-Likelihood for y = 0")
	ax.plot(x,cost_zero)
	ax = axs[1]
	ax.set(xlabel="$\hat{p}$", ylabel="Maximum Log-Likelihood", title="Maximum Log-Likelihood for y = 1")
	ax.plot(x,cost_one)
	plt.tight_layout()
	plt.show()

def logloss(true_label, predicted, eps=1e-15):
  p = np.clip(predicted, eps, 1 - eps)
  if true_label == 1:
    return -np.log(p)
  else:
    return -np.log(1 - p)

S01 - Bootcamp and Binary Classification/SLU08 - Classification with Logistic Regression/utils.py


import numpy as np

# ECDF
def ecdf(data):
    """Compute ECDF for a one-dimensional array of measurements."""
    # Number of data points: n
    n = len(data)

    # x-data for the ECDF: x
    x = np.sort(data)

    # y-data for the ECDF: y
    y = np.arange(1, n+1) / n

    return x, y

# Bootstrap Resampling

# bootstrap replicas
def bootstrap_replicate_1d(data, func):
     """Generate bootstrap replicate of 1D data."""
     bs_sample = np.random.choice(data, len(data))
     return func(bs_sample)

# many bootstraps replicas
def draw_bs_reps(data, func, size=1):
    """Draw bootstrap replicates."""

    # Initialize array of replicates: bs_replicates
    bs_replicates = np.empty(size)

    # Generate replicates
    for i in range(size):
        bs_replicates[i] = bootstrap_replicate_1d(data, func)

    return bs_replicates


import numpy as np
import math

def f(func_value, x):
    """
        Defining Function
    """
    if int(func_value) == 1 :
        return (x**3 - 3*x*x - x + 9)
    elif int(func_value) == 2 :
        return ((x**3 - 3*x*x - x + 9)*(math.exp(x)))
    else:
        print("Error Message")

def g(func_value, x):
    """
    Defining derivative of function
    """
    if int(func_value) == 1 :
        return (3*x*x - 6*x - 1)
    elif int(func_value) == 2 :
        return ((x**3 - 3*x*x - x + 9 + (3*x*x - 6*x - 1))*(math.exp(x)))
    else:
        print("Error Message")

def newtonRaphson(func_num,x0,e,N):
    """
        Implementing Newton Raphson Method
    """
    func_value_at_roots = []
    counter = 1
    flag = 1
    condition = True
    while condition:
        if g(func_num,x0) == 0.0:
            print('Divide by zero error!')
            break

        x1 = x0 - f(func_num,x0)/g(func_num,x0)
        #print('Iteration:{}, x1 = {} and f(x1) = {}'.format(counter, x1, f(func_num,x1)))
        func_value_at_roots.append((f(func_num,x1)))
        x0 = x1
        counter = counter + 1

        if counter > N:
            flag = 0
            break

        condition = abs(f(func_num,x1)) > e
    return func_value_at_roots
"""
    if flag==1:
        print('Required root is:{}'.format(x1))
        continue
    else:
        print('Not Convergent.')
"""

AS2101_Labwork/3.Trials-References/Task 3/Submission Codes/Newton-Raphson Method/nrm_plot/nrm.py


#!/usr/bin/python3

import numpy as np
from time import time

EPS = 1e-3

'''
    Sequential Implementation of https://doi.org/10.1007/978-3-319-11194-0_18
'''

def compute_next(mat, vec):
    sigma = np.diag(vec)
    sigma_inv = np.diag(1/vec)
    return np.matmul(np.matmul(sigma_inv, mat), sigma)

def sum_across_rows(mat):
    n = mat.shape[1]
    v = np.array([np.sum(mat[i]) for i in range(n)])
    return v

def stop(vec):
    return all(map(lambda e: e < EPS, [abs(vec[i] - vec[i-1])
                                       for i in range(1, len(vec))]))

def max_eigen_value_and_vector(mat):
    eigen_val = 0
    eigen_vec = np.ones(mat.shape[0])

    itr = 0
    while True:
        vec = sum_across_rows(mat)
        vec_max = np.max(vec)
        eigen_vec = np.array([j * (vec[i]/vec_max)
                              for i, j in enumerate(eigen_vec)])
        if stop(vec):
            eigen_val = vec[0]
            break

        mat = compute_next(mat, vec)
        itr += 1

    return eigen_val, eigen_vec, itr + 1

if __name__ == '__main__':
    # handwritten test begins
    mat = np.array([[1, 1, 2], [2, 1, 3], [2, 3, 5]])
    val, vec, _ = max_eigen_value_and_vector(mat)

    assert abs(val - 7.5311) < EPS
    assert abs(vec[0] - 0.3941) < EPS
    assert abs(vec[1] - 0.5788) < EPS
    assert abs(vec[2] - 0.9975) < EPS
    # handwritten test ends

    print('Sequential Similarity Transform, for finding maximum eigen value ( with vector )\n')
    for dim in range(5, 11):
        mat = np.random.random((1 << dim, 1 << dim))
        start = time() * 1000
        val, _, itr = max_eigen_value_and_vector(mat)
        end = time() * 1000

        assert val - np.max(np.linalg.eigvals(mat)) < EPS
        print(
            f'{1 << dim:<4} x {1 << dim:>4}\t\t{end - start:>6.2f} ms\t\t{itr:>8} round(s)')


import numpy as np
import math

def my_sphere(x):
    x1, x2 = x
    return x1 ** 2 + x2 ** 2

def my_rosenbrock(xy, a=2, b=5):
    x, y = xy
    return (a - x) ** 2 + b * ((y - x ** 2) ** 2)

def my_rastrigin(xy):
    x, y = xy
    return 10 * 2 + (x ** 2 - 10 * np.cos(2 * math.pi * x)) + (y ** 2 - 10 * np.cos(2 * math.pi * y))

def my_booth(xy):
    x, y = xy
    # min(1, 3) = 0
    z = (x + 2*y - 7)**2 + (2*x + y - 5)**2
    return z

def my_matyas(xy):
    x, y = xy
    # min(0, 0) = 0
    z = 0.26*(x**2 + y**2) - 0.48*x*y
    return z

def my_ackley(xy):
    x, y = xy
    # min(0, 0) = 0
    z = -20*np.exp(np.fabs(-0.2*np.sqrt(0.5*(x**2 + y**2)))) - np.exp(np.fabs(0.5*(np.cos(2*x*math.pi) + np.cos(2*y*math.pi)))) + math.e + 20
    return -z

def my_levi13(xy):
    x, y = xy
    # min(1, 1) = 0
    z = (np.sin(3*x*math.pi))**2 + ((x - 1)**2)*(1 + np.sin(3*y*math.pi)**2) + ((y - 1)**2)*(1 + np.sin(2*y*math.pi)**2)
    return z

def my_himmelblau(xy):
    x, y = xy
    # min(3, 2) = 0
    # min(-2.805118, 3.131312) = 0
    # min(-3.779310, -3.283186) = 0
    # min(3.584428, -1.848126) = 0
    z = (x**2 + y - 11)**2 + (x + y**2 - 7)**2
    return z

def my_beale(xy):
    x, y = xy
    # min(3, 0.5) = 0
    z = (1.5 - x + x*y)**2 + (2.25 - x + x*(y**2))**2 + (2.625 - x + x*(y**3))**2
    return z

def my_goldstein_price(xy):
    x, y = xy
    # min(0, -1) = 3
    z = ((1 + (x + y + 1)**2 * (19 - 14*x + 3*(x**2) - 14*y + 6*x*y + 3*(y**2))) * (30 + (2*x - 3*y)**2 * (18 - 32*x + 12*(x**2) + 48*y - 36*x*y + 27*(y**2))))
    return z


import numpy as np

"""[Recreates the adjacency matrix with which the steady state probabilities get multiplied iteratively
    The adjacency matrix A of a set of pages (nodes) defines the linking structure]

Returns:
    [numpy Matrix] -- [The matrix with which the steady state probabilities will get multipled]
"""
def recreate_adjacency_matrix(marchov_chain):
    # since the size of the matrix is n x n getting the size of the row is enough
    num_urls = marchov_chain.shape[0]

    # the probabilty of visiting a url, which is the 1 / total number of urls
    probability = 1 / num_urls

    probability_matrix = np.zeros((num_urls, num_urls))

    # probability_matrix will contain all similar values which is the probability of visiting a particular page
    probability_matrix[:] = probability

    '''
        In assigning a PageRank score to each node of the web graph, we use the teleport operation in two ways:
        (1) When at a node with no out-links, the surfer invokes the teleport operation.
        (2) At any node that has outgoing links, the surfer invokes the teleport operation with a probability of alpha.
        Typical value of alpha is 0.1
    '''
    alpha = 0.1

    adjacency_matrix = alpha * marchov_chain + ((1 - alpha) * probability_matrix)

    return adjacency_matrix

"""[Computes the page rank of a given marchov chain url]

Returns:
    [list] -- [A list of n page rank score where n is the total number of urls]
"""
def compute_page_rank(marchov_chain):
    # create the adjacency matrix with which the steady state probability will get multiplied iteratively
    adjacency_matrix = recreate_adjacency_matrix(marchov_chain)

    num_urls = adjacency_matrix.shape[0]

    # initial vector for the steady state probabilities will always be <1, 0, 0.....n>
    steady_state_probabilities = np.zeros((1, num_urls))
    # this creates a vector of <1, 0, 0...n>
    steady_state_probabilities[0][0] = 1

    # the steady_state probabilities always needs to be transposed
    steady_state_probabilities = np.transpose(steady_state_probabilities)

    previous_state_probabilities = steady_state_probabilities
    while True:
        steady_state_probabilities = adjacency_matrix * steady_state_probabilities

        # we stop when the values have converged and no longer change over the iterations
        if (previous_state_probabilities == steady_state_probabilities).all():
            # if the values converge then the steady_state_probabilities are returned which is the page rank score of the urls
            return steady_state_probabilities

        # otherwise the current steady state probabilities becomes the previous steady state probabilities as we are about to begin another iterations
        previous_state_probabilities = steady_state_probabilities

if __name__ == '__main__':

    '''
        NOTE - This marchov chain is transpose of the modified adjacency matrix of the graph.
        In an adjacency matrix the rows represent an individual url in the graph and columns
        represent the urls that the graph has already visited. This adjacency matrix will be
        transposed and modified to recreate the adjacency matrix. If a url has visited other urls then
        the columns will get replaced by 1 / n, where n is the total number of urls visited by that url.
        The matrix will essentially be a transpose of the adjacency matrix with the columns divided by total non zero entries.
    '''
    marchov_chain = np.matrix([[0, 0, 1],
            [1, 0.5, 0],
            [0, 0.5, 0]])

    page_rank = compute_page_rank(marchov_chain)

    print(page_rank)

    print(np.sum(page_rank))


from math import pi
from math import sqrt

import numpy as np

# Configure Parameters

XMIN = -20.0
XMAX = 20.0
RES = 256
STEP_SIZE = 0.05 * -1j  # imaginary time
TIMESTEPS = 100

# Init Variables

DX = 2 * XMAX / RES
X = np.arange(XMIN + (XMAX / RES), XMAX, DX)
DK = pi / XMAX
K_I = np.concatenate((np.arange(0, RES / 2), np.arange(-RES / 2, 0))) * DK

# Configure Operators

V = 0.5 * X ** 2
WFC = np.exp(-((X + 1) ** 2) / 2, dtype=complex)

# Init Operators

K = np.exp(-0.5 * (K_I ** 2) * STEP_SIZE * 1j, dtype=complex)
R = np.exp(-0.5 * V * STEP_SIZE * 1j, dtype=complex)

# Split-operator fourier method
for i in range(TIMESTEPS):
    # Half-step in real space
    WFC *= R

    # FFT to momentum space
    WFC = np.fft.fft(WFC)

    # Full step in momentum space
    WFC *= K

    # iFFT back to real space
    WFC = np.fft.ifft(WFC)

    # Half-step in real space
    WFC *= R

    # Density for plotting and potential
    density = np.abs(WFC) ** 2

    # Normalize for imaginary time
    if (np.iscomplex(STEP_SIZE)):
        factor = sum(density) * DX
        WFC /= sqrt(factor)

    # Outputting data to file. Plotting can also be done in a
    # similar way. This is set to output exactly 100 files, no
    # matter how many timesteps were specified.
    if (i % (TIMESTEPS // 100) == 0):
        filename = "output/output{}.dat".format(str(i).zfill(5))
        with open(filename, "w") as outfile:
            # Outputting for gnuplot. Any plotter will do.
            for j in range(len(density)):
                line = "{}\t{}\t{}\n".format(
                    X[j], density[j].real, V[j].real)
                outfile.write(line)
        print("Outputting step: ", i + 1)

# Calculate the energy < Psi | H | Psi >
# Creating real, momentum, and conjugate wavefunctions.
WFC_R = WFC
WFC_K = np.fft.fft(WFC_R)
WFC_C = np.conj(WFC_R)

# Finding the momentum and real-space energy terms
energy_k = 0.5 * WFC_C * np.fft.ifft((K_I ** 2) * WFC_K)
energy_r = WFC_C * V * WFC_R

# Integrating over all space
energy_final = sum(energy_k + energy_r).real

print('Final energy: ', energy_final * DX)


"""
This script contains routines for computation in Eulidean geometry.
"""

import numpy as np

def circle_approx_pts(centre, radius, num, from_to=[0,360]):
    """
    SUMMARY
        estimates a circle with evenly spaced points sampled on it
        sets current mode

    PARAMETERS
        centre: list of x coordinate and y coordinate
        radius: radius of circle
        num: number of samples
        from_to: with from_to we can set at what angle it starts and what angle it ends
            [0, 360] for full circle

    RETURNS
        [(float, float)]
    """
    centre = np.array(centre)
    if from_to[1] < from_to[0]:
        myrange = range(from_to[1], from_to[0]+360, (from_to[0]+360-from_to[1])//num)
    else:
        myrange = range(from_to[0], from_to[1], (from_to[1]-from_to[0])//num)

    return_pts = []
    for angle in myrange:
        return_pts.append(centre+radius*np.array([np.cos(np.radians(angle)), np.sin(np.radians(angle))]))

    return return_pts

def circumcentre(A,B,C):
    """
    SUMMARY
        computes the centre of the circumscribed circle

    PARAMETERS
        A: coordinates of vertex A
        B: coordinates of vertex B
        C: coordinates of vertex C

    RETURNS
        (float, float)
    """
    D = 2 * (A[0]*(B[1]-C[1]) + B[0]*(C[1]-A[1]) + C[0]*(A[1]-B[1]))
    K_x_A = (A[0]*A[0] + A[1]*A[1]) * (B[1]-C[1])
    K_x_B = (B[0]*B[0] + B[1]*B[1]) * (C[1]-A[1])
    K_x_C = (C[0]*C[0] + C[1]*C[1]) * (A[1]-B[1])
    K_x = (K_x_A + K_x_B + K_x_C) / D

    K_y_A = (A[0]*A[0] + A[1]*A[1]) * (C[0]-B[0])
    K_y_B = (B[0]*B[0] + B[1]*B[1]) * (A[0]-C[0])
    K_y_C = (C[0]*C[0] + C[1]*C[1]) * (B[0]-A[0])
    K_y = (K_y_A + K_y_B + K_y_C) / D

    return K_x, K_y

def circumradius(A, centre):
    """
    SUMMARY
        computes the radius of the circumscribed circle given a vertex

    PARAMETERS
        A: coordinates of vertex A
        centre: coordinates of the centre of the circle

    RETURNS
        float
    """
    return np.linalg.norm(np.array(A)-np.array(centre))

def circum_centre_and_radius(A,B,C):
    """
    SUMMARY
        combines circumcentre(A,B,C) and circumradius(A, centre) to compute both

    PARAMETERS
        A: coordinates of vertex A
        B: coordinates of vertex B
        C: coordinates of vertex C

    RETURNS
        ([float, float], float)
    """
    centre = circumcentre(A,B,C)
    radius = circumradius(A,centre)
    return centre, radius

def incentre(A,B,C):
    """
    SUMMARY
        computes the centre of the inscribed circle of a triangle

    PARAMETERS
        A: coordinates of vertex A
        B: coordinates of vertex B
        C: coordinates of vertex C

    RETURNS
        [float, float]
    """
    A = np.array(A)
    B = np.array(B)
    C = np.array(C)
    a = np.linalg.norm(C-B)
    b = np.linalg.norm(A-C)
    c = np.linalg.norm(B-A)

    I_x = (a*A[0]+b*B[0]+c*C[0]) / (a+b+c)
    I_y = (a*A[1]+b*B[1]+c*C[1]) / (a+b+c)
    return I_x, I_y

def inradius(A,B,C):
    """
    SUMMARY
        computes the radius of the circumscribed circle (uses Heron's formula)

    PARAMETERS
        A: coordinates of vertex A
        B: coordinates of vertex B
        C: coordinates of vertex C

    RETURNS
        float
    """
    a = np.linalg.norm(np.array(B)-np.array(C))
    b = np.linalg.norm(np.array(C)-np.array(A))
    c = np.linalg.norm(np.array(A)-np.array(B))
    s = (a+b+c)/2
    return np.sqrt(s*(s-a)*(s-b)*(s-c)) / s

def in_centre_and_radius(A,B,C):
    """
    SUMMARY
        combines circumcentre(A,B,C) and circumradius(A, centre) to compute both

    PARAMETERS
        A: coordinates of vertex A
        B: coordinates of vertex B
        C: coordinates of vertex C

    RETURNS
        ([float, float], float)
    """
    centre = incentre(A,B,C)
    radius = inradius(A,B,C)
    return centre, radius

def ll_intersection(A,B,P,Q):
    """
    SUMMARY
        computes the coordinates of the intersection of segment AB and segment PQ,
        (beware: when the segments are parallel the denominators are 0)

    PARAMETERS
        A: coordinates of vertex A
        B: coordinates of vertex B
        P: coordinates of vertex P
        Q: coordinates of vertex Q

    RETURNS
        ([float, float], float)
    """
    denominator = (A[0]-B[0]) * (P[1]-Q[1]) - (A[1]-B[1]) * (P[0]-Q[0])
    if denominator == 0:
        return (DEFAULT,DEFAULT)
    numerator_x = (A[0]*B[1]-B[0]*A[1]) * (P[0]-Q[0]) - (A[0]-B[0]) * (P[0]*Q[1]-Q[0]*P[1])
    numerator_y = (A[0]*B[1]-B[0]*A[1]) * (P[1]-Q[1]) - (A[1]-B[1]) * (P[0]*Q[1]-Q[0]*P[1])

    return numerator_x/denominator, numerator_y/denominator

# credit: https://stackoverflow.com/questions/55816902/finding-the-intersection-of-two-circles
def cc_intersection(O0, r0, O1, r1):
    """
    SUMMARY
        computes the intersection points of two circles
        (note that there can be two intersection points, or one, or none)

    PARAMETERS
        O0: centre of circle 0
        r0: radius of circle 0
        O1: centre of circle 1
        r1: radius of circle 1

    RETURNS
        [float, float]
    """
    # circle 1: (x0, y0), radius r0
    # circle 2: (x1, y1), radius r1
    x0, y0 = O0
    x1, y1 = O1

    d=np.sqrt((x1-x0)**2 + (y1-y0)**2)

    # non intersecting
    if d > r0 + r1 :
        return None
    # one circle within other
    if d < abs(r0-r1):
        return None
    # coincident circles
    if d == 0 and r0 == r1:
        return None
    else:
        a=(r0**2-r1**2+d**2)/(2*d)
        h=np.sqrt(r0**2-a**2)
        x2=x0+a*(x1-x0)/d
        y2=y0+a*(y1-y0)/d
        x3=x2+h*(y1-y0)/d
        y3=y2-h*(x1-x0)/d

        x4=x2-h*(y1-y0)/d
        y4=y2+h*(x1-x0)/d

        return (x3, y3, x4, y4)

def lc_intersection(O, r, A, B):
    """
    SUMMARY
        computes the intersection points of the circle (O,r) and segment AB
        (warning: the number of intersection points may be 0, 1, or 2)
    PARAMETERS
        O: centre of circle
        r: radius of circle
        A: endpoint of segment AB
        B: other endpoint of segment AB

    RETURNS
        [float, float]
    """
    sign = lambda x : 1 if x >= 0 else -1
    O_ = np.array(O)
    A_ = np.array(A)
    B_ = np.array(B)
    dx, dy = B_ - A_
    dr = np.sqrt(dx*dx + dy*dy)
    D = np.cross(A_-O_, B_-O_)
    discriminant = r*r*dr*dr-D*D

    if discriminant > 0:
        x1 = (D*dy+sign(dy)*dx*np.sqrt(discriminant)) / (dr*dr) + O_[0]
        y1 = (-D*dx+np.abs(dy)*np.sqrt(discriminant)) / (dr*dr) + O_[1]
        x2 = (D*dy-sign(dy)*dx*np.sqrt(discriminant)) / (dr*dr) + O_[0]
        y2 = (-D*dx-np.abs(dy)*np.sqrt(discriminant)) / (dr*dr) + O_[1]
        if np.cross(A_-O_,A_-B_) <= 0:
            if sign(dy) == 1:
                return [[x1,y1], [x2,y2]], True
            else:
                return [[x2,y2], [x1,y1]], True
        else:
            if sign(dy) == 1:
                return [[x1,y1], [x2,y2]], False
            else:
                return [[x2,y2], [x1,y1]], False
    elif discriminant == 0:
        x = D*dy / (dr*dr)
        y = -D*dx / (dr*dr)
        return [[x,y], [x,y]], False
    else:
        return [[0,0],[0,0]], False

# https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
def pt_segment_dist(A, B, P):
    """
    SUMMARY
        computes the distance of point P from segment AB
    PARAMETERS
        A: endpoint of segment AB
        B: other endpoint of segment AB
        P: point at some distance from AB

    RETURNS
        float
    """
    x1, y1 = A
    x2, y2 = B
    x3, y3 = P
    px = x2-x1
    py = y2-y1
    norm = px*px + py*py
    u =  ((x3 - x1) * px + (y3 - y1) * py) / float(norm)

    if u > 1:
        u = 1
    elif u < 0:
        u = 0

    x = x1 + u * px
    y = y1 + u * py
    dx = x - x3
    dy = y - y3

    dist = (dx*dx + dy*dy)**.5
    return dist

def pt_circle_dist(O, r, P):
    """
    SUMMARY
        computes the distance of point circle (O, r) from P

    PARAMETERS
        O: coordinates of the circle centre O
        r: radius of the circle
        P: point at some distance from the circle

    RETURNS
        float
    """
    return abs(np.linalg.norm(np.array(O)-np.array(P))-r)

def orthogonal_projection(A, B, P):
    """
    SUMMARY
        computes the orthogonal projection of P on AB

    PARAMETERS
        A: endpoint of segment AB
        B: other endpoint of segment AB
        P: point at some distance from AB

    RETURNS
        [float, float]
    """
    A_ = np.array(A)
    B_ = np.array(B)
    P_ = np.array(P)
    x = np.linalg.norm(P_ - A_) * (P_ - A_).dot(B_ - A_) / (np.linalg.norm(P_ - A_) * np.linalg.norm(B_ - A_))
    return A_ + (B_- A_) / np.linalg.norm(A_ - B_) * x

def bisector_point(A,B,C):
    """
    SUMMARY
        computes a point which lies on the bisector of the angle

        In order to get the exact distance from the angle point we follow the
        construction method of tkz-euclide.
        1. copy the first segment (A,B) on the second segment (B,C) to get (P)
        the result is the third coordinate of the equilateral triangle formed by AP.

    PARAMETERS
        A: point
        B: point where the angle is
        P: third point

    RETURNS
        [float, float]
    """
    A = np.array(A)
    B = np.array(B)
    C = np.array(C)
    P = B + np.linalg.norm(B-A) * (C - B) / np.linalg.norm(C-B)
    rotation_matrix = np.array([[np.cos(np.radians(60)), -np.sin(np.radians(60))],\
                               [np.sin(np.radians(60)), np.cos(np.radians(60))]])
    Q = A.reshape(2,1) + rotation_matrix @ (P-A).reshape(2,1)
    return Q.flatten()


'''
03 - Baseball player's BMI

The MLB also offers to let you analyze their weight data. Again, both are available as regular
Python lists: height and weight. height is in inches and weight is in pounds.

It's now possible to calculate the BMI of each baseball player. Python code to convert height to
a numpy array with the correct units is already available in the workspace. Follow the instructions
step by step and finish the game!

Instructions:

 - Create a numpy array from the weight list with the correct units. Multiply by 0.453592 to go from
   pounds to kilograms. Store the resulting numpy array as np_weight_kg.

 -  Use np_height_m and np_weight_kg to calculate the BMI of each player. Use the following equation:
    BMI=weight(kg)/height(m)2

 - Save the resulting numpy array as bmi.

 - Print out bmi.
'''
# height and weight are available as a regular lists

# Import numpy
import numpy as np

# Calculate the BMI: bmi
np_height_m = np.array(height) * 0.0254
np_weight_kg = np.array(weight) * 0.453592
bmi = np_weight_kg / np_height_m ** 2

# Create the light array
light = np.array(bmi < 21)

# Print out light
print(light)

# Print out BMIs of all baseball players whose BMI is below 21
print(bmi[light])