#!/usr/bin/env python
# coding: utf-8

# #   Creación de Complex class en Python
#  John Erick Cabrera Ramirez

# ## Ejemplo inicial:

# In[1]:

import numpy as np
# import inspect
# import threading
# from threading import Thread

# In[2]:

class Persona:
    def __init__(self,nombre,apellido):#Definimos atributos de la clase
        self.nombre=nombre
        self.apellido=apellido
    def saludo(self): #una función para los objetos de la clase
        print(f"Bienvenido sr. {self.nombre} {self.apellido}")

# In[3]:

p1=Persona("John","Cabrera")
print(p1.nombre)
print(p1.apellido)
p1.saludo()

# In[4]:

class Person:
  def __init__(self, name, age):
    self.name = name
    self.age = age

  def myfunc(self):
    print("Hello my name is " + self.name)

p1 = Person("John", 36)
p1.myfunc()

# ## Ejemplo 2: Clase que define los números complejos

# ### Creación de la clase "Compleja":

# In[5]:

class Compleja:
    #Atributos de un número complejo: Partes real e imaginaria
    def __init__(self, real, imaginaria):
        self.real = real
        self.imaginaria = imaginaria
    #definición de la suma, retorna un objeto complejo
    def suma(self,other,*args):
        a=self.real+other.real
        b=self.imaginaria+other.imaginaria
        return Compleja(a,b)
    #definición de la resta, retorna un objeto complejo
    def resta(self,other,*args):
        a=self.real-other.real
        b=self.imaginaria-other.imaginaria
        return Compleja(a,b)
    #definición del producto, retorna un objeto complejo
    #ver https://mathworld.wolfram.com/ComplexMultiplication.html
    def multiplicacion(self,other,*args):
        a=self.real*other.real-self.imaginaria*other.imaginaria
        b=self.real*other.imaginaria+self.imaginaria*other.real
        return Compleja(a,b)
    #módulo o norma de un número complejo
    def modulo(self,*args):
        return np.sqrt(self.real**2+self.imaginaria**2)
    #definición del cociente, retorna un objeto complejo
    #se calcula a partir de la norma al cuadrado del numero en el denominador
    #ver https://mathworld.wolfram.com/ComplexDivision.html
    def division(self,other,*args):
        a=(self.real*other.real+self.imaginaria*other.imaginaria)/(other.modulo()**2)
        b=(-self.real*other.imaginaria+self.imaginaria*other.real)/(other.modulo()**2)
        return Compleja(a,b)
    #Ingresa un objeto de la clase compleja
    #y retorna un string del número complejo escrito en la forma:
    #a+bi
    def imprimir(self,*args):
        if(self.imaginaria>=0):
            return f"{self.real}+{self.imaginaria}i"
        else:
            return f"{self.real}{self.imaginaria}i"

# ### Llamado e impresión de todas las funciones/métodos. Se hace con este código "misterioso", para poder hacerlo sobre el programa de cualquiera de los estudiantes.
#  Tomado de https://stackoverflow.com/questions/37075680/run-all-functions-in-class

# In[6]:

z1=Compleja(1,2)
z2=Compleja(3,4)
# z1=Compleja(3,-7)
# z2=Compleja(1,2)
z3=Compleja(4,5)
print('z1=',z1.imprimir())
print('z2=',z2.imprimir())
print('z3=',z3.imprimir())

# In[7]:

#dir(objeto)  proporciona todos los métodos de la clase.
#getattr(objeto,metodo) evalúa el objeto en el método/function
method_names = [method for method in dir(z1)
                if callable(getattr(z1, method))
                if not method.startswith('_')]
#En algunos casos los métodos proporcionados por el docente o estudiantes tienen como salida un string o float
#Se llama a la función según cada caso.
for method in method_names:
    if (type(getattr(z1, method)(z2))!=str)and(type(getattr(z1, method)(z2))!=np.float64):
        print(f"z1 {method} z2=",(getattr(z1, method)(z2)).imprimir())# call
    else:
        print(f"{method} z1=",getattr(z1, method)(z2))

# In[8]:

method_names = [method for method in dir(z1)
                if callable(getattr(z1, method))
                if not method.startswith('_')]  # 'private' methods start from _
for method in method_names:
    if (type(getattr(z1, method)(z3))!=str)and(type(getattr(z1, method)(z3))!=np.float64):
        print(f"z1 {method} z3=",(getattr(z1, method)(z3)).imprimir())# call
    else:
        print(f"{method} z1=",getattr(z1, method)(z3))

# ## Anexo
# Algunas de las posibles notas para la sección 1

# In[9]:

nota=0
cnt=0
while(nota<=50):
    print(cnt,nota)
    cnt+=1;
    if(nota<30):
        nota+=7.5
    else:
        nota+=5

# Algunas de las posibles notas para la sección 2

# In[10]:

nota=0
cnt=0
while(nota<=50):
    print(cnt,nota)
    cnt+=1;
    if(nota<30):
        nota+=6
    else:
        print("-")
        nota+=4

# In[ ]:


import numpy as np
import modern_robotics as mr
import math

print("Ex 1: Use Newton-Raphso iterative numerical root finding to perform two steps:")

def f_x_y(theta):
    return np.array([theta[0]**2-9, theta[1]**2-4])

def jacobian(theta):
    return np.array([[2*theta[0], 0], [0, 2*theta[1]]])

count = 0
theta = [1, 1] # initial guess, theta_0
while count < 2:
    count += 1
    theta -= np.dot(np.linalg.inv(jacobian(theta)), f_x_y(theta))

theta = np.around(theta, decimals=2)
print(theta)
# [3.4,2.05]

print("Ex 2: Give theta_d:")
Bs = np.array([[0, 0, 0],
               [0, 0, 0],
               [1, 1, 1],
               [0, 0, 0],
               [3, 2, 1],
               [0, 0, 0]])
M = np.array([[1, 0, 0, 3],
              [0, 1, 0, 0],
              [0, 0, 1, 0],
              [0, 0, 0, 1]])
T_sd = np.array([[-0.585, -0.811, 0, 0.076],
                 [ 0.811, -0.585, 0, 2.608],
                 [     0,      0, 1,     0],
                 [     0,      0, 0,     1]])
theta_0 = [math.pi/4, math.pi/4, math.pi/4]
e_w = 0.001 # omega
e_v = 0.0001

(theta, success) = mr.IKinBody(Bs, M, T_sd, theta_0, e_w, e_v)

if success:
    theta = np.around(theta, decimals=2)
    print(theta)
else:
    raise ValueError("Failed to converge")
# [0.93,0.59,0.68]


import numpy as np
from tqdm import tqdm
import scipy.spatial.distance  # type: ignore

def k_center_greedy(X, s, b):
    '''
    Args
    - X: np.array, shape [n, d]
    - s: list of int, indices of X that have already been selected
    - b: int, new selection budget

    Returns: np.array, shape [b], type int64, newly selected indices
    '''
    n = X.shape[0]
    p = np.setdiff1d(np.arange(n), s, assume_unique=True)  # pool indices
    sel = np.empty(b, dtype=np.int64)

    sl = len(s)
    D = np.zeros([sl + b, len(p)], dtype=np.float32)
    D[:sl] = scipy.spatial.distance.cdist(X[s], X[p], metric='euclidean')  # shape (|s|,|p|)
    mins = np.min(D[:sl], axis=0)  # vector of length |p|
    cols = np.ones(len(p), dtype=bool)  # columns still in use

    for i in tqdm(range(b), desc="Greedy k-Centers"):
        j = np.argmax(mins)
        u = p[j]
        sel[i] = u

        if i == b - 1:
            break

        mins[j] = -1
        cols[j] = False

        # compute dist between selected point and remaining pool points
        r = sl + i + 1
        D[r, cols] = scipy.spatial.distance.cdist(X[u:u+1], X[p[cols]])[0]
        mins = np.minimum(mins, D[r])

    return sel

def k_center_greedy_slow(X, s, b):
    '''
    Args
    - X: np.array, shape [n, d]
    - s: list of int, indices of X representing the existing pool
    - b: int, selection budget

    Returns: np.array, shape [b], type int64, newly selected indices
    '''
    n = X.shape[0]
    p = np.setdiff1d(np.arange(n), s, assume_unique=True).tolist()  # pool indices
    sel = list(s)

    for i in range(b):
        D = scipy.spatial.distance.cdist(X[sel], X[p], metric='euclidean')  # shape (|s|,|p|)
        j = np.argmax(np.min(D, axis=0))
        u = p[j]
        sel.append(u)
        p.pop(j)

    return np.asarray(sel[-b:])

if __name__ == '__main__':
    import time
    for i in range(10):
        n, d = np.random.randint(10, 1000, size=2)
        X = np.random.randn(n, d)
        s0_size = np.random.randint(1, int(n/2))
        s = np.random.choice(n, size=s0_size)
        b = np.random.randint(1, int((n - s0_size) / 2))
        start = time.time()
        fast = k_center_greedy(X, s, b)
        fast_time = time.time() - start
        start = time.time()
        slow = k_center_greedy_slow(X, s, b)
        slow_time = time.time() - start
        assert np.all(fast == slow)
        print(f'{i}: (n={n}, d={d}, b={b}), fast {fast_time:.2f}, slow {slow_time:.2f}')


import cv2
import numpy as np
from matplotlib import pyplot as plt
import math

underexposed = cv2.imread('images/underexposed.jpg')

plt.figure(1)
plt.title('underexposed image')
plt.imshow(underexposed[:, :, ::-1])
plt.show()

r = underexposed[:, :, 2]
g = underexposed[:, :, 1]
b = underexposed[:, :, 0]

# a)
print("Red Channel mean:", np.mean(r), ", standard deviation:", np.std(r))
print("Green Channel mean:", np.mean(g), ", standard deviation:", np.std(g))
print("Blue Channel mean:", np.mean(b), ", standard deviation:", np.std(b))

# b)
r1 = np.ravel(r)
g1 = np.ravel(g)
b1 = np.ravel(b)

## Calculate Histograms
(rHist, bin) = np.histogram(r1, np.arange(257))
(gHist, bin) = np.histogram(g1, np.arange(257))
(bHist, bin) = np.histogram(b1, np.arange(257))

## Calculate PDF
(rPDF, bin) = np.histogram(r1, np.arange(257), density=True)
(gPDF, bin) = np.histogram(g1, np.arange(257), density=True)
(bPDF, bin) = np.histogram(b1, np.arange(257), density=True)

plt.figure(2)

plt.subplot(311)
plt.plot(rHist, 'r')
plt.axis([-10, 256, -500, 15000])
plt.title("histograms of the underexposed image")

plt.subplot(312)
plt.plot(gHist, 'g')
plt.axis([-10, 256, -500, 15000])

plt.subplot(313)
plt.plot(bHist, 'b')
plt.axis([-10, 256, -500, 15000])

plt.show()

# c)
rCDF = np.cumsum(rPDF)
gCDF = np.cumsum(gPDF)
bCDF = np.cumsum(bPDF)

plt.figure(3)

plt.subplot(311)
plt.plot(rCDF, 'r')
plt.axis([-10, 256, 0, 1])
plt.title("cdfs of the underexposed image")

plt.subplot(312)
plt.plot(gCDF, 'g')
plt.axis([-10, 256, 0, 1])

plt.subplot(313)
plt.plot(bCDF, 'b')
plt.axis([-10, 256, 0, 1])

plt.show()

# d)
# If the CDF is our LUT, then the histogram
# gets equalized. We need to multiply it by 255
# to preserve its intensity values.
def equalize(image):
    width = np.size(image, axis=0)
    height = np.size(image, axis=1)
    # get the PDF for red
    (rPDF, bin) = np.histogram(np.ravel(image[:, :, 2]), np.arange(257), density=True)
    # convert PDF to CDF and use it as LUT
    rLUT = 255 * np.cumsum(rPDF)
    for x in range(width):
        for y in range(height):
            image[x, y, 2] = rLUT[math.floor(image[x, y, 2])]

    # apply the same thing to the other channels
    (gPDF, bin) = np.histogram(np.ravel(image[:, :, 1]), np.arange(257), density=True)
    gLUT = 255 * np.cumsum(gPDF)
    for x in range(width):
        for y in range(height):
            image[x, y, 1] = gLUT[math.floor(image[x, y, 1])]

    (bPDF, bin) = np.histogram(np.ravel(image[:, :, 0]), np.arange(257), density=True)
    bLUT = 255 * np.cumsum(bPDF)
    for x in range(width):
        for y in range(height):
            image[x, y, 0] = bLUT[math.floor(image[x, y, 0])]

    return image

plt.figure(4)
plt.title('Brightness Equalized Image')
plt.imshow(equalize(underexposed[:, :, ::-1]))
plt.show()

