import numpy as np
import scipy.stats

def gaussian_samples(n: int, mean: float = 1., sd: float = 1.) -> np.ndarray:

	return scipy.stats.norm(mean, sd).rvs(n)

def uniform_samples(n: int, left: float = 0., right: float = 2.) -> np.ndarray:

	return scipy.stats.uniform(left, right).rvs(n)

def poisson_samples(n: int, mu: float = 2) -> np.ndarray:

	return scipy.stats.poisson(mu).rvs(n)

def gamma_samples(n: int, a: float = 1.99) -> np.ndarray:

	return scipy.stats.gamma(a).rvs(n)

def exponential_samples(n: int) -> np.ndarray:

	return scipy.stats.expon().rvs(n)


import numpy as np

#############################################################################
#   CALCULATE THE EUCLIDEAN DISTANCE BETWEEN TWO POINTS

def ED(a, b):
  return np.sqrt(((a[0]-b[0])**2.0)+((a[1]-b[1])**2.0))

#############################################################################
#   CALCULATE THE ANGLE OF A VERTEX

def angle(A, vertex):
  if vertex == 1:
    p, q, r = ED(A[2],A[0]), ED(A[0],A[1]), ED(A[1],A[2])
  elif vertex == 2:
    p, q, r = ED(A[0],A[1]), ED(A[1],A[2]), ED(A[2],A[0])
  elif vertex == 3:
    p, q, r = ED(A[1],A[2]), ED(A[2],A[0]), ED(A[0],A[1])
  return np.arccos(((p**2.0)+(q**2.0)-(r**2.0))/(2.0*p*q))

#############################################################################
#   CALCULATE THE AREA OF TRIANGLE A

def area(A):
  a, b, c = ED(A[0],A[1]), ED(A[1],A[2]), ED(A[2],A[0])
  s = (a+b+c)/2.0
  return np.sqrt(s*(s-a)*(s-b)*(s-c))

#############################################################################
#   CALCULATE THE PERIMETER OF TRIANGLE A

def perimeter(A):
  return ED(A[0],A[1])+ED(A[1],A[2])+ED(A[2],A[0])

#############################################################################
#   CALCULATE THE CENTROID SIZE OF TRIANGLE A

def centroid_size(A):
  c = centroid(A)
  centroid_size = 0.0
  for vertex in A:
    centroid_size += ED(vertex, c)**2
  return np.sqrt(centroid_size)

#############################################################################
#   FIND THE CENTROID OF TRIANGLE A

def centroid(A):
  return np.array([(A[0][0]+A[1][0]+A[2][0])/3.0,(A[0][1]+A[1][1]+A[2][1])/3.0])

#############################################################################
# ROTATE TRIANGLE A SO THAT IT IS POINTING NORTH

def rotate(A):
  theta = rotation(A)
  c = centroid(A)
  return np.dot(A-c,np.array([[np.cos(theta),np.sin(theta)],[-np.sin(theta),np.cos(theta)]]))+c

#############################################################################
# RETURN THE ANGULAR DISTANCE FROM NORTH FOR TRIANGLE A BY ORIENTING SPOT

def rotation(A):
  B = A - centroid(A)
  return np.arctan2(B[0,0], B[0,1])

#############################################################################
# EQUILATERALNESS RATIO

def upper_bound_on_area(p):
  return p**2.0 / 20.784609690826528

def equilateralness(A):
  a = area(A)
  p = perimeter(A)
  return a / upper_bound_on_area(p)


import numpy as np
import time
import json

#definition of the activation function and its derivative
def sigmoid(x):
    return 1/(1+np.exp(-x))

#derivative of the sigmoid function
def d_sigmoid(x):
    return sigmoid(x)*(1-sigmoid(x))

#definition of the cost function
#squared error
def square_error(y_actual, y_expected):
    return (1/2)*np.square(np.subtract(y_actual,y_expected))

#derivative of the squared error
def d_square_error(y_actual, y_expected):
    return np.subtract(y_actual,y_expected)

#definition of the layer class so that the multi-layered neural network can be implemented
class Layer:
    #this will use a sigmoid activation function

    #this will initialize the layer object
    #if the layer is already trained, the user can input the bias, weight, and learning rate
    def __init__(self,number_inputs = None, number_neurons = None, bias = None, weights = None, learning_rate=None):

        if(number_inputs is not None and number_neurons is not None):
            # initializing the weights with random numbers between 0 to 1
            self.weights = np.random.randn(number_neurons, number_inputs)

            #initialize the bias with one
            self.bias = np.ones((number_neurons,1))

            #default learning rate is 0.5. Don't change these value pls.
            #use the training method to change the learning rate
            self.learning_rate = 0.5

        elif (bias is not None and weights is not None and learning_rate is not None):
            self.weights = weights

            self.bias = bias

            self.learning_rate = learning_rate

        else:
            raise Exception("The input arguements are not enough to initialize the layer object")

    def feedforward(self,inputs):
        #apply feedforward algorithm with equation 4 here
        self.previous_output = inputs
        self.Z = np.dot(self.weights,inputs) + self.bias
        self.A = sigmoid(self.Z)
        return self.A

    def backpropagate(self, dC_dA):
        #applying the formulas for updatting of the weights and biases
        #refer to equation 8
        dC_dZ = np.multiply(d_sigmoid(self.Z),dC_dA)

        #normalize the values
        #since the dot product and the np.sum is summed values from the samples.
        # With this code below, the results of the dot product and the summed values
        #will be average, respectively
        dC_dW = 1/ dC_dZ.shape[1] * np.dot(dC_dZ,self.previous_output.T)

        #np.sum is necessary to make the shape of the dC_dB and dC_dZ same
        #np.sum finding the sum along the column matrix to reduce it one column matrix.
        #it sums across the samples
        dC_dB =  1/ dC_dZ.shape[1] * np.sum(dC_dZ, axis=1,keepdims=True)
        #print("dC_dW : {}, dC_dB: {}, dC_dZ: {}". format(dC_dW,dC_dB,dC_dZ)) # uncomment this to see the respectively values

        #update the weights and biases using equation 7
        self.weights = self.weights - np.multiply(self.learning_rate,dC_dW)
        self.bias = self.bias - np.multiply(self.learning_rate,dC_dB)

        #return this value for the backpropagation of the different layer
        return np.dot(self.weights.T,dC_dZ)

#With the layer class, it will provide an abstraction to neural network for organization
class NeuralNetwork:

    # Documentation:
    #num_neurons_per_layer is a list that contains number of neurons per layer
    #each element represent a number of neurons for that layer
    #the minimum number of elements for num_neurons_per_layer is two
    #Ex. [1,2,3] - this signifies that there is one input in the input layer.
    # there are 2 neurons for the first idden layer and also, it has one hidden layer
    #Lastly, there are 3 neurons at the output layer
    #this constructor can load also from the json file which contains the model
    #filepath_model contains the filepath of the model to be loaded
    #By default, it will check for neural_network_model in the current relative location
    def __init__(self,num_neurons_per_layer=[],filepath_model="neural_network_model.json"):

        if(len(num_neurons_per_layer) > 0):
            #for the checking if num_neurons_per_layer have at least two elements
            assert len(num_neurons_per_layer) >= 2, "num_neurons_per_layer input should have at least two elements"

            #create a list of layers
            self.layers = [ Layer(number_inputs = num_neurons_per_layer[i], number_neurons = num_neurons_per_layer[i+1]) for i in range(len(num_neurons_per_layer) - 1)]
        else:

            #loading of json file
            with open(filepath_model,  "r") as read_file:
                model = json.load(read_file) #this holds a dictionary within a dictionary

            self.layers = []
            for nth_layer in model:

                learning_rate = model[nth_layer]["learning_rate"]
                weights = np.array(model[nth_layer]["weights"])
                bias =  np.array(model[nth_layer]["bias"])
                layer = Layer(learning_rate=learning_rate, weights=weights, bias = bias)

                self.layers.append(layer)

    def train(self, x_train, y_train, learning_rate = 0.5, epochs = 10000):

        if learning_rate != 0.5:
            #change the learning rate accrodingly for each of the layers
            for layer in self.layers:
                layer.learning_rate = learning_rate

        #TRAINING SESSION PROPER
        for epoch in range(epochs):

            #feedforward
            input = x_train
            for layer in self.layers:
                input = layer.feedforward(input)

            #print("Input: {}".format(input))

            #calculation of the derivative of the cost based on the input variable
            dCost = d_square_error(input,y_train)

            #uncomment this to know the cost for each of the epoch
            #size_y_train = y_train.shape[0]
            #cost = (1/size_y_train) * square_error(input,y_train).sum()
            #print("Output: {} Cost: {}".format(input,cost)) #uncomment this show the output

            #backpropagation
            for layer in reversed(self.layers):
                dCost = layer.backpropagate(dCost)

    def predict(self,x_predict):
        #implements the feed forward
        y_predict = x_predict
        for layer in self.layers:
                y_predict = layer.feedforward(y_predict)

        return y_predict

    #filepath input is the filepath were the model to be saved and its name
    #By default, it will save in the current relative location with neural_network_model as its filename
    #noted that it will use a json file
    def save_model(self,filepath="neural_network_model.json"):

        #this will create a dictionary for putting the text into the json file
        model ={}

        for i,layer in enumerate(self.layers):

            temp = vars(layer)#this will get the attributes of the layer object

            #this part will only get the important parts of the model
            #which is the bias, weights, and the learning rate
            layer_model =  {}

            layer_model["learning_rate"] = temp["learning_rate"]
            layer_model["weights"] = temp["weights"].tolist()
            layer_model["bias"] = temp["bias"].tolist()

            model[f"layer_{i+1}"] = layer_model

        #putting the model into a json file
        with open(filepath,'w') as nn_model:
            json.dump(model,nn_model, indent= 1)


import numpy as np
from numpy import array, dot
from math import atan2, asin, cos, sin

def quat2rpy(q):
	q_norm = (q[0]**2+q[1]**2+q[2]**2+q[3]**2)
	q = [q_i/q_norm for q_i in q]
	w = q[0]
	x = q[1]
	y = q[2]
	z = q[3]
	rpy = [atan2(2*(w*x + y*z), w*w + z*z - (x*x + y*y)),
  		   asin(2*(w*y - z*x)),
           atan2(2*(w*z + x*y), w*w + x*x - (y*y + z*z))]
	return rpy

def rotx(theta):
    c = cos(theta)
    s = sin(theta)
    M = array([[1,0,0],[0,c,-s],[0,s,c]])
    return M

def roty(theta):
    c = cos(-theta)
    s = sin(-theta)
    M = array([[c,0,-s],[0,1,0],[s,0,c]])
    return M

def rotz(theta):
    c = cos(theta)
    s = sin(theta)
    M = array([[c,-s,0],[s,c,0],[0,0,1]])
    return M

def rpy2rotmat(rpy):
    R = dot(rotz(rpy[2]),dot(roty(rpy[1]),rotx(rpy[0])))
    return R

def body2world(rpy, xyz):
    R = rpy2rotmat(rpy)
    xyz_world = dot(R,array(xyz))
    return xyz_world.tolist()

def world2body(rpy, xyz):
    R = rpy2rotmat(rpy)
    xyz_body = dot(R.T,array(xyz))
    return xyz_body.tolist()

def angularvel2rpydot(rpy, omega):
	p = rpy[1]
	y = rpy[2]
	sy = sin(y)
	cy = cos(y)
	sp = sin(p)
	cp = cos(p)
	tp = sp/cp
	Phi = array([[cy/cp, sy/cp, 0],[-sy, cy, 0],[cy*tp, tp*sy, 1]])
	rpydot = dot(Phi,omega)
	return rpydot.tolist()


"""

Gianluca Scarpellini - lab1 - Computer and Robot Vision

g.scarpellini1[at]disco.unimib.it

DLT: projection matrix estimation using SVD and MSE

"""

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

points_im = np.array([
    (1864, 1682),
    (1572, 1862),
    (2628, 1719),
    (654, 1818),
    (1271, 1604),
    (136, 2145),
    (3096, 1678),
    (2086, 1944)
])

points_w = np.array([
    (450, 740, 45),
    (297, 591, 50),
    (510, 1190, 1),
    (252, 130, 49),
    (492, 385, 85),
    (0, 0, 0),
    (538, 1415, 13),
    (140, 870, 35)
])

H = np.zeros([16, 12])
i = 0
for point_im, point_w in (zip(points_im, points_w)):
    H[i, :] = np.array([-point_w[0], -point_w[1], -point_w[2], -1, 0, 0, 0, 0,
                        point_im[0] * point_w[0], point_im[0] * point_w[1],
                        point_im[0] * point_w[2], point_im[0]
                        ])

    H[i + 1, :] = np.array([0, 0, 0, 0, -point_w[0], -point_w[1], -point_w[2], -1,
                            point_im[1] * point_w[0], point_im[1] * point_w[1],
                            point_im[1] * point_w[2], point_im[1]
                            ])
    i += 2

