from scipy.linalg import eigh
from utils import *
import numpy as np

import numpy as np

def get_weights(data, nbors_idx, reg_func=None):
    """
    Calculate weights

    Parameters
    ----------
    data: (d,n) array, Input data
        d is its dimensionality
        n is the number of points.
    nbors: (n,k) array. Indices of neghbours
        n is the number of points
        k is the number of neighbours
    reg: regularization function

    Returns
    -------
    weights: (n,n) array. Weight matrix in row-major order
        weights[i,:] is weights of x_i
    """

    n = data.shape[1]
    weights = np.zeros((n, n))

    eps = 1e-3
    for i in range(n):
        x = data[:, i].reshape(-1, 1)
        k = nbors_idx[i].shape[0]  # number of neighbors
        ones = np.ones((k, 1))

        # k-neareast neighbors
        eta = data[:, nbors_idx[i]]
        eta_t = eta.T
        C = eta_t.dot(eta)

        # regularization term
        if reg_func is None:
            trace = np.trace(C)
            if trace > 0:
                R = eps/k*trace
            else:
                R = eps
            C += np.eye(k)*R
        else:
            C += reg_func(C, k)

        # C_inv = np.linalg.inv(C)
        C_inv = np.linalg.pinv(C)

        # calculate lagranian multipler lamda
        tmp = eta_t.dot(x)
        lam_num = 1. - ones.T.dot(C_inv).dot(tmp)
        lam_denom = ones.T.dot(C_inv).dot(ones)
        lam = lam_num / (lam_denom + 1e-15)
        w = C_inv.dot(tmp + lam*ones)
        weights[i, nbors_idx[i]] = w.reshape(-1)

    return weights

def Y_(Weights, d):
    """
    Calculate embedded coordinates in target space

    Parameters
    ----------
    Weights: (n,n) array, weight matrix
    d: dimensionality of target space

    Returns
    -------
    Y: (n,d) array
        Embedded coordinates in target space
    """
    n, p = Weights.shape
    I = np.eye(n)
    m = (I-Weights)
    M = m.T.dot(m)

    eigvals, eigvecs = eigh(M)
    ind = np.argsort(np.abs(eigvals))

    return(eigvecs[:, ind[1:d+1]])

class LocalLinearEmbedding:
    def __init__(self, n_dim=2,
                 n_neighbors=None,
                 epsilon=None):
        assert(n_neighbors != None or epsilon != None)

        self.dist_func = nearest_neighbor_distance if n_neighbors != None else fixed_radius_distance
        self.n_neighbors = n_neighbors
        self.n_dim = n_dim
        self.epsilon = epsilon

    def fit(self, X):
        # Select neighbors
        if self.epsilon is not None:
            _, nbors = self.dist_func(X, self.epsilon)
        elif self.n_neighbors is not None:
            _, nbors = self.dist_func(X, self.n_neighbors)

        # Reconstruct with linear weights
        Weights = get_weights(X, nbors, None)

        # Map to embedded coordinates
        Y = Y_(Weights, self.n_dim)
        return Y.T


import numpy as np
from scipy.spatial.distance import cdist

class KMeans:
    def __init__(
            self,
            k: int,
            metric: str = "euclidean",
            tol: float = 1e-6,
            max_iter: int = 100,
            ):
        """
        inputs:
            k: int
                the number of centroids to use in cluster fitting
            metric: str
                the name of the distance metric to use
            tol: float
                the minimum error tolerance from previous error during optimization to quit the model fit
            max_iter: int
                the maximum number of iterations before quitting model fit
        """
        self.k = k
        if self.k==0:
            raise ValueError('k value cannot be zero')
        self.metric = metric
        self.tol = tol
        self.max_iter = max_iter
        self._err = np.inf
        self._all_scores = []

    def fit(self, mat: np.ndarray):
        """
        fits the kmeans algorithm onto a provided 2D matrix

        inputs:
            mat: np.ndarray
                A 2D matrix where the rows are observations and columns are features
        """
        if self.k>len(mat):
            raise ValueError('more cluster centers than points')

        closest_centroid = np.array([np.random.randint(0,self.k) for e in range(len(mat))]) # randomly assign each point in mat to a k
        itr=0
        while itr <= self.max_iter: # max iterations to go for before exiting
            centroids = []
            for k_idx in range(self.k): # for each k center
                # find a new centroid at the middle of the assigned points
                centroids.append(mat[closest_centroid==k_idx].mean(axis=0))
            self._centroids = centroids
            dist_to_centroids = cdist(mat, centroids, self.metric) # find dist of each point to each of the new centroids
            closest_centroid = np.array([np.argmin(d) for d in dist_to_centroids]) # find the closest centroid to each point
            itr+=1

            new_err = self._calc_mse(mat, np.array(closest_centroid)) # calculate the error of this round
            if len(self._all_scores) > 0:
                self._all_scores.append(new_err)
            else:
                self._all_scores= [new_err]
            if abs(self._err - new_err) < self.tol: # if new error is the value of tol less than the previous error
                self._err = new_err
                break
            else:
                self._err = new_err

    def _calc_mse(self, mat, closest_centroid):
        """
        calculates the mean square error of a matrix and the centroids

        inputs:
            mat: np.ndarray
                A 2D matrix where the rows are observations and columns are features
            closest_centroid:
                the closest centroid to each point in mat
        returns:
            float
                the mean square error score
        """
        sq_err = []
        for k_idx in range(self.k):

            n = len(mat[closest_centroid==k_idx]) # denominator of mse (# of points assigned to that k)
            # numerator of mse (square of distances of points to centroid)
            num = sum(np.square(cdist(mat[closest_centroid==k_idx],[self._centroids[k_idx]], self.metric)))
            if n>0:
                sq_err.append((num/n)[0])
        return sum(sq_err) # final mse is sum of this for all k's

    def predict(self, mat: np.ndarray) -> np.ndarray:
        """
        predicts the cluster labels for a provided 2D matrix

        inputs:
            mat: np.ndarray
                A 2D matrix where the rows are observations and columns are features

        outputs:
            np.ndarray
                a 1D array with the cluster label for each of the observations in `mat`
        """
        dist_to_centroids = cdist(mat, self._centroids) # use the centroids determined in fit
        closest_centroid = np.array([np.argsort(d)[0] for d in dist_to_centroids]) # find closest centroid to each point
        return closest_centroid

    def get_error(self) -> float:
        """
        returns the final squared-mean error of the fit model

        outputs:
            float
                the squared-mean error of the fit model
        """
        return self._err

    def get_all_errors(self) -> np.ndarray:
        """
        returns all the MSE error scores for each itteration

        outputs:
            np.ndarray
                an array of each score
        """
        return self._all_scores

    def get_centroids(self) -> np.ndarray:
        """
        returns the centroid locations of the fit model

        outputs:
            np.ndarray
                a `k x m` 2D matrix representing the cluster centroids of the fit model
        """
        return self._centroids


from pymc3 import *
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
import pandas as pd
import theano
from scipy.stats.kde import gaussian_kde
NUM_SAMPLES = 30
SDEV = 0.3
np.random.seed(42)

def f(x):
    return 7 *(x**3 -1.3 *x**2+0.5*x - 0.056)

gridx = np.linspace(0,1,200)
x = np.random.random(NUM_SAMPLES)
truey = f(x)
y = truey + SDEV*np.random.randn(NUM_SAMPLES)

# poly = PolynomialFeatures(degree = 10)
# xpow = poly.fit_transform(x.reshape(NUM_SAMPLES,1))
# linpow = poly.fit_transform(gridx.reshape(200,1))
data = dict(x1=theano.shared(x), y=theano.shared(y))
print(data['x1'][:5])
fig = plt.figure()
ax = fig.add_subplot(111, title = 'Bayesian approach to degree 10 polynomial regression')
ax.scatter(x,y, label = 'Sampled data')
ax.plot(gridx, f(gridx), label = 'True curve', linewidth = 4 )

with Model() as model:
    # glm.GLM.from_formula('y ~ x1 + x2 + x3 ', data)
    coeff = Normal('coeff', mu = 0, sd = 10, shape = 11)
    sigma = HalfNormal('sigma', sd = 1)
    # mu = coeff[0] + coeff[1]*data['x1'] + coeff[2]*data['x2']+ coeff[3]*data['x3']
    mu = np.sum([coeff[i]*x**i for i in range(0,11)])
    y_obs = Normal('y_obs', mu = mu, sd = sigma, observed = data['y'])
    trace = sample(15000, njobs = 4, tune = 1000 )
    # print(trace['coeff'])
    print(stats.df_summary(trace).to_html())
    mcoeff = np.mean(trace['coeff'], axis = 0)
    def g(x):
        return np.sum([mcoeff[i]*x**i for i in range(11)], axis =0)
    ax.plot(gridx, g(gridx), label = 'Bayesian mean polynomial fit', color = 'Black', alpha = 0.5, lw = 2.5)
    ax.set_ylim(-1., 1.25)

    for i in range(50):
        def h(x):
            return np.sum([trace['coeff'][-i][j]*x**j for j in range(4)], axis =0)
        ax.plot(gridx, h(gridx), color = 'Black', alpha = 0.1)
    gridx = np.linspace(0,1,30)
    data['x1'].set_value(gridx)
    # data['x2'].set_value(linpow[:,2])
    # data['x3'].set_value(linpow[:,3])
    data['y'].set_value(np.zeros_like(gridx))
    post_pred = sample_ppc(trace, samples = 200)
    print(post_pred['y_obs'].shape)

    ax.plot(gridx, np.mean(post_pred['y_obs'], axis = 0), label = 'Bayesian mean posterior', alpha = 0.5, color = 'Red')
    ax.fill_between(gridx, np.mean(post_pred['y_obs'], axis = 0)-np.std(post_pred['y_obs'], axis = 0), np.mean(post_pred['y_obs'], axis = 0)+np.std(post_pred['y_obs'], axis = 0), label = 'Bayesian error band', alpha = 0.1, color = 'Red')
    plt.legend(loc = 2)
    plt.savefig("bayreg10.png")
    plt.show()
    plt.clf()
    plt.close()
    plt.figure(figsize = (5, 24))
    fig, axs = plt.subplots(12)
    for i in range(11):
        tdata = trace['coeff'][:,i]
        kde = gaussian_kde(tdata)
        tgridx = np.linspace(min(tdata), max(tdata), 200)
        axs[i].plot(tgridx, kde(tgridx))
        axs[i].set_ylim(axs[i])
        axs[i].set_title('Coeff{}'.format(i))

    tdata = trace['sigma']
    kde = gaussian_kde(tdata)
    tgridx = np.linspace(min(tdata), max(tdata), 200)
    axs[11].plot(tgridx, kde(tgridx))
    axs[11].set_ylim(axs[11])
    axs[11].set_title('Sigma')
    plt.savefig("trace10.png")

    # plot_posterior_predictive_glm(trace, samples = 200, label ='Posterior predictive regression')
    # traceplot(trace)
    # plt.tight_layout()

import numpy,from scipy,import theano,from pymc3