## Calculate Histograms
(rHist, bin) = np.histogram(np.ravel(underexposed[:, :, 2]), np.arange(257))
(gHist, bin) = np.histogram(np.ravel(underexposed[:, :, 1]), np.arange(257))
(bHist, bin) = np.histogram(np.ravel(underexposed[:, :, 0]), np.arange(257))

plt.figure(5)

plt.subplot(311)
plt.plot(rHist, 'r')
plt.axis([-10, 256, 0, 15000])
plt.title("histograms of the equalized image")

plt.subplot(312)
plt.plot(gHist, 'g')
plt.axis([-10, 256, 0, 15000])

plt.subplot(313)
plt.plot(bHist, 'b')
plt.axis([-10, 256, 0, 15000])

plt.show()

# PDF is almost flattened, the image gets more illuminated
print("For illuminated image :")

# Red Channel mean: 130.593717228 , standard deviation: 69.6745152191
print("Red Channel mean:", np.mean(r), ", standard deviation:", np.std(r))
# Green Channel mean: 128.879831461 , standard deviation: 71.8700792653
print("Green Channel mean:", np.mean(g), ", standard deviation:", np.std(g))
# Blue Channel mean: 129.821797753 , standard deviation: 70.3049839773
print("Blue Channel mean:", np.mean(b), ", standard deviation:", np.std(b))


import numpy as np

### 1
def fib_matrix(n):
    for i in range(n):
        res = pow((np.matrix([[1, 1], [1, 0]], dtype='int64')), i) * np.matrix([[1], [0]])
        print(int(res[0][0]))

# 调用
fib_matrix(100)

### 2
# 使用矩阵计算斐波那契数列
def Fibonacci_Matrix_tool(n):
    Matrix = np.matrix("1 1;1 0", dtype='int64')
    # 返回是matrix类型
    return np.linalg.matrix_power(Matrix, n)

def Fibonacci_Matrix(n):
    result_list = []
    for i in range(0, n):
        result_list.append(np.array(Fibonacci_Matrix_tool(i))[0][0])
    return result_list

# 调用
Fibonacci_Matrix(100)

### pow 速度 比 双**号快, np.linalg.matrix_power也是一种方法


from __future__ import division
import numpy
import matplotlib.pyplot as pyplot
import matplotlib.colors

USER = "Maciej Grabias"
USER_ID = "njgh39"

# starting points, x&y ranges and different gammas:
x00 = 0.3
y00 = 1.0
r0 = numpy.array((x00,y00))
gammas = numpy.array((0.004, 0.001, 0.0001, 0.00001, 0.000001))
x0, x1 = -0.2, 1.2
y0, y1 = -0.2, 1.2

N_points = 1000
dx = (x1-x0)/N_points
dy= (y1-y0)/N_points

N_iterations = 100000

# define functions
def f(x,y):
    return (1-x)**2 + 100 * (y - x**2)**2

def grad((x,y)):
    df_dx = 2 * (200 * x**3 - 200 * x * y + x - 1)
    df_dy = 200 * (y - x**2)
    grad = numpy.array((df_dx, df_dy))
    return grad

def grad_descent((x, y), gamma):
    r = numpy.array((x,y))
    trajectory = numpy.zeros((N_iterations,2))
    for i in range(N_iterations):
        trajectory[i] = r
        r = r - gamma * grad(r)
    return trajectory

# ranges for graph
x_axis = numpy.arange(x0, x1, dx)
y_axis = numpy.arange(y0, y1, dy)

dat = numpy.zeros((len(y_axis), len(x_axis)))

for ix, x in enumerate(x_axis):
    for iy, y in enumerate(y_axis):
        dat[ix, iy] = f(x,y)

pyplot.figure()
im = pyplot.imshow( numpy.transpose(dat),  extent=(x0, x1, y0, y1),
                    origin='lower',
                    cmap = matplotlib.cm.gray,
                    norm=matplotlib.colors.LogNorm(vmin=0.01, vmax=200))

colours = ['blue', 'red', 'green', 'yellow', 'orange']
labels = ['$\gamma = 0.004$', '$\gamma = 0.001$', '$\gamma = 0.0001$',
            '$\gamma = 0.00001$', '$\gamma = 0.000001$']
value = numpy.zeros((5,2))
z=0
for gamma in gammas:
    path = grad_descent(r0, gamma)
    pyplot.plot(path[:,0], path[:,1], color=colours[z], label=labels[z])
    value[z,:] = path[-1]
    z = z+1
xmin, ymin = value[1,0], value[1,1]

pyplot.legend(loc = 'lower left')
pyplot.title("GD Method for Determining the Minima of Rosenbrock's \n"
            "Banana Function for Various Step Sizes $\gamma$")
pyplot.xlim(-0.2, 1.2)
pyplot.ylim(-0.2, 1.2)
pyplot.xlabel("x")
pyplot.ylabel('y')
pyplot.colorbar(im, orientation="vertical",
                label ="$f(x,y)$")
pyplot.show()

ANSWER1 = """ When the step size parameter, gamma, is too big, say of 4x10^-3,
GD method doesn't work as the 'downhill' line converges in some 'odd' place
in a rather hectic manner (blue line on the graph). The appropriate value of
gamma that should be used is of about 10^-3 (red line on the graph) - the
'downhill' line converges at (1.0, 1.0) as expected. For smaller gammas,
ie. <= 10^-4 the 'downhill' line does not precisely converge to the desired
(1.0, 1.0) (green, yellow and orange lines on the graph) for the number
of iterations used (I chose that number to be of N = 100,000). """

ANSWER2 = 'Minima occurs at %.2f, %.2f' % (xmin, ymin)


"""
An implementation of elementary cellular automaton with adjustable neighbourhood and number of possible states.
Inspired by https://matplotlib.org/matplotblog/posts/elementary-cellular-automata/.
"""

import numpy as np, matplotlib.pyplot as plt, sys

# Default values for a number of states and number of neighbours
BASE, NNEIGH = 3, 1

def rule_index(multiplet: list, base: int):
    """Computes an index assigned by a rule."""
    return int((base**3-1)-np.sum([base**(len(multiplet)-1-i)*x for (i,x) in enumerate(multiplet)]))

def cell_automaton(initial_state: list, n_itr: int, rule_num: int, base=BASE,nneigh=NNEIGH):
    """Evaluate evolution of an elementary cellular automaton.

    Parameters
    ----------
    initial_state: array-like Initial state of the automaton
    n_itr: int Number of iterations of evolution
    rule_num: int A rule to be used, following Wolfram naming convention
    base: int,optional Number of states for the cells. The default is 3
    nneigh: int,optional Number of neighbours from each side for a single cell that constitute 'neighbourhood'. The default is 1.

    Returns
    -------
    CA_run: numpy.ndarray m*n_itr grid, where m is the size of the automaton, rows represent steps of evolution.

    """
    # check if provided rule exceeds the maximum value for a given base
    if rule_num<0 or np.log(rule_num)>(base**3*np.log(base)):
        raise ValueError(f"Invalid rule for base {base}.")
    # number of all possible inputs for the evolution function
    n_pos = base**(nneigh*2+1)
    # conversion of the rule number to a given base
    rule = np.zeros(n_pos)
    str_rule = np.fromiter(np.base_repr(rule_num,base=base),dtype=int)
    rule[n_pos-len(str_rule):] = str_rule

    m = len(initial_state)
    # a valiable containing all states of the automaton
    CA_run = np.zeros((n_itr, m))
    CA_run[0, :] = initial_state

    for i in range(1, n_itr):
        all_multiplets = np.stack([np.roll(CA_run[i-1,:],j) for j in range(-nneigh,nneigh+1)])
        CA_run[i, :] = rule[np.apply_along_axis(rule_index, 0, all_multiplets,base=base)]
    return CA_run

if __name__ == "__main__":

    argv = sys.argv[1:]
    init_state, n_itr, rule = argv[:3]
    rule = int(rule)
    n_itr = int(n_itr)
    init_state = np.fromiter(init_state,dtype=int)
    u_base, u_nneigh = None, None
    if len(argv)>3:
        u_base = int(argv[3])
        if len(argv)>4:
            u_nneigh = int(argv[4])

    base = u_base if u_base else BASE
    nneigh = u_nneigh if u_nneigh else NNEIGH

    cells = cell_automaton(init_state,n_itr,rule,base,nneigh)

    fig, ax = plt.subplots()
    ax.matshow(cells)
    ax.set_title(f"""Cellular automaton evolution ({n_itr} steps),
        rule No. {rule},
        No. of states: {base}, neighbourhood: {2*nneigh}.
        """)
    ax.axis(False)
    plt.tight_layout()
    plt.show()


import numpy as np
import math
import matplotlib.pyplot as plt

class RegressionLinearData:
    def __init__(self, a_coeff, b_coeff):
        self.a = a_coeff
        self.b = b_coeff

    def generate_data(self, n_examples=100):
        n_features = 1
        x_data = np.random.rand(n_examples, n_features).astype(np.float32)
        y_data = x_data * self.a + self.b
        return x_data, y_data

    def plot_data(self, ax, x, y, plot_ideal_line=False, model_a=None, model_b=None):
        plt.sca(ax)
        plt.cla()
        ax.scatter(x, y, label='Data')
        xlims = np.asarray([np.min(x)-0.1, np.max(x)+0.1], dtype=np.float32)
        ylims = np.asarray([np.min(y)-0.01, np.max(y)+0.01], dtype=np.float32)
        if plot_ideal_line:
            pred_y = xlims * self.a + self.b
            plt.plot(xlims, pred_y, 'b', label='Ideal')
        if model_a is not None and model_b is not None:
            pred_y_model = xlims * model_a + model_b
            plt.plot(xlims, pred_y_model, 'r', label='Modelled')
        plt.xlim(xlims)
        plt.ylim(ylims)
        plt.legend(loc='lower right')
        plt.draw()

class ClassificationLinearData:
    def __init__(self, a_coeff=0.2, b_coeff=-0.7, c_coeff=0, min_x=-5, max_x=5):
        self.a = a_coeff
        self.b = b_coeff
        self.c = c_coeff
        self.min_x = min_x
        self.max_x = max_x

    def generate_data(self, n_points=1000):
        """Simple two-class classes linearly separable by line a*x_1 + b*x_2 + c = 0."""

        datax = (self.max_x-self.min_x) * np.random.rand(n_points * 2).reshape((n_points, 2)) + self.min_x
        datax.astype('float32')
        labels = datax[:, 0] * self.a + datax[:, 1] * self.b + self.c > 0
        labels = labels.astype('float32').reshape(labels.size, 1)
        return datax, labels

    def plot_data(self, ax, data_x, labels, plot_ideal_line=True, x_grid=None, y_grid=None, z_model=None,
                  add_bar=True):
        plt.sca(ax)
        plt.cla()

        xlims = np.array([self.min_x, self.max_x])
        ylims = np.array([self.min_x, self.max_x])

        if x_grid is not None and y_grid is not None and z_model is not None:
            CS = ax.contourf(x_grid, y_grid, z_model, np.linspace(0., 1., 51),
                             alpha=.8, cmap=plt.cm.get_cmap("bwr"), hold="on")
            decision_line = plt.contour(CS, levels=[0.5], hold="on", colors='k')
            if add_bar:
                cbar = plt.colorbar(CS)
                cbar.add_lines(decision_line)
                cbar.ax.set_ylabel('output value')

        if plot_ideal_line:
            x2 = -xlims*(self.a/self.b) - (self.c/self.b)
            plt.plot(xlims, x2, 'b', label='Ideal')

        ax.scatter(data_x[:, 0], data_x[:, 1], c=labels.flatten(), s=40)
        plt.xlabel('x1')
        plt.ylabel('x2')
        plt.xlim(xlims)
        plt.ylim(ylims)
        plt.draw()