_, _, vh = np.linalg.svd(H, full_matrices=True)

# Column with lower singular value
P = np.reshape(vh[-1, :], [3, 4])

results = [P @ np.array([x, y, z, 1]) for x, y, z in points_w]
results = np.array([(int(result[0] / result[2]), int(result[1] / result[2])) for result in results])

im = cv2.cvtColor(cv2.imread('./im.jpg'), cv2.COLOR_BGR2RGB)

plt.imshow(im)
for x, y in results:
    plt.plot(x, y, 'bo', markersize=3)
for u, v in points_im:
    plt.plot(u, v, 'go', markersize=3)
plt.show()

mse = (np.linalg.norm(results - points_im, axis=0)).mean(axis=0)
print(f'MSE {mse}')


from typing import List
import numpy as np
from matplotlib import pyplot as plt
from math import sin, pi

def f(x):
    return sin(2 * pi * x)

def h(x, theta):
    n = theta.shape[0] - 1
    result = 0
    for i in range(n + 1):  # from 0 to n
        result += theta[i] * x ** i
    return result

def loss(x_list, y_list, theta, lmd = 1e-8):
    sum = 0
    for x, y in zip(x_list, y_list):
        sum += (h(x, theta) - y) ** 2
    for i in range(theta.shape[0]):
        sum += lmd * abs(theta[i])
    return sum / 2

def grad_descent(x_list, y_list, theta, l):
    alpha = 1e-2
    n = theta.shape[0] - 1
    result = np.zeros(n + 1)
    for k in range(n + 1):
        for x, y in zip(x_list, y_list):
            result[k] += x ** k * (h(x, theta) - y)
    temp = theta - alpha * result
    return temp, l(x_list, y_list, temp)

def l1_grad_descent(x_list, y_list, theta, l = loss):
    alpha = 1e-2
    n = theta.shape[0] - 1
    result = np.copy(theta)
    prev = l(x_list, y_list, result)
    for k in range(n + 1):
        while True:
            result[k] += alpha
            temp_loss = l(x_list, y_list, result)
            if prev <= temp_loss:
                result[k] -= alpha
                break
            prev = temp_loss
        while True:
            result[k] -= alpha
            temp_loss = l(x_list, y_list, result)
            if prev <= temp_loss:
                result[k] += alpha
                break
            prev = temp_loss
    return result, prev

def fit(x_list: List[int], y_list: List[int], k: int = 4, descent = grad_descent, l = loss):
    count = 0
    max_round = 10000000
    theta = np.zeros(k + 1)
    while count < max_round:
        temp, cost = descent(x_list, y_list, theta, l)
        # if np.linalg.norm(temp - theta) < np.linalg.norm(theta) * eps:
        if cost <= 1:
            break
        count += 1
        theta = temp
        if count % 10000 == 0:
            print(count, cost)
    # print(theta)
    return theta

if __name__ == '__main__':
    n = 30
    x_list = np.linspace(0, 1, n)
    y_list = np.sin(2 * np.pi * x_list)
    plt.plot(x_list, y_list)
    plt.savefig('./data/sin.jpg')

    theta = fit(x_list, y_list, 30, grad_descent)
    new_y_list = h(x_list, theta)
    plt.plot(x_list, new_y_list, color='r')
    plt.savefig('./data/gradient_descent.jpg')

    theta = fit(x_list, y_list, 30, l1_grad_descent)
    new_y_list = h(x_list, theta)
    plt.plot(x_list, new_y_list, color='g')
    plt.savefig('./data/lasso.jpg')


import numpy as np

def regConvex(x, lamda, arg=None):
    """
    Returns f, g, Hv of lamda*||x||^2
    """
    f = 0.5*lamda*np.dot(x.T, x)
    if arg == 'f':
        return f

    g = lamda*x
    if arg == 'g':
        return g
    if arg == 'fg':
        return f, g

    Hv = lambda v: lamda*v

    if arg is None:
        return f, g, Hv

def regNonconvex(x, lamda, a=1, arg=None):
    """
    returns  f, g, Hv of lamda*a*x^2/(1+a*x^2)
    """
    ax2 = a*x**2
    f = lamda*sum(ax2/(1+ax2))
    if arg == 'f':
        return f

    g = lamda*2*a*x/(1+ax2)**2
    if arg == 'g':
        return g
    if arg == 'fg':
        return f, g

    Hv = lambda v: lamda*2*a*(1-3*ax2)/(1+ax2)**3*v

    if arg is None:
        return f, g, Hv


import numpy as np

# linear algebra utility functions

def normalize_cols(M):
  return M.astype(float) / M.sum(axis=0)

def normalize_rows(M):
  return normalize_cols(M.T).T

def is_col_stochastic(M, threshold=1e-8):
  return np.all(np.absolute(M.sum(axis=0) - np.ones(M.shape[1])) <= threshold)

def is_row_stochastic(M, threshold=1e-8):
  return is_col_stochastic(M.T, threshold)


import numpy as np

# passed in gradient from the next layer
# for the purpose of this example we're going to use
# an array of an incremental gradient values
dvalues = np.array([[1, 1, 1],
                    [2, 2, 2],
                    [3, 3, 3]])

# we have 3 sets of inputs - samples
inputs = np.array([[1, 2, 3, 2.5],
                   [2, 5, -5, 2],
                   [-1.5, 2.7, 3.3, -0.8]])

# we have 3 sets of weights - one set for each neuron
# we have 4 inputs, thus 4 weights
# recall that we keep weights transposed
weights = np.array([[0.2, 0.8, -0.5, 1],
                    [0.5, -0.91, 0.26, -0.5],
                    [-0.26, -0.27, 0.17, 0.87]]).T

# one bias for each neuron
# biases are the row vector with a shape (1, neurons)
biases = np.array([[2, 3, 0.5]])

# forward pass
# dense layer
layer_outputs = np.dot(inputs, weights) + biases
# ReLU activation
relu_outputs = np.maximum(0, layer_outputs)

#let's optimize and test backpropagation here
# ReLU activation - simulates derivative with respect to input values
# from next layer passed to current layer during backpropagation
drelu = relu_outputs.copy()
drelu[layer_outputs<=0] = 0

# dense layer
# dinputs - multiply by weights
dinputs = np.dot(drelu, weights.T)
# dweights - multiply by inputs
dweights = np.dot(inputs.T, drelu)
# dbiases - sum values, do this over samples (first axis), keepdims
# since this by default will produce a plain list -
# we explained this in the chapter 4
dbiases = np.sum(drelu, axis=0, keepdims=True)

# update parameters
weights += -0.001 * dweights
biases += -0.001 * dbiases

print(weights)
print(biases)


"""
Computes equilibrium price and quantities, take 2.
"""
from numpy import exp
from scipy.optimize import bisect

def supply(price, b):
   return exp(b * price) - 1

def demand(price, a, epsilon):
   return a * price**(-epsilon)

def compute_equilibrium(a, b, epsilon):
    plow = 0.1
    phigh = 10.0
    def excess_supply(price):
        return supply(p, b) - demand(p, a, epsilon)

    pclear = bisect(excess_supply, plow, phigh)
    return pclear

parameter_list = ((1, 0.1, 1),
                  (2, 0.1, 1),
                  (1, 0.2, 1),
                  (1, 0.1, 2))

for parameters in parameter_list:
    print("Price = {}".format(compute_equilibrium(*parameters)))


import pandas as pd
import numpy_financial as npf
import matplotlib.pyplot as plt
import seaborn as sns

def payments_per_period(rate_in_months, per, period_in_months, principal):
    # The interest payment for a given period
    rate_in_months = rate_in_months / 100 / 12
    period_in_months = period_in_months * 12
    interest_payment = npf.ipmt(rate_in_months, per, period_in_months, -principal)

    # The principal payment for a given period
    principal_payment = npf.ppmt(rate_in_months, per, period_in_months, -principal)

    total_payment = interest_payment + principal_payment

    print(f'Period # {per}')
    print('Interest Payment: {:,.2f}'.format(interest_payment))
    print('Principal Payment: {:,.2f}'.format(principal_payment))
    print('Total Payment: {:,.2f}'.format(total_payment))
    print('Split: {:.2%} Interest {:,.2%} Principal'.format(interest_payment / total_payment, principal_payment / total_payment))

def create_table(start_date, rate_in_months, period_in_years, principal, additional_pmt, show = True):
    period_in_months = period_in_years * 12
    rate_in_months = rate_in_months / 100 / 12
    rng = pd.date_range(start= pd.to_datetime(start_date).date(), periods= period_in_months, freq='MS')
    rng

    # Creading the data frame

    df = pd.DataFrame(index=rng, columns=['Payment','Principal','Interest','Additional Principal','Balance'])
    df.index.name = "Period"

    # Adding the payment is easy since it will always be the same

    df['Payment'] = npf.pmt(rate_in_months, period_in_months, principal)

    # The principal and interest change over time but since we can grab the periods using df.index, we can just use that in our formula
    df['Principal'] = npf.ppmt(rate_in_months, df.reset_index().index + 1, period_in_months, -principal)
    df['Interest'] = npf.ipmt(rate_in_months, df.reset_index().index + 1, period_in_months, -principal)
    df['Additional Principal'] = additional_pmt

    df['Cumulative Principal'] = (df['Principal'] + df['Additional Principal']).cumsum()
    df['Cumulative Principal'] = df['Cumulative Principal'].clip(upper = principal)
    df['Balance'] = principal - df['Cumulative Principal']
    pd.set_option('display.max_rows', None)

    # There is a better way to do the below thing but im too tired to figure it out
    if (df['Balance'] <= 0).sum() == 0:
        if show == True:
            print(f"Total Interest Paid: ${df['Interest'].sum():,.2f}")
            display(df.round(2))
        else:
            return df
    elif (df['Balance'] <= 0).sum() >= 1:
        df = df.reset_index()
        last_payment = df[df['Balance'] <= 0][:1].index
        df.loc[last_payment, "Principal"] = df.loc[last_payment[0] - 1, "Balance"]
        df.loc[last_payment, "Payment"] = - df.loc[last_payment[0], ["Principal", "Interest"]].sum()
        df.loc[last_payment, "Additional Principal"] = 0
        df = df.loc[0:last_payment[0]]
        if show == True:
            print(f"Total Interest Paid: ${df['Interest'].sum():,.2f}")
            df = df.set_index('Period')
            pd.options.display.float_format = '{:,}'.format
            display(df.round(2))
        else:
            return df

### Creating the amortization graph
def create_graph(start_date, rate_in_months, period_in_years, principal, additional_pmt):
   """ This function takes in the input data and then displays a graph representing the balance of an amortized loan"""
   df = create_table(
        start_date,
        rate_in_months,
        period_in_years,
        principal,
        additional_pmt,
        show=False,
    )
   if additional_pmt == 0:
      display(x=sns.lineplot(x="Period", y="Balance", data=df))
   else:
      base_df = create_table(
         start_date, rate_in_months, period_in_years, principal, 0, show=False
      )
      display(
         x=sns.lineplot(
            x="Period", y="Balance", data=df, label="With Additional Payments"
         ),
         y=sns.lineplot(
            x="Period",
            y="Balance",
            data=base_df,
            label="Without additional Payments",
         ),
         z=plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.0),
      )


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

import numpy as np
import scipy.stats as sp

def d1(option, spot, riskfree, dividend, volatility):
    t = option.days_to_expiry() / 365
    d1 = (np.log(spot / option.strike) + ((riskfree - dividend) + 0.5 * volatility ** 2) * t) / (volatility * np.sqrt(t))
    return d1

def d2(option, spot, riskfree, dividend, volatility):
    t = option.days_to_expiry() / 365
    d2 = d1(option, spot, riskfree, dividend, volatility) - volatility * np.sqrt(t)
    return d2

def price(option, spot, riskfree, dividend, volatility):
    cdf = sp.norm(0, 1).cdf
    t = option.days_to_expiry() / 365
    if (option.type == "call"):
        price = spot * np.exp(-dividend * t) * cdf(d1(option, spot, riskfree, dividend, volatility)) - option.strike * np.exp(-riskfree * t) * cdf(d2(option, spot, riskfree, dividend, volatility))
    elif (option.type == "put"):
        price = np.exp(-riskfree * t) * option.strike * cdf(-d2(option, spot, riskfree, dividend, volatility)) - spot * np.exp(-dividend * t) * cdf(-d1(option, spot, riskfree, dividend, volatility))
    return price