import numpy as np
import pandas as pd
def linear_model(df1_x,df1_y):

    x=df1_x.iloc[:,0]
    x=pd.to_numeric(x)

    y=df1_y.iloc[:,0]
    y = pd.to_numeric(y)

    df1_x_mean=np.mean(df1_x)
    df1_y_mean=np.mean(df1_y)

    deviation_x=[]
    for i in x:
        deviation_x.append(i-df1_x_mean)

    deviation_y=[]
    for i in y:
        deviation_y.append(i-df1_y_mean)

    product_deviation=np.array(deviation_x)*np.array(deviation_y)

    Sum_product_deviation=np.sum(product_deviation)

    Sq_x_deviation=np.array(deviation_x)**2

    Sum_Sq_x_deviation=np.sum(Sq_x_deviation)

    Regression_coefficient=Sum_product_deviation/Sum_Sq_x_deviation
    print(Regression_coefficient)
    intercept=float(df1_y_mean)-(Regression_coefficient*float(df1_x_mean))
    print(intercept)

    return [Regression_coefficient, intercept]


'''
# Defining the sigmoid function for activations
def sigmoid(x):
    return 1/(1+np.exp(-x))

# Derivative of the sigmoid function
def sigmoid_prime(x):
    return sigmoid(x) * (1 - sigmoid(x))

# Input data
x = np.array([0.1, 0.3])
# Target
y = 0.2
# Input to output weights
weights = np.array([-0.8, 0.5])

# The learning rate, eta in the weight step equation
learnrate = 0.5

# The neural network output (y-hat)
nn_output = sigmoid(x[0]*weights[0] + x[1]*weights[1])
# or nn_output = sigmoid(np.dot(x, weights))

# output error (y - y-hat)
error = y - nn_output

# error term (lowercase delta)
error_term = error * sigmoid_prime(np.dot(x,weights))

# Gradient descent step
del_w = [ learnrate * error_term * x[0],
                 learnrate * error_term * x[1]]
# or del_w = learnrate * error_term * x
'''
import numpy as np
from data_prep import features, targets, features_test, targets_test

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

def sigmoid_prime(x):
    '''
    Derivative of the sigmoid function
    '''
    return sigmoid(x) * (1 - sigmoid(x))

# Use to same seed to make debugging easier
np.random.seed(42)

n_records, n_features = features.shape
last_loss = None

# Initialize weights
weights = np.random.normal(scale=1 / n_features**.5, size=n_features)

# Neural Network hyperparameters
epochs = 1000
learnrate = 0.5

for e in range(epochs):
    del_w = np.zeros(weights.shape)
    for x, y in zip(features.values, targets):
        # Loop through all records, x is the input, y is the target

        # TODO: Calculate the output
        output = sigmoid(x.dot(weights))

        # TODO: Calculate the error
        error = y - output

        # TODO: Calculate change in weights
        del_w += learnrate * error * sigmoid_prime(x.dot(weights)) * x

        # TODO: Update weights
    weights += del_w

    # Printing out the mean square error on the training set
    if e % (epochs / 10) == 0:
        out = sigmoid(np.dot(features, weights))
        loss = np.mean((out - targets) ** 2)
        if last_loss and last_loss < loss:
            print("Train loss: ", loss, "  WARNING - Loss Increasing")
        else:
            print("Train loss: ", loss)
        last_loss = loss

# Calculate accuracy on test data
tes_out = sigmoid(np.dot(features_test, weights))
predictions = tes_out > 0.5
accuracy = np.mean(predictions == targets_test)
print("Prediction accuracy: {:.3f}".format(accuracy))


# --------------
#Importing header files
import pandas as pd
import scipy.stats as stats
import math
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.stats.weightstats import ztest
from statsmodels.stats.weightstats import ztest
from scipy.stats import chi2_contingency

import warnings

warnings.filterwarnings('ignore')
#Sample_Size
sample_size=2000

#Z_Critical Score
z_critical = stats.norm.ppf(q = 0.95)

# Critical Value
critical_value = stats.chi2.ppf(q = 0.95, # Find the critical value for 95% confidence*
                      df = 6)   # Df = number of variable categories(in purpose) - 1

#Reading file
data=pd.read_csv(path)

#Code starts here

#Confidence Interval
data_sample = data.sample(n=sample_size,random_state=0)

confidence_interval = [data_sample['installment'].mean() - (z_critical*data_sample['installment'].std()/math.sqrt(sample_size)), data_sample['installment'].mean() + (z_critical*data_sample['installment'].std()/math.sqrt(sample_size))]
print("confidence interval: ",confidence_interval)

true_mean = data['installment'].mean()
print("true mean: ",true_mean)

#CLT
sample_size = np.array([20,50,100])

fig, axes = plt.subplots(3,1, figsize=(10,20))

for i in range(len(sample_size)):
    m = []

    for j in range(1000):

        mean = data['installment'].sample(sample_size[i]).mean()

        m.append(mean)

    mean_series = pd.Series(m)

    axes[i].hist(mean_series)

plt.show()

#Small Business Interests
data['int.rate.new'] = data['int.rate'].map(lambda x: str(x)[:-1])

data['int.rate.new'] = data['int.rate.new'].astype(float)/100

z_statistic_1, p_value_1 = ztest(x1 = data[data['purpose'] == 'small_business']['int.rate.new'], value = data['int.rate.new'].mean(), alternative='larger')

print("z-statistic : ",z_statistic_1)
print("p-value : ",p_value_1)

#Installment vs Loan Defaulting
z_statistic_2, p_value_2 = ztest(x1 = data[data['paid.back.loan'] == 'No']['installment'], x2 = data[data['paid.back.loan'] == 'Yes']['installment'])

print("z-statistic 2 : ",z_statistic_2)
print("p-value 2 : ",p_value_2)

#Purpose vs Loan Defaulting
yes = data[data['paid.back.loan'] == 'Yes']['purpose'].value_counts()

no = data[data['paid.back.loan'] == 'No']['purpose'].value_counts()

observed = pd.concat([yes.transpose(), no.transpose()], 1, keys=['Yes','No'])

chi2, p , dof, ex = chi2_contingency(observed)

print("chi statistic is : ", chi2)
print("critical value : ", critical_value)

import numpy,import scipy,from scipy,from statsmodels


from numba import jit

'''
the problem is to find the kth smallest number in the multiplication table (n rows, m columns)
for k = 9 the answer is 5
to solve it:
the idea is to guess a number between 1 and n * m, lets call this number mid and
count the numbers up to mid (including repetitions) with the following conditions:
// every number below mid / m_i will be bigger than mid, so:
    if mid / m_i( = 1,2,...,m) = maxNumberReachedPerColumn > n, then
        count += n
    else
        count += maxNumberReachedPerColumn
now that we have the count of numbers lesser than mid
we check:
    if (count < k)
        we discard left
        left = mid + 1
    else
        we discard right
        right = mid - 1
we do this until left > right
now the answer is left, since right will be always one below the correct answer
(Binary Search)
'''
@jit
def main():
    n, m, k = map(int, input().split())

    lf = 1
    r = n * m

    while lf <= r:
        mid = int((lf + r) / 2)
        count = 0
        limit = m + 1
        for i in range(1, limit):
            maxInColumn = int(mid / i)
            if maxInColumn > n:
                count += n
            else:
                count += maxInColumn
        if count < k:
            lf = mid + 1
        else:
            r = mid - 1

    print(int(lf))

if __name__ == "__main__":
    main()


import numpy
from scipy import stats as stat

class PacketLength:
    """This class extracts features related to the Packet Lengths.

    Attributes:
        mean_count (int): The row number.
        grand_total (float): The cummulative total of the means.

    """
    mean_count = 0
    grand_total = 0

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

    def get_packet_length(self) -> list:
        """Creates a list of packet lengths.

        Returns:
            packet_lengths (List[int]):

        """

        return [len(packet) for packet, _ in self.feature.packets]

    def first_fifty(self) -> list:
        """Returns first 50 packet sizes

        Return:
            List of Packet Sizes

        """
        return self.get_packet_length()[:50]

    def get_var(self) -> float:
        """The variation of packet lengths in a network Feature.

        Returns:
            float: The variation of packet lengths.

        """
        return numpy.var(self.get_packet_length())

    def get_std(self) -> float:
        """The standard deviation of packet lengths in a network flow.

        Rens:
            float: The standard deviation of packet lengths.

        """
        return numpy.sqrt(self.get_var())

    def get_mean(self) -> float:
        """The mean of packet lengths in a network flow.

        Returns:
            float: The mean of packet lengths.

        """
        mean = 0
        if self.get_packet_length() != 0:
            mean = numpy.mean(self.get_packet_length())

        return mean

    def get_median(self) -> float:
        """The median of packet lengths in a network flow.

        Returns:
            float: The median of packet lengths.

        """
        return numpy.median(self.get_packet_length())

    def get_mode(self) -> float:
        """The mode of packet lengths in a network flow.

        Returns:
            float: The mode of packet lengths.

        """
        mode = -1
        if len(self.get_packet_length()) != 0:
            mode = int(stat.mode(self.get_packet_length())[0])

        return mode

    def get_skew(self) -> float:
        """The skew of packet lengths in a network flow using the median.

        Returns:
            float: The skew of packet lengths.

        """
        mean = self.get_mean()
        median = self.get_median()
        dif = 3 * (mean - median)
        std = self.get_std()
        skew = -10

        if std != 0:
            skew = dif / std

        return skew

    def get_skew2(self) -> float:
        """The skew of the packet lengths ina network flow using the mode.

        Returns:
            float: The skew of the packet lengths.

        """
        mean = self.get_mean()
        mode = self.get_mode()
        dif = (mean - mode)
        std = self.get_std()
        skew2 = -10

        if std != 0:
            skew2 = dif / std

        return skew2

    def get_cov(self) -> float:
        """The coefficient of variance of packet lengths in a network flow.

        Returns:
            float: The coefficient of variance of a packet lengths list.

        """
        cov = -1
        if self.get_mean() != 0:
            cov = self.get_std() / self.get_mean()

        return cov


# Regresion polinomial
import matplotlib.pyplot as plt
import numpy as np

def createMatrix(m,n, valor=0):
    C = []
    for i in range(m):
        C.append([])                            #could be c.append([valor]*n)
        for j in range(n):
            C[i].append(valor)
    return C

def getDimensions(A):
    return (len(A),len(A[0]))

def copyMatrix(B):
    m,n = getDimensions(B)
    A = createMatrix(m,n)
    for i in range(m):
        for j in range(n):
            A[i][j]=B[i][j]
    return A

def sumMatrix(A,B):
    Am, An = getDimensions(A)
    Bm, Bn = getDimensions(B)
    if (Am != Bm or An != Bn):
        print("Error, matrix of diferent size")
        return []
    C = createMatrix(Am,An)
    for i in range(Am):
        for j in range(An):
            C[i][j] = A[i][j] + B[i][j]
    return C

def restaMatrix(A,B):
    Am, An = getDimensions(A)
    Bm, Bn = getDimensions(B)
    if (Am != Bm or An != Bn):
        print("Error, matrix of diferent size")
        return []
    C = createMatrix(Am,An)
    for i in range(Am):
        for j in range(An):
            C[i][j] = A[i][j] - B[i][j]
    return C

def multMatrix(A,B):
    Am, An = getDimensions(A)
    Bm, Bn = getDimensions(B)
    if (An != Bm):
        print("Error multiplicacion # columnas y # renglos no son iguales")
        return []
    C = createMatrix(Am,Bn)
    counter = 0
    for i in range(Am):
        for j in range(Bn):
            for k in range(An):
                C[i][j] += A[i][k] * B[k][j]
    return C