class ClassificationTwoSpiralsData:
    def __init__(self, degrees=570, start=40, min_x=-12, max_x=12):
        """
        Adopted from Matlab code at:
            http://www.mathworks.com/matlabcentral/fileexchange/
            41459-6-functions-for-generating-artificial-datasets/content/twospirals.m

        :type degrees: int
        :param degrees: length of the spirals

        :type start: int
        :param start: how far from the origin the spirals start, in degrees

        :param min_x:
        :param max_x:
        """
        self.degrees = degrees
        self.start = start
        self.min_x = min_x
        self.max_x = max_x

    def generate_data(self, n_points=2000, noise=0.5, seed=1234):
        """
        :type n_points: int
        :param n_points: number of instances
        :type noise: float
        :param noise: 0 is no noise, at 1 the spirals will start to overlap
        """

        deg2rad = 2.*math.pi/360.
        start = self.start*deg2rad

        N1 = int(math.floor(n_points / 2))
        N2 = n_points - N1

        rng = np.random.RandomState(seed)

        n = start + np.sqrt(rng.rand(N1)) * self.degrees * deg2rad
        d1 = np.column_stack((-np.cos(n)*n + rng.rand(N1)*noise,
                              np.sin(n)*n + rng.rand(N1)*noise, np.zeros_like(n)))

        n = start + np.sqrt(rng.rand(N2)) * self.degrees * deg2rad
        d2 = np.column_stack((np.cos(n)*n + rng.rand(N2)*noise,
                              -np.sin(n)*n + rng.rand(N2)*noise, np.ones_like(n)))

        d = np.vstack((d1, d2))

        order = np.arange(len(d))
        rng = np.random.RandomState(seed)
        rng.shuffle(order)
        d = d[order]
        data_x = d[:, 0:2]
        data_labels = d[:, 2]
        data_labels = data_labels.reshape(data_labels.size, 1)

        return data_x, data_labels

    def plot_data(self, ax, data_x, labels, x_grid=None, y_grid=None, z_model=None,
                  add_bar=True, title=""):
        plt.sca(ax)
        plt.cla()

        xlims = np.array([self.min_x, self.max_x])
        ylims = np.array([self.min_x, self.max_x])

        if x_grid is not None and y_grid is not None and z_model is not None:
            CS = ax.contourf(x_grid, y_grid, z_model, np.linspace(0., 1., 51),
                             alpha=.8, cmap=plt.cm.get_cmap("bwr"), hold="on")
            decision_line = plt.contour(CS, levels=[0.5], hold="on", colors='k')
            if add_bar:
                cbar = plt.colorbar(CS)
                cbar.add_lines(decision_line)
                cbar.ax.set_ylabel('output value')

        ax.scatter(data_x[:, 0], data_x[:, 1], c=labels.flatten(), s=40)
        plt.xlabel('x1')
        plt.ylabel('x2')
        plt.title(title)
        plt.xlim(xlims)
        plt.ylim(ylims)
        plt.draw()

def input_noise(datax, noise_level=0.01):
    noisy_datax = datax + noise_level * np.random.randn(datax.size).reshape(datax.shape)
    return noisy_datax

def label_noise(labels, noise_level=0.05):
    r = np.random.rand(labels.size).reshape(labels.shape) < noise_level
    noisy_labels = np.logical_xor(labels, r).astype('float32')
    return noisy_labels

def prepare_grid_points_for_contour_plot(min_x, max_x, grid_size):
    values = np.linspace(min_x, max_x, grid_size)
    X1, X2 = np.meshgrid(values, values)
    X1vec = X1.reshape((X1.size, 1))
    X2vec = X2.reshape((X2.size, 1))
    return X1, X2, np.hstack((X1vec, X2vec))


from numpy.core.numeric import identity
from .model import Model
from ..util.metrics import mse
import numpy as np

class LinearRegression(Model):

    def __init__(self, gd=False, epochs=1000, lr=0.001):
        """Linear regression Model

        epochs: number of epochs
        lr: learning rate for GD
        """
        super(LinearRegression, self).__init__()
        self.gd = gd
        self.theta = None
        self.epochs = epochs
        self.lr = lr

    def fit(self, dataset):
        X, Y = dataset.getXy()

        # add the x with only 1 that corresponds to the independent term
        X = np.hstack(
            (np.ones((X.shape[0], 1)), X))

        self.X = X
        self.Y = Y

        # Closed form or GD
        self.train_gd(X, Y) if self.gd else self.train_closed(X, Y)  # implement closed train form (see notes)
        self.is_fitted = True

    def cost(self):
        y_pred = np.dot(self.X, self.theta)
        return mse(self.Y, y_pred) / 2

    def train_closed(self, X, Y):
        """Uses closed form linear algebra to fit the model.

        theta=inv(XT*X)*XT*y
        """
        self.theta = np.linalg.inv(X.T.dot(X)).dot(X.T).dot(Y)

    def train_gd(self, X, Y):
        m = X.shape[0]
        n = X.shape[1]

        self.history = {}
        self.theta = np.zeros(n)

        for epoch in range(self.epochs):
            grad = 1 / m * (X.dot(self.theta) - Y).dot(X)  # gradient by definition
            self.theta -= self.lr * grad
            self.history[epoch] = [self.theta[:], self.cost()]

    def predict(self, X):
        assert self.is_fitted, "Model must be fit before predicting"

        _x = np.hstack(([1], X))
        return np.dot(self.theta, _x)

class LinearRegressionReg(LinearRegression):

    def __init__(self, gd=False, epochs=100, lr=0.01, lbd=1):
        """Linear regression model with L2 regularization

        :param bool gd: if True uses gradient descent (GD) to train the model
                        otherwise closed form lineal algebra. Default False.
        :param int epochs: Number of epochs for GD
        :param int lr: Learning rate for GD
        :param float lbd: lambda for the regularization"""

        super(LinearRegressionReg, self).__init__()
        self.gd = gd
        self.epochs = epochs
        self.lr = lr
        self.lbd = lbd
        self.theta = None

    def train_closed(self, X, Y):
        """Use closed form linear algebra to fit the model.

        theta=inv(XT*X+lbd*I')*XT*y"""

        n = X.shape[1]
        identity = np.eye(n)
        identity[0, 0] = 0

        self.theta = np.linalg.inv(X.T.dot(X) + self.lbd * identity).dot(X.T).dot(Y)
        self.is_fitted = True

    def train_gd(self, X, Y):
        """Uses gradient descent to fit the model"""

        m = X.shape[0]
        n = X.shape[1]

        self.history = {}
        self.theta = np.zeros(n)

        lbds = np.full(m, self.lbd)
        lbds[0] = 0  # so that theta(0) is excluded from regularization form

        for epoch in range(self.epochs):
            grad = (X.dot(self.theta) - Y).dot(X)  # gradient by definition
            self.theta -= (self.lr / m) * (lbds + grad)
            self.history[epoch] = [self.theta[:], self.cost()]

    def predict(self, X):
        assert self.is_fitted, "Model must be fit before predicting"

        _x = np.hstack(([1], X))
        return np.dot(self.theta, _x)

    def cost(self):
        y_pred = np.dot(self.X, self.theta)
        return mse(self.Y, y_pred) / 2


"""
### Closest pairs algorithm

Complexity:
- new recursive calls per call $a=2$.
- input size shrinkage $b=2$
- exponent on combine step $O(n)$ hence $d=1$
- Total: $a=b^d$ so $O(n\log(n))$
"""
import numpy as np
import random

class ClosestPair:
    """ Computes the closest pair of points in P """

    def closest_pair(self, P):
        n = len(P)
        if n <= 3:
            return self.brute_force_closest_pair(P)
        Px = sorted(P, key=lambda x: x[0])
        Py = sorted(P, key=lambda x: x[1])
        return self.closest_pair_(Px, Py, n)

    @staticmethod
    def dist(x, y):
        return np.linalg.norm(x - y)

    def brute_force_closest_pair(self, P):
        n = len(P)
        min_dist = float("inf")
        p, q = None, None
        for i in range(n - 1):
            for j in range(i + 1, n):
                d = self.dist(P[i], P[j])
                if min_dist > d:
                    min_dist = d
                    p, q = (P[i], P[j])
        return p, q, min_dist

    def closest_pair_(self, Px, Py, n):
        mid = n // 2
        if n <= 3:
            return self.brute_force_closest_pair(Px)
        Qx = Px[:mid]
        Rx = Px[mid:]
        Qy = []
        Ry = []
        mid_x = Px[mid][0]
        for p in Py:
            if p[0] <= mid_x:
                Qy.append(p)
            else:
                Ry.append(p)
        p1, q1, dist1 = self.closest_pair_(Qx, Qy, mid)
        p2, q2, dist2 = self.closest_pair_(Rx, Ry, n - mid)
        if dist1 <= dist2:
            best_pair = (p1, q1)
            best_dist = dist1
        else:
            best_pair = (p2, q2)
            best_dist = dist2
        p3, q3, dist3 = self.closest_pair_split(Px, Py, mid, best_dist, best_pair)
        return p3, q3, dist3

    def closest_pair_split(self, Px, Py, mid, best_dist, best_pair):
        x_bar = Px[mid - 1][0]  # biggest x_coord in left of P
        # S_y contains points with x-coord in [x_bar - best_dist, x_bar + best_dist]
        S_y = [p for p in Px if x_bar - best_dist <= p[0] <= x_bar + best_dist]
        n_S_y = len(S_y)
        for i in range(n_S_y - 1):
            for j in range(i + 1, min(i + 8, n_S_y)):
                p, q = S_y[i], S_y[j]
                d = self.dist(p, q)
                if d < best_dist:
                    best_dist = d
                    best_pair = (p, q)
        return best_pair[0], best_pair[1], best_dist

if __name__ == "__main__":

    def test_case(length: int = 10000, size: int = 100):
        Px = [random.randint(-size, size) for i in range(length)]
        Py = [random.randint(-size, size) for i in range(length)]
        P = np.array(list(zip(Px, Py)))
        return P

    P = test_case(length=10, size=1000)
    # print(P)

    # import matplotlib.pyplot as plt
    # plt.scatter(P[:, 0], P[:, 1])
    # plt.show()
    C = ClosestPair()
    print(C.closest_pair(P))


import math
import random
import numpy as np

# The Sparse Vector algorithm as in Dwork and Roth 2014.
# NOTE! This code is used for academic simulations.
# It is not actually guaranteed to be private because it uses python's floating-point
# arithmetic and random numbers, whereas the theorems are proved for true real numbers
# and true randomness.

# Parameters
# Set the sparse vector threshold as this fraction of alpha
THRESHOLD_FRACTION = 0.5

# --Input--
#   data: a database
#   f_list: a list of functions of the form f(data) = real_number
#   sv_thresh: a real number
#   epsilon: guarantee epsilon-differential privacy
#   sensitivity: maximimum sensitivity of any query
# --Output--
#   index i of the first function in f_list that is approximately lower than sv_thresh
#   (return -1 if none)
def below_threshold(data, f_list, sv_thresh, epsilon, sensitivity):
  noisy_threshold = sv_thresh + np.random.laplace(scale=2.0*sensitivity / epsilon)
  for i, f in enumerate(f_list):
    if f(data) + np.random.laplace(scale=4.0*sensitivity / epsilon) <= noisy_threshold:
      return i
  return -1

# --Input--
#   alpha: the "real" threshold, a positive number
#   gamma: probability of failure
#   sensitivity: maximum sensitivity of any query
#   num_steps: the maximum number of time steps
#   auto_compute_threshold: see below
#
# --Output--
#   (sv_thresh, epsilon) to use in below_threshold
#   so that, with prob 1-gamma, below_threshold's answer is below alpha
# Here failure means that below_threshold returns no answer, or
# an answer whose accuracy is not as good as alpha.
#
def compute_epsilon(alpha, gamma, sensitivity, num_steps, auto_compute_threshold=True):
  if auto_compute_threshold:
    # use sv_thresh calculated to optimize epsilon
    b = alpha / (4.0 * math.log(1.5**0.75 * math.sqrt(num_steps) / gamma) )
    sv_thresh = 0.25*alpha + 0.75*b*math.log(1.5) - 0.5*b*math.log(num_steps)
  else:
    # use parameter for sv_thresh, then calculated value of b
    sv_thresh = alpha * THRESHOLD_FRACTION
    b = (alpha-sv_thresh) / (3.0*math.log( (0.5 + num_steps**(2.0/3))/gamma ))

  # need to add noise with laplace parameter b = 2*sensitivity/epsilon
  # for threshold and parameter 2b = 4*sensitivity/epsilon for queries
  eps = 2.0 * sensitivity / b
  return sv_thresh, eps

# auto_compute_threshold
# If True, set the sparse-vector threshold as the value that maximizes
# the privacy obtained from the sparse vector method
# (note this will probably be a very low threshold, so it will usually
# end up with a much higher accuracy