def main():
    pass

if __name__ == "__main__":
    main()


"""
A file for executing math functions.
"""

from typing import Union
import numpy as np

def euler(n = 10):
    """
    This function computes :math:`e` via a Taylor series.

    .. math::

        e = 1 + \\sum_n^\\infty \\frac{1}{n!}

    Parameters
    ----------
    n : int
        The order of the Taylor expansion.

    Returns
    -------
    e_value : float
        The computed value of Euler's number.
    """

    if n < 0:
        raise ValueError("Only positive integers are allowed")

    n += 1
    e_value = 0
    for i in range(n):
        e_value += 1 / factorial(i)
    return e_value

def factorial(n):
    if n > 0:
        return n * factorial(n - 1)
    else:
        return 1

def pi(n: Union[int, float] = 1e4) -> float:
    return 4 * np.mean(np.linalg.norm(np.random.rand(int(n), 2), axis=-1) < 1)


#2次のRunge-Kutta法による常微分方程式の解法プログラム
import numpy as np  #NumPyライブラリ
import matplotlib.pyplot as plt  #データ可視化ライブラリ

#導関数dy/dt
def func_dydt(t, y):
    return t**2.0 -3.0  #dy/dt = t^2 -3.0

#2次のRunge-Kutta法
def runge_kutta2(func_dydt, t, y, dt):
    k1 = func_dydt(t, y)*dt
    k2 = func_dydt(t+dt, y+k1)*dt
    dy = (k1 +k2)/2.0  #変化量を計算

    t = t +dt  #変数を更新
    y = y +dy  #変化量を加えて更新

    return t, y

#常微分方程式を逐次計算
def ode_calc(func_dydt, y_start, t_start, t_end, dt):
    num_calc = 0  #計算回数
    t_div = np.abs((t_end-t_start)/dt)  #格子分割数
    if(t_end<t_start):  #負の方向に計算する時は刻み幅の符号を反転
        dt = -dt

    #初期値
    t = t_start  #独立変数t
    y = y_start  #従属変数y
    print("t = {:.7f},  y = {:.7f}".format(t, y))

    #グラフ用データを追加
    t_list = [t]
    y_list = [y]

    #ずっと繰り返す
    while(True):
        t, y  = runge_kutta2(func_dydt, t, y, dt)  #変化量を計算
        print("t = {:.7f},  y = {:.7f}".format(t, y))

        #グラフ用データを追加
        t_list.append(t)
        y_list.append(y)

        num_calc += 1  #計算回数を数える

        #「計算回数が格子分割数以上」ならば終了
        if(t_div<=num_calc):
            print("Finished.")
            print()
            break

    return t_list, y_list

#可視化
def visualization(t_list, y_list):
    plt.xlabel("$t$")  #x軸の名前
    plt.ylabel("$y(t)$")  #y軸の名前
    plt.grid()  #点線の目盛りを表示

    plt.plot(t_list,y_list, label="$y(t)$", color='#ff0000')  #折線グラフで表示
    plt.legend(loc='best') #凡例(グラフラベル)を表示
    plt.show()  #グラフを表示

#メイン実行部
if (__name__ == '__main__'):
    #2次のRunge-Kutta法でdt離れた点の値を取得（導関数、tの初期値、yの初期値、刻み幅dt）
    t, y = runge_kutta2(func_dydt, 0.0, 0.0, 1e-1)
    print("t = {:.7f},  y = {:.7f}".format(t, y))

    #常微分方程式を逐次計算（導関数、yの初期値、tの開始点、tの終了点、刻み幅dt）
    t_list, y_list = ode_calc(func_dydt, 0.0, -5.0, 5.0, 1e-1)

    #結果を可視化
    visualization(t_list, y_list)


from numpy import dot, linalg

class ols(object):
    '''The ordinary least squares (OLS) or linear least squares is a method
for estimating the unknown parameters in a linear regression model.

The matrix formulation of OLS is given by the linear system of equations:

.. math::

    y = X \beta + \epsilon

.. attribute:: y

    The *regressand* also known as the the *endogenous* or *dependent* variable.
    This is a n-dimensional vector.

.. attribute:: x

    The *regressor* also known as the *exogenous* or *independent* variable
    This is a :math:`n \times k` matrix, where each column of which is a
    n-dimensional vector which must be linearly independent from the others.
'''
    def __init__(self, y, X):
        self.y = y
        self.X = X

    def beta(self):
        '''\
The linear estimation of the parameter vector :math:`\beta` given by

.. math::

    \beta = (X^T X)^-1 X^T y

'''
        t = self.X.transpose()
        XX = dot(t,self.X)
        XY = dot(t,self.y)
        return linalg.solve(XX,XY)


import matplotlib.pyplot as plt
import numpy as np
from scipy.linalg import svd

def f(x):
    return 1 / (1 + 25 * x * x)

def get_A(x, n):
    A = np.empty((x.size, n))
    for column in range(n):
        A[:, column] = x ** column
    return A

def p(x, alpha):
    my_y = 0
    for i, alpha_i in enumerate(alpha):
        my_y += alpha_i * x ** i
    return my_y

def error(x, alpha):
    return abs(f(x) - p(x, alpha))

x_first = -1
x_last = 1
m = 10
n_start = 1
n_stop = 4
accuracy = 100

x = np.linspace(x_first, x_last, m)
x_accurate = np.linspace(x_first, x_last, accuracy)
y = f(x)
for n in range(n_start, n_stop):
    A = get_A(x, n)
    U, s, Vh = svd(A)
    alpha = np.zeros(n)
    for i in range(n):
        alpha += (U[:,i] @ y) * Vh[i,:] / s[i]
    print('m = ', m)
    print('n = ', n)
    print('error in 0: ', error(0, alpha))
    print('error in -0.7: ', error(-0.7, alpha))
    print('error in +0.7: ', error(0.7, alpha))
    plt.plot(x_accurate, f(x_accurate))
    plt.plot(x_accurate, p(x_accurate, alpha))
    plt.show()


import numpy as np

N = 4

def get_xp_operators(N:int,s:float,hbar=1) -> np.ndarray:
    """Using the representation of position and momentum operators in the number-operator basis of the Harmonic oscillator and truncating it to an integer value N, we obtain the x,p operator in matrix forms"""
    diag_elems = np.sqrt(np.arange(1,N))
    x = (s/np.sqrt(2))*(np.diag(diag_elems,-1) + np.diag(diag_elems,+1))
    p = 1j/(s*np.sqrt(2))*(np.diag(diag_elems,-1) - np.diag(diag_elems,+1))
    return x,p

N = 20
x,p = get_xp_operators(N,1)

# Quadratic/ Harmonic Oscillator
H = (p @ p)/2  + (x @ x )/2

egval,_ = np.linalg.eig(H)
print('Eigenvalues of the Hamiltonian for the Quantum Harmonic Oscillator :\n',egval)

# Quartic Oscillator
# H0 = (p @ p)/2  + (x @ x @ x @ x)/2
#
# egval,egvec = np.linalg.eig(H0)
#
# print(np.sort(egval))


"""A collection of functions for summary statistics."""
import numpy as _np

def mad(x):
    """
    Calculate the median absolute deviation.

    Parameters
    ----------
    x : array_like
        The sample

    Returns
    -------
    float
        The median absolute deviation of the sample.

    """
    return _np.nanmedian(_np.abs(x - _np.nanmedian(x)))

def zscore(x, robust_zscore=False):
    """
    Calculate Z-score of the sample.

    Parameters
    ----------
    x : array_like
        The sample
    robust_zscore : bool, optional
        If `True`, use the Iglewicz-Hoaglin robust Z-score [IH93]_ instead of
        the original Z-score. Default is `False`.

    Returns
    -------
    float
        The calculated Z-score of the sample.

    References
    ----------
    .. [IH93] Iglewicz, Boris and Hoaglin, David C. (1993). How to Detect and
        Handle Outliers. ASQC Quality Press, Milwaukee, WI, 1993.

    """
    if not robust_zscore:
        return (x - _np.nanmean(x)) / _np.nanstd(x, ddof=1)
    else:
        return 0.6745 * (x - _np.nanmedian(x)) / mad(x)

def interquartile(x, axis=None):
    """
    Calculate the interquartile range of an array.

    Parameters
    ----------
    x : array_like
        The sample.
    axis : int, optional
        Axis along which the percentiles are computed. Default is to ignore
        and compute the flattened array.
        (Same as the `axis` argument in `numpy.nanpercentile()`.)

    Returns
    -------
    float or array_like
        The interquartile range of the sample.

    """
    if _np.sum(_np.isfinite(x)) > 0:
        q1, q3 = _np.nanpercentile(x, [25.0, 75.0], axis=axis)
        return q3 - q1
    else:
        return _np.nan

def resist_mean(x, inlier_range=1.5):
    """
    Calculate outlier-resistant mean of the sample using Tukey's outlier test.

    Warning: Does not support calculation along an axis, unlike `numpy.mean()`.

    Parameters
    ----------
    x : array_like
        The sample. Must be one-dimensional.
    inlier_range : float, optional
        Parameter to control the inlier range defined by
        [Q_1 - inlier_range * (Q_3 - Q_1), Q_3 - inlier_range * (Q_3 - Q_1)]
        Default value is 1.5.

    Returns
    -------
    rmean : float
        The resistant mean of the sample with outliers removed.

    References
    ----------
    .. [T77] John W. Tukey (1977). Exploratory Data Analysis. Addison-Wesley.

    """
    x = _np.array(x)
    if _np.sum(_np.isfinite(x)) <= 1:
        return _np.nanmean(x)
    else:
        q1, q3 = _np.nanpercentile(x, [25.0, 75.0])
        iqr = q3 - q1
        uplim = q3 + inlier_range * iqr
        lolim = q1 - inlier_range * iqr
        rmean = _np.nanmean(x[(x >= lolim) & (x <= uplim)])
        return rmean

def resist_std(x, inlier_range=1.5):
    """
    Calculate outlier-resistant standard deviation of the sample using
    Tukey's outlier test.

    Warning: Does not support calculation along an axis, unlike `numpy.std()`.

    Parameters
    ----------
    x : array_like
        The sample. Must be one-dimensional.
    inlier_range : float, optional
        Parameter to control the inlier range defined by
        [Q_1 - inlier_range * (Q_3 - Q_1), Q_3 - inlier_range * (Q_3 - Q_1)]
        Default value is 1.5.

    Returns
    -------
    rstd : float
        The resistant standard deviation of the sample with outliers removed.
        Degree of freedom = 1 is enforced for the sample standard deviation.

    References
    ----------
    .. [T77] John W. Tukey (1977). Exploratory Data Analysis. Addison-Wesley.

    """
    x = _np.array(x)
    if _np.sum(_np.isfinite(x)) <= 1:
        return _np.nanstd(x, ddof=1)
    else:
        q1, q3 = _np.nanpercentile(x, [25.0, 75.0])
        iqr = q3 - q1
        uplim = q3 + inlier_range * iqr
        lolim = q1 - inlier_range * iqr
        rstd = _np.nanstd(x[(x >= lolim) & (x <= uplim)], ddof=1)
        return rstd