def getAdyacente(A,r,c):
    Am, An = getDimensions(A)
    C = createMatrix(Am-1, An-1, 0)
    for i in range(Am):
        if (i == r):
            continue
        for j in range(An):
            if (j == c):
                continue
            ci = 0
            cj = 0
            if (i < r):
                ci = i
            else:
                ci = i-1
            if (j < c):
                cj = j
            else:
                cj = j-1
            C[ci][cj] = A[i][j]
    return C

def detMatrix(A):
    m,n = getDimensions(A)
    if m!=n:
        print("Matriz no es cuadrada")
        return []
    if (n==1):
        return A[0][0]
    if (n==2):
        return (A[0][0]*A[1][1]) - (A[1][0]*A[0][1])
    det = 0
    for j in range(m):
        det += (-1)**j*A[0][j]*detMatrix(getAdyacente(A,0,j))
    return det

def getMatrizTranspuesta(A):
    m,n = getDimensions(A)
    C = createMatrix(n,m,0)
    for i in range(m):
        for j in range(n):
            C[j][i] = A[i][j]
    return C

def getMatrizAdjunta(A):
    m,n = getDimensions(A)
    if m != n:
        print("La matriz no es cuadrada")
        return []
    C = createMatrix(m,n,0)
    for i in range(m):
        for j in range(n):
            C[i][j] = ((-1)**(i+j))*detMatrix(getAdyacente(A,i,j))
    return C

def getMatrizInversa(A):
    m,n = getDimensions(A)
    if m != n:
        print("La matriz no es cuadrada")
        return []
    detA = detMatrix(A)
    if detA== 0:
        print("La matriz no tiene inversa")
        return []
    At = getMatrizTranspuesta(A)
    adjA = getMatrizAdjunta(At)
    invDetA = 1/detA
    C = createMatrix(m,n,0)
    for i in range(m):
        for j in range(n):
            C[i][j]=invDetA*adjA[i][j]
    return C

def regPolinomial(grado, x, y):
    grado += 1
    A = createMatrix(grado,grado)
    for i in range(grado):
        for j in range(grado):
            A[i][j] = sum( xi**(i+j) for xi in x)
    C = createMatrix(grado,1)
    for i in range(grado):
        C[i][0] = sum((xi**i)*yi for xi,yi in zip(x,y))
    invA = getMatrizInversa(A)
    return multMatrix(invA,C)

def evalPolinomio(coef,x):
    y = []
    coef = np.asarray(coef)
    for i in range(len(x)):
        y.append(0)
        for c in range(len(coef)):
            y[i] += (x[i]**c) * coef[c]
    return y

x = [8, 16, 24, 32] #gradoMAX X-1
y = [4.1, 4.5, 5.1, 6.1]
plt.plot(x,y,'rx')
#plt.show()

coef = regPolinomial(3, x, y) #aqui se cambia para el grado de la funcion a buscar
print(coef)

x2 = np.linspace(5,35,100)
y2 = evalPolinomio(coef,x2)
plt.plot(x2,y2)
plt.show()

yesp = evalPolinomio(coef,[7])
print("peso esperado 7 semanas --> ", yesp[0][0])

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

# Regresion polinomial
import matplotlib.pyplot as plt
import numpy as np

def createMatrix(m,n, valor=0):
    C = []
    for i in range(m):
        C.append([])                            #could be c.append([valor]*n)
        for j in range(n):
            C[i].append(valor)
    return C

def getDimensions(A):
    return (len(A),len(A[0]))

def copyMatrix(B):
    m,n = getDimensions(B)
    A = createMatrix(m,n)
    for i in range(m):
        for j in range(n):
            A[i][j]=B[i][j]
    return A

def sumMatrix(A,B):
    Am, An = getDimensions(A)
    Bm, Bn = getDimensions(B)
    if (Am != Bm or An != Bn):
        print("Error, matrix of diferent size")
        return []
    C = createMatrix(Am,An)
    for i in range(Am):
        for j in range(An):
            C[i][j] = A[i][j] + B[i][j]
    return C

def restaMatrix(A,B):
    Am, An = getDimensions(A)
    Bm, Bn = getDimensions(B)
    if (Am != Bm or An != Bn):
        print("Error, matrix of diferent size")
        return []
    C = createMatrix(Am,An)
    for i in range(Am):
        for j in range(An):
            C[i][j] = A[i][j] - B[i][j]
    return C

def multMatrix(A,B):
    Am, An = getDimensions(A)
    Bm, Bn = getDimensions(B)
    if (An != Bm):
        print("Error multiplicacion # columnas y # renglos no son iguales")
        return []
    C = createMatrix(Am,Bn)
    counter = 0
    for i in range(Am):
        for j in range(Bn):
            for k in range(An):
                C[i][j] += A[i][k] * B[k][j]
    return C

def getAdyacente(A,r,c):
    Am, An = getDimensions(A)
    C = createMatrix(Am-1, An-1, 0)
    for i in range(Am):
        if (i == r):
            continue
        for j in range(An):
            if (j == c):
                continue
            ci = 0
            cj = 0
            if (i < r):
                ci = i
            else:
                ci = i-1
            if (j < c):
                cj = j
            else:
                cj = j-1
            C[ci][cj] = A[i][j]
    return C

def detMatrix(A):
    m,n = getDimensions(A)
    if m!=n:
        print("Matriz no es cuadrada")
        return []
    if (n==1):
        return A[0][0]
    if (n==2):
        return (A[0][0]*A[1][1]) - (A[1][0]*A[0][1])
    det = 0
    for j in range(m):
        det += (-1)**j*A[0][j]*detMatrix(getAdyacente(A,0,j))
    return det

def getMatrizTranspuesta(A):
    m,n = getDimensions(A)
    C = createMatrix(n,m,0)
    for i in range(m):
        for j in range(n):
            C[j][i] = A[i][j]
    return C

def getMatrizAdjunta(A):
    m,n = getDimensions(A)
    if m != n:
        print("La matriz no es cuadrada")
        return []
    C = createMatrix(m,n,0)
    for i in range(m):
        for j in range(n):
            C[i][j] = ((-1)**(i+j))*detMatrix(getAdyacente(A,i,j))
    return C

def getMatrizInversa(A):
    m,n = getDimensions(A)
    if m != n:
        print("La matriz no es cuadrada")
        return []
    detA = detMatrix(A)
    if detA== 0:
        print("La matriz no tiene inversa")
        return []
    At = getMatrizTranspuesta(A)
    adjA = getMatrizAdjunta(At)
    invDetA = 1/detA
    C = createMatrix(m,n,0)
    for i in range(m):
        for j in range(n):
            C[i][j]=invDetA*adjA[i][j]
    return C

def regPolinomial(grado, x, y):
    grado += 1
    A = createMatrix(grado,grado)
    for i in range(grado):
        for j in range(grado):
            A[i][j] = sum( xi**(i+j) for xi in x)
    C = createMatrix(grado,1)
    for i in range(grado):
        C[i][0] = sum((xi**i)*yi for xi,yi in zip(x,y))
    invA = getMatrizInversa(A)
    return multMatrix(invA,C)

def evalPolinomio(coef,x):
    y = []
    coef = np.asarray(coef)
    for i in range(len(x)):
        y.append(0)
        for c in range(len(coef)):
            y[i] += (x[i]**c) * coef[c]
    return y

x = [1, 2, 3, 4, 5] #gradoMAX X-1
y = [88, 87, 84, 82, 79]
plt.plot(x,y,'rx')
#plt.show()

coef = regPolinomial(2, x, y) #aqui se cambia para el grado de la funcion a buscar
print(coef)

x2 = np.linspace(0,8,100)
y2 = evalPolinomio(coef,x2)
plt.plot(x2,y2)
plt.show()

yesp = evalPolinomio(coef,[7])
print("peso esperado 7 semanas --> ", yesp[0][0])


from __future__ import absolute_import, division, print_function, unicode_literals

import sys
import os
from datetime import date
import math
import numpy as np
import matplotlib.pyplot as plt

import csv

from termcolor import colored
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier

import requests

def linear_regression(input_i, output_o, return_val):
    """
    linear regression, give two array and a value to predict it's output.

    Parameters:
    input_i (list): list with input
    output_o (list): list with output
    return_val (int): the input to predict output with

    Returns:
    float: the predicted value based on the input (return_val), round to 3 decimal degit if needed.
    """
    input_i = np.array(input_i, dtype=float)
    output_o = np.array(output_o, dtype=float)
    m, b = np.polyfit(input_i, output_o, 1)
    return round(int(m) * int(return_val) + int(b), 3)

def graph(input_i, output_o):
    """
    graph, give two array and return a graph with all datapoint and line of best fit.

    Parameters:
    input_i (list): list with input
    output_o (list): list with output

    Returns:
    graph: a graph contained with all datapoint provided and a line of best fit
    """
    input_i = np.array(input_i, dtype=float)
    output_o = np.array(output_o, dtype=float)
    m, b = np.polyfit(input_i, output_o, 1)
    plt.plot(input_i, output_o, 'o')
    plt.plot(input_i, m*output_o + b)
    plt.show()

class Predict:
    """
    This is a class for predicting key information/cause for a epidemic.

    Attributes:
        year (int): The year that you want to estimate the value
    """

    def __init__(self, year):
        """
        The constructor for Predict class.

        Parameters:
           year (int): The year that you want to estimate the value
        """
        self.year = year

    def population(self):
        """
        The function to predict population.

        Returns:
            int: predicted population
        """
        return linear_regression([1500, 1650, 1750, 1804, 1850, 1900, 1927, 1950, 1955, 1960, 1965, 1970, 1975, 1980, 1985, 1990, 1995, 1999, 2006, 2009, 2011, 2020], [450000000, 500000000, 700000000, 1000000000, 1200000000, 1600000000, 2000000000, 2550000000, 2800000000, 3000000000, 3300000000, 3700000000, 4000000000, 4500000000, 4850000000, 5300000000, 5700000000, 6000000000, 6500000000, 6800000000, 7000000000, 7800000000], self.year)

    def climate_change(self):
        """
        The function to predict abnormal temperature.

        Returns:
            float: predicted abnormal temperature
        """
        # Source: https://data.giss.nasa.gov/gistemp/graphs/graph_data/Global_Mean_Estimates_based_on_Land_and_Ocean_Data/graph.txt
        return linear_regression([2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019], [0.65, 0.66, 0.69, 0.74, 0.78, 0.83, 0.87, 0.91, 0.95, 0.98], self.year)

    def democracy_index(self):
        """
        The function to predict democracy index.

        Returns:
            float: predicted democracy index
        """
        # Source: https://en.wikipedia.org/wiki/Democracy_Index#Democracy_Index_by_region
        return linear_regression([2006, 2008, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018], [5.52, 5.55, 5.46, 5.49, 5.52, 5.53, 5.55, 5.55, 5.52, 5.48, 5.48], self.year)

    def poverty(self):
        """
        The function to predict abnormal temperature.

        Returns:
            float: predicted abnormal temperature
        """
        # Source: https://data.worldbank.org/topic/poverty
        return linear_regression([1999, 2005, 2008, 2010, 2011, 2012, 2013, 2015], [25.5, 20.7, 18.2, 15.7, 13.7, 12.8, 11.2, 10], self.year)

    def gdp(self):
        """
        The function to predict world GDP average.

        Returns:
            float: predicted GDP average
        """
        # Source: https://www.worldometers.info/gdp/, unit: Trillion
        return linear_regression([2010, 2011, 2012, 2013, 2014, 2015, 2016], [66.04, 73.37, 75.06, 77.22, 73.73, 75.83, 77.80], self.year)

    def life_expectancy(self):
        """
        The function to predict world average life expectancy.

        Returns:
            float: predicted life expectancy
        """
        # Source: https://data.worldbank.org/indicator/SP.DYN.LE00.IN
        return linear_regression([2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017], [70.556, 70.884, 71.172, 71.462, 71.742, 71.948, 72.182, 72.383], self.year)

    def global_health_gdp_average(self):
        """
        The function to predict abnormal temperature.

        Returns:
            float: predicted abnormal temperature
        """
        # Source: https://www.healthsystemtracker.org/chart-collection/health-spending-u-s-compare-countries/#item-since-1980-the-gap-has-widened-between-u-s-health-spending-and-that-of-other-countries___2018
        return linear_regression([2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017], [10, 10.11, 10.24, 10.46, 10.52, 10.57, 10.62, 10.54], self.year)

    def flights(self):
        """
        The function to predict global flights count.

        Returns:
            float: predicted flights count
        """
        # Source: https://www.statista.com/statistics/564769/airline-industry-number-of-flights/
        return linear_regression([2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018], [27.8, 30.1, 31.2, 32, 33, 34, 35.2, 36.4, 38.1], self.year)