# CALCULATIONS
#
# Given alpha, gamma, and num_steps, either:
#  - use the fixed sv_thresh = alpha*THRESHOLD_FRACTION, or
#  - calculate the optimal sv_thresh myopically optimizing SV's privacy (even though
#    it may set the threshold low)
#
# and calculate the Laplace noise parameter b.
# Goal: with probability at least 1-gamma,
# we don't "fail", meaning the noisy threshold goes below 0 or the noisy_threshold plus
# some query noise goes above alpha.
#
# Here the noisy threshold is sv_thresh + Lap(b)
# and each query noise is Lap(2b).
#
# So always, epsilon = 2 * sensitivity / b.
#
# -------------
#
# 1. Fixed THRESHOLD_FRACTION
#
# Determining b as a function of sv_thresh, num_steps, and gamma.
#
# Let b = 2*sensitivity/epsilon. T is the total number of time steps and gamma is the failure probability.
#
#     gamma <= Pr[ noisy_thresh <= 0 ]  + Pr[ noisy_thresh + max(query_noise) >= alpha ].
#     gamma = 0.5*exp[ -sv_thresh/b ]  +  TERM.
#
# Now, TERM <= Pr[ thresh_noise > z*(alpha-sv_thresh) ] + Pr[ exists t with query_noise(t) > (1-z)*(alpha-sv_thresh) ]
#           <= 0.5*exp[ -z*(alpha - sv_thresh)] + T*Pr[ query_noise(1) > (1-z)*(alpha - sv_thresh) ]
#            = 0.5*[ exp[-z*(a-s)/b]  +  T*exp[-(1-z)(a-s)/(2b)] ].
#
# This holds for all z in (0,1). Using calculus, we find the optimal z = 1/3 - (2b/(3(a-s)))*ln(T). (at the optimal z, both terms are equal to 0.5*exp[-(a-s)/(3b) + (2/3)ln(T)].)
#
#     gamma <= 0.5*exp[ -s/b ] + T^(2/3)exp[-(a-s)/(3b)]
#            = (0.5*exp[(a-4s)/3b] + T^(2/3)) * exp[-(a-s)/(3b)].
# Now for sv_thresh >= a/4, we get
#     gamma <= (0.5 + T^(2/3)) * exp[-(a-s)/(3b)]
# so
#     b <= (a-s) / 3ln( (0.5 + T^(2/3))/gamma ).
#
# ----------
#
# USE_OPT_THRESHOLD
#
# We can go further and compute the optimal sv_threshold s, if we didn't care about the effect setting it low would have on the noise reduction technique.
#
#     gamma <= 0.5*exp[ -s/b ] + T^(2/3)exp[-(a-s)/(3b)].
#
# Using calculus to find the optimal s in [0,a], we get s = (a/4) + (3/4)*b*ln(1.5) - (1/2)*b*ln(T)).
#     gamma <=    exp[ ln(0.5) -0.25a/b - 0.75*ln(1.5) + 0.5*ln(T) ]
#              +  exp[ (2/3)ln(T) - a/(3b) + a/(12b) + 0.25*ln(1.5) - (1/6)ln(T) ]
#            = T^0.5 * 1.5^(3/4) * exp[-0.25a/b].
#
# So a/(4b) = ln(1.5^(3/4) * sqrt(T) / gamma)
#    b = a / [4 ln(1.5^(3/4) * sqrt(T) / gamma) ].
#    s = (a/4) * (1 + [1/ln(1.5^0.75 * sqrt(T) / gamma)]*(0.75*ln(1.5) - 0.5*ln(T))
#    s = (a/4) * (1 - FRAC)
#
# FRAC = 0.5*ln(T) - ln(1.5^0.75)
#        ---------------------------------------
#        0.5ln(T) + ln(1.5^0.75) + ln(1.0/gamma)


import numpy as np

def addBiasColumn(X):
    return np.column_stack((np.ones(X.shape[0]), X))

# It always add the bias term. The bias term is the first element on weights array
class MCP:
    def __init__(self, inputDimension, outputFunction=lambda x: x, initialWeights=False):
        self.inputDimension = inputDimension
        self.weights = inputDimension + 1
        self.outputFunction = np.vectorize(outputFunction)

        if initialWeights:
            self.outputWeights = np.array(initialWeights)
            if self.outputWeights.shape[0] != self.weights:
                raise Exception("Initial weights has wrong dimension")

        else:
            self.outputWeights = np.zeros(self.weights)

    def train(self, X, y, learningRate=0.01, maxIterations=1, tolerance=1e-9):
        for _ in range(maxIterations):
            iterationError = 0

            for index, row in enumerate(X):
                augmentedRow = addBiasColumn(
                    row.reshape((1, self.inputDimension))
                )
                yApprox = self.predict(augmentedRow, True)[0]

                error = y[index]-yApprox
                iterationError += error ** 2
                self.outputWeights = self.outputWeights + learningRate*error*augmentedRow[0]

            iterationError /= X.shape[0]
            if iterationError <= tolerance:
                break
        pass

    def predict(self, X, hasBiasColumn=False):
        if hasBiasColumn:
            dotProduct = self.outputWeights.dot(X.T)
        else:
            dotProduct = self.outputWeights.dot(addBiasColumn(X).T)

        return self.outputFunction(dotProduct)

    def getWeights(self):
        return self.weights

def Adaline(inputDimension, initialWeights=False):
    return MCP(inputDimension, lambda x: x, initialWeights)

def Perceptron(inputDimension, initialWeights=False):
    return MCP(inputDimension, lambda x: 1 if x >= 0 else 0, initialWeights)


"""
Goal: Toss a coin N times and compute the running proportion of heads.
"""
import matplotlib.pyplot as plt
plt.style.use('seaborn-darkgrid')
import numpy as np

# Specify the total number of flips, denoted N.
N = 500
# Generate a random sample of N flips for a fair coin (heads=1, tails=0);
np.random.seed(47405)
flip_sequence = np.random.choice(a=(0, 1), p=(.5, .5), size=N, replace=True)
# Compute the running proportion of heads:
r = np.cumsum(flip_sequence)
n = np.linspace(1, N, N)  # n is a vector.
run_prop = r/n  # component by component division.

# Graph the running proportion:
plt.plot(n, run_prop, '-o', )
plt.xscale('log')  # an alternative to plot() and xscale() is semilogx()
plt.xlim(1, N)
plt.ylim(0, 1)
plt.xlabel('Flip Number')
plt.ylabel('Proportion Heads')
plt.title('Running Proportion of Heads')
# Plot a dotted horizontal line at y=.5, just as a reference line:
plt.axhline(y=.5, ls='dashed')

# Display the beginning of the flip sequence.
flipletters = ''
for i in flip_sequence[:10]:
    if i == 1:
        flipletters += 'H'
    else:
        flipletters += 'T'

plt.text(10, 0.8, 'Flip Sequence = %s...' % flipletters)
# Display the relative frequency at the end of the sequence.
plt.text(25, 0.2, 'End Proportion = %s' % run_prop[-1])

plt.savefig('Figure_3.1.png')


# !/usr/bin/env python
# -- coding: utf-8 --
# @Author zengxiaohui
# Datatime:9/28/2021 11:14 AM
# @File:numpy_convolution
import numpy as np

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

def pad_to(a, N):
    "pads to NxN on the right and bottom"
    h, w = a.shape[:2]
    return np.pad(a, ((0, N - h), (0, N - w)))

def roll2(a, *, x=0, y=0):  # keyword-only arguments
    a = np.roll(a, axis=0, shift=y)
    a = np.roll(a, axis=1, shift=x)
    return a

def convolve(f, g):
    ff = np.fft.fft2(f)
    gg = np.fft.fft2(g)
    ffgg = ff * gg
    fg = np.fft.ifft2(ffgg)
    return fg

def print_array(a):
    "prints signed single digit integers, best not give it anything else"
    a = a.copy()
    a[np.isclose(a, 0)] = 0  # suppress negative zeros

    def fmt(val):
        if val == 0:  # isclose took care of that
            return " _"
        else:
            return f"{val: 1.0f}"  # space means - or blank sign

    for row in a:
        print(" ".join(fmt(el) for el in row))

    print()

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

np.set_printoptions(suppress=True, sign=' ')

N = 10

print("observe how the signs work")

# diagonal
# f = np.eye(N, dtype=np.float32)

f = np.zeros((N, N), dtype=np.float32)

# a cross
f[3:5, 1:7] = 1
f[2:8, 3:5] = 1

# a little dot, note what the convolution makes of it
f[8, 7] = 1

print_array(f)

# g = np.float32([[0, 1], [0, 0]]) # translate +x
# g = np.float32([[0, 0], [1, 0]]) # translate +y

# some simple filters
# g = np.float32([[-1,  0,  0]])
# g = np.float32([[-1, +1]])
g = np.float32([[-1, 0, +1]])
# try [-1, 0, 0] to see how an "identity" (negated) looks like vs the other filters
# it does appear as if each element of f scales a copy of g, then
# all those scaled copies overlay on each other and sum "through" (superposition)

# sum of scaled copies:
# image:       _  _  1  1  _  _  _ :  first and second are zero, then two ones
#             -------------------- : "instances" of [-1, 0, +1]
#              0  0  0             : * 0
#                 0  0  0          : * 0
#                   -1  0 +1       : * 1
#                      -1  0 +1    : * 1
#                          0  0  0 : * 0
#             -------------------- : summed/integrated
# sum:         0  0 -1 -1 +1 +1  0

# equivalently the "moving window" idea:
# image:       _  _  _  1  1  _  _  _
#             ----------------------- : use [-1, 0, +1] reversed, then dot product
#             +1  0 -1                : =>  0
#                +1  0 -1             : => -1
#                   +1  0 -1          : => -1
#                      +1  0 -1       : => +1
#                         +1  0 -1    : => +1
#                            +1  0 -1 : =>  0
#             -----------------------
# result             0 -1 -1 +1 +1  0

# try transposing to get the filter in y direction
# g = g.T

g = pad_to(g, N)  # pad to NxN with zeros

# g = roll2(g, x=-1) # use this to shift around

print_array(g)

fg = convolve(f, g)
print_array(np.real(fg))

print()
print("prewitt is separable")

f = np.float32([[-1, 0, +1]])  # gradient filter
g = np.float32([  # a box blur
    [1],
    [1],  # use 2 here to get sobel (closer to gaussian blur)
    [1]
])

f = pad_to(f, N)
g = pad_to(g, N)
print_array(f)
print_array(g)

fg = convolve(f, g)

print_array(np.real(fg))

print()
print("convolution of two other filters")

f = np.float32([[-1, 0, +1]])
g = np.float32([
    [1, 2, 1],
    [2, 4, 2],
    [1, 2, 1]
])

f = pad_to(f, N)
g = pad_to(g, N)
print_array(f)
print_array(g)

fg = convolve(f, g)

print_array(np.real(fg))


import numpy as np

class Relu:

    def __call__(self, z):
        return np.maximum(z, 0)

    def derivative(self, z):
        return (z > 0).astype(z.dtype)

class Sigmoid:

    def __call__(self, z):
        return 1 / (1 + np.exp(-z))

    def derivative(self, z):
        return z * (1 - z)

class LeakyRelu:

    def __init__(self, alpha=0.01):
        self.alpha = alpha

    def __call__(self, z):
        return np.where(z > 0, z, z * self.alpha)

    def derivative(self, z):
        dz = np.ones_like(z)
        dz[z < 0] = self.alpha
        return dz

class Tanh:

    def __call__(self, z):
        return np.tanh(z)

    def derivative(self, z):
        return 1 - (self(z) ** 2)

class Softmax:

    def __call__(self, z):
        t = np.exp(z)
        return t / np.sum(t, axis=1, keepdims=True)

aliases = {
    'relu': Relu(),
    'sigmoid': Sigmoid(),
    'tanh': Tanh(),
    'leaky_relu': LeakyRelu(),
    'softmax': Softmax()
}

def get(initializer):
    if isinstance(initializer, str):
        return aliases[initializer]
    elif callable(initializer):
        return initializer
    else:
        raise ValueError('Parameter type not understood')


"""Orthogonalization of vectors."""

from functools import partial
from typing import Callable, Iterable, Optional, Union

import numpy as np

from probnum import linops

from ._inner_product import induced_norm, inner_product as inner_product_fn

def gram_schmidt(
    v: np.ndarray,
    orthogonal_basis: Iterable[np.ndarray],
    inner_product: Optional[
        Union[
            np.ndarray,
            linops.LinearOperator,
            Callable[[np.ndarray, np.ndarray], np.ndarray],
        ]
    ] = None,
    normalize: bool = False,
) -> np.ndarray:
    r"""Orthogonalize a vector with respect to an orthogonal basis and inner product.

    Computes a vector :math:`v'` such that :math:`\langle v', b_i \rangle = 0` for
    all basis vectors :math:`b_i \in B` in the orthogonal basis.

    Parameters
    ----------
    v
        Vector (or stack of vectors) to orthogonalize against ``orthogonal_basis``.
    orthogonal_basis
        Orthogonal basis.
    inner_product
        Inner product defining orthogonality. Can be either a :class`numpy.ndarray` or
        a :class:`Callable` defining the inner product. Defaults to the euclidean inner
        product.
    normalize
        Normalize the output vector, s.t. :math:`\langle v', v' \rangle = 1`.

    Returns
    -------
    v_orth :
        Orthogonalized vector.
    """
    orthogonal_basis = np.atleast_2d(orthogonal_basis)

    if inner_product is None:
        inprod_fn = inner_product_fn
        norm_fn = partial(induced_norm, axis=-1)
    elif isinstance(inner_product, (np.ndarray, linops.LinearOperator)):
        inprod_fn = lambda v, w: inner_product_fn(v, w, A=inner_product)
        norm_fn = lambda v: induced_norm(v, A=inner_product, axis=-1)
    else:
        inprod_fn = inner_product
        norm_fn = lambda v: np.sqrt(inprod_fn(v, v))

    v_orth = v.copy()

    for u in orthogonal_basis:
        v_orth -= (inprod_fn(u, v)[..., None] / inprod_fn(u, u)) * u

    if normalize:
        v_orth /= norm_fn(v_orth)[..., None]

    return v_orth