def dixon_test(x, left=True, right=True, q_conf="q95"):
    """
    Use Dixon's Q test to identify one or two outliers. The test is based upon
    two assumptions: (1) data must be normally distributed; (2) the test may
    only be used once to a dataset and not repeated.

    Adapted from: <http://sebastianraschka.com/Articles/2014_dixon_test.html>
    (Retrieved 23 Apr 2017).

    Parameters
    ----------
    x : array_like
        Data points. Must be one-dimensional.
    left : bool, optional
        If True, test the minimum value.
    right : bool, optional
        If True, test the maximum value.
        (At least one of the two, `left` or `right`, must be True.)
    q_conf : str, optional
        Confidence level: 'q95' -- 95% confidence (default). Other options
        supported are 'q90' (90% C.I.) and 'q99' (99% C.I.).

    Returns
    -------
    outliers : list
        A list of two values containing the outliers or None. The first element
        corresponds to the minimum value, and the second element corresponds
        to the maximum value. If the tested value is not an outlier, return
        None at its position.

    References
    ----------
    .. [1] Dean, R. B. and Dixon, W. J. (1951). Simplified Statistics for Small
       Numbers of Observations. Anal. Chem., 23(4), 636—638.
    .. [2] Dixon, W. J. (1953). Processing data for outliers Reference.
       J. Biometrics, 9, 74–89.
    .. [3] Rorabacher, D. B. (1991). Statistical Treatment for Rejection of
       Deviant Values: Critical Values of Dixon Q Parameter and Related
       Subrange Ratios at the 95 percent Confidence Level. Anal. Chem., 63(2),
       139–146.

    """
    # critical Q value table
    q_dicts = {
        "q90": [
            0.941,
            0.765,
            0.642,
            0.560,
            0.507,
            0.468,
            0.437,
            0.412,
            0.392,
            0.376,
            0.361,
            0.349,
            0.338,
            0.329,
            0.320,
            0.313,
            0.306,
            0.300,
            0.295,
            0.290,
            0.285,
            0.281,
            0.277,
            0.273,
            0.269,
            0.266,
            0.263,
            0.260,
        ],
        "q95": [
            0.970,
            0.829,
            0.710,
            0.625,
            0.568,
            0.526,
            0.493,
            0.466,
            0.444,
            0.426,
            0.410,
            0.396,
            0.384,
            0.374,
            0.365,
            0.356,
            0.349,
            0.342,
            0.337,
            0.331,
            0.326,
            0.321,
            0.317,
            0.312,
            0.308,
            0.305,
            0.301,
            0.290,
        ],
        "q99": [
            0.994,
            0.926,
            0.821,
            0.740,
            0.680,
            0.634,
            0.598,
            0.568,
            0.542,
            0.522,
            0.503,
            0.488,
            0.475,
            0.463,
            0.452,
            0.442,
            0.433,
            0.425,
            0.418,
            0.411,
            0.404,
            0.399,
            0.393,
            0.388,
            0.384,
            0.38,
            0.376,
            0.372,
        ],
    }
    # cast to numpy array and remove NaNs
    x_arr = _np.array(x)
    x_arr = x_arr[_np.isfinite(x_arr)]
    # minimum and maximum data sizes allowed
    min_size = 3
    max_size = len(q_dicts[q_conf]) + min_size - 1
    if len(x_arr) < min_size:
        raise ValueError(
            "Sample size too small: "
            + "at least %d data points are required" % min_size
        )
    elif len(x_arr) > max_size:
        raise ValueError("Sample size too large")

    if not (left or right):
        raise ValueError(
            "At least one of the two options, "
            + "`left` or `right`, must be True."
        )

    q_crit = q_dicts[q_conf][len(x_arr) - 3]

    # for small dataset, the built-in `sorted()` is faster than `np.sort()`
    x_sorted = sorted(x_arr)

    x_range = x_sorted[-1] - x_sorted[0]
    if x_range == 0:
        outliers = [None, None]
    else:
        Q_min = abs((x_sorted[1] - x_sorted[0]) / x_range)
        Q_max = abs((x_sorted[-1] - x_sorted[-2]) / x_range)
        outliers = [
            x_sorted[0] if (Q_min > q_crit) and (Q_min >= Q_max) else None,
            x_sorted[-1] if (Q_max > q_crit) and (Q_max >= Q_min) else None,
        ]

    return outliers


# Probabilidades

#1. nominal, continua, continua
# diagnosis = ['M', 'B']
hrange = [[df["radius1"].min(), df["radius1"].max()],
          [df["texture1"].min(), df["texture1"].max()]]

#2
def ECDF(x):
    x = np.sort(x)
    n = len(x)
    def result(a):
        return np.searchsorted(x, a, side='right')/n
    return result

rango_radius = np.linspace(df["radius1"].min(), df["radius1"].max())

fig, ax = plt.subplots()
for diag in ['M', 'B']:
    sample = df.loc[df.diagnosis==diag]["radius1"]
    cumprob = np.cumsum(counts).astype(np.double) / sample.size
    ax.plot(rango_radius, ECDF(sample)(rango_radius), label=diag)
ax.set_xlabel('Radio')
ax.legend()

#3.
for diag in ["M", "B"]:
    sample = df.loc[df.diagnosis==diag]["radius1"]
    print(diag, ECDF(sample)(15.))

#4.
for diag in ["M", "B"]:
    sample = df.loc[df.diagnosis==diag]["texture1"]
    print(diag, ECDF(sample)(20.) - ECDF(sample)(10.))

#5.
# Fe de erratas: Estoy asumiendo P(A y B) = P(A)P(B) Es decir que las probabilidades de ambos eventos son independientes
# Pero en la práctica es un supuesto que debemos comprobar
for diag in ["M", "B"]:
    sample = df.loc[df.diagnosis==diag][["radius1", "texture1"]].values
    print(diag, ECDF(sample[:, 0])(15.)*(ECDF(sample[:, 1])(20.) - ECDF(sample[:, 1])(10.)))

# Histogramas

fig, ax = plt.subplots(figsize=(6, 3), tight_layout=True)
ax.hist(df.loc[df.diagnosis=='M']["radius1"], alpha=0.5, label='Maligno', density=True)
ax.hist(df.loc[df.diagnosis=='B']["radius1"], alpha=0.5, label='Benigno', density=True)
ax.set_xlabel('Radio')
ax.legend();

fig, ax = plt.subplots(figsize=(6, 3), tight_layout=True)
ax.hist(df.loc[df.diagnosis=='M']["texture1"], alpha=0.5, label='Maligno', density=True)
ax.hist(df.loc[df.diagnosis=='B']["texture1"], alpha=0.5, label='Benigno', density=True)
ax.set_xlabel('Textura')
ax.legend();

fig, ax = plt.subplots(1, 2, figsize=(6, 3), tight_layout=True, sharex=True, sharey=True)

hrange = [[df["radius1"].min(), df["radius1"].max()],
          [df["texture1"].min(), df["texture1"].max()]]

print(hrange)
ax[0].hist2d(df.loc[df.diagnosis=='M']["radius1"],
             df.loc[df.diagnosis=='M']["texture1"],
             cmap=plt.cm.Blues, alpha=0.5, label='Maligno', bins=15, range=hrange)
ax[1].hist2d(df.loc[df.diagnosis=='B']["radius1"],
             df.loc[df.diagnosis=='B']["texture1"],
             cmap=plt.cm.Oranges, alpha=0.5, label='Benigno', bins=15, range=hrange)
ax[0].set_xlabel('Radius')
ax[0].set_ylabel('Textura');

# Dados y Teorema central del límite

import scipy.stats
import matplotlib.pyplot as plt
from matplotlib import animation

fig, ax = plt.subplots(figsize=(8, 3))

def update_plot(k):
    ax.cla()
    ax.set_title("Promedio de {0} lanzamiento/s de dado".format(k+1))
    dist = scipy.stats.multinomial(n=k+1, p=[1/6]*6)
    repeats = dist.rvs(size=1000)
    average_dice = np.sum(repeats*range(1, 7)/(k+1), axis=1)
    ax.hist(average_dice, bins=12, density=True)
    ax.set_xlim([1, 6])

anim = animation.FuncAnimation(fig, update_plot, frames=15, interval=1000,
                               repeat=True, blit=False)


import math
import numpy as np
import quaternion

def convert_euler_to_quaternion(roll, yaw, pitch):
    """Converts roll, yaw, pitch to a quaternion.
    """

    cy = math.cos(math.radians(roll) * 0.5)
    sy = math.sin(math.radians(roll) * 0.5)

    cp = math.cos(math.radians(yaw) * 0.5)
    sp = math.sin(math.radians(yaw) * 0.5)

    cr = math.cos(math.radians(pitch) * 0.5)
    sr = math.sin(math.radians(pitch) * 0.5)

    w = cy * cp * cr + sy * sp * sr
    x = cy * cp * sr - sy * sp * cr
    y = sy * cp * sr + cy * sp * cr
    z = sy * cp * cr - cy * sp * sr

    quat = np.array([w, x, y, z])
    quat = quat / np.linalg.norm(quat)
    return quat

def get_relative_rotation(src_orientation, dest_orientation):
    a = quaternion.quaternion(src_orientation[0], src_orientation[1],
                              src_orientation[2], src_orientation[3])
    b = quaternion.quaternion(dest_orientation[0], dest_orientation[1],
                              dest_orientation[2], dest_orientation[3])

    relative_rotation = quaternion.as_float_array(b * a.inverse())

    return relative_rotation

def get_frame_index(name, frame):
    for idx in range(len(frame)):
        if frame.iloc[idx, 0] == name:
            return idx
    raise Exception(
        "Could not find image {} in data frame, unsuccessful in finding frame index".format(
            name))

def convert_euler_to_quaternion_idiap(yaw, pitch, roll):
    yaw = math.radians(yaw)
    roll = math.radians(roll)
    pitch = math.radians(pitch)
    cy = math.cos(yaw * 0.5)
    sy = math.sin(yaw * 0.5)
    cp = math.cos(pitch * 0.5)
    sp = math.sin(pitch * 0.5)
    cr = math.cos(roll * 0.5)
    sr = math.sin(roll * 0.5)

    w = cy * cp * cr + sy * sp * sr
    x = cy * cp * sr - sy * sp * cr
    y = sy * cp * sr + cy * sp * cr
    z = sy * cp * cr - cy * sp * sr

    quat = np.array([w, x, y, z])
    quat = quat / np.linalg.norm(quat)
    return quat

def quaternion_to_euler(w, x, y, z):
    sinr_cosp = +2.0 * (w * x + y * z)
    cosr_cosp = +1.0 - 2.0 * (x * x + y * y)
    pitch = math.atan2(sinr_cosp, cosr_cosp)

    sinp = +2.0 * (w * y - z * x)
    sinp = +1.0 if sinp > +1.0 else sinp
    sinp = -1.0 if sinp < -1.0 else sinp
    yaw = math.asin(sinp)

    siny_cosp = +2.0 * (w * z + x * y)
    cosy_cosp = +1.0 - 2.0 * (y * y + z * z)
    roll = math.atan2(siny_cosp, cosy_cosp)

    return roll, yaw, pitch


from abc import ABC, abstractmethod
import numpy as np

__all__ = [
    "Regularizer",
    "L1",
    "L2",
]

class Regularizer(ABC):
    @abstractmethod
    def __call__(self, W):
        pass

    @abstractmethod
    def gradient(self, W):
        pass

# ==================================================================================================== #
# ==================================================================================================== #

class L1(Regularizer):
    """Performs absolute norm"""

    def __init__(self, lambd):
        self.lambd = lambd

    def __call__(self, W):
        return self.lambd * np.sum(np.abs(W))

    def gradient(self, W):
        return self.lambd * np.abs(W)

# ==================================================================================================== #

class L2(Regularizer):
    """Performs Forbenius Norm (squared norm)"""

    def __init__(self, lambd):
        self.lambd = lambd

    def __call__(self, W):
        return (self.lambd / 2) * np.sum(np.square(W))

    def gradient(self, W):
        return self.lambd * W

# ==================================================================================================== #


#import necessary packages
import numpy as np
import math
import matplotlib.pyplot as plt
from scipy.stats import norm
from pandas_datareader import data

#download Apple price data into DataFrame
apple = data.DataReader('AAPL', 'yahoo',start='1/1/2000')

#calculate the compound annual growth rate (CAGR) which
#will give us our mean return input (mu)
days = (apple.index[-1] - apple.index[0]).days
cagr = ((((apple['Adj Close'][-1]) / apple['Adj Close'][1])) ** (365.0/days)) - 1
print ('CAGR =',str(round(cagr,4)*100)+"%")
mu = cagr

#create a series of percentage returns and calculate
#the annual volatility of returns
apple['Returns'] = apple['Adj Close'].pct_change()
vol = apple['Returns'].std()*sqrt(252)
print ("Annual Volatility =",str(round(vol,4)*100)+"%")

#Define Variables
S = apple['Adj Close'][-1] #starting stock price (i.e. last available real stock price)
T = 252 #Number of trading days
mu = 0.2309 #Return
vol = 0.4259 #Volatility

#create list of daily returns using random normal distribution
daily_returns=np.random.normal(mu/T,vol/math.sqrt(T),T)+1

#set starting price and create price series generated by above random daily returns
price_list = [S]

for x in daily_returns:
    price_list.append(price_list[-1]*x)

#Generate Plots - price series and histogram of daily returns
plt.plot(price_list)
plt.show()
plt.hist(daily_returns-1, 100) #Note that we run the line plot and histogram separately, not simultaneously.
plt.show()

import numpy as np
import math
import matplotlib.pyplot as plt
from scipy.stats import norm

#Define Variables
S = apple['Adj Close'][-1] #starting stock price (i.e. last available real stock price)
T = 252 #Number of trading days
mu = 0.2309 #Return
vol = 0.4259 #Volatility

#choose number of runs to simulate - I have chosen 1000
for i in range(1000):
    #create list of daily returns using random normal distribution
    daily_returns=np.random.normal(mu/T,vol/math.sqrt(T),T)+1

    #set starting price and create price series generated by above random daily returns
    price_list = [S]

    for x in daily_returns:
        price_list.append(price_list[-1]*x)

    #plot data from each individual run which we will plot at the end
    plt.plot(price_list)