def Predict_Virus_Growth(day1, day2, day3, day4, day5, predict):
    """
    Predict_Virus_Growth, predict virus/infection/death growth in given day. Use latest 5 consecutive days of data

    Parameters:
    day1 (int): day 1 infection/death data
    day2 (int): day 2 infection/death data
    day3 (int): day 3 infection/death data
    day4 (int): day 4 infection/death data
    day5 (int): day 5 infection/death data
    predict (int): day to predict

    Returns:
    int: virus/infection/death predicted
    """
    # expoential
    b = day2 / day1
    a = day1
    x = 4

    expo = a * (b ** x)

    # linear
    line = linear_regression([1, 2, 3], [day1, day2, day3], 5)

    if abs(expo - day5) > abs(line - day5):
        return int(a * (b ** predict))
    else:
        return int(linear_regression([1, 2, 3, 4, 5], [day1, day2, day3, day4, day5], predict))

def train_epidemic(predict_evidence, year):
    """
    train the model for epidemic

    Parameters:
    predict_evidence
    predict (int): day to predict

    Returns:
    int: virus/infection/death predicted
    """

    request = requests.get('https://boyuan12.github.io/epidemic/epidemic.csv')
    wrapper = csv.reader(request.text.strip().split('\n'))

    evidence = []
    labels = []
    next(wrapper)

    for row in wrapper:
        # 1 = True, 0 = False

        if row[2] == 'True':
            if row[0] == str(year):
                return 1, 1, str(row[1])

            labels.append(1)
        else:
            if row[0] == str(year):
                return 1, 0, 'no'

            labels.append(0)

        population = float(row[3])
        climate_change = float(row[4])
        democracy_index = float(row[5])
        poverty = float(row[6])
        global_health = float(row[7])
        flight = float(row[8])

        evidence.append([population, climate_change,
                         democracy_index, poverty, global_health, flight])

    X_train, X_test, y_train, y_test = train_test_split(
        evidence, labels, test_size=0.1
    )

    model = KNeighborsClassifier(n_neighbors=3)
    model.fit(X_train, y_train)

    predictions = model.predict([predict_evidence])

    correct = round((y_test == predictions).sum() / len(y_test), 2)
    incorrect = round((y_test != predictions).sum() / len(y_test), 2)

    return correct, predictions[0], None

def Predict_Epidemic(year):

    population = Predict(year).population()
    climate_change = Predict(year).climate_change()
    democracy_index = Predict(year).democracy_index()
    poverty = Predict(year).poverty()
    global_health = Predict(year).global_health_gdp_average()
    flight = Predict(year).flights()
    correct, prediction, epi = train_epidemic(
        [population, climate_change, democracy_index, poverty, global_health, flight], year)
    return correct, prediction, epi


import math
import random
import csv
import cProfile
import numpy as np
import hashlib

memoization = {}

class Similarity:
    """
    This class contains instances of similarity / distance metrics. These are used in centroid based clustering
    algorithms to identify similar patterns and put them into the same homogeneous sub sets
    :param minimum: the minimum distance between two patterns (so you don't divide by 0)
    """

    def __init__(self, minimum=1.0e-16):
        self.e = minimum
        self.vector_operators = VectorOperations()

    def manhattan_distance(self, p_vec, q_vec):
        """
        This method implements the manhattan distance metric
        :param p_vec: vector one
        :param q_vec: vector two
        :return: the manhattan distance between vector one and two
        """
        return max(np.sum(np.fabs(p_vec - q_vec)), self.e)

    def square_euclidean_distance(self, p_vec, q_vec):
        """
        This method implements the squared euclidean distance metric
        :param p_vec: vector one
        :param q_vec: vector two
        :return: the squared euclidean distance between vector one and two
        """
        diff = p_vec - q_vec
        return max(np.sum(diff**2), self.e)

    def euclidean_distance(self, p_vec, q_vec):
        """
        This method implements the euclidean distance metric
        :param p_vec: vector one
        :param q_vec: vector two
        :return: the euclidean distance between vector one and two
        """
        return max(math.sqrt(self.square_euclidean_distance(p_vec, q_vec)), self.e)

    def half_square_euclidean_distance(self, p_vec, q_vec):
        """
        This method implements the half squared euclidean distance metric
        :param p_vec: vector one
        :param q_vec: vector two
        :return: the half squared euclidean distance between vector one and two
        """
        return max(0.5 * self.square_euclidean_distance(p_vec, q_vec), self.e)

    def cosine_similarity(self, p_vec, q_vec):
        """
        This method implements the cosine similarity metric
        :param p_vec: vector one
        :param q_vec: vector two
        :return: the cosine similarity between vector one and two
        """
        pq = self.vector_operators.product(p_vec, q_vec)
        p_norm = self.vector_operators.norm(p_vec)
        q_norm = self.vector_operators.norm(q_vec)
        return max(pq / (p_norm * q_norm), self.e)

    def tanimoto_coefficient(self, p_vec, q_vec):
        """
        This method implements the cosine tanimoto coefficient metric
        :param p_vec: vector one
        :param q_vec: vector two
        :return: the tanimoto coefficient between vector one and two
        """
        pq = self.vector_operators.product(p_vec, q_vec)
        p_square = self.vector_operators.square(p_vec)
        q_square = self.vector_operators.square(q_vec)
        return max(pq / (p_square + q_square - pq), self.e)

    def fractional_distance(self, p_vec, q_vec, fraction=0.5):
        """
        This method implements the fractional distance metric. I have implemented memoization for this method to reduce
        the number of function calls required. The net effect is that the algorithm runs 400% faster. A similar approach
        can be used with any of the above distance metrics as well.
        :param p_vec: vector one
        :param q_vec: vector two
        :param fraction: the fractional distance value (power)
        :return: the fractional distance between vector one and two
        """
        # memoization is used to reduce unnecessary calculations ... makes a BIG difference
        memoize = True
        if memoize:
            key = self.get_key(p_vec, q_vec)
            x = memoization.get(key)
            if x is None:
                diff = p_vec - q_vec
                #diff_fraction = diff**fraction
                diff_fraction = np.power(np.abs(diff), fraction)
                return max(np.power(abs(np.sum(diff_fraction)), float(1.0/fraction)), self.e)
            else:
                return x
        else:
            diff = p_vec - q_vec
            diff_fraction = diff**fraction
            return max(math.pow(np.sum(diff_fraction), 1/fraction), self.e)

    @staticmethod
    def get_key(p_vec, q_vec):
        """
        This method returns a unique hash value for two vectors. The hash value is equal to the concatenated string of
        the hash value for vector one and vector two. E.g. is hash(p_vec) = 1234 and hash(q_vec) = 5678 then get_key(
        p_vec, q_vec) = 12345678. Memoization improved the speed of this algorithm 400%.
        :param p_vec: vector one
        :param q_vec: vector two
        :return: a unique hash
        """
        # return str(hash(tuple(p_vec))) + str(hash(tuple(q_vec)))
        return str(hashlib.sha1(p_vec)) + str(hashlib.sha1(q_vec))

class VectorOperations():
    """
    This class contains useful implementations of methods which can be performed on vectors
    """

    @staticmethod
    def product(p_vec, q_vec):
        """
        This method returns the product of two lists / vectors
        :param p_vec: vector one
        :param q_vec: vector two
        :return: the product of p_vec and q_vec
        """
        return p_vec * q_vec

    @staticmethod
    def square(p_vec):
        """
        This method returns the square of a vector
        :param p_vec: the vector to be squared
        :return: the squared value of the vector
        """
        return p_vec**2

    @staticmethod
    def norm(p_vec):
        """
        This method returns the norm value of a vector
        :param p_vec: the vector to be normed
        :return: the norm value of the vector
        """
        return np.sqrt(p_vec)


import unittest
import numpy as np
from SDA import *

class SDAVectorMathTestCase(unittest.TestCase):

    def test_multi_vector_magnitude(self):
        """
        Test the algorithm to determine the distance between two points
        """
        vector_one = np.array([0, 0])
        vector_two = np.array([10, 10])

        dist = VectorMath.get_magnitude(vector_one, vector_two)

        self.assertEqual(dist, 200**0.5)

    def test_single_vector_magnitude(self):
        """
        Test the algorithm to determine the distance between two points
        """
        vector_one = np.array([10, 10])

        dist = VectorMath.get_vector_magnitude(vector_one)

        self.assertEqual(dist, 200**0.5)

    def test_multi_unit_vector(self):
        """
        Test the algorithm to determine two points' unit vector
        """
        vector_one = np.array([0, 0])
        vector_two = np.array([10, 10])

        unit_vector = VectorMath.get_unit_vector_from_two_vectors(vector_one, vector_two)

        self.assertEqual(unit_vector.all(), np.array([0.7071067814, 0.7071067814]).all())

    def test_single_unit_vector(self):
        """
        Test the algorithm to determine two points' unit vector
        """
        vector_one = np.array([10, 10])

        unit_vector = VectorMath.get_single_unit_vector(vector_one)

        self.assertEqual(unit_vector.all(), np.array([1, 1]).all())

    def test_vector_projection(self):
        """
        Test the algorithm for vector projection
        """
        vector_one = np.array([5, 5])
        vector_two = np.array([0, 10])

        projection_vector = VectorMath.get_vector_projection(vector_one, vector_two)
        correct_projection_vector = np.array([0, 5])

        self.assertEqual(correct_projection_vector.all(), projection_vector.all())


import numpy as np

dvar = np.float(0.0000000001)

#def deriv(func, val):
#    return (func(val+dvar)-func(val-dvar))/(dvar*2)

def deriv_func(func):
# description:  finding the derivate of the input function
# param:        func: the target function
# return:       func_out: the derivation function
    def func_out(val):
        result = (func(val+dvar)-func(val-dvar))/(dvar*2)
        return result
    return func_out

def solve_newton_method(func, initval):
# description:  find the zero point of input func, using Newton's method
# param:        func: the target function
#               initval: initial value of Newton's method
# return:       temp: the zero point
    func_deriv = deriv_func(func)
    temp = initval
    while abs(func(temp))>0.00001:
        if(func_deriv(temp)!=0):
            temp = temp - func(temp)/func_deriv(temp)
    return temp

#def func(x):
#    return x**2+2*x+1

def get_roc(func, val):
    deriv_1 = deriv(func, val)
    deriv_2 = (deriv(func, val+dvar)-deriv(func, val-dvar))/(dvar*2)
    return (1+deriv_1**2)**1.5/deriv_2