def modified_gram_schmidt(
    v: np.ndarray,
    orthogonal_basis: Iterable[np.ndarray],
    inner_product: Optional[
        Union[
            np.ndarray,
            linops.LinearOperator,
            Callable[[np.ndarray, np.ndarray], np.ndarray],
        ]
    ] = None,
    normalize: bool = False,
) -> np.ndarray:
    r"""Stabilized Gram-Schmidt process.

    Computes a vector :math:`v'` such that :math:`\langle v', b_i \rangle = 0` for
    all basis vectors :math:`b_i \in B` in the orthogonal basis in a numerically stable
    fashion.

    Parameters
    ----------
    v
        Vector (or stack of vectors) to orthogonalize against ``orthogonal_basis``.
    orthogonal_basis
        Orthogonal basis.
    inner_product
        Inner product defining orthogonality. Can be either a :class:`numpy.ndarray` or
        a :class:`Callable` defining the inner product. Defaults to the euclidean inner
        product.
    normalize
        Normalize the output vector, s.t. :math:`\langle v', v' \rangle = 1`.

    Returns
    -------
    v_orth :
        Orthogonalized vector.
    """
    orthogonal_basis = np.atleast_2d(orthogonal_basis)

    if inner_product is None:
        inprod_fn = inner_product_fn
        norm_fn = induced_norm
    elif isinstance(inner_product, (np.ndarray, linops.LinearOperator)):
        inprod_fn = lambda v, w: inner_product_fn(v, w, A=inner_product)
        norm_fn = lambda v: induced_norm(v, A=inner_product)
    else:
        inprod_fn = inner_product
        norm_fn = lambda v: np.sqrt(inprod_fn(v, v))

    v_orth = v.copy()

    for u in orthogonal_basis:
        v_orth -= (inprod_fn(u, v_orth)[..., None] / inprod_fn(u, u)) * u

    if normalize:
        v_orth /= norm_fn(v_orth)[..., None]

    return v_orth

def double_gram_schmidt(
    v: np.ndarray,
    orthogonal_basis: Iterable[np.ndarray],
    inner_product: Optional[
        Union[
            np.ndarray,
            linops.LinearOperator,
            Callable[[np.ndarray, np.ndarray], np.ndarray],
        ]
    ] = None,
    normalize: bool = False,
    gram_schmidt_fn: Callable = modified_gram_schmidt,
) -> np.ndarray:
    r"""Perform the (modified) Gram-Schmidt process twice.

    Computes a vector :math:`v'` such that :math:`\langle v', b_i \rangle = 0` for
    all basis vectors :math:`b_i \in B` in the orthogonal basis. This performs the
    (modified) Gram-Schmidt orthogonalization process twice, which is generally more
    stable than just reorthogonalizing once. [1]_ [2]_

    Parameters
    ----------
    v
        Vector (or stack of vectors) to orthogonalize against ``orthogonal_basis``.
    orthogonal_basis
        Orthogonal basis.
    inner_product
        Inner product defining orthogonality. Can be either a :class:`numpy.ndarray` or
        a :class:`Callable` defining the inner product. Defaults to the euclidean inner
        product.
    normalize
        Normalize the output vector, s.t. :math:`\langle v', v' \rangle = 1`.
    gram_schmidt_fn
        Gram-Schmidt process to use. One of :meth:`gram_schmidt` or
        :meth:`modified_gram_schmidt`.

    Returns
    -------
    v_orth :
        Orthogonalized vector.

    References
    ----------
    .. [1] L. Giraud, J. Langou, M. Rozloznik, and J. van den Eshof, Rounding error
           analysis of the classical Gram-Schmidt orthogonalization process,
           Numer. Math., 101 (2005), pp. 87–100
    .. [2] L. Giraud, J. Langou, and M. Rozloznik, The loss of orthogonality in the
           Gram-Schmidt orthogonalization process, Comput. Math. Appl., 50 (2005)
    """
    v_orth = gram_schmidt_fn(
        v=v,
        orthogonal_basis=orthogonal_basis,
        inner_product=inner_product,
        normalize=normalize,
    )
    return gram_schmidt_fn(
        v=v_orth,
        orthogonal_basis=orthogonal_basis,
        inner_product=inner_product,
        normalize=normalize,
    )


import numpy as np

from scipy.spatial.distance import correlation as _correlation
np.seterr(divide='ignore', invalid='ignore')

def minkowski(x1, x2, power):
    """Minkowski Distance Metric

    Parameters
    ----------
    x1: numpy.ndarray
        Vector one
    x2: numpy.ndarray
        Vector two
    power: int
        L_{power} norm order

    Returns
    -------
    distance: float
        Minkowski distance between `x1` and `x2`
    """
    return np.linalg.norm(x1 - x2, power)

def cosine(x1, x2):
    """Cosine Distance Metric

    Parameters
    ----------
    x1: numpy.ndarray
        Vector one
    x2: numpy.ndarray
        Vector two

    Returns
    -------
    distance: float
        Cosine distance between `x1` and `x2`
    """
    return np.dot(x1.T, x2) / (np.linalg.norm(x1, 2) * np.linalg.norm(x2, 2))

def chisquare(x1, x2):
    """Chi-Square Distance Metric

    Parameters
    ----------
    x1: numpy.ndarray
        Vector one
    x2: numpy.ndarray
        Vector two

    Returns
    -------
    distance: float
        Chi-Square distance between `x1` and `x2`
    """
    return np.sum((x1 - x2)**2 / (x1 + x2))

def correlation(x1, x2):
    """Correlation Distance

    Parameters
    ----------
    x1: numpy.ndarray
        Vector one
    x2: numpy.ndarray
        Vector two

    Returns
    -------
    distance: float
        Correlation distance between `x1` and `x2`
    """
    return _correlation(x1, x2)

def intersection(x1, x2):
    """Histogram Intersection

    Parameters
    ----------
    x1: numpy.ndarray
        Vector one
    x2: numpy.ndarray
        Vector two

    Returns
    -------
    distance: float
        Histogram intersection between `x1` and `x2`
    """
    assert(len(x1) == len(x2))
    minsum = 0
    for i in range(len(x1)):
        minsum += min(x1[i], x2[i])
    return float(minsum) / min(sum(x1), sum(x2))


import math
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
from scipy.stats import norm
from matilda.quantitative_analysis.risk_factor_modeling import CapitalAssetPricingModel

'''
Risk Deviation Measures
'''

def standard_deviation(portfolio_returns, period=252):
    """

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param period: period to compute statistics of returns for. For instance, to compute yearly, then input 252, and to compute monthly, then input 21.
    :return:
    """
    return portfolio_returns.std() * np.sqrt(period)

# def standard_deviation_portfolio(returns, weights, period=252):
#     return np.sqrt(np.dot(weights.T, np.dot(returns.cov() * period, weights)))

def average_absolute_deviation(portfolio_returns, period=252):
    return stats.median_absolute_deviation(portfolio_returns) * np.sqrt(period)

def lower_semi_standard_deviation(portfolio_returns, period=252):
    returns_below_mean = portfolio_returns[portfolio_returns < portfolio_returns.mean()]
    return np.std(returns_below_mean) * np.sqrt(period)

'''
Value at Risk (VAR) quantifies the minimum loss over the whole range of outcomes in the 1% tail?
'''

# Historical Simulation: calculating daily portfolio changes in value to determine the probability distribution of returns.
def value_at_risk_historical_simulation(portfolio_returns, confidence_level=0.05):
    portfolio_returns = portfolio_returns.dropna()
    plt.hist(portfolio_returns, bins=40)
    plt.xlabel('Returns')
    plt.ylabel('Frequency')
    plt.grid(True)
    plt.show()
    sorted_returns = np.sort(portfolio_returns)
    total_count = len(sorted_returns)
    index = int(confidence_level * total_count)
    return abs(sorted_returns[index])

# Variance Covariance: calculate the inverse of the normal cumulative distribution (PPF)
def value_at_risk_variance_covariance(portfolio_returns, confidence_level=0.05, period=252):
    return abs(norm.ppf(confidence_level,
                        np.mean(portfolio_returns) * period,
                        np.std(portfolio_returns) * np.sqrt(period)))

# Monte Carlo
def value_at_risk_monte_carlo(portfolio_returns):
    pass

def conditional_value_at_risk(portfolio_returns, confidence_level=0.05, period=252):
    """
    Expected Shortfall, otherwise known as CVaR, or conditional value at risk, is simply the expected loss of the worst case scenarios of returns.
    ES answers this question — What is the average loss over the whole range of outcomes in the 1% tail?
    For example, if your portfolio has a VaR(95) of -3%, then the CVaR(95) would be the average value of all losses exceeding -3%.

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param confidence_level:
    :param period: period to compute statistics of returns for. For instance, to compute yearly, then input 252, and to compute monthly, then input 21.
    :return:
    """
    var = value_at_risk_historical_simulation(portfolio_returns=portfolio_returns, confidence_level=confidence_level)
    returns_below_var = portfolio_returns[portfolio_returns < var]
    return returns_below_var.mean()

def lpm(returns, threshold, order):
    """
    Returns a lower partial moment of the returns. Example usage:

    >>>  r = nrand.uniform(-1, 1, 50)
    >>>  print('lpm(0.0)_1 = ', lpm(r, 0.0, 1))

    :param returns:
    :param threshold:
    :param order:
    :return:
    """
    # create an array the same length as returns containing the minimum return threshold
    threshold_array = np.empty(len(returns))
    threshold_array.fill(threshold)
    diff = threshold_array - returns  # calculate the difference between the threshold and the returns
    diff = diff.clip(min=0)  # set the minimum of each to 0
    # return the sum of the different to the power of order
    return np.sum(diff ** order) / len(returns)

def hpm(returns, threshold, order):
    """
    Returns a higher partial moment of the returns. Example usage:

    >>> r = nrand.uniform(-1, 1, 50)
    >>> print('hpm(0.0)_1 = ', hpm(r, 0.0, 1))

    :param returns:
    :param threshold:
    :param order:
    :return:
    """
    # create an array the same length as returns containing hte minimum return threshold
    threshold_array = np.empty(len(returns))
    threshold_array.fill(threshold)
    diff = returns - threshold_array  # calc the diff between the returns and the threshold
    diff = diff.clip(min=0)  # set min of each to 0
    # return sum of the different to the power of order
    return np.sum(diff ** order) / len(returns)

def drawdown_risk(portfolio_returns, trailing_period=252, max=False):
    """
    Drawdown risk is the maximum (or average) historical 'drawdown' of the portfolio.
    A drawdown is the percentage loss between peak and trough.

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param trailing_period:
    :param max:
    :return:
    """
    if max:
        trailing_period = len(portfolio_returns)
    # Calculate the max drawdown in the past window days for each day in the series.
    # Use min_periods=1 if you want to let the first 252 days data have an expanding window
    rolling_max = portfolio_returns.rolling(trailing_period, min_periods=1).max()
    daily_drawdown = portfolio_returns / rolling_max - 1.0

    # Next we calculate the minimum (negative) daily drawdown in that window.
    max_daily_drawdown = daily_drawdown.rolling(trailing_period, min_periods=1).min()

    # Plot the results
    daily_drawdown.plot()
    max_daily_drawdown.plot()
    plt.show()
    return {
        'daily_drawdown': daily_drawdown,
        'max_daily_drawdown': max_daily_drawdown,
        'average_daily_drawdown': np.mean(daily_drawdown)
    }