#show the plot of multiple price series created above
plt.show()

import numpy as np
import math
import matplotlib.pyplot as plt
from scipy.stats import norm

#set up empty list to hold our ending values for each simulated price series
result = []

#Define Variables
S = apple['Adj Close'][-1] #starting stock price (i.e. last available real stock price)
T = 252 #Number of trading days
mu = 0.2309 #Return
vol = 0.4259 #Volatility

#choose number of runs to simulate - I have chosen 10,000
for i in range(10000):
    #create list of daily returns using random normal distribution
    daily_returns=np.random.normal(mu/T,vol/math.sqrt(T),T)+1

    #set starting price and create price series generated by above random daily returns
    price_list = [S]

    for x in daily_returns:
        price_list.append(price_list[-1]*x)

    #plot data from each individual run which we will plot at the end
    plt.plot(price_list)

    #append the ending value of each simulated run to the empty list we created at the beginning
    result.append(price_list[-1])

#show the plot of multiple price series created above
plt.show()

#create histogram of ending stock values for our mutliple simulations
plt.hist(result,bins=50)
plt.show()

#use numpy mean function to calculate the mean of the result
print(round(np.mean(result),2))

print("5% quantile =",np.percentile(result,5))
print("95% quantile =",np.percentile(result,95))
plt.hist(result,bins=100)
plt.axvline(np.percentile(result,5), color='r', linestyle='dashed', linewidth=2)
plt.axvline(np.percentile(result,95), color='r', linestyle='dashed', linewidth=2)
plt.show()

# ref: http://www.pythonforfinance.net/2016/11/28/monte-carlo-simulation-in-python/


# -*- coding: utf-8 -*-
"""
Created on Wed Mar 27 11:56:18 2019

INSTITUTO FEDERAL DE EDUCAÇÃO, CIÊNCIA E TECNOLOGIA DO PÁRA - IFPA ANANINDEUA

@author:
        Prof. Dr. Denis C. L. Costa

        Discentes:
            Heictor Alves de Oliveira Costa
            Lucas Pompeu Neves

Grupo de Pesquisa:
                 Gradiente de Modelagem Matemática e
                 Simulação Computacional - GM²SC

Assunto:
        Operações com Matrizes

Nome do sript: operacoes_matrizes

Disponível em:
    https://github.com/GM2SC/DEVELOPMENT-OF-MATHEMATICAL-METHODS-IN-
    COMPUTATIONAL-ENVIRONMENT/blob/master/SINEPEM_2019/operacoes_matrizes.py

"""
# Biblioteca: numpy
import numpy as np

print('')
print('=======================================')
# Operações com Matrizes
# Adição de Matrizes
print("Adição de Matrizes")
A = np.array([[4, 2],[3, -1]])
B = np.array([[5, 2],[1, 0]])
C = A + B
print("A =\n",A,"\n")
print("B =\n",B,"\n")
print('C = A + B:')
print("C =\n",C,"\n")
print('')

# Multiplicação de Matrizes
print("Multiplicação de Matrizes")
D = np.array([[3, 6, 7],[5, -3, 0]])
print("D =\n",D)
print('')
E = np.array([[1,1],[2,1],[3,-3]])
print("E =\n",E)
print('')

F = D.dot(E)
print('F = DxE:')
print("F = \n",F)
print('')

# Subtração de Matrizes
print("Subtração de Matrizes")
G = A + B
print('G = A - B:')
print("G =\n",G,"\n")
print('')

# Matriz Transposta
print("At = Transposta de A:")
At = A.transpose()
print("At =\n",At)
print('')

# Matriz Inversa
Inv_A = np.linalg.inv(A)
print("Inv_A = Inversa de A:")
print("Inv_A =\n",Inv_A)
print('')

# Determinante de uma Matriz
print("Determinante da Matriz A:")
Det_A = np.linalg.det(A)
print("Det_A =\n",Det_A)
print('')
print('=======================================')
print(' ---> Fim do Programa operacoes_matrizes <---')


import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
plt.style.use('fivethirtyeight')

# Numerical Solution of Yoyo despinning

![Yoyo Despin on a track](./yoyo-despin_01.svg)

The image above shows a proposed yoyo despin mechanism that has two
masses attached to a sliding track that keeps the two small masses, $m$,
in line with the larger cylindrical mass, $M$.

$\ddot{r} = r \dot{\theta}^2$

$\ddot{r} = r\left(\frac{2mr_0^2+MR^2/2}{2mr^2+MR^2/2}\right)^2 \dot{\theta}(0)$

where $\dot{\theta} = \Omega(t)$

Here you plot the relation between angular velocity and distance from center of cylinder just based upon conservation of angular momentum:

m=0.1 #kg
M=1 #kg
R=0.1 #meter
w0=10 # rad/s
r=np.linspace(0.1,2)
h0=(2*m*r[0]**2+M*r[0]**2/2)*w0

wt = h0/(2*m*r**2+M*R**2/2)

plt.plot(r,wt)
plt.xlabel('yoyo distance (m)')
plt.ylabel('angular velocity (rad/s)')
plt.title('conservation of angular momentum\n h=constant');

## Define the state and d/dt(state)

In this part, we define the second order differential equation as a state-space form

the state = $[r,~\dot{r}]$

and the derivative of the state is

d/dt(state) = $[\dot{r},~\ddot{r}]=[\dot{r},~\frac{F_r}{m}]$

We call the function, `yoyo_ode(y,t)`, where `y` is the state and `t` is the current time.

def yoyo_ode(y,t):
    '''define d2r/dt2= r*(h0/2m)^2/(M*R^2/4m+r^2)^2'''
    dr=np.zeros(np.shape(y))
    dr[0]=y[1]
    dr[1]=y[0]*h0**2/(2*m*y[0]**2+M*R**2/2)**2
    return dr

yoyo_ode([0.15,1],0)

The function `odeint` integrates our `yoyo_ode` based upon the initial condtions,

$[r(0),~\dot{r}(0)] = [0.1~m,~0~m/s]$

and the time span of interest,

$t = [0-0.5~s]$

in the line, `t=np.linspace(0,0.5)`

t=np.linspace(0,0.5)
r=odeint(yoyo_ode,[0.1,0],t)

plt.plot(t,r[:,0])
plt.xlabel('time (s)')
plt.ylabel('yoyo pos (m)')

## To save to file from python output

Have to join the time, $r$, and $\dot{r}$ into an array then save to a file:

np.savetxt('t_r_rdot.csv',np.array([t,r[:,0],r[:,1]]).T,delimiter=',')

This line of code organizes a comma-separated-value file into the file t_r_rdot.csv with no headers.

|time (s)| r (m) | $\dot{r}$ (m/s)|
|---|---|---|
|0| 0.1| 0 |
|...|...|...|


import numpy as np

run_test = False

def make_hilbert_tensor(shape):
    # T_ijkl = 1/(i+j+k+l)
    kk = np.unravel_index(np.arange(np.prod(shape)), shape)
    T = np.zeros(shape)
    T[kk] = 1. / np.sum(tuple(np.array(kk) + 1), axis=0)
    return T

if run_test:
    shape = (3,2,5,4)
    T_true = np.zeros(shape)
    for ii in [1,2,3]:
        for jj in [1,2]:
            for kk in [1,2,3,4,5]:
                for ll in [1,2,3,4]:
                    T_true[ii-1, jj-1, kk-1, ll-1] = 1. / (ii+jj+kk+ll)

    T = make_hilbert_tensor(shape)

    err_make_hilbert_tensor = np.linalg.norm(T - T_true)/np.linalg.norm(T_true)
    print('err_make_hilbert_tensor=', err_make_hilbert_tensor)

    S = make_hilbert_tensor((3,3))
    S_true = np.array([[1./(1.+1.), 1./(1.+2.), 1./(1.+3.)],
                       [1./(2.+1.), 1./(2.+2.), 1./(2.+3.)],
                       [1./(3.+1.), 1./(3.+2.), 1./(3.+3.)]])
    err_3x3 = np.linalg.norm(S_true - S)/np.linalg.norm(S_true)
    print('err_3x3=', err_3x3)


import scipy as sp
import scipy.linalg
from scipy.io import savemat
#---------------------------------------------------------------------------------------
def rlsloo_ll1( V, D, Y, lambd):
	"""
	Computes cs and the actual LOO errors for a single value of lambda. (lambd)
	"""
        n = V.shape[0]
        cl = Y.shape[1]

        inner  = 1/(D + lambd)
	inner = inner.conj()
        VtY = sp.dot(V.T, Y)
	VtY = VtY.conj()

        # Because of signs of D are flipped (scipy.linalg.eig returns
        # flipped signs for complex part of the eigenvalues)
        in_dot = sp.ones((n,1)) * inner
        ViD = V * in_dot
        cs = sp.dot(ViD, VtY)
        dGi = sp.sum(ViD*V, axis = 1)
        # -- till here works fine
        #check matrix dimensions
        looerrs = cs.ravel()/sp.real(dGi.ravel())
	looerrs = sp.real(looerrs)
        cs = sp.real(cs.transpose())

        return cs.ravel(), looerrs

#---------------------------------------------------------------------------------------
def rlsloo_ll(V, D, Y, lambdas=None):
	"""
	Input:
	V, D = eigenvectors and eigen values from eigen value decomposition
	lambdas = default used in our computation
	Output:
	cs = is a matrix of size representing function weights of lambda
	loos = total LOO error vector for nonlinear RLS with lambda

	"""

	n  = V.shape[0]
	cl = Y.shape[1]
	l = len(lambdas)

	cs = sp.zeros((l, cl, n))
	loos = sp.zeros((l,cl))
	loos[:] = sp.inf

	for i in range(l):
		#        print D, Y, lambdas[i]
		csll, looerrsll = rlsloo_ll1(V, D, Y, lambdas[i])
		cs[i][:][:] = csll
		loos[i][:] = sp.sqrt( sp.sum( looerrsll**2, axis=0) )

	return cs, loos

#---------------------------------------------------------------------------------------
def rlsloo(K, Y, lambdas=None):
	"""
	Nonlinear regularized least squares.
	Input:
	K = is an n by n symmetric kernel matrix.
	Y = Classes
	lamdas = default as used by rifkin lambdas = logspace(-6,6,30);

	Output:
	cs = is a matrix of size representing function weights of lambda
	loos = total LOO error vector for nonlinear RLS with lambda

	"""

    # This is where the problem is:
    # we are getting different signs
	D, V = scipy.linalg.eig(K)

	print "Eig shape D and V", D.shape, V.shape
	cs, loos = rlsloo_ll(V, D, Y, lambdas)
	D = D.conj()
	# for some odd reason we have to take conjugate to make the values as in matlab
	print cs.shape, loos.shape
	return cs, loos


# For this part of the assignment, You can use inbuilt functions to compute the fourier transform
# You are welcome to use fft that are available in numpy and opencv

from numpy import sqrt, zeros
import matplotlib.pyplot as plt