def get_min_roc(func, upper_lim, lower_lim):
    itera = np.linspace(upper_lim, lower_lim, 100)
    result = np.zeros(100)
    i = 0
    while i<100:
#        a = get_roc(func, itera[i])
        result[i]=(get_roc(func, itera[i]))
        i = i+1
#    print(np.min(result))
    for i in range(len(result)-1,-1,-1):
      if(result[i]<=0):
        result[i] = np.max(result)#negative replacer

    return np.min(result)


import numpy as np

# f = w*x

# f = 2*x
X = np.array([1,2,3,4], dtype=np.float32)
Y = np.array([2,4,6,8], dtype=np.float32)

w = 0.0

# model prediction
def forward(x): return w * x

# loss = MSE
def loss(y, y_predicted):
    return ((y - y_predicted)**2).mean()

# gradient
# MSE = 1/N * (w*x - y)**2
# dJ/dw = 1/N 2x (w*x - y)
def gradient(x, y, y_predicted):
    return np.dot(2*x, y_predicted-y).mean()

print(f'Prediction before training: f(5) = {forward(5):.3f}')

# Training
learning_rate = 0.01
n_iters = 10

for epoch in range(n_iters):
    # prediction = forward pass
    y_pred = forward(X)

    # loss
    l = loss(Y, y_pred)

    # gradients
    dw = gradient(X, Y, y_pred)

    # update weights
    w -= learning_rate * dw

    if epoch % 1 == 0:
        print(f'epoch {epoch+1}: w = {w:.3f}, loss = {l:.8f}')

print(f'Prediction after training: f(5) = {forward(5):.3f}')


import numpy as np
import pandas as pd
#
from pyta.overlays.simple_moving_average import simple_moving_average as sma
from pyta.helper import mean_dev

def commodity_channel_index(h: pd.Series, l: pd.Series, c: pd.Series, n: int = 20, const: float = 0.015) -> pd.Series:
    tp = (h + l + c) / 3
    tp_sma = sma(tp, n)
    tp_dev = mean_dev(tp, n)
    cci = (tp - tp_sma) / (tp_dev * const)
    return cci

commodity_channel_index.__doc__ = """Commodity Channel Index (CCI)

CCI essentially measures the current price level relative to an average over a given time.

Source:
    https://school.stockcharts.com/doku.php?id=technical_indicators:commodity_channel_index_cci

Formula:
    TP = (High + Low + Close) / 3
    CCI = (TP - SMA(n) of TP) / (const * MeanDev of TP)

Arguments:
    h (pdSeries)  : High series.
    ; (pdSeries)  : Low series.
    c (pdSeries)  : Close series.
    n (int)       : Period for MA [Default: 20]
    const (float) : Multiplier for MeanDev [Default: 0.015]

Returns:
    pdSeries
"""


import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm

df_beetles=pd.read_csv('data/beetle.csv' , index_col=0)
df_beetles

# setting up the model
y=df_beetles[["ndied","nalive"]]
#since we want our random variable to be represented by a binomial we need two parameters
#to define a binomial.

X = df_beetles["dose"]
X = sm.add_constant(X)
model = sm.GLM(y, X,family=sm.families.Binomial())
results = model.fit()

print( results.summary() )

# adding a couple of diagnostic plots
fig, ax = plt.subplots(ncols=2,figsize=(14,6))
ax[0].plot(results.mu , results.resid_pearson , 'bo')
ax[0].set_xlabel("predicted values")
ax[0].set_ylabel("pearson residuals")

# results.mu corresponds to the fitted proportion of dead beetle
# equivalent to : 1./(1+np.exp(-(-14.8230+0.2494*df_beetles["dose"])))
ax[1].plot(results.mu , df_beetles["prop"] , 'bo')
ax[1].plot( [0,max(y)], [0,max(y)] )
ax[1].set_xlabel("predicted values")
ax[1].set_ylabel("observed values")

plt.tight_layout()

# plotting the prediction
predictedProportionOfDead =  results.mu
# equivalent to : 1./(1+np.exp(-(-14.8230+0.2494*df_beetles["dose"])))

fig, ax = plt.subplots(ncols=1,figsize=(6,6))
ax.plot(df_beetles["dose"] , df_beetles["prop"] ,'bo',label='data')
ax.plot(df_beetles["dose"], predictedProportionOfDead,'r--',label='prediction GLM')

ax.set_xlabel('dose')
ax.set_ylabel('proportion of dead')
ax.legend(loc='best',fontsize=12)


'''

Problem number - 2
Difficulty rating - Hard
Company - Uber

Given an array of integers, return a new array such that each element at index i
of the new array is the product of all the numbers in the original array except
the one at i.

For example, if our input was [1, 2, 3, 4, 5], the expected output would be
[120, 60, 40, 30, 24]. If our input was [3, 2, 1], the expected output would be
[2, 3, 6].

Follow-up: what if you can't use division?

'''
import numpy as np

def naive(array):

    prod = 1
    for i in array :
        prod = prod * i

    ret_arr = []
    for i in array :
        ret_arr.append(int(prod/i))

    return ret_arr

def main():

    # generate array
    array = np.random.randint(1,10,5)
    print(array)
    print(naive(array))

if __name__ == "__main__" :
    main()


from numpy import sign

from .general_method import Method

class Bisection(Method,):
    """Bisection method class for bisection algorithm (https://en.wikipedia.org/wiki/Bisection_method).

    Attributes:
        f (function): function to search root against
        a (float): first interval boundary
        b (float): second interval boundary
        tol (float): tolerance
        max_iter (int): maximum number of iteration
    """

    def __init__(self, f, a, b, tol=1e-2, max_iter=1000):
        Method.__init__(self, f=f, tol=tol, max_iter=max_iter)
        self.a = a
        self.b = b

    def solve(self,):

        """run the Bisection root-finding method starting between points a and b.
        Stops if tolerance is matched or raise error if max iteration reached.

        Returns:
            float: computed root within interval.
        """
        a = self.a
        b = self.b
        self.epochs = []

        if 0 == self.f(a):
            return a
        elif 0 == self.f(b):
            return b

        if sign(self.f(a)) == sign(self.f(b)):
            raise ValueError(
                f"Same sign. f(a): {sign(self.f(a))},\tf(b): {sign(self.f(b))}"
            )

        for _ in range(self.max_iter):
            c = (a + b) / 2
            self.epochs.append(c)
            if (0 == self.f(c)) or ((b - a) / 2 < self.tol):  # solution found
                return c

            if sign(self.f(c)) == sign(self.f(a)):  # new interval
                a = c
            else:
                b = c
        raise Exception(
            "Convergence error. Reached maximum number of iterations without finding a stable enough solution"
        )

    def __repr__(self):
        return super().__repr__()

if __name__ == "__main__":
    print(Bisection(lambda x: x * x, 1e-1, 100))


"""
demo_Nd_unconstrained_optimization.py.py

Minimize a (quadratic) function of two variables:
Minimize f(X), where X = [x0, x1].T

Gradient algorithm with variable step size (steepest descent).
    Only for quadratic f(X) function. Requires gradient and hessian.
    Guaranteed to converge in n steps for n parameters, i.e. for X = [x1, ..., xn].T.
    X0 converges to X* for any X0.
    In parameter space, moving along orthogonal directions with variable step size.

Gradient algorithm with fixed step size.
    Only for quadratic f(X) function. Requires gradient and hessian.
    X0 converges to X* for any X0.
    In parameter space, moving along orthogonal directions with fixed step size.

Gradient algorithm with variable step size based on line search
    General algorithm for any f(X) function. Requires only gradient.
    X0 converges to X* for any X0.
    In parameter space, moving along orthogonal directions with variable step size determined by line search.
    Step size is determined by line search to ensure the descent property f(Xn+1) < f(Xn)

Conjugate gradient algorithm with variable step size (steepest descent).
    Only for quadratic f(X) function. Requires gradient and hessian.
    Guaranteed to converge in n steps for n parameters, i.e. for X = [x1, ..., xn].T.
    X0 converges to X* for any X0.
    In parameter space, moving along mutually conjugate directions with variable step size.

Conjugate gradient algorithm with variable step size based on line search
    General algorithm for any f(X) function. Requires only gradient.
    X0 converges to X* for any X0.
    In parameter space, moving along mutually conjugate directions with variable step size.
    Step size is determined by line search to ensure the descent property f(Xn+1) < f(Xn).
    For non-quadratic f(X) conjugate direction vectors will tend to deteriorate and need to be
        reset to the negative gradient every n steps.

Newton's algorithm.
    General algorithm for any f(X) function. Requires gradient and hessian.
    Can converge in as few as 1 step (for quadratic f(X)).
    X0 should be close to X*.
    In parameter space, moving to the MIN of parabola in 1 step.

Newton's algorithm with variable step size based on line search
    General algorithm for any f(X) function. Requires gradient and hessian.
    Can converge in as few as 1 step (for quadratic f(X)).
    X0 converges to X* for any X0.
    In parameter space, moving to the MIN of parabola in 1 step. Might be affected by linesearch.
    Step size is determined by line search to ensure the descent property f(Xn+1) < f(Xn).

Quasi-Newton algorithm with variable step size (steepest descent).
    Only for quadratic f(X) function. Requires gradient and hessian.
    Guaranteed to converge in n steps for n parameters, i.e. for X = [x1, ..., xn].T.
    X0 converges to X* for any X0.
    In parameter space, moving along mutually conjugate directions with variable step size.
    For non-quadratic f(X) conjugate direction vectors will tend to deteriorate and need to be
        reset to the negative gradient every n steps.

Quasi-Newton algorithm with variable step size based on line search
    General algorithm for any f(X) function. Requires only gradient.
    X0 converges to X* for any X0.
    In parameter space, moving along mutually conjugate directions with variable step size.
    Step size is determined by line search to ensure the descent property f(Xn+1) < f(Xn).
    For non-quadratic f(X) conjugate direction vectors will tend to deteriorate and need to be
        reset to the negative gradient every n steps.

Naive random walk algorithm
    General algorithm for any f(X) function.
    X0 converges to X* for any X0.

Simulated annealing algorithm
    General algorithm for any f(X) function.
    X0 converges to X* for any X0.

Particle swarm optimization algorithm
    General algorithm for any f(X) function.
    X0 converges to X* for any X0.
"""

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter
from mpl_toolkits.mplot3d import Axes3D
from gradient_algorithm import gradient_algorithm_var_alpha, gradient_algorithm_fixed_alpha, gradient_algorithm_linesearch
from conjugate_gradient_algorithm import conjugate_gradient_algorithm, conjugate_gradient_algorithm_linesearch
from newton_algorithm import newton_algorithm, newton_algorithm_linesearch
from quasi_newton_algorithm import quasi_newton_algorithm, quasi_newton_algorithm_linesearch
from naive_random_search_algorithm import naive_random_search_algorithm
from simulated_annealing_algorithm import simulated_annealing_algorithm
from particle_swam_optimization_algorithm import particle_swam_optimization_algorithm
from print_report import print_report
from plot_progress_y import plot_progress_y
from plot_progress_x import plot_progress_x
import time