def treynor_ratio(portfolio_returns, risk_free_rates, benchmark_returns=None, period=252):
    """
    The **Treynor reward to volatility model** is a measurement of the returns earned in excess of that which could have
    been earned on an investment that has no diversifiable risk (e.g., Treasury bills or a completely diversified portfolio),
    per unit of market risk assumed. In other words, it calculates the excess returns generated by a portfolio
    :math:`r_i - r_f`, where :math:`r_i` is the portfolio :math:`i`'s returns and :math:`r_f` is the risk-free
    rate, then discounts it by portfolio :math:`i`'s beta, :math:`\\beta_i`.

    Like the Sharpe ratio, the Treynor ratio (T) does not quantify the value added, if any, of active portfolio
    management. It is a ranking criterion only. A ranking of portfolios based on the Treynor Ratio is only useful if
    the portfolios under consideration are sub-portfolios of a broader, fully diversified portfolio.
    If this is not the case, portfolios with identical systematic risk, but different total risk, will be rated the same.
    But the portfolio with a higher total risk is less diversified and therefore has a higher unsystematic risk which is
    not priced in the market.

    An alternative method of ranking portfolio management is Jensen's alpha, which quantifies the added return as the
    excess return above the security market line in the capital asset pricing model. As these two methods both determine
    rankings based on systematic risk alone, they will rank portfolios identically.

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param benchmark_returns: Pandas series representing percentage changes of the benchmark (i.e. S&P500) returns over time.
    :param risk_free_rates: Pandas series representing percentage changes of a risk-free asset's returns over time. It should be same time range and frequency as portfolio_returns
    :param period: period to compute statistics of returns for. For instance, to compute yearly, then input 252, and to compute monthly, then input 21.
    :return: :math:`T = \\frac{r_i - r_f}{\\beta_i}`
    """
    portfolio_beta = capm_beta(benchmark_returns=benchmark_returns, portfolio_returns=portfolio_returns)
    return ((portfolio_returns.mean() - risk_free_rates.mean()) * period) / portfolio_beta

def sharpe_ratio(portfolio_returns, risk_free_rates, period=252):
    """
    The **Sharpe ratio** was developed by William F. Sharpe in 1966. It is an extension of the *Traynor ratio*, as
    it discounts expected excess returns by the volatility. It measures the performance of an investment
    (e.g., a security or portfolio) compared to a risk-free asset, after adjusting for its risk.
    In other words, it is the difference between the returns of the investment  and the risk-free return, divided by
    the standard deviation of the investment (i.e., its volatility). It represents the additional amount of return
    that an investor receives per unit of increase in risk.

    The *ex-ante* ratio is defined as :math:`\\frac{E[R_a - R_b]}{\\sigma_a}` where :math:`R_{a}` is the asset return,
    :math:`R_b` is the risk-free return (such as a U.S. Treasury security). :math:`E[R_{a}-R_{b}]` is the expected
    value of the excess of the asset return over the benchmark return, and :math:`\\sigma _{a}` is the standard deviation
    of the asset excess return. The *ex-post* ratio uses the same equation as the one above but with realized returns
    of the asset and benchmark rather than expected returns

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates Pandas series representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param risk_free_rates: Pandas series representing percentage changes of a risk-free asset's returns over time. It should be same time range and frequency as portfolio_returns Pandas series representing percentage changes of a risk-free asset's returns over time. It should be same time range and frequency as portfolio_returns
    :param period: period to compute statistics of returns for. For instance, to compute yearly, then input 252, and to compute monthly, then input 21. Period to compute statistics of returns for. For instance, to compute yearly, then input 252, and to compute monthly, then input 21.
    :return: :math:`S = \\frac{E[R_a - R_b]}{\\sigma_a}`
    """
    portfolio_volatility = portfolio_returns.std() * np.sqrt(period)
    return ((portfolio_returns.mean() - risk_free_rates.mean()) * period) / portfolio_volatility

def information_ratio(portfolio_returns, benchmark_returns, period=252):
    """
    The **information ratio** is similar to the Sharpe ratio, the main difference being that the Sharpe ratio uses a
    risk-free return as benchmark whereas the information ratio uses a risky index as benchmark (such as the S&P500).

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param benchmark_returns: Pandas series representing percentage changes of the benchmark (i.e. S&P500) returns over time. Pandas series representing percentage changes of the benchmark (i.e. S&P500) returns over time.
    :param period: period to compute statistics of returns for. For instance, to compute yearly, then input 252, and to compute monthly, then input 21.
    :return:
    """
    return_difference = portfolio_returns - benchmark_returns
    return (return_difference.mean() * period) / (return_difference.std() * np.sqrt(period))

def modigliani_ratio(portfolio_returns, benchmark_returns, risk_free_rates, period=252):
    """
    The **Modigliani risk-adjusted performance** is a combination of the Sharpe and info ratios.
    It adjusts the expected excess returns of the portfolio above the risk free rate by the expected excess
    returns of a benchmark portfolio, above the risk free rate.

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param benchmark_returns: Pandas series representing percentage changes of the benchmark (i.e. S&P500) returns over time.
    :param risk_free_rates: Pandas series representing percentage changes of a risk-free asset's returns over time. It should be same time range and frequency as portfolio_returns
    :param period: period to compute statistics of returns for. For instance, to compute yearly, then input 252, and to compute monthly, then input 21.
    :return:
    """
    benchmark_volatility = np.std(benchmark_returns.std(), period)
    sharpe = sharpe_ratio(portfolio_returns, risk_free_rates, period)
    return (sharpe * benchmark_volatility) + (risk_free_rates.mean() * period)

def excess_return_value_at_risk(portfolio_returns, risk_free_rates, confidence_level: float = 0.05, period=252):
    """
    Discounts the excess return of the portfolio above the risk-free rate by the value at risk of the portfolio

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param risk_free_rates: Pandas series representing percentage changes of a risk-free asset's returns over time. It should be same time range and frequency as portfolio_returns
    :param confidence_level:
    :param period: period to compute statistics of returns for. For instance, to compute yearly, then input 252, and to compute monthly, then input 21.
    :return:
    """
    return ((portfolio_returns.mean() - risk_free_rates.mean()) * period) / \
           value_at_risk_historical_simulation(portfolio_returns=portfolio_returns, confidence_level=confidence_level)

def conditional_sharpe_ratio(portfolio_returns, risk_free_rates, confidence_level: float = 0.05, period: int = 252):
    """
    Discounts the excess return of the portfolio above the risk-free rate by the conditional VaR of the portfolio

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param risk_free_rates: Pandas series representing percentage changes of a risk-free asset's returns over time. It should be same time range and frequency as portfolio_returns
    :param confidence_level:
    :param period: period to compute statistics of returns for. For instance, to compute yearly, then input 252, and to compute monthly, then input 21.
    :return:
    """
    return ((portfolio_returns.mean() - risk_free_rates.mean()) * period) / \
           conditional_value_at_risk(portfolio_returns=portfolio_returns, confidence_level=confidence_level)

def capm_beta(portfolio_returns, benchmark_returns=None):
    """

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param benchmark_returns: Pandas series representing percentage changes of the benchmark (i.e. S&P500) returns over time.
    :return:
    """
    return CapitalAssetPricingModel(from_date=portfolio_returns.index[0], to_date=portfolio_returns.index[-1],
                                    factor_dataset=benchmark_returns) \
        .regress_factor_loadings(portfolio=portfolio_returns).params[1]

def jensens_alpha(portfolio_returns, benchmark_returns):
    """
    The **Jensen's alpha** determines the abnormal return of a security or portfolio of securities over the theoretical
    expected return. It is a version of the standard alpha based on a theoretical performance index instead of a market index.
    The theoretical return is predicted by a market model, most commonly the capital asset pricing model (CAPM).
    The market model uses statistical methods to predict the appropriate risk-adjusted return of an asset.

    The CAPM for instance uses beta as a multiplier. The CAPM return is supposed to be 'risk adjusted',
    which means it takes account of the relative riskiness of the asset. This is based on the concept that riskier
    assets should have higher expected returns than less risky assets. If an asset's return is even higher than the
    risk adjusted return, that asset is said to have "positive alpha" or "abnormal returns".
    Investors are constantly seeking investments that have higher alpha.

    Since Eugene Fama, many academics believe financial markets are too efficient to allow for repeatedly earning
    positive Alpha, unless by chance.

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param benchmark_returns: Pandas series representing percentage changes of the benchmark (i.e. S&P500) returns over time.
    :return:
    """
    return CapitalAssetPricingModel(from_date=portfolio_returns.index[0], to_date=portfolio_returns.index[-1],
                                    factor_dataset=benchmark_returns) \
        .regress_factor_loadings(portfolio=portfolio_returns).params[0]

def omega_ratio(portfolio_returns, risk_free_rates, target=0, period: int = 252):
    """
    Notice the denominator is power of 1/1.

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param risk_free_rates: Pandas series representing percentage changes of a risk-free asset's returns over time. It should be same time range and frequency as portfolio_returns
    :param target: minimum acceptable return, below which the returns are less desirable.
    :param period: period to compute statistics of returns for. For instance, to compute yearly, then input 252, and to compute monthly, then input 21.
    :return:
    """
    return ((portfolio_returns.mean() - risk_free_rates.mean()) * period) / lpm(portfolio_returns, target, 1)

def sortino_ratio(portfolio_returns, risk_free_rates, target=0, period: int = 252):
    """
    Notice the denominator is power of 1/2.

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param risk_free_rates: Pandas series representing percentage changes of a risk-free asset's returns over time. It should be same time range and frequency as portfolio_returns
    :param target: minimum acceptable return, below which the returns are less desirable.
    :param period: period to compute statistics of returns for. For instance, to compute yearly, then input 252, and to compute monthly, then input 21.
    :return:
    """
    return ((portfolio_returns.mean() - risk_free_rates.mean()) * period) / np.sqrt(lpm(portfolio_returns, target, 2))

def kappa_three_ratio(portfolio_returns, risk_free_rates, target=0, period: int = 252):
    """
    Notice the denominator is power of 1/3.

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param risk_free_rates: Pandas series representing percentage changes of a risk-free asset's returns over time. It should be same time range and frequency as portfolio_returns
    :param target: minimum acceptable return, below which the returns are less desirable.
    :param period: period to compute statistics of returns for. For instance, to compute yearly, then input 252, and to compute monthly, then input 21.
    :return:
    """
    return ((portfolio_returns.mean() - risk_free_rates.mean()) * period) / math.pow(lpm(portfolio_returns, target, 3),
                                                                                float(1 / 3))

def gain_loss_ratio(portfolio_returns, target=0):
    """

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param target: minimum acceptable return, below which the returns are less desirable.
    :return:
    """
    return hpm(portfolio_returns, target, 1) / lpm(portfolio_returns, target, 1)

def upside_potential_ratio(portfolio_returns, target=0):
    """
    The **upside-potential ratio** is a measure of a return of an investment asset relative to the minimal acceptable
    return. The measurement allows a firm or individual to choose investments which have had relatively good upside
    performance, per unit of downside risk.

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param target: minimum acceptable return, below which the returns are less desirable. minimum acceptable return, below which the returns are less desirable.
    :return:
    """
    return hpm(portfolio_returns, target, 1) / math.sqrt(lpm(portfolio_returns, target, 2))

def roys_safety_first_criterion(portfolio_returns: pd.Series, minimum_threshold=0.02, period=252):
    """

    :param portfolio_returns: Pandas series or dataframe representing percentage changes of the security (or portfolio) returns over time. It should be same time range and frequency as risk free rates
    :param minimum_threshold: minimum acceptable return, below which the returns are less desirable.
    :param period: period to compute statistics of returns for. For instance, to compute yearly, then input 252, and to compute monthly, then input 21.
    :return:
    """
    return (portfolio_returns.mean() * period - minimum_threshold) / (portfolio_returns.std() * math.sqrt(period))

if __name__ == '__main__':
    from matilda.portfolio_management.Portfolio import Portfolio
    from datetime import datetime

    assets = ['AAPL', 'V', 'KO', 'CAT']
    portfolio = Portfolio(assets=assets)
    portfolio.slice_dataframe(to_date=datetime(2021, 1, 1), from_date=datetime(2016, 1, 1))
    # portfolio_returns = portfolio.get_weighted_sum_returns(weights=np.ones(len(assets)) / len(assets))
    # print(portfolio_returns.head())
    print(roys_safety_first_criterion(portfolio_returns=portfolio.df_returns, minimum_threshold=0.02, period=252))


from sympy import *

x = symbols('x[0]')
y = symbols('x[1]')
z = symbols('x[2]')
uu = sin(x)*exp(x+y+z)
uv = sin(y)*exp(x+y+z)
uw = sin(z)*exp(x+y+z)
u = diff(uw,y)-diff(uv,z)
v = diff(uu,z)-diff(uw,x)
w = diff(uv,x)-diff(uu,y)
p = sin(2*pi*y)*sin(2*pi*x)*sin(2*pi*z)
print "u:    ",u
print "\n"
print "v:    ",v
print "\n"
print "w:    ",w
print "\n"
print "p:    ",p
print "\n"
L1 = diff(v,x,y) - diff(u,y,y) - diff(u,z,z) +diff(w,x,z)
L2 = diff(w,y,z) - diff(v,z,z) - diff(v,x,x) +diff(u,x,y)
L3 = diff(u,x,z) - diff(w,x,x) - diff(w,y,y) +diff(v,y,z)
print "Curl 1:    ",L1
print "\n"
print "Curl 2:    ",L2
print "\n"
print "Curl 3:    ",L3
print "\n"