class Filtering:
    image = None
    filter = None
    cutoff = None
    order = None

    def __init__(self, image, filter_name, cutoff, order = 0):
        """initializes the variables frequency filtering on an input image
        takes as input:
        image: the input image
        filter_name: the name of the mask to use
        cutoff: the cutoff frequency of the filter
        order: the order of the filter (only for butterworth
        returns"""
        self.filter_name = filter_name
        self.image = image
        if filter_name == 'ideal_l':
            self.filter = self.get_ideal_low_pass_filter
        elif filter_name == 'ideal_h':
            self.filter = self.get_ideal_high_pass_filter
        elif filter_name == 'butterworth_l':
            self.filter = self.get_butterworth_low_pass_filter
        elif filter_name == 'butterworth_h':
            self.filter = self.get_butterworth_high_pass_filter
        elif filter_name == 'gaussian_l':
            self.filter = self.get_gaussian_low_pass_filter
        elif filter_name == 'gaussian_h':
            self.filter = self.get_gaussian_high_pass_filter

        self.cutoff = cutoff
        self.order = order

    def get_ideal_low_pass_filter(self, shape, cutoff):
        """Computes a Ideal low pass mask
        takes as input:
        shape: the shape of the mask to be generated
        cutoff: the cutoff frequency of the ideal filter
        returns a ideal low pass mask"""

        mask = zeros(shape)
        row_size, col_size = shape[0], shape[1]
        center_row, center_col = row_size/2 , col_size/2
        for r in range(0, row_size):
            for c in range(0, col_size):
                freq_dist = sqrt( (r-center_row)**2 + (c-center_col)**2 )
                mask[r,c] = 0.0 if freq_dist > cutoff else 1.0

        return mask

    def get_ideal_high_pass_filter(self, shape, cutoff):
        """Computes a Ideal high pass mask
        takes as input:
        shape: the shape of the mask to be generated
        cutoff: the cutoff frequency of the ideal filter
        returns a ideal high pass mask"""

        #Hint: May be one can use the low pass filter function to get a high pass mask
        return 1 - self.get_ideal_low_pass_filter(shape, cutoff)

    def get_butterworth_low_pass_filter(self, shape, cutoff, order):
        """Computes a butterworth low pass mask
        takes as input:
        shape: the shape of the mask to be generated
        cutoff: the cutoff frequency of the butterworth filter
        order: the order of the butterworth filter
        returns a butterworth low pass mask"""

        mask = zeros(shape)
        row_size, col_size = shape[0], shape[1]
        center_row, center_col = row_size/2 , col_size/2
        for r in range(0, row_size):
            for c in range(0, col_size):
                freq_dist = sqrt( (r-center_row)**2 + (c-center_col)**2 )
                mask[r,c] = (1/(1+(freq_dist/cutoff)*order)) if freq_dist > cutoff else 1.0

        return mask

    def get_butterworth_high_pass_filter(self, shape, cutoff, order):
        """Computes a butterworth high pass mask
        takes as input:
        shape: the shape of the mask to be generated
        cutoff: the cutoff frequency of the butterworth filter
        order: the order of the butterworth filter
        returns a butterworth high pass mask"""

        #Hint: May be one can use the low pass filter function to get a high pass mask
        mask = zeros(shape)
        row_size, col_size = shape[0], shape[1]
        center_row, center_col = row_size/2 , col_size/2
        for r in range(0, row_size):
            for c in range(0, col_size):
                freq_dist = sqrt( (r-center_row)**2 + (c-center_col)**2 )
                mask[r,c] = (1/(1+(freq_dist/cutoff)*order*2)) if freq_dist < cutoff else 1.0

        return mask
        # return 1 - self.get_butterworth_low_pass_filter(shape, cutoff, order*2) # wrong ?

    def get_gaussian_low_pass_filter(self, shape, cutoff):
        """Computes a gaussian low pass mask
        takes as input:
        shape: the shape of the mask to be generated
        cutoff: the cutoff frequency of the gaussian filter (sigma)
        returns a gaussian low pass mask"""
        from numpy import exp

        mask = zeros(shape)
        row_size, col_size = shape[0], shape[1]
        center_row, center_col = row_size/2 , col_size/2
        for r in range(0, row_size):
            for c in range(0, col_size):
                freq_dist = sqrt( (r-center_row)**2 + (c-center_col)**2 )
                mask[r,c] = (exp(-(freq_dist**2)/(cutoff*2)**2)) if freq_dist > cutoff else 1.0

        return mask

    def get_gaussian_high_pass_filter(self, shape, cutoff):
        """Computes a gaussian high pass mask
        takes as input:
        shape: the shape of the mask to be generated
        cutoff: the cutoff frequency of the gaussian filter (sigma)
        returns a gaussian high pass mask"""

        #Hint: May be one can use the low pass filter function to get a high pass mask
        from numpy import exp
        mask = zeros(shape)
        row_size, col_size = shape[0], shape[1]
        center_row, center_col = row_size/2 , col_size/2
        for r in range(0, row_size):
            for c in range(0, col_size):
                freq_dist = sqrt( (r-center_row)**2 + (c-center_col)**2 )
                mask[r,c] = 1 - (exp(-(freq_dist**2)/(cutoff*2)**2)) if r < cutoff else 1.0

        return mask

    def post_process_image(self, image):
        """Post process the image to create a full contrast stretch of the image
        takes as input:
        image: the image obtained from the inverse fourier transform
        return an image with full contrast stretch
        -----------------------------------------------------
        1. Full contrast stretch (fsimage)
        2. take negative (255 - fsimage)
        """

        from numpy import max, min

        max_value, min_value = max(image), min(image)
        # print(min_value, max_value)
        # print(image)
        min_value = 0 if min_value < 0 else min_value
        _image = 255 * ( ( image - min_value )/ (max_value - min_value) )
        # print('next')
        # print(min(_image), max(_image))
        # print(_image)
        return 255 - _image.astype('uint8')

    def filtering(self):
        """Performs frequency filtering on an input image
        returns a filtered image, magnitude of DFT, magnitude of filtered DFT
        ----------------------------------------------------------
        You are allowed to used inbuilt functions to compute fft
        There are packages available in numpy as well as in opencv
        Steps:
        1. Compute the fft of the image
        2. shift the fft to center the low frequencies
        3. get the mask (write your code in functions provided above) the functions can be called by self.filter(shape, cutoff, order)
        4. filter the image frequency based on the mask (Convolution theorem)
        5. compute the inverse shift
        6. compute the inverse fourier transform
        7. compute the magnitude
        8. You will need to do a full contrast stretch on the magnitude and depending on the algorithm you may also need to
        take negative of the image to be able to view it (use post_process_image to write this code)
        Note: You do not have to do zero padding as discussed in class, the inbuilt functions takes care of that
        filtered image, magnitude of DFT, magnitude of filtered DFT: Make sure all images being returned have grey scale full contrast stretch and dtype=uint8
        """
        from numpy import fft
        import numpy as np

        _image_dft = fft.fft2(self.image)
        _image_dft = fft.fftshift(_image_dft)
        # dft = DFT.DFT()
        # plt.figure(1)
        # plt.imshow(self.image)
        # plt.figure(2)
        # plt.imshow(20*np.log10(abs(_image_dft)))
        # print(_image_dft)
        # print(abs(_image_dft))
        # plt.show()
        filter = self.filter(self.image.shape, self.cutoff, self.order) \
                 if self.filter_name.startswith('butterworth') \
                    else self.filter(self.image.shape, self.cutoff)

        _image_dft_filtered = _image_dft * filter
        _image_filtered = abs(fft.ifft2(_image_dft_filtered))

        return [ self.post_process_image(_image_filtered), \
                 self.post_process_image(20*np.log10(abs(_image_dft)+.00001)),   \
                 self.post_process_image(20*np.log10(abs(_image_dft_filtered)+.00001)) ]

# filtered image, magnitude of DFT, magnitude of filtered DFT


import numpy as np
from scipy.spatial.distance import squareform
from random import randint

# there are more efficient algorithms for this
# https://people.csail.mit.edu/virgi/6.890/papers/APBP.pdf
def max_min(A, B):
    '''max-min product of two square matrices
    params:
        A, B: NxN numpy arrays '''
    assert A.shape == B.shape
    return np.max(np.minimum(A[:, :, None], B[None, :, :]), axis=1)

def mat_gromov_prod(dists, base):
    '''Gromov products of N-point metric space relative to base point
    Args:
        dists (ndarray): NxN matrix of pairwise distances
        base (int): index of the basepoint in 0...N-1 '''
    assert dists.shape[0] == dists.shape[1] and 0 <= base < dists.shape[0]
    row = dists[base, :][None, :]
    col = dists[:, base][:, None]
    return 0.5*(row+col-dists)

def delta_rel(dists, base=None):
    ''' Measure the delta-hyperbolicity constant of data
    with respect to basepoint, normalized by the diameter (max dist).
    Args:
        dists (ndarray): NxN matrix of pairwise distances
        base (int): index of basepoint in 0...N-1 (default = random)
    '''
    if base is None:
        base = randint(0,dist.shape[0]-1)
    assert is_metric(dists) and 0 <= base < dists.shape[0]
    G = mat_gromov_prod(dists, base)
    delta = np.max(max_min(G,G)-G)
    diam = np.max(dists)
    return delta/diam

def delta_sample(X, **kwargs):
    bs = kwargs.get("bs", X.shape[0])
    tries = kwargs.get("tries", 10)
    dist = kwargs.get("dist", None)
    deltas = []
    for i in range(tries):
        idx = np.random.choice(X.shape[0], bs)
        batch = X[idx]
        if dist is None:
            dists = np.linalg.norm(
                        batch[None:,]-batch[:,None],
                        axis=-1)
        else:
            dists = dist(batch,batch)
        deltas.append(
                delta_rel(dists,randint(0,bs-1))
                )
    return deltas

def is_metric(X, tol=1e-8):
    return len(X.shape) == 2 and \
            np.all( np.abs(X-X.T)<tol ) and\
            np.all( np.abs(np.diag(X))<tol ) and\
            np.all(X >= 0)

def avg_distortion(metric1, metric2):
    ''' Average distortion between two metrics.
        Args:
            metric1, metric2 (ndarray): N x N distance matrices,
                    or length N*(N-1)//2 compressed distance matrices
        Returns:
            average distortion (float)
    '''
    assert metric1.shape == metric2.shape
    if len(metric1.shape) > 1:
        assert is_metric(metric1)
        X = squareform(metric1)
    else:
        X = metric1
    if len(metric2.shape) > 1:
        assert is_metric(metric2)
        Y = squareform(metric2)
    else:
        Y = metric2
    return np.mean( np.abs(X-Y)/Y )


import csv
from sympy import symbols, lambdify
import numpy as np
import math
import copy

def approxinterp_read_csv(filename):
    """
    Функция считывания данных из файла .csv

    Parameters
    ----------
    filename : str
        Абсолютный или локальный путь к файлу.

    Returns
    -------
    x_list : [float, float, ...]
        Список координат х.
    y_list : [float, float, ...]
        Список координат у.
    n : int
        Длина выборки координат.

    """

    with open(filename, 'r') as file:
        data = csv.reader(file, delimiter=',')
        data = list(data)
        y_list = []
        x_list = []
        for i in range(len(data)):
            x_list.append(eval(data[i][0]))
            y_list.append(eval(data[i][1]))
    x_list = np.array(x_list)
    y_list = np.array(y_list)
    n = len(x_list)
    return x_list, y_list, n

def polinom_chebyshev(x_list, y_list, n):
    """
    Фильтрация выборки координа методом полинома Чебышева

    Parameters
    ----------
    x_list : [float, float, ...]
        Список координат х.
    y_list : [float, float, ...]
        Список координат у.
    n : int
        Длина выборки координат.

    Returns
    -------
    new_x_list : [float, float, ...]
        Новый список координат х.
    new_y_list : [float, float, ...]
        Новый список координат у.
    new_n : int
        Новая длина выборки координат.

    """

    new_n = n // 8
    new_index = sorted(list(set([round((np.math.cos((2 * k + 1) * np.math.pi / (2 * new_n + 2)) + 1) * (n - 1) / 2) for k in range(new_n)][::-1])))
    new_x_list = x_list[new_index]
    new_y_list = y_list[new_index]
    return new_x_list, new_y_list, new_n

def lagranje(x_list, y_list):
    """
    Интерполяция выборки методом Лагранжа

    Parameters
    ----------
    x_list : [float, float, ...]
        Список координат х.
    y_list : [float, float, ...]
        Список координат у.

    Returns
    -------
    new_L : function
        Интерполированная функция для введённой выборки.

    """

    x = symbols('x')
    L = 0
    l = []
    for j in range(len(x_list)):
        l.append(1)
        for k in range(len(x_list)):
            if k!=j:
                l[j] *= ((x-x_list[k])/(x_list[j]-x_list[k]))
        L += y_list[j]*l[j]
    new_L = lambdify(x, L)
    return new_L

def newton(x_list, y_list, interp_type=True):
    """
    Интерполяция выборки методом Ньютона

    Parameters
    ----------
    x_list : [float, float, ...]
        Список координат х.
    y_list : [float, float, ...]
        Список координат у.
    interp_type : bool, optional
        Тип интерполяции (вперёд\назад).
        По умолчанию True - вперёд.
        False - назад.

    Raises
    ------
    ValueError
        Возникает при передаче не равноотстоящих узлов.

    Returns
    -------
    func : function
        Интерполированная функция для введённой выборки.

    """

    for i in range(len(x_list)-2):
        if abs((x_list[i+1] - x_list[i]) - (x_list[i+2] - x_list[i+1])) > 0.1 ** 12:
            raise ValueError('Данный метод применим только для равноотстоящих узлов!')

    #конечные разности
    d = []
    d.append(y_list)
    for i in range(len(y_list)-1):
        temp = d[i]
        d.append([])
        for j in range(len(temp)-1):
            d[i+1].append(temp[j+1]-temp[j])

    if interp_type:
        #интерполяция вперед
        x = symbols('x')
        h = x_list[1] - x_list[0]
        n = len(x_list)-1
        qv = (x - x_list[0])/h
        Pv = 0
        qspisv = []
        qspisv.append(1)
        for i in range(n):
            tmep = qspisv[i]
            qspisv.append(tmep*(qv-i))

        for i in range(n+1):
            Pv += qspisv[i]*d[i][0]/np.math.factorial(i)

        func = lambdify(x, Pv)

    else:
        #интерполяция назад
        x = symbols('x')
        h = x_list[1] - x_list[0]
        n = len(x_list)-1
        qn = (x - x_list[-1])/h
        Pn = 0
        qspisn = []
        qspisn.append(1)
        for i in range(n):
            tmep = qspisn[i]
            qspisn.append(tmep*(qn+i))

        for i in range(n+1):
            Pn += qspisn[i]*d[i][-1]/np.math.factorial(i)

        func = lambdify(x, Pn)
    return func