reg_coeff = 0.01; # regularization coefficient
# Minimize the following objective function:
# X = [x0, x1].T;
# f(X) = 1 / 2 * X.T @ Q @ X + X.T @ B + C + reg_coeff * X.T @ X;
Q = np.array([[8, 2 * np.sqrt(2)], [2 * np.sqrt(2), 10]]);
B = np.array([[3], [6]]);
C = 24;
func = lambda X : 1 / 2 * X.T @ Q @ X + X.T @ B + C + reg_coeff * X.T @ X;
func_grad = lambda X : Q @ X + B + 2 * reg_coeff * X;
func_hessian = lambda X : Q + 2 * reg_coeff;
# Objective function for particle swarm optimization algorithm (particles along row dimension, axis = 0)
func_ps = lambda X: 3 * X[:, [0]] + 6 * X[:, [1]] + X[:, [0]] * (4 * X[:, [0]] + np.sqrt(2) * X[:, [1]]) + X[:, [1]] \
    * (5 * X[:, [1]] + np.sqrt(2) * X[:, [0]]) + 24 + reg_coeff * (X[:, [0]] * X[:, [0]] + X[:, [1]] * X[:, [1]]);

fig = plt.figure();
X_data = np.arange(-100, 100, 5);
Y_data = np.arange(-100, 100, 5);
Z_data = np.zeros(shape = (X_data.size,Y_data.size));
for iX in range (X_data.size):
    for iY in range (Y_data.size):
        Z_data[iX][iY] = func(np.array([X_data[iX], Y_data[iY]]));
X_data, Y_data = np.meshgrid(X_data, Y_data);

# Plot the surface
ax = fig.add_subplot(1, 2, 1, projection ='3d')
surf = ax.plot_surface(X_data, Y_data, Z_data, cmap = cm.coolwarm,
                       linewidth = 0, antialiased = False)
ax.set_xlabel('X')
ax.set_xlim(-100, 100)
ax.set_ylabel('Y')
ax.set_ylim(-100, 100)
plt.title('Objective function: surface plot')

# Plot the contour
ax = fig.add_subplot(1, 2, 2)
cset = ax.contour(X_data, Y_data, Z_data, 50, cmap=cm.coolwarm)
ax.set_xlabel('X')
ax.set_xlim(-100, 100)
ax.set_ylabel('Y')
ax.set_ylim(-100, 100)
plt.title('Objective function: contour plot')
plt.show()