print "\n"
P1 = diff(p,x)
P2 = diff(p,y)
P3 = diff(p,z)
print "Pressure 1:    ",P1
print "\n"
print "Pressure 2:    ",P2
print "\n"
print "Pressure 3:    ",P3

print diff(u,x)+diff(v,y)+diff(w,z)

 # (exp(x + y)*sin(y) + exp(x + y)*cos(y))**2 - 2*exp(2*x + 2*y)*sin(y)*cos(y)


import time

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

tol = 10.0 ** (-7)
h0 = 0.1
T = 1
EULER_ACCURACY = 1
RUNGE_KUTTA_ACCURACY = 4

A = 0
B = T

alpha = 0.8
sigma = 10
b = 8./3
r = 28

y0 = np.array([10, 1, r - 1])
a = np.array([0, 1./2, 1./2, 1])
c = np.array([1./6, 2./6, 2./6, 1./6])
ans = []
dataset = []

def draw(dataset):
    mpl.rcParams['legend.fontsize'] = 10

    fig = plt.figure()
    ax = fig.gca(projection=Axes3D.name)

    for r in dataset:
        x, y, z = r['data'][:, 0], r['data'][:, 1], r['data'][:, 2]
        ax.plot(x, y, z, label=r['name'])
        ax.legend()

    ax.set_xlabel('y1')
    ax.set_ylabel('y2')
    ax.set_zlabel('y3')
    plt.show()

def f(y1, y2, y3):
    return np.array([
        -sigma*(y1 - y2),
        -y1*y3 + r*y1 - y2,
        y1*y2 - b*y3
    ])

def iterate(method, y, h, num=1):
    ans.append(y)
    for i in range(num):
        y = method(y, h)
        ans.append(y)
    return y

def euler(y, h):
    return y + h * f(*y.tolist())

def runge_kutta(y, h):
    k = np.array(f(*y.tolist()), ndmin=2)
    for j in range(1, 4):
        k = np.vstack((k, f(*(y + a[j] * h * k[j - 1]).tolist())))
    return y + h * np.dot(c, k)

def runge_estimate(yh, y2h, p):
    return np.abs(la.norm(yh - y2h) / (2 ** p - 1))

def get_new_step(h_old, err, p):
    delta = (tol / err) ** (1.0 / (p + 1))
    return delta, alpha * delta * h_old

def find_step(method, p, h):
    n = 0
    y = np.array(y0)
    great = True

    while great:
        y2h = iterate(method, y, 2 * h)
        yh = iterate(method, y, h, 2)
        err = runge_estimate(yh, y2h, p)

        great = err > tol
        n += 1

        if great:
            delta, h_new = get_new_step(h, err, p)
            if delta < 1:
                h = h_new
            else:
                y = y2h
                h = min(h_new, (B - A - 2 * h) / 2)
    return h, n

print 'solve Euler:'
t_s = time.time()

h_e, N = find_step(euler, EULER_ACCURACY, h0)
n = int((B - A) / h_e)

y_n = iterate(euler, np.array(y0), h_e, n)

t_f = time.time()

print '\ttol:\t{}'.format(tol)
print '\th:\t{}'.format(h_e)
print '\tn:\t{}'.format(n)
print '\tN:\t{}'.format(N)
print '\tyn:\t{}'.format(y_n)
print '\tt:\t{}s\n'.format(t_f - t_s)

dataset.append({'data': np.array(ans), 'name': 'euler'})
ans = []

print 'solve Runge-Kutta:'
t_s = time.time()

h_rk, N = find_step(runge_kutta, RUNGE_KUTTA_ACCURACY, h0)
n = int((B - A) / h_rk)
y_n = iterate(runge_kutta, np.array(y0), h_rk, n)

t_f = time.time()

print '\ttol:\t{}'.format(tol)
print '\th:\t{}'.format(h_rk)
print '\tn:\t{}'.format(n)
print '\tN:\t{}'.format(N)
print '\tyn:\t{}'.format(y_n)
print '\tt:\t{}s\n'.format(t_f - t_s)

dataset.append({'data': np.array(ans), 'name': 'runge-kutta'})
draw(dataset)


from math import exp, log
import numpy as np

def calculate_prices(rates: np.ndarray, t: np.ndarray) -> np.ndarray:
    """Calculate prices from zero-coupon rates

    Args:
        rates: zero-coupon rates vector of length n
        t: time to maturity vector (in years) of length n

    Returns:
        Prices as vector of length n
    """

    return np.power(1 + rates, -t)

def ufr_discount_factor(ufr: float, t: np.ndarray) -> np.ndarray:
    """Calculate Ultimate Forward Rate (UFR) discount factors.

    Takes the UFR with a vector of maturities and returns for each of the
    maturities the discount factor
        d_UFR = e^(-UFR * t)

    Note that UFR is expected to be annualy compounded and that
    this function performs the calculation of the log return prior
    to applying the formula above.

    Args:
        ufr: Ultimate Forward Rate (annualized/annual compounding)
        t: time to maturity vector (in years) of length n

    Returns:
        UFR discount factors as vector of length n
    """

    # Convert annualized ultimate forward rate to log-return
    ufr = log(1 + ufr)
    return np.exp(-ufr * t)

def wilson_function(t1: np.ndarray, t2: np.ndarray, alpha: float, ufr: float) -> np.ndarray:
    """Calculate matrix of Wilson functions

    The Smith-Wilson method requires the calculation of a series of Wilson
    functions. The Wilson function is calculated for each maturity combination
    t1 and t2. If t1 and t2 are scalars, the result is a scalar. If t1 and t2
    are vectors of shape (m, 1) and (n, 1), then the result is a matrix of
    Wilson functions with shape (m, n) as defined on p. 16:
        W = e^(-UFR * (t1 + t2)) * (α * min(t1, t2) - 0.5 * e^(-α * max(t1, t2))
            * (e^(α * min(t1, t2)) - e^(-α * min(t1, t2))))

    Source: EIOPA QIS 5 Technical Paper; Risk-free interest rates – Extrapolation method; p.11ff
    https://wayback.archive-it.org/org-1495/20191229100044/https:/eiopa.europa.eu/Publications/QIS/ceiops-paper-extrapolation-risk-free-rates_en-20100802.pdf

    Args:
        t1: time to maturity vector of length m
        t2: time to maturity vector of length n
        alpha: Convergence speed parameter
        ufr: Ultimate Forward Rate (annualized/annual compounding)

    Returns:
        Wilson-matrix of shape (m, n) as numpy matrix
    """

    # Take time vectors of shape (nx1) and (mx1) and turn them into matrices of shape (mxn).
    # This is achieved by repeating the vectors along the axis 1. The operation is required
    # because the Wilson function needs all possible combinations of maturities to construct
    # the Wilson matrix
    m = len(t1)
    n = len(t2)
    t1_Mat = np.repeat(t1, n, axis=1)
    t2_Mat = np.repeat(t2, m, axis=1).T

    # Calculate the minimum and maximum of the two matrices
    min_t = np.minimum(t1_Mat, t2_Mat)
    max_t = np.maximum(t1_Mat, t2_Mat)

    # Calculate the UFR discount factor - p.16
    ufr_disc = ufr_discount_factor(ufr=ufr, t=(t1_Mat + t2_Mat))
    W = ufr_disc * (alpha * min_t - 0.5 * np.exp(-alpha * max_t) * \
        (np.exp(alpha * min_t) - np.exp(-alpha * min_t)))

    return W

def fit_parameters(rates: np.ndarray, t: np.ndarray, alpha: float, ufr: float) -> np.ndarray:
    """Calculate Smith-Wilson parameter vector ζ

    Given the Wilson-matrix, vector of discount factors and prices,
    the parameter vector can be calculated as follows (p.17):
        ζ = W^-1 * (μ - P)

    Source: EIOPA QIS 5 Technical Paper; Risk-free interest rates – Extrapolation method; p.11ff
    https://wayback.archive-it.org/org-1495/20191229100044/https:/eiopa.europa.eu/Publications/QIS/ceiops-paper-extrapolation-risk-free-rates_en-20100802.pdf

    Args:
        rates: Observed zero-coupon rates vector of length n
        t1: Observed time to maturity vector (in years) of length n
        alpha: Convergence speed parameter
        ufr: Ultimate Forward Rate (annualized/annual compounding)

    Returns:
        Wilson-matrix of shape (m, n) as numpy matrix
    """

    # Calcualte square matrix of Wilson functions, UFR discount vector and price vector
    # The price vector is calculated with zero-coupon rates and assumed face value of 1
    # For the estimation of zeta, t1 and t2 correspond both to the observed maturities
    W = wilson_function(t1=t, t2=t, alpha=alpha, ufr=ufr)
    mu = ufr_discount_factor(ufr=ufr, t=t)
    P = calculate_prices(rates=rates, t=t)

    # Calculate vector of parameters (p. 17)
    # To invert the Wilson-matrix, conversion to type matrix is required
    zeta = np.matrix(W).I * (mu - P)
    zeta = np.array(zeta) # Convert back to more general array type

    return zeta

def fit_smithwilson_rates(rates_obs: np.ndarray, t_obs: np.ndarray, t_target: np.ndarray,
                                alpha: float, ufr: float) -> np.ndarray:
    """Calculate zero-coupon yields with Smith-Wilson method based on observed rates.

    This function expects the rates and initial maturity vector to be
    before the Last Liquid Point (LLP). The targeted maturity vector can
    contain both, more granular maturity structure (interpolation) or terms after
    the LLP (extrapolation).

    The Smith-Wilson method calculated first the Wilson-matrix (p. 16):
        W = e^(-UFR * (t1 + t2)) * (α * min(t1, t2) - 0.5 * e^(-α * max(t1, t2))
            * (e^(α * min(t1, t2)) - e^(-α * min(t1, t2))))

    Given the Wilson-matrix, vector of discount factors and prices,
    the parameter vector can be calculated as follows (p.17):
        ζ = W^-1 * (μ - P)

    With the Smith-Wilson parameter and Wilson-matrix, the zero-coupon bond
    prices can be represented as (p. 18) in matrix notation:
        P = e^(-t * UFR) - W * zeta

    In the last case, t can be any maturity vector

    Source: EIOPA QIS 5 Technical Paper; Risk-free interest rates – Extrapolation method; p.11ff
    https://wayback.archive-it.org/org-1495/20191229100044/https:/eiopa.europa.eu/Publications/QIS/ceiops-paper-extrapolation-risk-free-rates_en-20100802.pdf

    Args:
        rates_obs: Initially observed zero-coupon rates vector before LLP of length n
        t_obs: Initially observed time to maturity vector (in years) of length n
        t_target: New targeted maturity vector (in years) with interpolated/extrapolated terms
        alpha: Convergence speed parameter
        ufr: Ultimate Forward Rate (annualized/annual compounding)

    Returns:
        Vector of zero-coupon rates with Smith-Wilson interpolated or extrapolated rates
    """

    # Convert list to numpy array and use reshape to convert from 1-d to 2-d array
    # E.g. reshape((-1, 1)) converts an input of shape (10,) with second dimension
    # being empty (1-d vector) to shape (10, 1) where second dimension is 1 (2-d vector)
    rates_obs = np.array(rates_obs).reshape((-1, 1))
    t_obs = np.array(t_obs).reshape((-1, 1))
    t_target = np.array(t_target).reshape((-1, 1))

    zeta = fit_parameters(rates=rates_obs, t=t_obs, alpha=alpha, ufr=ufr)
    ufr_disc = ufr_discount_factor(ufr=ufr, t=t_target)
    W = wilson_function(t1=t_target, t2=t_obs, alpha=alpha, ufr=ufr)

    # Price vector - equivalent to discounting with zero-coupon yields 1/(1 + r)^t
    # for prices where t_obs = t_target. All other matuirites are interpolated or extrapolated
    P = ufr_disc - W @ zeta  # '@' in numpy is the dot product of two matrices

    # Transform price vector to zero-coupon rate vector (1/P)^(1/t) - 1
    return np.power(1/P, 1/t_target) - 1


import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import sys

training_data = [] # Training Data Matrix
labels = []

def readData(filenameX,filenameY): # Read data from filenames
	global training_data
	global labels,mean,stddev
	training_data = pd.read_csv(filenameX,header=None)
	training_data[2] = training_data[1]
	training_data[1] = training_data[0]
	training_data[0] = 1
	labels = pd.read_csv(filenameY,header=None)

readData(sys.argv[1],sys.argv[2])

examples_count = training_data.shape[0]
dimensions = training_data.shape[1]
training_data = np.array(training_data.values,dtype='f')
labels = np.array(labels.values,dtype='f')
theta = np.zeros([dimensions,1])