def jdmethod_fast(a):
    """
    Решение СЛАУ методом Жордана-Гаусса, используя numpy

    Parameters
    ----------
    a : [[float, float, ...],
         [float, float, ...],
         ...]
        Матрица, введённая пользователем.

    Returns
    -------
    [float, float, ...]
        Решение СЛАУ.

    """

    a = np.array(a, float)
    n = len(a)
    for k in range(n):
        if np.fabs(a[k,k]) < 1.0e-12:
            for i in range(k+1,n):
                if np.fabs(a[i,k]) > np.fabs(a[k,k]):
                    a[[i, k]] = a[[k, i]]
                    break
        pivot = a[k,k]
        a[k] = a[k] / pivot

        a[np.arange(n)!=k] -= a[np.arange(n)!=k][:, k].reshape((n-1, 1)) * a[k]
    return a[:, -1:]

def qubic_splines(x_list, y_list):
    """

    Parameters
    ----------
    x_list : [float, float, ...]
        Список координат х.
    y_list : [float, float, ...]
        Список координат у.

    Returns
    -------
    S : [function, function, ...]
        Список функций для каждого сплайна на указанном отрезке.

    """
    x = symbols('x')
    S = []
    a = y_list[:]
    b = []
    d = []
    number_of_splines = len(x_list)-1
    h = np.array(x_list[1:]) - np.array(x_list[:-1])
    C_to_solve = np.zeros((number_of_splines + 1, number_of_splines + 2))
    for j in range(number_of_splines + 1):
        if j == 0 or j == number_of_splines:
            C_to_solve[j][j] = 1
        else:
            C_to_solve[j][j - 1] = h[j - 1] / 3
            C_to_solve[j][j] = 2 * (h[j - 1] + h[j]) / 3
            C_to_solve[j][j + 1] = h[j] / 3
            C_to_solve[j][-1] = (y_list[j + 1] - y_list[j]) / h[j] - (y_list[j] - y_list[j - 1]) / h[j - 1]
    c = jdmethod_fast(C_to_solve).transpose()[0]
    for i in range(number_of_splines):
        d.append((c[i+1] - c[i])/(3*h[i]))
        b.append(((y_list[i+1]-y_list[i])/h[i]) - h[i]*(c[i+1]+2*c[i])/3)
        S.append(a[i] + b[i]*(x-x_list[i]) + c[i]*(x-x_list[i])*(x-x_list[i]) + d[i]*(x-x_list[i])*(x-x_list[i])*(x-x_list[i]))
    S.append(S[-1])
    return S

def linear_approximation(x_list, y_list):
    """
    Линейная аппроксимация выборки

    Parameters
    ----------
    x_list : [float, float, ...]
        Список координат х.
    y_list : [float, float, ...]
        Список координат у.

    Returns
    -------
    k : float
        Коэффициент k в уравнении y=kx+b
    b : float
        Коэффициент b в уравнении y=kx+b

    """
    summ1 = 0
    n = len(x_list)
    for i in range(len(x_list)):
        summ1 += x_list[i]*y_list[i]
    summ2 = sum(x_list)*sum(y_list)
    k = (n*summ1 - summ2)/(n*sum([i**2 for i in x_list])-sum(x_list)**2)
    b = (sum(y_list)-k*sum(x_list))/n
    return k,b

def dispersion_linear(x_list, y_list, k, b):
    """
    Функция находит дисперсию для линейной аппроксимации

    Parameters
    ----------
    x_list : [float, float, ...]
        Список координат х.
    y_list : [float, float, ...]
        Список координат у.
    k : float
        Коэффициент k в уравнении y=kx+b
    b : float
        Коэффициент b в уравнении y=kx+b

    Returns
    -------
    summ_d : float
        Дисперсия линейной аппроксимации

    """

    x_values = np.linspace(x_list[0], x_list[-1], num=len(x_list))
    y_values = np.poly1d([k, b])(x_values)
    summ_d = 0
    for i in range(len(x_list)):
        summ_d = (y_values[i] - y_list[i])**2
    return summ_d

def quadratic_approximation(x_list, y_list):
    """
    Аппроксимация квадратичной функцией

    Parameters
    ----------
    x_list : [float, float, ...]
        Список координат х.
    y_list : [float, float, ...]
        Список координат у.

    Returns
    -------
    x_koef : [float, float, float]
        Список из коэффициентов k1,k2,k3 в уравнении y=k1*x^2+k2*x+k3

    """
    n = len(x_list)
    summ_kv = 0
    for i in range(len(x_list)):
        summ_kv+= x_list[i]**2
    summ_cube = 0
    for i in range(len(x_list)):
        summ_cube+= x_list[i]**3
    summ_four = 0
    for i in range(len(x_list)):
        summ_four+= x_list[i]**4
    summ_xy = 0
    for i in range(len(x_list)):
        summ_xy += x_list[i]*y_list[i]
    summ_xkvy = 0
    for i in range(len(x_list)):
        summ_xkvy += (x_list[i]**2)*y_list[i]

    A = [[summ_kv, sum(x_list), n, sum(y_list)], [summ_cube, summ_kv, sum(x_list), summ_xy], [summ_four, summ_cube, summ_kv, summ_xkvy]]
    x_koef = jdmethod_fast(A).transpose()[0]
    return x_koef

def dispersion_quadratic(x_list, y_list, x_koef):
    """
    Функця находит дисперсию для квадратичной аппроксимации

    Parameters
    ----------
    x_list : [float, float, ...]
        Список координат х.
    y_list : [float, float, ...]
        Список координат у.
    x_koef : [float, float, float]
        Список из коэффициентов k1,k2,k3 в уравнении y=k1*x^2+k2*x+k3

    Returns
    -------
    summ_d : float
        Дисперсия линейной аппроксимации

    """
    x_values = np.linspace(x_list[0], x_list[-1], num=len(x_list))
    y_values = np.poly1d(x_koef)(x_values)
    summ_d = 0
    for i in range(len(x_list)):
        summ_d = (y_values[i] - y_list[i])**2
    return summ_d

def Gauss_approximation(x_list):
    """
    Аппроксимация методом Гаусса для формулы
    y_i = a * exp(-(x_i-b)^2/c^2)

    Parameters
    ----------
    x_list : [float, float, ...]
        Список координат х.

    Returns
    -------
    a : float
        Коэф а.
    b : float
        Коэф b.
    c : float
        Коэф c.
    """
    n = len(x_list)
    x__ = 1/n*(sum(x_list))
    p = 1/200
    summ_ = 0
    for i in range(len(x_list)):
        summ_ += (x_list[i] - x__)**2
    S_0 = math.sqrt((1/(n-1))*summ_)
    c = S_0/(math.sqrt(S_0))
    summ_E = 0
    for i in range(len(x_list)):
        summ_E += x_list[i]*p
    b = summ_E
    a = 1/(c*(math.sqrt(2*math.pi)))
    return a, b, c

def dispersion_Gauss(x_list, y_list, a, b, c):
    """
    Дисперсия для аппроксимации методом Гаусса

    Parameters
    ----------
    x_list : [float, float, ...]
        Список координат х.
    y_list : [float, float, ...]
        Список координат у.
    a : float
        Коэф а уравненяи.
    b : float
        Коэф b уравненяи.
    c : float
        Коэф c уравненяи.

    Returns
    -------
    summ_d : float
        Дисперсия линейной аппроксимации

    """
    x_values = np.linspace(x_list[0], x_list[-1], num=len(x_list))
    y_values = [a*math.e**(-((x_i-b)**2)/(c**2)) for x_i in x_values]
    summ_d = 0
    for i in range(len(x_list)):
        summ_d = (y_values[i] - y_list[i])**2
    return summ_d


import numpy as np
from numpy.linalg import norm
import random
import math
import scipy
import os

def overdetermined_ls_test_matrix_generator(m, n, diagonal=None, fill_diagonal_method='one large', condition_number=None, a_cond=24, distribution_fn=np.random.uniform, a=0, b=1, seed=123):
    """
    Test matrix generator

    Keyword arguments:

    m: Number of rows of A
    n: Number of columns of A
    distribution_fn: Distribution function from which to draw entries from
    a: alpha
    b: beta
    diagonal: User-defined diagonal entries
    fill_diagonal_method: Method to use when filling diagonal {'one large', 'one small', 'geometric', 'arithmetic', 'loguniform', 'random'}
    condition_number: Condition Number
    a_cond: We pick condition_number with log_2 k distributed uniformly in [0, a_cond]. Defaults to 24, assuming machine precision is 2^{-24}

    Returns: A (m x n matrix), b (n x 1 matrix)

    TO-DO:

    1. Add Householders Transformation / Given Rotations from Lawn 9

    """

    np.random.seed(seed)
    # ----------------------------------------------------------------------------------------------------------------------------
    # 1. Generate condition_number: Randomly pick a condition number k with log_2 k distributed uniformly in [0, a_cond]
    # ----------------------------------------------------------------------------------------------------------------------------
    if condition_number is None:
        condition_number = 2 ** np.random.uniform(0, a_cond, 1)[0]

    # #----------------------------------------------------------------------------------------------------------------------------
    # # 1. Generate initial matrix if one is not specified
    # #----------------------------------------------------------------------------------------------------------------------------
    # if A is None:
    #     A = distribution_fn(a, b, n*m).reshape(n, m)
    # else:
    #     (n, m) = A.shape

    # ----------------------------------------------------------------------------------------------------------------------------
    # 2. Set diagonal based on user specification
    # ----------------------------------------------------------------------------------------------------------------------------
    # If specified, use input from user.
    # if diagonal is not None:
    #     np.fill_diagonal(A, diagonal)

    N = min(m, n)

    # If fill_diagonal_method = 'one large', set D(1) = 1 and D(i) = 1 / condition_number for i > 1
    if fill_diagonal_method == 'one large':
        diagonal = np.array([1] + [1 / condition_number] * (n - 1))

    # If fill_diagonal_method = 'one small', set D(N) = 1 / condition_number  and D(i) = 1  for i > 1
    elif fill_diagonal_method == 'one small':
        diagonal = ([1] * (N - 1)) + [1 / condition_number]

    # If fill_diagonal_method = 'geometric', set D(i) form a geometric sequence from 1 to 1/COND
    elif fill_diagonal_method == 'geometric':
        # N = min(m, n). We want r^{N-1} = 1 / condition_number => r = (1 / condition_number)^{1/N-1}
        r = (1 / condition_number) ** (1 / (N - 1))
        diagonal = np.array([r ** i for i in range(N)])

    # If fill_diagonal_method = 'arithmetic', set D(i) form a geometric sequence from 1 to 1/COND
    elif fill_diagonal_method == 'arithmetic':
        # We want 1 + d(N-1) = 1 / condition_number => d = (1 - COND) / COND * (N-1)
        d = (1 - condition_number) / (condition_number * (N - 1))
        diagonal = np.array([1 + (d * i) for i in range(N)])

    # If fill_diagonal_method = 'loguniform', then D(i) are random in the range [1 / COND, 1] with uniformly distributed logarithms
    elif fill_diagonal_method == 'loguniform':
        diagonal = np.exp(np.random.uniform(1 / condition_number, 1, N))

    # if fill_diagonal_method == 'random':
    #     diagonal = np.diag(A)
    # else:
    #     np.fill_diagonal(A, diagonal)

    # ----------------------------------------------------------------------------------------------------------------------------
    # 3. Generate A
    # ----------------------------------------------------------------------------------------------------------------------------
    # Swap largest diagonal entry with sigma_11 and smallest with sigma_kk
    k = random.sample([3, math.floor(n / 2), n], 1)[0]

    max_idx = np.argmax(diagonal)
    if max_idx != 0:
        diagonal[0], diagonal[max_idx] = diagonal[max_idx], diagonal[0]

    min_idx = np.argmin(diagonal)
    if min_idx != k:
        diagonal[k - 1], diagonal[min_idx] = diagonal[min_idx], diagonal[k - 1]

    U, _ = np.linalg.qr(np.random.standard_normal((m, n)))
    if n == k:
        V, _ = np.linalg.qr(np.random.standard_normal((n, n)))
    else:
        V_1, _ = np.linalg.qr(np.random.standard_normal((k, k)))
        V_2, _ = np.linalg.qr(np.random.standard_normal((n - k, n - k)))
        V = np.concatenate((np.concatenate((V_1, np.zeros([k, n - k])), axis=1), np.concatenate((np.zeros([n - k, k]), V_2), axis=1)), axis=0)

    A = (U*diagonal).dot(V.T)

    # Q_1 = ortho_group.rvs(n)

    # if n == m and check_symmetric(A):
    #     A = np.matmul(np.matmul(Q_1, A), Q_1.T)
    # else:
    #     Q_2 = ortho_group.rvs(m)
    #     A = np.matmul(np.matmul(Q_1, A), Q_2)

    # ----------------------------------------------------------------------------------------------------------------------------
    # 3. Generate b
    # ----------------------------------------------------------------------------------------------------------------------------
    # Select random x. b_1 = Ax then normalize b_1 so that ||b_1||_2 = 1
    x = np.random.rand(n, 1)
    b_1 = np.matmul(A, x)
    b_1 = b_1 / np.sqrt(np.sum(b_1 ** 2))

    # A = QR. b_2 = d - QQ.T d. Normalize b_2
    Q = np.linalg.qr(A)[0]
    d = np.random.uniform(-1, 1, (m, 1))
    b_2 = d - np.matmul(np.matmul(Q, Q.T), d)
    b_2 = b_2 / np.sqrt(np.sum(b_2 ** 2))

    # Generate initial theta
    u = np.random.uniform(-26, -1, 1)[0]
    theta = math.pi * (2 ** u)
    # Flip theta with probability 0.5
    if np.random.uniform(0, 1, 1)[0] >= 0.5:
        theta = (math.pi / 2) - theta
    # Calculate final b
    b = (b_1 * math.cos(theta)) + (b_2 * math.sin(theta))

    # Calculate true x
    x = np.matmul(np.linalg.pinv(A), b)

    return A, x, b