# Gradient algorithm with variable step size (steepest descent)
print('***********************************************************************');
print('Gradient algorithm with variable step size (steepest descent)');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.array([[-100], [-100]]);
start = time.time();
X, report = gradient_algorithm_var_alpha(X0, func, func_grad, func_hessian, options);
end = time.time();
print_report(func, report);
# Plot path to X* for Y
algorithm_name = 'Gradient algorithm with variable alpha';
plot_progress_y(algorithm_name, report);
# Plot path to X* for X
plot_progress_x(X_data, Y_data, Z_data, algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Gradient algorithm with fixed step size
print('***********************************************************************');
print('Gradient algorithm with fixed step size');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.array([[-100], [-100]]);
start = time.time();
X, report = gradient_algorithm_fixed_alpha(X0, func, func_grad, func_hessian, options);
end = time.time();
print_report(func, report);
# Plot path to X* for Y
algorithm_name = 'Gradient algorithm with fixed alpha';
plot_progress_y(algorithm_name, report);
# Plot path to X* for X
plot_progress_x(X_data, Y_data, Z_data, algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Gradient algorithm with variable step size based on line search
print('***********************************************************************');
print('Gradient algorithm with variable step size based on line search');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.array([[-100], [-100]]);
start = time.time();
X, report = gradient_algorithm_linesearch(X0, func, func_grad, options);
end = time.time();
print_report(func, report);
# Plot path to X* for Y
algorithm_name = 'Gradient algorithm with line search';
plot_progress_y(algorithm_name, report);
# Plot path to X* for X
plot_progress_x(X_data, Y_data, Z_data, algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Conjugate gradient algorithm with variable step size (steepest descent)
print('***********************************************************************');
print('Conjugate gradient algorithm with variable step size (steepest descent)');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.array([[-100], [-100]]);
start = time.time();
X, report = conjugate_gradient_algorithm(X0, func, func_grad, func_hessian, options);
end = time.time();
print_report(func, report);
# Plot path to X* for Y
algorithm_name = 'Conjugate gradient algorithm with variable alpha';
plot_progress_y(algorithm_name, report);
# Plot path to X* for X
plot_progress_x(X_data, Y_data, Z_data, algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Conjugate gradient algorithm with variable step size based on line search
print('***********************************************************************');
print('Conjugate gradient algorithm with variable step size based on line search');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.array([[-100], [-100]]);
start = time.time();
X, report = conjugate_gradient_algorithm_linesearch(X0, func, func_grad, options);
end = time.time();
print_report(func, report);
# Plot path to X* for Y
algorithm_name = 'Conjugate gradient algorithm with line search';
plot_progress_y(algorithm_name, report);
# Plot path to X* for X
plot_progress_x(X_data, Y_data, Z_data, algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Newton's algorithm
print('***********************************************************************');
print('Newton\'s algorithm');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.array([[-100], [-100]]);
start = time.time();
X, report = newton_algorithm(X0, func, func_grad, func_hessian, options);
end = time.time();
print_report(func, report);
# Plot path to X* for Y
algorithm_name = 'Newton\'s algorithm';
plot_progress_y(algorithm_name, report);
# Plot path to X* for X
plot_progress_x(X_data, Y_data, Z_data, algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Newton's algorithm with variable step size based on line search
print('***********************************************************************');
print('Newton\'s algorithm with variable step size based on line search');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.array([[-100], [-100]]);
start = time.time();
X, report = newton_algorithm_linesearch(X0, func, func_grad, func_hessian, options);
end = time.time();
print_report(func, report);
# Plot path to X* for Y
algorithm_name = 'Newton\'s algorithm with line search';
plot_progress_y(algorithm_name, report);
# Plot path to X* for X
plot_progress_x(X_data, Y_data, Z_data, algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Quasi-Newton algorithm with variable step size (steepest descent)
print('***********************************************************************');
print('Quasi-Newton algorithm with variable step size (steepest descent)');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.array([[-100], [-100]]);
start = time.time();
X, report = quasi_newton_algorithm(X0, func, func_grad, func_hessian, options);
end = time.time();
print_report(func, report);
# Plot path to X* for Y
algorithm_name = 'Quasi-Newton algorithm with variable alpha';
plot_progress_y(algorithm_name, report);
# Plot path to X* for X
plot_progress_x(X_data, Y_data, Z_data, algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Quasi-Newton algorithm with variable step size based on line search
print('***********************************************************************');
print('Quasi-Newton algorithm with variable step size based on line search');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.array([[-100], [-100]]);
start = time.time();
X, report = quasi_newton_algorithm_linesearch(X0, func, func_grad, options);
end = time.time();
print_report(func, report);
# Plot path to X* for Y
algorithm_name = 'Quasi-Newton algorithm with line search';
plot_progress_y(algorithm_name, report);
# Plot path to X* for X
plot_progress_x(X_data, Y_data, Z_data, algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Naive random walk algorithm
print('***********************************************************************');
print('Naive random walk algorithm');
N_iter_max = 1000;
tolerance_x = 10e-8;
tolerance_y = 10e-8;
X_lower = np.array([[-150], [-150]]); # X lower bound
X_upper = np.array([[150], [150]]); # X upper bound
alpha = 5; # step size
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max, 'x_lower' : X_lower,
           'x_upper' : X_upper, 'alpha' : alpha};
X0 = np.array([[-20], [-20]]); # X0 = X_lower + (X_upper - X_lower) * np.random.rand(X_lower.size, 1);
start = time.time();
X, report = naive_random_search_algorithm(X0, func, options);
end = time.time();
print_report(func, report);
# Plot path to X* for Y
algorithm_name = 'Naive random walk algorithm';
plot_progress_y(algorithm_name, report);
# Plot path to X* for X
plot_progress_x(X_data, Y_data, Z_data, algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Simulated annealing algorithm
print('***********************************************************************');
print('Simulated annealing algorithm');
N_iter_max = 1000;
tolerance_x = 10e-8;
tolerance_y = 10e-8;
X_lower = np.array([[-150], [-150]]); # X lower bound
X_upper = np.array([[150], [150]]); # X upper bound
alpha = 5; # step size
gamma = 1.5; # controls temperature decay, gamma > 0
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max, 'x_lower' : X_lower,
           'x_upper' : X_upper, 'alpha' : alpha, 'gamma' : gamma};
X0 = np.array([[-20], [-20]]); # X0 = X_lower + (X_upper - X_lower) * np.random.rand(X_lower.size, 1);
start = time.time();
X, report = simulated_annealing_algorithm(X0, func, options);
end = time.time();
print_report(func, report);
# Plot path to X* for Y
algorithm_name = 'Simulated annealing algorithm';
plot_progress_y(algorithm_name, report);
# Plot path to X* for X
plot_progress_x(X_data, Y_data, Z_data, algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Particle swarm optimization algorithm
print('***********************************************************************');
print('Particle swarm optimization algorithm');
N_iter_max = 1000;
tolerance_x = 10e-8;
tolerance_y = 10e-8;
X_lower = np.array([[-150], [-150]]); # X lower bound
X_upper = np.array([[150], [150]]); # X upper bound
d_lower = -1; # direction (aka velocity) lower bound
d_upper = 1; # direction (aka velocity) upper bound
N_ps = 10000; # number of particles
w = 0.9; # inertial constant, w < 1
c1 = 1.5; # cognitive/independent component, c1 ~ 2
c2 = 1.5; # social component, c2 ~ 2
alpha = 1; # step size
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max, 'x_lower' : X_lower,
           'x_upper' : X_upper, 'alpha' : alpha, 'd_lower' : d_lower, 'd_upper' : d_upper, 'N_ps' : N_ps, 'w' : w, 'c1' : c1, 'c2' : c2};
start = time.time();
X, report = particle_swam_optimization_algorithm(func_ps, options);
end = time.time();
print_report(func, report);
# Plot path to X* for Y
algorithm_name = 'Particle swarm optimization algorithm';
plot_progress_y(algorithm_name, report);
# Plot path to X* for X
plot_progress_x(X_data, Y_data, Z_data, algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');


"""  """

class Node(object):
    """  This class represents a node in MiniFlow architecture. """
    def __init__(self, inbound_nodes=[]):
        # Node(s) from which this Node receives values
        self.inbound_nodes = inbound_nodes
        # Node(s) to which this Node passes values
        self.outbound_nodes = []
        # For each inbound_node, add the current Node as an outbound_node.
        for n in self.inbound_nodes:
            n.outbound_nodes.append(self)
        # A calculated value
        self.value = None

    def forward(self):
        """ Forward propagation.

        Compute the output value based on `inbound_nodes` and store the result in self.value.

        :return:
        """
        raise NotImplemented

class Input(Node):
    """  This class represents a Inputs node in MiniFlow architecture. """
    def __init__(self):
        # An Input node has no inbound nodes,
        # so no need to pass anything to the Node instantiator.
        Node.__init__(self)

    # NOTE: Input node is the only node where the value
    # may be passed as an argument to forward().
    #
    # All other node implementations should get the value
    # of the previous node from self.inbound_nodes
    #
    # Example:
    # val0 = self.inbound_nodes[0].value
    def forward(self, value=None):
        """ Forward propagation.

        Compute the output value based on `inbound_nodes` or `passed arguments` and store the result in self.value.

        :return:
        """
        # Overwrite the value if one is passed in.
        if value is not None:
            self.value = value

class Add(Node):
    """  This class represents a add node in MiniFlow architecture. """
    # def __init__(self, x, y):
    # You could access `x` and `y` in forward with
    # self.inbound_nodes[0] (`x`) and self.inbound_nodes[1] (`y`)
    # Node.__init__(self, [x, y])
    # You may need to change this...
    def __init__(self, *inputs):
        Node.__init__(self, inputs)

    def forward(self):
        """
        Set the value of this node (`self.value`) to the sum of its inbound_nodes.
        Remember to grab the value of each inbound_node to sum!

        Your code here!
        """
        self.value = sum([n.value for n in self.inbound_nodes])

class Mul(Node):
    """  This class represents a multiplication node in MiniFlow architecture. """
    # def __init__(self, x, y):
    # You could access `x` and `y` in forward with
    # self.inbound_nodes[0] (`x`) and self.inbound_nodes[1] (`y`)
    # Node.__init__(self, [x, y])
    # You may need to change this...
    def __init__(self, *inputs):
        Node.__init__(self, inputs)

    def forward(self):
        """
        Set the value of this node (`self.value`) to the sum of its inbound_nodes.
        Remember to grab the value of each inbound_node to sum!

        Your code here!
        """
        from numpy import prod as npprod
        self.value = npprod([n.value for n in self.inbound_nodes])

class Linear(Node):
    """ This class represents a linear node in MiniFlow architecture. """
    def __init__(self, inputs, weights, bias):
        Node.__init__(self, [inputs, weights, bias])

        # NOTE: The weights and bias properties here are not
        # numbers, but rather references to other nodes.
        # The weight and bias values are stored within the
        # respective nodes.

    def forward(self):
        """
        Set self.value to the value of the linear function output.

        Your code goes here!
        """
        # from numpy import multiply as npmultiply
        # self.value = sum(npmultiply(self.inbound_nodes[0].value, self.inbound_nodes[1].value)) + self.inbound_nodes[2].value
        from numpy import dot as npdot
        self.value = npdot(self.inbound_nodes[0].value, self.inbound_nodes[1].value) + self.inbound_nodes[2].value

class Sigmoid(Node):
    """
    You need to fix the `_sigmoid` and `forward` methods.
    """
    def __init__(self, node):
        Node.__init__(self, [node])

    def _sigmoid(self, x):
        """
        This method is separate from `forward` because it
        will be used later with `backward` as well.

        `x`: A numpy array-like object.

        Return the result of the sigmoid function.

        Your code here!
        """
        from numpy import exp as npexp
        return 1/(1 + npexp(-x))

    def forward(self):
        """
        Set the value of this node to the result of the
        sigmoid function, `_sigmoid`.

        Your code here!
        """
        # This is a dummy value to prevent numpy errors
        # if you test without changing this method.
        self.value = self._sigmoid(self.inbound_nodes[0].value)

class MSE(Node):
    def __init__(self, y, a):
        """
        The mean squared error cost function.
        Should be used as the last node for a network.
        """
        # Call the base class' constructor.
        Node.__init__(self, [y, a])

    def forward(self):
        """
        Calculates the mean squared error.
        """
        # NOTE: We reshape these to avoid possible matrix/vector broadcast
        # errors.
        #
        # For example, if we subtract an array of shape (3,) from an array of shape
        # (3,1) we get an array of shape(3,3) as the result when we want
        # an array of shape (3,1) instead.
        #
        # Making both arrays (3,1) insures the result is (3,1) and does
        # an elementwise subtraction as expected.
        y = self.inbound_nodes[0].value.reshape(-1, 1)
        a = self.inbound_nodes[1].value.reshape(-1, 1)
        # TODO: your code here
        from numpy import square as npsquare
        from numpy.linalg import norm as npnorm
        self.value = npsquare(npnorm(y - a)) / len(y)

def topological_sort(feed_dict):
    """
    Sort generic nodes in topological order using Kahn's Algorithm.

    `feed_dict`: A dictionary where the key is a `Input` node and the value is the respective value feed to that node.

    Returns a list of sorted nodes.
    """

    input_nodes = [n for n in feed_dict.keys()]

    G = {}
    nodes = [n for n in input_nodes]
    while len(nodes) > 0:
        n = nodes.pop(0)
        if n not in G:
            G[n] = {'in': set(), 'out': set()}
        for m in n.outbound_nodes:
            if m not in G:
                G[m] = {'in': set(), 'out': set()}
            G[n]['out'].add(m)
            G[m]['in'].add(n)
            nodes.append(m)

    L = []
    S = set(input_nodes)
    while len(S) > 0:
        n = S.pop()

        if isinstance(n, Input):
            n.value = feed_dict[n]

        L.append(n)
        for m in n.outbound_nodes:
            G[n]['out'].remove(m)
            G[m]['in'].remove(n)
            # if no other incoming edges add to S
            if len(G[m]['in']) == 0:
                S.add(m)
    return L

def forward_pass(output_node, sorted_nodes=None):
    """
    Performs a forward pass through a list of sorted nodes.

    Arguments:

        `output_node`: A node in the graph, should be the output node (have no outgoing edges).
        `sorted_nodes`: A topologically sorted list of nodes.

    Returns the output Node's value
    """
    if sorted_nodes:
        for n in sorted_nodes:
            n.forward()
        return output_node.value
    else:
        # Forward pass
        for n in output_node:
            n.forward()

def main(which):
    """ This is main function. """
    if which == 1:
        x, y = Input(), Input()
        f = Add(x, y)
        feed_dict = {x: 10, y: 5}
        sorted_nodes = topological_sort(feed_dict)
        output = forward_pass(f, sorted_nodes)
        # NOTE: because topological_sort sets the values for the `Input` nodes we could also access
        # the value for x with x.value (same goes for y).
        print("{} + {} = {} (according to miniflow)".format(feed_dict[x], feed_dict[y], output))
    elif which == 2:
        x, y, z = Input(), Input(), Input()
        f = Add(x, y, z)
        feed_dict = {x: 4, y: 5, z: 10}
        graph = topological_sort(feed_dict)
        output = forward_pass(f, graph)
        print("{} + {} + {} = {} (according to miniflow)".format(feed_dict[x], feed_dict[y], feed_dict[z], output))
    elif which == 3:
        x, y, z = Input(), Input(), Input()
        f = Mul(x, y, z)
        feed_dict = {x: 4, y: 5, z: 10}
        graph = topological_sort(feed_dict)
        output = forward_pass(f, graph)
        print("{} * {} * {} = {} (according to miniflow)".format(feed_dict[x], feed_dict[y], feed_dict[z], output))
    elif which == 4:
        inputs, weights, bias = Input(), Input(), Input()
        f = Linear(inputs, weights, bias)
        feed_dict = {
            inputs: [6, 14, 3],
            weights: [0.5, 0.25, 1.4],
            bias: 2
        }
        graph = topological_sort(feed_dict)
        output = forward_pass(f, graph)
        print(output)  # should be 12.7 with this example
    elif which == 5:
        import numpy as np
        X, W, b = Input(), Input(), Input()
        f = Linear(X, W, b)
        g = Sigmoid(f)
        X_ = np.array([[-1., -2.], [-1, -2]])
        W_ = np.array([[2., -3], [2., -3]])
        b_ = np.array([-3., -5])
        feed_dict = {X: X_, W: W_, b: b_}
        graph = topological_sort(feed_dict)
        output = forward_pass(g, graph)
        """
        Output should be:
        [[  1.23394576e-04   9.82013790e-01]
         [  1.23394576e-04   9.82013790e-01]]
        """
        print(output)
    elif which == 6:
        import numpy as np
        y, a = Input(), Input()
        cost = MSE(y, a)
        y_ = np.array([1, 2, 3])
        a_ = np.array([4.5, 5, 10])
        feed_dict = {y: y_, a: a_}
        graph = topological_sort(feed_dict)
        # forward pass
        forward_pass(graph)
        """
        Expected output

        23.4166666667
        """
        print(cost.value)
    return None

if __name__ == "__main__":
    main(which=1)
    main(which=2)
    main(which=3)
    main(which=4)
    main(which=5)
    main(which=6)


# Copyright 2018 - Jonathan Alcantara e Osmar Fernandes

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import numpy as np

tolerance = 0.00001
A = [[1, 0.2, 0], [0.2, 1, 0.5], [0, 0.5, 1]]

def power_method(A):
    X = [1]*len(A[0])

    new_X = list(X)
    eigenvalue_candidate = 1
    solution_residue = eigenvalue_candidate

    while solution_residue > tolerance:
        print '\nLargest Eigenvalue Candidate: ', eigenvalue_candidate
        print 'Associated Eigenvector: ', X
        X = np.matmul(A, X)

        solution_residue = abs(X[0] - eigenvalue_candidate)/abs(X[0])
        eigenvalue_candidate = X[0]

        for X_element in range(len(X)):
            X[X_element] = X[X_element]/float(eigenvalue_candidate)

    print '\nLargest Eigenvalue: ', eigenvalue_candidate
    print 'Associated Eigenvector: ', X


"""
var = sd^2 = summ(Xi^2 - mu)/N = summ(Xi^2)/N - mu^2

Observation is:
Biased_Variance = (n-1)/n Population/Total_Variance
OR,
summ(Xi^2 - mu)/n = (n-1)/n Population/Total_Variance
=> Population_Variance = summ(Xi^2 - mu)/(n-1)
is a better Variance estimate than Sample Variance
"""

from math import sqrt, pi, e
from itertools import izip, imap
from numpy import var
import scipy.stats
from pandas import DataFrame, read_pickle
#zscore, t score, f score
from scipy.stats import norm, t, f, manwhitneyu, shapiro

def mean(x): return reduce(lambda a,b: (a+b), x)/float(len(x))

"""
access like this df[0.01][38][4] - df[critical percent][df][num of samples]
#Make sure the q_table csv is in same parent directory else calc_tukey_hsd won't work
"""
q_table = read_pickle('q_table')

def mean_arr(x):
    sumr = 0.
    prod_list = []
    for i in x:
        sumr += i
        prod_list.append(i*i)
    return sumr/len(x), prod_list

def variance_biased(x):
    """x is a list of numbers"""
    mean_x, prod_list = mean_arr(x)
    return sum(prod_list)/float(len(x)) - (mean_x)**2

def variance_unbiased(x):
    """x is a list of numbers"""
    xbar = mean(x)
    return sum([(i-xbar)**2 for i in x])/float(len(x)-1)

def std_deviation_biased(x):
    return sqrt(variance_biased(x))

def std_deviation_unbiased(x):
    return sqrt(variance_unbiased(x))

def normal_distro_probability(x, xbar, var):
    z2 = float(x-xbar)**2/(2*var)  #z squared
    return 1./sqrt((2*pi*var) * (e**z2))

def normal_sample_variance(original_var, sample_size):
    """
    given original variance but not all data points.
    sqrt(var/n) = Std. Error
    """
    return original_var/sample_size

############# ALL ABOUT CONFIDENCE INTERVAL & INFERENTIAL STATS ################

#standard error
se = lambda sd, sample_size: (sd/sqrt(sample_size))

def se_independent_sample(sd1, sd2, n1, n2):
    """
    1. WELCH's T TEST
    2. USE WHEN WE CMP 2 DIFF MEANS
    ---------------------------------
    Useful for finding out
    CI and comparing Z stats Given a population metric
    """
    return sqrt((sd1**2 + sd2**2)/float(n1)) if n1==n2 else sqrt((sd1**2/float(n1)) + (sd2**2/float(n2)))

def df_two_sampled_welch(var1, var2, n1, n2):
    #IMP dof FUNC!!
    #2 sample Deg of Freedom for 2 Sample Welch's T Test
    df1, df2 = n1-1, n2-1
    return float(var1/float(n1) + var2/float(n2))**2/\
    float((var1**2/float(n1**2 * df1)) + (var2**2/float(n2**2 * df2)))

pooled_variance = lambda x,y,n1,n2: ((var(x)*n1) + (var(y)*n2))/float(n1+n2-2)

def se_pooled_t(x, y):
    """
    Useful when calculating T stats reports
    since sample metrics are given.
    x, y: arrays
    n1, n2: sizes of x, y respectively.
    """
    n1,n2 = len(x), len(y)
    #sd^2 = \summ(x-xbar)^2 + \summ(y-ybar)^2/df1+df2
    pooled_var = pooled_variance(x,y,n1,n2)
    return round(sqrt(pooled_var * (1./n1 + 1./n2)), 2)

df_independent_sample = lambda n1,n2: (n1+n2-2)

#marginal error given z score range
marginal_z = lambda z_score, se: z_score * se

def calc_z(mu, xbar, sd, sample_size):
    """
    Calculate Z score given normal sample distribution mean
    and population mean, its std dev and the sample size for the
    sample distribution.
    """
    #std_error = se(sd, sample_size)
    return float(xbar - mu)/se(sd, sample_size)

#half if two tailed else not and /100
def critical_z(percentile, one_tailed):
    #calculate critical z from critical percentage
    return round(abs(scipy.stats.norm.ppf((100 - percentile)/100.)), 3) if one_tailed \
    else round(abs(scipy.stats.norm.ppf((100 - percentile)/200.)), 3)

def calc_probability_sample_mean(mu, xbar, sd, sample_size):
    #calculate percentage from z score
    return scipy.stats.norm.cdf(calc_z(mu, xbar, sd, sample_size))

def calc_t(mu, xbar, sd, sample_size):
    #calculating critical t
    return round(calc_z(mu, xbar, sd, sample_size), 2)

def calc_t_independent_sample(mu, xbar, se):
    #calculating critical t when se is given/independent samples
    return round(float(xbar-mu)/se, 2)

def t_percentile(t_val, df, one_tailed=0):
    """
    Find P-Value Given T score, DF and if its 1 Tailed or 2 Tailed
    """
    return t.sf(t_val, df) if one_tailed else t.sf(t_val, df) * 2

def critical_t(percentile, df, one_tailed):
    return round(t.isf((100-percentile)/100., df), 3) if one_tailed else round(t.isf((100-percentile)/200., df), 3)

#same as critical_t, but in proportion
def t_val_from_t_percentile(t_percentile, df, one_tailed = 0):
    """
    Find T score given T percentile, DF
    and if its 1 Tailed or 2 Tailed
    t_percentile: % in proportion
    """
    return round(t.isf(t_percentile, df), 3) if one_tailed else round(t.isf(t_percentile/2., df), 3)

def t_cmp(calculated_t, critical_t):
    """
    Given Critical T Value and T score,
    Return if its Significant or Not.
    """
    return abs(calculated_t) > abs(critical_t)

### Correlation Measures using T Stats ####
def t_r_squared(t_score, df):
    #calculated t score cousin of r squared for line fitting - correlation
    """
    R square generally tells us how much of the difference between 2 means
    in a particular hypothetisis is due to the given statistical factors
    OR R squared Error;
    Higher the Value better the Model of Assumption.
    Lower the value implies we are missing out on some stat factors and that
    its a weak hypothesis.
    """
    t= t_score**2
    return t/float(t+df)

def t_significant(x, y, percentile, one_sided):
    """
    by calculating from independent sampling from a bunch of samples
    when population params are not given
    """
    df = df_independent_sample(len(x), len(y))
    t_critical = critical_t(percentile, df, one_sided)
    xbar= mean(x)
    ybar = mean(y)
    std_dev = se_pooled_t(x, y)
    t_score = calc_t_independent_sample(ybar, xbar, std_dev)
    return t_score, t_cmp(t_score, t_critical)

def ci_t_margin_error(sd, sample_size, df, t_score = None, percentile = None, one_tailed = 0):
    critical_t_score = 0.
    if percentile:
        critical_t_score = critical_t(percentile, df, one_tailed)
    elif t_score:
        critical_t_score = t_score  #this is the Critical T value!
    #std_error = se(sd, sample_size)
    return marginal_z(critical_t_score, se(sd, sample_size))

def ci_t(xbar, sd, sample_size, df, t_score = None, percentile = None, one_tailed = 0):
    margin_error = ci_t_margin_error(sd, sample_size, df, t_score = t_score, percentile = percentile, one_tailed = one_tailed)
    return (xbar - margin_error, xbar + margin_error)

def ci_margin_error(sd, sample_size, z_score = None, percentile = None, one_tailed = 0):
    """
    z score * std_error is the margine of error i C.I. from the sample mean
    """
    critical_z_score = 0.
    if percentile: critical_z_score = critical_z(percentile, one_tailed)
    elif z_score: critical_z_score = z_score
    #std_error = se(sd, sample_size)
    return marginal_z(critical_z_score, se(sd, sample_size))

#confidence interval
def ci(xbar, sd, sample_size, z_score = None, percentile = None, one_tailed = 0):
    """
    xbar: sample mean
    percentile: in %
    """
    margin_error = ci_margin_error(sd, sample_size, z_score = z_score, percentile = percentile, one_tailed=one_tailed)
    return (xbar - margin_error, xbar + margin_error)

def ci_generic(xbar, margin_error):
    return (xbar-margin_error, xbar+margin_error)

#Decision process component
def z_cmp(calculated_z_score_proportion, criticalz_percentage_proportion):
    """
    calculated_z_score_proportion: the z score calculated from mu and xbar in proportion
    criticalz_percentage_proportion: Given Critical Value proportion for acceptance criteria
    if calculated_z_score_proportion > criticalz_percentage_proportion
    then the chance of that happening is p < criticalz_percentage_proportion
    """
    return abs(calculated_z_score_proportion) > scipy.stats.norm.cdf(scipy.stats.norm.ppf((100-criticalz_percentage_proportion)/100.))

######## ANALYSIS OF VARIANCE - ANOVA ###########
"""
F = Between Group Variability/Within Group Variability
"""
f_critical = lambda alpha, dfb, dfw: round(f.isf(alpha, dfb, dfw), 4)
#between group var
#xbar_groups is an array of x means
df_ssb = lambda xbar_groups: float(len(xbar_groups) - 1)

ssb = lambda xg, xbar_groups, each_grp_sample_size: \
sum([ni * (xbar - xg)**2 for ni, xbar in izip(each_grp_sample_size, xbar_groups)])/df_ssb(xbar_groups)

#within group var
#x_samples is an array of lists - this is like df for pooled variance
df_ssw = lambda x_samples: float(reduce(lambda x,y: x+y, imap(len, x_samples)) - len(x_samples))

#this is like pooled variance for more than 2 samples - generic
ssw = lambda x_samples: sum([var(samples) * len(samples) for samples in x_samples])/df_ssw(x_samples)

#F ratio stats
anova_ratio = lambda xg, xbar_groups, x_samples, each_grp_sample_size:ssb(xg, xbar_groups, each_grp_sample_size)/ssw(x_samples)

#if greater we reject null hypothesis else we fail to reject
f_cmp = lambda f_score, f_critical: abs(f_score) > abs(f_critical)

###### CALCULATING EFFECT MEASURES (TUKEYS HSD)- measures of significance not due to samplign error or chances #########

#this can be used when the num of samples per group are not the same N
var_num_samples = lambda x_samples: len(x_samples)/reduce(lambda n1,n2: (1/float(n1) + 1/float(n2)), imap(len, x_samples))

"""
#honesty significant differences - which samples are significantly different?
#q = df[critical_per][dfw][num_samples]
This is how you calculate Q:
Q = ml - ms / sqrt of mssw / n
k = num of groups
n = Total num of elements per group across all groups for an experiment where each group has same elements, else refer to var_num_samples(x_samples)
to calculate n;
"""

calc_tukey_hsd = lambda critical_per, dfw, ssw_score, k, n: round(q_table[critical_per][dfw][k] * sqrt(float(ssw_score)/n), 4)

def tukey_hsd_cmp(mean_diff, tukey_hsd):
    """
    Reject Null if mean diff > tukey hsd
    """
    return mean_diff > tukey_hsd

#cohens d multiple comparisons ANOVA
cohens_d_multiple = lambda x1,x2,ssw_score: float(x1-x2)/sqrt(ssw_score)

#eta squared - proportion of explained differences - sort of r sqrd
#Analogous to R squared Difference of T Test;
eta_sqrd = lambda ssb_score, ss: float(ssb_score)/ss
#ss = total or ssb+ssw = \summ((xi - xg)**2) summation of all xi minus grand xg

######## OTHER TESTS ON NON GAUSSIAN DATASETS ###############
"""
Man - Whitney U-Test
u,p = manwhitneyu(x_arr, y_arr)
Non-parametric test;
- non-normal data
- find descriptive variables.
"""
manwhitneyu_test = lambda x_arr, y_arr: manwhitneyu(x_arr, y_arr)

"""
Shapiro-Wilk Test
- Plot Histogram to check for Bell Curve
- Perform Shapiro-Wilk Test
s, p = shapiro(x_arr)
"""
shapiro_test = lambda x_arr: shapiro(x_arr)


# Applying PCA on two dimensional dummy data .............................

# PCA focuses only the data and not the output labels.
# Note: Reduction is always dependent on data. Which features to reduce, and how much information will be lost is dependent on the dataset.
import numpy as np
from sklearn.decomposition import PCA
from matplotlib import pyplot as plt

# Using PCA on 2-Dimensional Data.....................
# Plotting random/dummy 2-D data..........
x1 = np.array([1,2,3,4,5,6])
x2 = np.array([7.5, 11, 16, 18, 20, 26])
plt.scatter(x1, x2)
plt.show()

# Concatenating the two single feature arrays for 2-D data. It is done to create a two dimensional dataset(x1, x2 as features).
X = np.c_[x1, x2]           # Concatenate function,combines two lists.

# Creating the PCA object.....
# n_components: This parameter specifies how many features we need finally. If n_components is not passed then the no. of new dimensions is same as no of original features.
# Here we can try any number of feature values less than equal to the total number of features we originally had.
pca = PCA(n_components = 1)

# Here when we fit we try to find a line that finds a best line for the data.
# With transform function we transform the data according to x1' and x2' features. These new values are according to new directions.
X_reduced = pca.fit_transform(X)
print(X_reduced)
# The compnents_ parameter tells us about the unit vector in the new direction(basically i-cap and j-cap values (because originally its two dimensional data) for the x1' and x2' directions.)
# These are unit vectors along the new features which hold maximum variance. This is sorted and the first direction holds the information with maximum variance.
print(pca.components_)

# To bring back the data to original dimensions/directions we can use the inverse_transform function.
# If we reduce the feature values we will not be able to get back our data as some information is lost. The values will differ from original values.
X_approx = pca.inverse_transform(X_reduced)

#  Plotting the new x values after reducing in the original direction. After reducing components the point lie on a line as one direction is reduced.
plt.plot(X_approx[:, 0], X_approx[:, 1])
plt.scatter(x1, x2)
plt.show()