def newtonMethodIteration(theta): # Calculates the change in theta per iteration
	sigmoid_vector = np.zeros([examples_count,1])
	for i in range(examples_count):
		sigmoid_vector[i] = 1/(1+np.exp(-1*np.matmul(np.transpose(theta),training_data[i])))
	gradient = np.matmul(np.transpose(training_data),np.subtract(labels,sigmoid_vector))
	sigmoid_matrix = np.zeros([examples_count,examples_count])
	for i in range(examples_count):
		sigmoid_matrix[i][i] = sigmoid_vector[i]*(1-sigmoid_vector[i])
	hessian = -1*np.matmul(np.transpose(training_data),np.matmul(sigmoid_matrix,training_data))
	return(np.matmul(np.linalg.inv(hessian),gradient))

while 1:
	change = newtonMethodIteration(theta)
	theta = np.subtract(theta,change)
	if np.linalg.norm(change)/np.linalg.norm(theta) < 0.01: # Convergence condition
		break

zeros = {}
ones = {}
zeros["x1"] = []
zeros["x2"] = []
ones["x1"] = []
ones["x2"] = []

for i in range(examples_count):
	if labels[i] == 1:
		ones["x1"].append(training_data[i][1])  # Separate the data into classes
		ones["x2"].append(training_data[i][2])
	elif labels[i] == 0:
		zeros["x1"].append(training_data[i][1])
		zeros["x2"].append(training_data[i][2])

min1 = min([min(zeros["x1"]),min(ones["x1"])])
max1 = max([max(zeros["x1"]),max(ones["x1"])])

plt.scatter(zeros["x1"],zeros["x2"],c='b',marker='o',label='Date (Label 0)') # Plot the data
plt.scatter(ones["x1"],ones["x2"],c='r',marker='^',label='Date (Label 1)')

x1 = []
x2 = []
for i in range(examples_count):
	x1.append(training_data[i][1])
	x2.append(-1*(theta[0]+theta[1]*x1[-1])/theta[2]) # Get the predicted values

x1 = np.array(x1)
x2 = np.array(x2)

print("Theta obtained :",theta.flatten())

plt.plot(x1,x2,c='k',label='Decision Boundary') # Plot the decision boundary
plt.legend(loc='upper left')
plt.xlabel('X1')
plt.ylabel('X2')
plt.title('Logistic Regression')
plt.show()


import numpy as np
import pandas as pd
from scipy.stats import rankdata

def wcov(x, y, w, ms):
    return np.sum(w * (x - ms[0]) * (y - ms[1]))

def wrank(x, w):
    (unique, arr_inv, counts) = np.unique(rankdata(x), return_counts=True, return_inverse=True)
    a = np.bincount(arr_inv, w)
    return (np.cumsum(a) - a)[arr_inv]+((counts + 1)/2 * (a/counts))[arr_inv]

def wpearson(x, y, w):
    mx, my = (np.sum(i * w) / np.sum(w) for i in [x, y])
    return wcov(x, y, w, [mx, my]) / np.sqrt(wcov(x, x, w, [mx, mx]) * wcov(y, y, w, [my, my]))

def wspearman(x, y, w):
    return wpearson(wrank(x, w), wrank(y, w), w)

class WeightedCorr:
    def __init__(self, x=None, y=None, w=None, xyw=None, df=None, wcol=None):
        ''' Weighted Correlation class. Either supply xyw, (x, y, w), or (df, wcol). Call the class to get the result, i.e.:
        WeightedCorr(xyw=mydata[[x, y, w]])(method='pearson')
        :param xyw: pd.DataFrame with shape(n, 3) containing x, y, and w columns (column names irrelevant)
        :param x: pd.Series (n, ) containing values for x
        :param y: pd.Series (n, ) containing values for y
        :param w: pd.Series (n, ) containing weights
        :param df: pd.Dataframe (n, m+1) containing m phenotypes and a weight column
        :param wcol: str column of the weight column in the dataframe passed to the df argument.
        '''
        if (df is None) and (wcol is None):
            if np.all([i is None for i in [xyw, x, y, w]]):
                raise ValueError('No data supplied')
            if isinstance(w, int):
                w = [w] * len(x)
            if not ((isinstance(xyw, pd.DataFrame)) != (np.all([isinstance(i, (pd.Series, np.ndarray, list)) for i in [x, y, w]]))):
                raise TypeError('xyw should be a pd.DataFrame, or x, y, w should be pd.Series')
            if isinstance(x, np.ndarray) or isinstance(x, list):
                x, y, w = pd.Series(x), pd.Series(y), pd.Series(w)
            xyw = pd.concat([x, y, w], axis=1).dropna() if xyw is None else xyw.dropna()
            self.x, self.y, self.w = (pd.to_numeric(xyw[i], errors='coerce').values for i in xyw.columns)
            self.df = None
        elif (wcol is not None) and (df is not None):
            if (not isinstance(df, pd.DataFrame)) or (not isinstance(wcol, str)):
                raise ValueError('df should be a pd.DataFrame and wcol should be a string')
            if wcol not in df.columns:
                raise KeyError('wcol not found in column names of df')
            self.df = df.loc[:, [x for x in df.columns if x != wcol]]
            self.w = pd.to_numeric(df.loc[:, wcol], errors='coerce')
        else:
            raise ValueError('Incorrect arguments specified, please specify xyw, or (x, y, w) or (df, wcol)')

    def _wcov(self, x, y, ms):
        return np.sum(self.w * (x - ms[0]) * (y - ms[1]))

    def _pearson(self, x=None, y=None):
        x, y = (self.x, self.y) if ((x is None) and (y is None)) else (x, y)
        mx, my = (np.sum(i * self.w) / np.sum(self.w) for i in [x, y])
        return self._wcov(x, y, [mx, my]) / np.sqrt(self._wcov(x, x, [mx, mx]) * self._wcov(y, y, [my, my]))

    def _wrank(self, x):
        (unique, arr_inv, counts) = np.unique(rankdata(x), return_counts=True, return_inverse=True)
        a = np.bincount(arr_inv, self.w)
        return (np.cumsum(a) - a)[arr_inv]+((counts + 1)/2 * (a/counts))[arr_inv]

    def _spearman(self, x=None, y=None):
        x, y = (self.x, self.y) if ((x is None) and (y is None)) else (x, y)
        return self._pearson(self._wrank(x), self._wrank(y))

    def spearman(self):
        return self._spearman()

    def pearson(self):
        return self._pearson()

    def __call__(self, method='pearson'):
        '''
        :param method: Correlation method to be used: 'pearson' for pearson r, 'spearman' for spearman rank-order correlation.
        :return: if xyw, or (x, y, w) were passed to __init__ returns the correlation value (float).
                 if (df, wcol) were passed to __init__ returns a pd.DataFrame (m, m), the correlation matrix.
        '''
        if method not in ['pearson', 'spearman']:
            raise ValueError('method should be one of [\'pearson\', \'spearman\']')
        cor = {'pearson': self._pearson, 'spearman': self._spearman}[method]
        if self.df is None:
            return cor()
        else:
            out = pd.DataFrame(np.nan, index=self.df.columns, columns=self.df.columns)
            for i, x in enumerate(self.df.columns):
                for j, y in enumerate(self.df.columns):
                    if i >= j:
                        out.loc[x, y] = cor(x=pd.to_numeric(self.df[x], errors='coerce'), y=pd.to_numeric(self.df[y], errors='coerce'))
                        out.loc[y, x] = out.loc[x, y]
            return out


import numpy as np

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

import matplotlib.pyplot as plt
z = np.linspace(-8, 8, 1000)
y = sigmoid(z)
plt.plot(z, y)
plt.axhline(y=0, ls='dotted', color='k')
plt.axhline(y=0.5, ls='dotted', color='k')
plt.axhline(y=1, ls='dotted', color='k')
plt.yticks([0.0, 0.25, 0.5, 0.75, 1.0])
plt.xlabel('z')
plt.ylabel('y(z)')
plt.show()

# plot sample cost vs y_hat (prediction), for y (truth) = 1
y_hat = np.linspace(0, 1, 1000)
cost = -np.log(y_hat)
plt.plot(y_hat, cost)
plt.xlabel('Prediction')
plt.ylabel('Cost')
plt.xlim(0, 1)
plt.ylim(0, 7)
plt.show()

# plot sample cost vs y_hat (prediction), for y (truth) = 0
y_hat = np.linspace(0, 1, 1000)
cost = -np.log(1 - y_hat)
plt.plot(y_hat, cost)
plt.xlabel('Prediction')
plt.ylabel('Cost')
plt.xlim(0, 1)
plt.ylim(0, 7)
plt.show()


#!/usr/bin/env python3

import numpy as np
import matplotlib.pyplot as plt
import math

from time import perf_counter

n_tosses = 10000

#<---- start timing
start = perf_counter()

x = np.random.rand(n_tosses)
y = np.random.rand(n_tosses)

n_in_circle = 0
for i in range(0, n_tosses - 1):
    if (x[i]**2 + y[i]**2 <= 1):
        n_in_circle += 1

end = perf_counter()
#<---- end timing

pi_estimate = 4 * (n_in_circle / n_tosses)
print("Monte-Carlo Pi Estimator")
print("Estimated π: ", pi_estimate)
print("Percent error: ", 100 * math.fabs(math.pi - pi_estimate) / math.pi, "%")
print("Elapsed time: ", end - start, "seconds")

circle_x = x[np.sqrt(x**2 + y**2) <= 1]
circle_y = y[np.sqrt(x**2 + y**2) <= 1]

fig = plt.figure()
plot = fig.add_subplot(111)
plot.scatter(x, y, marker='.', color='blue')
plot.scatter(circle_x, circle_y, marker='.', color='red')

x = np.linspace(0, 1, 100)
y = np.sqrt(1 - x**2)
plot.plot(x, y, color='black')

plot.set_aspect(1.0)

plt.show()


import numpy as np
import pandas as pd
import math
import scipy
np.set_printoptions(suppress=True)

# Function computing a least squares fit
def beta_ols(Y, X):
    '''
    Estimate OLS coefficients

    Parameters
    ----------
    Y : Nx1 Matrix
    X : Matrix

    Returns
    -------
    beta_hat : vector of coefficients
    '''

    if len(Y.shape) != 1:
        return print('ERROR: Y must be an Nx1 matrix')
    elif Y.shape[0] != X.shape[0]:
        return print('ERROR: Y and X must have the same number of observations')
    else:
        left = X.transpose().dot(X).values
        right = X.transpose().dot(Y).values
        beta_hat = np.linalg.inv(left).dot(right)
        return beta_hat

def resids(Y, X):
    '''
    Estimate OLS residuals

    Parameters
    ----------
    Y : Nx1 Matrix
    X : Matrix

    Returns
    -------
    e : vector of residuals
    '''
    e = beta_ols(Y,X).dot(X.values.T)-Y
    return e

def Sigma(Y,X):
    '''
    ≈

    Parameters
    ----------
    Y : Nx1 Matrix
    X : Matrix

    Returns
    -------
    Sigma : var-cov matrix as pandas df
    '''
    e = resids(Y,X)
    std_hat = e.dot(e.T)/(X.shape[1]-1)
    Sigma = std_hat*np.linalg.inv(X.transpose().dot(X).values)
    return pd.DataFrame(Sigma)

def variance_ols(Y,X):
    '''
    Estimate OLS variance-covariance matrix

    Parameters
    ----------
    Y : Nx1 Matrix
    X : Matrix

    Returns
    -------
    var : variance of coefficients
    '''
    diags = np.diagonal(Sigma(Y, X))
    var = np.sqrt(diags)
    return var

def r2_ols(Y, X):
    '''
    Estimate R^2 for OLS

    Parameters
    ----------
    Y : Nx1 Matrix
    X : Matrix

    Returns
    -------
    R2 : value of R^2
    '''

    y_hat = beta_ols(Y,X).dot(X.values.T)
    y_bar = np.mean(Y)

    SSR = np.sum((y_hat - y_bar)**2)
    SST = np.sum((Y - y_bar)**2)

    r2 = SSR / SST
    return r2

def least_sq(Y, X):
    '''
    Output nicely OLS results

    Parameters
    ----------
    Y : Nx1 Matrix
    X : Matrix

    Returns
    -------
    R2 : value of R^2
    '''

    print('Coefficients = ', beta_ols(Y, X))
    print('Coeff. SErrs = ', np.sqrt(np.diagonal(Sigma(Y,X))))
    print('')
    print('95% Confidence Interval for Coefficients')
    print('     Lower Bound:', beta_ols(Y, X) - 1.96*np.sqrt(np.diagonal(Sigma(Y,X))))
    print('     Upper Bound:', beta_ols(Y, X) + 1.96*np.sqrt(np.diagonal(Sigma(Y,X))))
    print('')
    print('R-Squared:', r2_ols(Y,X))
    print('')
    print("Variance-Covariance Matrix:")
    print(Sigma(Y,X))