def create_fast_solver_test_matrix(n, d, path, save_matrices=False, condition_number=400, sparse=False, density=0.25):
    if sparse:
        A = scipy.sparse.random(n, d, density=density).toarray()
    else:
        # Calculate sigma based on condition number required.
        sigma = round(1 / (condition_number ** (1 / (d - 1))), 2)

        # 1. Create test matrix A
        U, _ = np.linalg.qr(np.random.standard_normal((n, d)))
        V, _ = np.linalg.qr(np.random.standard_normal((d, d)))
        s = [sigma ** (i + 1) for i in range(d)]
        A = (U * s).dot(V.T)
        if n < d:
            A = A.T

    # 2. Create x
    x = np.random.normal(loc=0, scale=1 / d, size=d).reshape(d, 1)

    # 3. Sample a response vector b ~ N(Ax, I_n)
    b = np.random.multivariate_normal(np.matmul(A, x).reshape(n, ), np.eye(n)).reshape(n, 1)

    # 4. True x
    true_x = np.matmul(np.linalg.pinv(A), b)

    # Option to save generated matrices.
    if save_matrices:
        np.savetxt(os.path.join(path, 'A_{}_{}.txt'.format(n, d)), A, fmt='%d')
        np.savetxt(os.path.join(path, 'x_{}_{}.txt'.format(n, d)), true_x, fmt='%d')
        np.savetxt(os.path.join(path, 'b_{}_{}.txt'.format(n, d)), b, fmt='%d')

    return A, true_x, b


# -*- coding: utf-8 -*-
"""
Created on Mon Jan 25 09:30:08 2021

@author: Nacho
"""

# =============================================================================
# =============================================================================
"""                 --- LINEAR REGRESSION MODEL --- """
# =============================================================================
# =============================================================================

# =============================================================================
""" STEP 1: IMPORT OUR LIBRARIES """
# =============================================================================

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# =============================================================================
""" STEP 2: IMPORT OUR DATA"""
# =============================================================================

# price_data = pd.read_excel("oil_exxon.xlsx", engine="openpyxl")
price_data = pd.read_csv("oil_exxon.txt")

""" How to change one colummn for the index """

price_data.index = pd.to_datetime(price_data["date"])

price_data = price_data.drop("date", 1)

# # print(price_data.head())

# =============================================================================
""" STEP 3: CLEAN THE DATA"""
# =============================================================================

# # import missingno as msn

# # msn.matrix(price_data)
# # # msn.bar(price_data)
# # # msn.heatmap(price_data, figsize=(5,3), fontsize=(12))

# print(price_data.dtypes)
# print(price_data.isnull().sum())
# print(price_data.info())

""" Rename the column names """

# # columns = ["exxon_price", "oil_price"]
# # price_data.columns = columns

new_columns_names = {"exon_price": "exxon_price"}
price_data = price_data.rename(columns = new_columns_names)

# """ Get the exxon_price and oil_price mean """

# # print(price_data["exxon_price"].mean())
# # print(price_data["oil_price"].mean())

# # price_data["exxon_price"].replace(np.nan, 85, inplace=True)
# # price_data["oil_price"].replace(np.nan, 62, inplace=True)

""" Find the missing values """

# print(price_data.isnull().sum())
# print(price_data.info())
# print(price_data.isna().sum())

""" Remove the missing values """

price_data.dropna(axis=0, how="any", inplace=True)
# print(price_data.isna().sum())

# =============================================================================
"""          -- STEP 4: EXPLORE THE DATA -- """
# =============================================================================

""" We can visualize the relationship between the X and Y variables """

"""
BUILD A SCATTER PLOT
"""
""" Define the X and Y data """

# x = price_data["exxon_price"]
# y = price_data["oil_price"]

""" Make a scatter plot with the plot method """

# plt.scatter(x, y, c="cadetblue", marker="o", alpha=0.2, label="Daily Price")
# plt.show()

# # # plt.scatter(x, y, marker="o", color="cadetblue", alpha=0.5,label="Daily Price")

# plt.title("Exxon Vs. Oil")
# plt.xlabel("Exxon Mobile")
# plt.ylabel("Oil")

# plt.legend()
# plt.show()

""" MEASURE THE CORRELATION

# Very strong relationship (|r|>0.8 =>)
# Strong relationship (0.6≤|r|)
# Moderate relationship (0.4≤|r|)
# Weak relationship (0.2≤|r|)
# Very weak relationship (|r|)

# """

# print(price_data.corr())

""" CREATE A STATISTICAL SUMMARY

# FOR WHAT?

# To find OUTLIERS

# How we know if we have all our data into the distribution?

# If all the data falls within 3 Standard deviations of the mean
# """

# print(price_data.describe())

""" CHECKING FOR OUTLIERS AND SKEWNESS """

from scipy import stats
from scipy.stats import kurtosis,skew

""" Make a histogram to see the skew of the data """

# price_data.hist(grid=False, color="cadetblue")

# plt.figure()
# plt.subplot(1,2,1)
# plt.hist(X_price, color="cadetblue")

# plt.subplot(1,2,2)
# plt.hist(Y_price, color="cadetblue")

# plt.show()

""" Calculate the excess Kurtosis using the Fisher method """

# exxon_kurtosis = kurtosis(price_data["exxon_price"], fisher=True)
# oil_kurtosis = kurtosis(price_data["oil_price"], fisher=True)

# print("---------------")
# print("Exxon Kurtosis: {:.2f}".format(exxon_kurtosis))
# print("Oil Kurtosis: {:.2f}".format(oil_kurtosis))

# """ Calculate the skewness """

# exxon_skew = skew(price_data["exxon_price"])
# oil_skew = skew(price_data["oil_price"])

# print("--------------")
# print("Exxon Skew: {:.2f}".format(exxon_skew))
# print("Oil Skew: {:.2f}".format(oil_skew))

""" Create the kurtosis test """

# print("-- Exxon --")

# print(stats.kurtosistest(price_data["exxon_price"]))
# print(stats.skewtest(price_data["exxon_price"]))

# print("-- Oil --")

# print(stats.kurtosistest(price_data["oil_price"]))
# print(stats.skewtest(price_data["oil_price"]))

""" lOS TESTS nos dan que rechazariamos la normalidad en 3 de los 4
casos, incluso si los datos fueran ligeramente kurtosis o skewed. Esto es
por lo que primero calculamos las metricas y visualizamos el dato antes de
realizar el test estadistico """

""" Si hubiera que transformar los datos ¿Como lo realizariamos? """

# =============================================================================
"""          -- STEP 5: BUILT THE MODEL  --             """
# =============================================================================

"""
Althought the data are slightly skewed  we are comfortable with de data,
Nothing else is stopping us...

"""

""" Split the data """

from sklearn.model_selection import train_test_split

# X = price_data[["exxon_price"]].to_numpy()
# Y = price_data[["oil_price"]].to_numpy()

X = price_data[["oil_price"]]
Y = price_data[["exxon_price"]]

X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=1)

""" Create and fit the model """

from sklearn import linear_model

model = linear_model.LinearRegression()

model.fit(X_train, y_train)

""" Explore the output """

""" En funcion de si lo hacemos mediante dataframes o arrays, vamos a tener
que añadir [0] o podremos usar el format de una forma u otra """

# print("PARAMETERS")
# print("------------------")

# print("Coefficient: ", model.coef_.round(2)[0][0])
# print("Intercept: ", model.intercept_.round(2)[0])

# """ Taking a single prediction """

# prediction = model.predict([[67.33]])

# print("Predictive value: ", 67.33)
# print("Prediction value: {:.2f}".format(prediction[0][0]))

""" Making multiple predictions """

from sklearn.metrics import r2_score

y_pred = model.predict(X_test)

# print("R2: {:.2f}".format(r2_score(y_test, y_pred)))

# =============================================================================
"""         -- STEP 6: EVALUATING THE MODEL --              """
# =============================================================================

import statsmodels.api as sm

""" Create a OLS model with statsmodels """

X2 = sm.add_constant(X)

model_sm = sm.OLS(Y, X2)

est = model_sm.fit()

# print(est.summary())

""" Confident intervals """

# print(est.conf_int())

""" Hypothesis Testing

Estimate the p-values

NULL HYPOTHESIS: If it's more than 0.05, it means our coefficient equals 0

ALTERNATIVE HYPOTHESIS: If not the Coefficient does not equal 0

  """

# print(est.pvalues)

# """

# p-value is much less than 0.05

# --> We reject the NULL HYPOTHESIS

# """

# # =============================================================================
# """                 ---STEP 7: MODEL FIT ---                             """
# # =============================================================================

from sklearn.metrics import mean_squared_error, mean_absolute_error
# import math

# """ Metrics to calculate how our model fit to the data """

# print("Mean Squared Error (MSE): {:.2f}".format(mean_squared_error
#                                                 (y_test, y_pred)))

# print("Mean Absolute Error (MAE): {:.2f}".format(mean_absolute_error
#                                                   (y_test, y_pred)))

# print("Root Mean Squared Error (MSE): {:.2f}".format(math.sqrt
#                                                       (mean_squared_error
#                                                                 (y_test,y_pred))))

""" Plot the residuals """

# bins = np.arange(-18, 20, 4)
# res = (y_test - y_pred)

# # print(res.min())
# # print(res.max())

# plt.hist(res, color="royalblue", bins=bins)
# plt.xticks(bins)

# # (y_test - y_pred).hist(grid=False, color="royalblue")

# plt.title("- MODEL RESIDUALS -")
# plt.show()

""" Plot our line """

# plt.scatter(X_test, y_test, color="royalblue", alpha=0.3, label="Price")

# plt.plot(X_test, y_pred, "r-", linewidth=3, label="Linear Regression")

# plt.xlabel("Oil")
# plt.ylabel("Exxon Mobile")
# plt.title("Exxon Mobile Vs. Oil")

# plt.legend()
# plt.show()

# print("Oil Coefficient: {:.2f}".format(model.coef_[0][0]))
# print("Mean Squared Error (MSE): {:.2f}".format(mean_squared_error
#                                                 (y_test, y_pred)))
# print("R2: {:.2f}".format(r2_score
#                           (y_test, y_pred)))

# =============================================================================
"""         STEP 8: SAVE THE MODEL FOR FUTURE USE """
# =============================================================================

import pickle

""" pickle the model """

with open("linear_regression_model.sav", "wb") as f:
    pickle.dump(model,f)

""" load it back in"""

with open("linear_regression_model.sav", "rb") as picklefile:
    mdl_ex = pickle.load(picklefile)

""" Make a new prediction """

print(mdl_ex.predict([[67.33]]))

import numpy,from scipy,import statsmodels