import numpy as np
import scipy.stats as ss
import matplotlib.pyplot as plt

#------------------------------------------------------------------------------------------------#
#------------------------------------------ Generals --------------------------------------------#
#------------------------------------------------------------------------------------------------#
class generals:
    """
    generals Class contain the general statistics methods to work with probabilities,
    distributions, confidence intervals and hypothesis testing.

    Methods:
    --------------------------
    get_z(x, mu ,sd, n=None)
        Function to get the Z value.
    """

    def get_z(x, mu, sd, **kwargs):
        """
        Function to get the Z value (standard deviations number under or over the average)
        correspondent of the x value (parameter) in the standard normal distribution.

        Parameters:
        --------------------------
        x : double, float
            Specific value from the random variable.

        mu : double, float
            Population average given.

        sd : double, float
            Population or sample standard deviation given.

        n : int, optional
            Sample size, if you are working with sample distributions. (default=None)

        Returns:
        --------------------------
        z : float, double
            The Z value (standard deviations number under or over the average)
            correspondent of the x value (parameter) in the standard normal distribution.
        """
        if not kwargs:
            z = ((x - mu) / sd)
            return z
        else:
            n = kwargs.get('n', None)
            if (n <= 30):
                print("The sample size must be greater than 30.")
            else:
                z = ((x - mu) / (sd / n**0.5))
                return z

    def get_z_2p(x_1, x_2, s1, s2, n1, n2):
        """

        """
        if (n1 <= 30 or n2 <= 30):
            print("The sample sizes must be greater than 30.")
        else:
            s_error = ((s1**2 / n1) + (s2**2 / n2))**0.5

            z = (x_1 - x_2) / s_error
            return z

    def get_t(x, mu, s, n):
        """
        Function to get the T value correspondent of the X value in the t-student
        distribution.

        Parameters:
        --------------------------
        x : double, float
            Specific value from the random variable.

        mu : double, float
            Population average given.

        sd : double, float
            Sample standard deviation given.

        n : int, optional
            Sample size, less than 30.

        Returns:
        --------------------------
        t : float, double
            The T value correspondent of the X value in the t-student.
        """
        if n > 30:
            print("The sample size must be less than 30.")
        else:
            t = (x - mu) / (s / (n**0.5))
            return t

    def get_t_2p(x_1, x_2, s1, s2, n1, n2, var):
        """
        """
        if (n1 > 30 or n2 > 30):
            print("The sample sizes must be less than 30.")
        else:
            if (var is True):
                df = n1 + n2 - 2
                s2p = ((s1**2 * (n1 - 1)) + ((s2**2) * (n2 - 1))) / df
                s2p_error = ((s2p/n1) + (s2p/n2))**0.5

                t = (x_1 - x_2) / s2p_error
                return t
            else:
                s2p_error = ((s1**2 / n1) + (s2**2 / n2))**0.5
                t = (x_1 - x_2) / s2p_error
                return t

    def get_z_prop(p, pi, n):
        """
        Function to get the Z value (standard deviations number under or over the average)
        correspondent of the x value (parameter) in the standard normal distribution.
        Applied in proportions.

        Parameters:
        --------------------------
        p : double, float
            "Succeses" observations sample proportion.
            Each value should be in the range [0, 1].

        pi : double, float
            Population proportion given.
            Each value should be in the range [0, 1].

        n : int
            Sample size.

        Returns:
        --------------------------
        z : float, double
            The Z value (standard deviations number under or over the average)
            correspondent of the x value (parameter) in the standard normal distribution.
        """
        error = ((pi * (1 - pi)) / n)**0.5
        z = (p - pi) / error
        return z

    def get_z_2prop(p1, p2, n1, n2):
        """
        """
        sp_error = (((p1 * (1 - p1)) / n1) + ((p2 * (1 - p2)) / n2))**0.5

        z = (p1 - p2) / sp_error
        return z

    def get_df_var(n1, n2, s1, s2):
        """
        """
        df = ((((s1**2) / n1) + ((s2**2) / n2))**2) / (((((s1**2) / n1)**2) /
                (n1 - 1)) + ((((s2**2) / n2)**2) / (n2 - 1)))
        return df

    def get_d_pair(n, cond1, cond2):
        """
        """
        d1 = [cond1[x] - cond2[x] for x in range(n)]
        d21 = [x**2 for x in d1]
        sumd1 = sum(d1)
        sumd21 = sum(d21)
        d_ = sumd1 / n
        sd = ((sumd21 - (n * (d_**2))) / (n - 1))**0.5
        sd_error = sd / (n**0.5)

        d = d_ / sd_error
        return d

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

#------------------------------------------------------------------------------------------------#
#---------------------------------- Probability Distributions -----------------------------------#
#------------------------------------------------------------------------------------------------#

class distributions:

    def graficar_discreta(distribucion, title):
        """
        Función para graficar la distribución discreta recibida como parámetro
        """
        valores_x = np.arange(distribucion.ppf(0.01), distribucion.ppf(0.99))
        fmp = distribucion.pmf(valores_x)
        plt.plot(valores_x, fmp, '-')
        plt.title(title)
        plt.ylabel("Probabilidad")
        plt.xlabel("Valores")
        plt.show()

    def graficar_discreta_cdf(distribucion, a, b):
        """
        Función para graficar la sumatoria de la distribución discreta recibida como parámetro
        """
        valores_x = np.arange(distribucion.ppf(0.01), distribucion.ppf(0.99))
        x2 = np.arange(a,(b+1))
        fmp2 = distribucion.pmf(x2)
        fmp = distribucion.pmf(valores_x)
        plt.plot(valores_x, fmp)
        plt.vlines(x2, 0, fmp2, colors='b', lw=5, alpha=0.5)
        plt.title("Probabilidad encontrada")
        plt.ylabel("Probabilidad")
        plt.xlabel("Valores")
        plt.show()

    def graficar_continua(distribucion, title):
        """
        Función para graficar la distribución continua recibida como parámetro
        """
        valores_x = np.linspace(distribucion.ppf(0.01), distribucion.ppf(0.99), 100)
        fp = distribucion.pdf(valores_x)
        plt.plot(valores_x, fp, '-')
        plt.title(title)
        plt.ylabel("Probabilidad")
        plt.xlabel("Valores")
        plt.show()

    def graficar_continua_cdf(distribucion, a, b):
        """
        Función para graficar la integral de la distribución continua recibida como parámetro
        """
        valores_x = np.linspace(distribucion.ppf(0.01), distribucion.ppf(0.99), 100)
        x2 = np.linspace(a, b)
        fp2 = distribucion.pdf(x2)
        fp = distribucion.pdf(valores_x)
        plt.plot(valores_x, fp)
        plt.vlines(x2, 0, fp2, colors='b', lw=5, alpha=0.5)
        plt.title("Probabilidad encontrada")
        plt.ylabel("Probabilidad")
        plt.xlabel("Valores")
        plt.show()

    def cdf_exponential(mu, x): #mu = mu
        """
        Función para calcular la función de probabilidad acumulada de la distribución exponencial continua
        """
        if x <= 0:
            cdf = 0
        else:
            cdf = 1 - np.exp(-mu * x)
        return cdf

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

#------------------------------------------------------------------------------------------------#
#------------------------------------- Confidence Intervals -------------------------------------#
#------------------------------------------------------------------------------------------------#
class intervals:
    """
    interval Class contain the methods to get the confidence intervals to
    estimate some unknown parameter.

    Methods:
    --------------------------
    norm(n, x_ ,sd, alpha)
        Function to get the confidence interval of average population in large
        samples.
    """

    def norm(n, x_, sd, alpha):
        """
        Function to get the confidence interval to estimate the average population
        when we have large samples (n >= 30).

        Parameters:
        --------------------------
        n : int
            Sample size.

        x_ : float, double
            Sample average.

        sd : float, double
            Sample or population standard deviation.

        alpha : float, double
            Confidence level to estimate our unknown parameter (the average population).
            Each value should be in the range [0, 1].

        Returns:
        --------------------------
        interval : tuple
            The confidence interval, with the minimum value at [0] and the maximum value
            at [1] for the unknown parameter (the average population).
        """
        if (n <= 30):
            print("The sample size must be greater than 30.")
        else:
            sd_error = sd / (n**0.5)
            z_value = ss.norm.interval(alpha)[1]

            minimum = x_ - (z_value * sd_error)
            maximum = x_ + (z_value * sd_error)

            return minimum, maximum

    def norm_2p(n1, n2, x_1, x_2, sd1, sd2, alpha):
        """
        Function to get the confidence interval to estimate the difference between
        2 population averages in large samples (n >= 30).

        Parameters:
        --------------------------
        n1 : int
            Sample 1 size.

        n2 : int
            Sample 2 size.

        x_1 : float, double
            Sample 1 average.

        x_2 : float, double
            Sample 2 average.

        sd1 : float, double
            Sample or population 1 standard deviation.

        sd2 : float, double
            Sample or population 2 standard deviation.

        alpha : float, double
            Confidence level to estimate our unknown parameter (the difference between
            2 population averages).
            Each value should be in the range [0, 1].

        Returns:
        --------------------------
        interval : tuple
            The confidence interval, with the minimum value at [0] and the maximum value
            at [1] for the unknown parameter (the difference between
            2 population averages).
        """
        if (n1 <= 30 or n2 <= 30):
            print("The sample sizes must be greater than 30.")
        else:
            sd_error = ((sd1**2 / n1) + (sd2**2 / n2))**0.5
            z_value = ss.norm.interval(alpha)[1]

            minimum = (x_1 - x_2) - (z_value * sd_error)
            maximum = (x_1 - x_2) + (z_value * sd_error)

            return minimum, maximum

    def t(n, x_, s, alpha):
        """
        Function to get the confidence interval to estimate the average population
        when we have small samples (n < 30).

        Parameters:
        --------------------------
        n : int
            Sample size.

        x_ : float, double
            Sample average.

        s : float, double
            Sample standard deviation.

        alpha : float, double
            Confidence level to estimate our unknown parameter (the average population).
            Each value should be in the range [0, 1].

        Returns:
        --------------------------
        interval : tuple
            The confidence interval, with the minimum value at [0] and the maximum value
            at [1] for the unknown parameter (the average population).
        """
        if (n > 30):
            print("The sample size must be less than 30.")
        else:
            df = n - 1
            s_error = s / (n**0.5)
            t_value = ss.t.interval(alpha, df)[1]

            minimum = x_ - (t_value * s_error)
            maximum = x_ + (t_value * s_error)

            return minimum, maximum

    def t_2p(n1, n2, x_1, x_2, s1, s2, alpha, var):
        """
        Function to get the confidence interval to estimate the difference between
        2 population averages in small samples (n < 30).

        Parameters:
        --------------------------
        n1 : int
            Sample 1 size.

        n2 : int
            Sample 2 size.

        x_1 : float, double
            Sample 1 average.

        x_2 : float, double
            Sample 2 average.

        s1 : float, double
            Sample 1 standard deviation.

        s2 : float, double
            Sample 2 standard deviation.

        alpha : float, double
            Confidence level to estimate our unknown parameter (the difference between
            2 population averages).
            Each value should be in the range [0, 1].

        var : bool
            Are the population variances equal?
            True if population variances are equal.
            False if population variances are different.

        Returns:
        --------------------------
        interval : tuple
            The confidence interval, with the minimum value at [0] and the maximum value
            at [1] for the unknown parameter (the difference between
            2 population averages).
        """
        if (n1 > 30 or n2 > 30):
            print("The sample sizes must be less than 30.")
        else:
            if (var is True):
                df = n1 + n2 - 2
                s2p = (((s1**2) * (n1 - 1)) + (s2**2 * (n2 - 1))) / df
                sp_error = ((s2p / n1) + (s2p / n2))**0.5
                t_value = ss.t.interval(alpha, df)[1]

                minimum = (x_1 - x_2) - (t_value * sp_error)
                maximum = (x_1 - x_2) + (t_value * sp_error)

                return minimum, maximum
            else:
                df = generals.get_df_var(n1, n2, s1, s2)

                sp_error = (((s1**2) / n1) + ((s2**2) / n2))**0.5
                t_value = ss.t.interval(alpha, df)[1]

                minimum = (x_1 - x_2) - (t_value * sp_error)
                maximum = (x_1 - x_2) + (t_value * sp_error)

                return minimum, maximum

    def prop(n, p, alpha):
        """
        Function to get the confidence interval to estimate the population proportion.

        Parameters:
        --------------------------
        n : int
            Sample size.

        p : float, double
            Sample proportion.
            Each value should be in the range [0, 1].

        alpha : float, double
            Confidence level to estimate our unknown parameter (the population proportion).
            Each value should be in the range [0, 1].

        Returns:
        --------------------------
        interval : tuple
            The confidence interval, with the minimum value at [0] and the maximum value
            at [1] for the unknown parameter (the population proportion).
        """
        sp_error = ((p * (1 - p)) / n)**0.5
        z_value = ss.norm.interval(alpha)[1]

        minimum = p - (z_value * sp_error)
        maximum = p + (z_value * sp_error)

        return minimum, maximum

    def prop_2p(n1, n2, p1, p2, alpha):
        """
        Function to get the confidence interval to estimate the difference between
        2 population proportions.

        Parameters:
        --------------------------
        n1 : int
            Sample 1 size.

        n2 : int
            Sample 2 size.

        p1 : float, double
            Sample 1 proportion.
            Each value should be in the range [0, 1].

        p2 : float, double
            Sample 2 proportion.
            Each value should be in the range [0, 1].

        alpha : float, double
            Confidence level to estimate our unknown parameter (the population proportion).
            Each value should be in the range [0, 1].

        Returns:
        --------------------------
        interval : tuple
            The confidence interval, with the minimum value at [0] and the maximum value
            at [1] for the unknown parameter (the difference between
            2 population proportions).
        """
        sp_error = (((p1 * (1 - p1)) / n1) + ((p2 * (1 - p2)) / n2))**0.5
        z_value = ss.norm.interval(alpha)[1]

        minimum = (p1 - p2) - (z_value * sp_error)
        maximum = (p1 - p2) + (z_value * sp_error)

        return minimum, maximum

    def pair_2p(n, cond1, cond2, alpha):
        """
        Function to get the confidence interval to estimate the difference between
        2 population averages in paired samples.

        Parameters:
        --------------------------
        n : int
            Sample sizes.

        cond1 : list
            Sample initial condition list.

        cond2 : list
            Sample final condition list.

        alpha : double, float
            Confidence level to estimate our unknown parameter (the difference between
            2 population averages).
            Each value should be in the range [0, 1].

        Returns:
        --------------------------
        interval : tuple
            The confidence interval, with the minimum value at [0] and the maximum value
            at [1] for the unknown parameter (the difference between
            2 population averages).

        """
        d1 = [cond1[x] - cond2[x] for x in range(n)]
        d21 = [x**2 for x in d1]
        sumd1 = sum(d1)
        sumd21 = sum(d21)
        d_ = sumd1 / n
        sd = ((sumd21 - (n * (d_**2))) / (n - 1))**0.5
        sd_error = sd / (n**0.5)

        if n <= 30:
            df = n - 1
            t_value = ss.t.interval(alpha, df)[1]

            minimum = d_ - (t_value * sd_error)
            maximum = d_ + (t_value * sd_error)

            return minimum, maximum

        else:
            z_value = ss.norm.interval(alpha)[1]

            minimum = d_ - (z_value * sd_error)
            maximum = d_ + (z_value * sd_error)

            return minimum, maximum

    def n_avg(alpha, s, error):
        """
        Function to determine the appropiate sample size to estimate the average population.

        Parameters:
        --------------------------
        alpha : float, double
            Confidence level to determine our sample size.
            Each value should be in the range [0, 1].

        s: float, double
            Pilot sample standard deviation, or population standar deviation.

        error: float, double
            Tolerable size error.

        Returns:
        --------------------------
        n : int
            The appropiate sample size to estimate the average population.
            This sample size is rounded.
        """
        z_value = ss.norm.interval(alpha)[1]

        n = ((z_value**2)*(s**2)) / (error**2)

        return round(n)

    def n_avg_2p(alpha, s1, s2, error):
        """
        Function to determine the appropiate sample size to estimate the difference
        between 2 average populations.

        Parameters:
        --------------------------
        alpha : float, double
            Confidence level to determine our sample size.
            Each value should be in the range [0, 1].

        s1 : float, double
            Pilot sample 1 standard deviation, or population standar deviation.

        s2 : float, double
            Pilot sample 2 standard deviation, or population standar deviation.

        error: float, double
            Tolerable size error.

        Returns:
        --------------------------
        n : int
            The appropiate sample size to estimate the he difference
            between 2 average populations.
            This sample size is rounded.
        """
        z_value = ss.norm.interval(alpha)[1]

        n = ((z_value**2)*(s1**2 + s2**2)) / (error**2)

        return round(n)

    def n_prop(alpha, p, error):
        """
        Function to determine the appropiate sample size to estimate the population proportion.

        Parameters:
        --------------------------
        alpha : float, double
            Confidence level to determine our sample size.
            Each value should be in the range [0, 1].

        p: float, double
            Sample proportion.
            Each value should be in the range [0, 1].

        error: float, double
            Tolerable size error.

        Returns:
        --------------------------
        n : int
            The appropiate sample size to estimate the population proportion.
            This sample size is rounded.
        """
        z_value = ss.norm.interval(alpha)[1]

        n = ((z_value**2) * (p * (1 - p))) / (error**2)

        return round(n)

    def n_prop_2p(alpha, p1, p2, error):
        """
        Function to determine the appropiate sample size to estimate the
        difference between 2 population proportions.

        Parameters:
        --------------------------
        alpha : float, double
            Confidence level to determine our sample size.
            Each value should be in the range [0, 1].

        p1: float, double
            Sample proportion 1.
            Each value should be in the range [0, 1].

        p2: float, double
            Sample proportion 2.
            Each value should be in the range [0, 1].

        error: float, double
            Tolerable size error.

        Returns:
        --------------------------
        n : int
            The appropiate sample size to estimate the difference between
            2 population proportions.
            This sample size is rounded.
        """
        z_value = ss.norm.interval(alpha)[1]

        n = ((z_value**2) * ((p1 * (1 - p1)) + ((p2 * (1 - p2))))) / (error**2)

        return round(n)

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

#------------------------------------------------------------------------------------------------#
#------------------------------------- Hypothesis Testing ---------------------------------------#
#------------------------------------------------------------------------------------------------#
class hypothesis:
    """
    hypothesis Class contain the methods to work on the hypothesis testing.

    Methods:
    --------------------------
    crit_val_norm(sign, tail)
        Function to calculate the critical values in the standard normal distribution.
    """

    def crit_val_norm(sign, tail):
        """
        Function to calculate, with the standard normal distribution, the critical
        values according to the level of significance and the tail-type test.

        Parameters:
        --------------------------
        sign : float, double
            Level of significance of the test.
            Each value should be in the range [0, 1].

        tail : string
            Tail-type test, it could be 'left', 'right' or 'two'.

        Returns:
        --------------------------
        crit_val : tuple, float
            The critical values or value, according to the level of significance and
            the tail-type test.
        """
        if tail == 'two':
            alpha = 1 - (sign/2)
            crit_val = ss.norm.ppf(1 - alpha), ss.norm.ppf(alpha)
            return crit_val

        if tail == 'left':
            alpha = 1 - sign
            crit_val = ss.norm.ppf(1 - alpha)
            return crit_val

        if tail == 'right':
            alpha = 1 - sign
            crit_val = ss.norm.ppf(alpha)
            return crit_val

        print("You must input a valid tail ('two', 'left' or 'right')")

    def crit_val_t(df, sign, tail):
        """
        Function to calculate, with the t-student distribution, the critical
        values according to the level of significance and the tail-type test.

        Parameters:
        --------------------------
        df : int
            Degrees of freedom (n - 1). It's the sample size minus 1.

        sign : float, double
            Level of significance of the test.
            Each value should be in the range [0, 1].

        tail : string
            Tail-type test, it could be 'left', 'right' or 'two'.

        Returns:
        --------------------------
        crit_val : tuple, float
            The critical values or value, according to the level of significance and
            the tail-type test.
        """
        if tail == 'two':
            alpha = 1 - (sign/2)
            crit_val = ss.t.ppf(1 - alpha, df), ss.t.ppf(alpha, df)
            return crit_val

        if tail == 'left':
            alpha = 1 - sign
            crit_val = ss.t.ppf(1 - alpha, df)
            return crit_val

        if tail == 'right':
            alpha = 1 - sign
            crit_val = ss.t.ppf(alpha, df)
            return crit_val

        print("You must input a valid tail ('two', 'left' or 'right')")

    def crit_val_f(df1, df2, sign):
        """
        """
        alpha = 1 - sign
        crit_val = ss.f.ppf(alpha, df1, df2)
        return crit_val

    def reject_h0(crit_val, value, tail):
        """
        Function to determine if reject the null hypothesis or not reject it based
        on the tail-type test.

        Parameters:
        --------------------------
        crit_val : tuple, float
            Critical values to consider.

        value : float, double
            Value to compare with critical values.

        tail : string
            Tail-type test, it could be 'left', 'right' or 'two'.

        Returns:
        --------------------------
        decision : bool
            True if we should reject null hypothesis,
            False if we shouldn't reject null hypothesis.
        """
        if tail == 'two':
            return (value < crit_val[0] or value > crit_val[1])

        if tail == 'left':
            return value < crit_val

        if tail == 'right':
            return value > crit_val

        print("You must input a valid tail ('two', 'left' or 'right')")

    def get_p(z, tail):
        """
        Function to determine the p-value (minimum significance level to reject
        the null hypothesis).

        Parameters:
        --------------------------
        z : double, float
            Z-value of the test statistic.

        tail : string
            Tail-type test, it could be 'left', 'right' or 'two'.

        Returns:
        --------------------------
        p : double, float
            P-value, the minimum significance level to reject the null hypothesis.
        """
        z_area = ss.norm.cdf(abs(z))

        if tail == 'two':
            p = 2 * (1 - z_area)
            return p

        else:
            p = 1 - z_area
            return p

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

#------------------------------------------------------------------------------------------------#
#------------------------------------------- ANOVA ----------------------------------------------#
#------------------------------------------------------------------------------------------------#
class anova:

    def get_sct(*args):
        """
        """
        arr_tot = np.concatenate(args)
        big_mean = np.mean(arr_tot)
        sct = sum([(x - big_mean)**2 for x in arr_tot])
        return sct

        #d1 = [cond1[x] - cond2[x] for x in range(n)]
        #d21 = [x**2 for x in d1]

    def get_sctr(*args):
        """
        """
        arr_tot = np.concatenate(args)
        big_mean = np.mean(arr_tot)
        sctr = sum([(len(x) * ((np.mean(x) - big_mean)**2)) for x in args])
        return sctr

    def get_sce(*args):
        """
        """
        sce = 0

        for x in args:
            for xj in x:
                sce += (xj - np.mean(x))**2
        return sce

    def get_sce2(*args):
        """
        """
        sct = anova.get_sct(*args)
        sctr = anova.get_sctr(*args)
        scbl = anova.get_scbl(*args)

        sce = sct - sctr - scbl
        return sce

    def get_scbl(*args):
        """
        """
        lists = args
        blocks = list(map(list, zip(*lists)))
        arr_tot = np.concatenate(blocks)
        big_mean = np.mean(arr_tot)
        scbl = sum([(len(x) * ((np.mean(x) - big_mean)**2)) for x in blocks])

        return scbl

    def get_cmt(*args):
        """
        """
        arr_tot = np.concatenate(args)
        df = len(arr_tot) - 1
        sct = anova.get_sct(*args)
        cmt = sct / df
        return cmt

    def get_cmtr(*args):
        """
        """
        sctr = anova.get_sctr(*args)
        df = len(args) - 1
        cmtr = sctr / df
        return cmtr

    def get_cme(*args):
        """
        """
        arr_tot = np.concatenate(args)
        sce = anova.get_sce(*args)
        df = len(arr_tot) - len(args)
        cme = sce / df
        return cme

    def get_cme2(*args):
        """
        """
        sce = anova.get_sce2(*args)
        df = (len(args) - 1) * (len(args[0]) - 1)
        cme = sce / df
        return cme

    def get_cmbl(*args):
        """
        """
        scbl = anova.get_scbl(*args)
        df = len(args[0]) - 1
        cmbl = scbl / df
        return cmbl

    def get_fratio(*args):
        """
        """
        cmtr = anova.get_cmtr(*args)
        cme = anova.get_cme(*args)
        fratio = cmtr / cme
        return fratio

    def get_fratio2(*args):
        """
        """
        cmtr = anova.get_cmtr(*args)
        cme = anova.get_cme2(*args)
        fratio = cmtr / cme
        return fratio

    def get_fratio2_cmbl(*args):
        """
        """
        cmbl = anova.get_cmbl(*args)
        cme = anova.get_cme2(*args)
        fratio = cmbl / cme
        return fratio

    def get_dms(*args, sign):
        """
        """
        cme = anova.get_cme(*args)

        for x in range(len(args)):
            for xj in range(x+1, len(args)):
                if(len(args[x]) == len(args[xj])):
                    fratio = ss.f.ppf((1 - sign), 1, (len(np.concatenate(args)) - len(args)))
                    dms = (((1 / len(args[x])) + (1 / len(args[xj]))) * cme * fratio)**0.5
                    x_mean = np.mean(args[x])
                    xj_mean = np.mean(args[xj])
                    x_diff = abs(x_mean - xj_mean)
                    if (x_diff > dms):
                        print("|x_{} - x_{}| = |{} - {}| = {} > {}".format(x+1, xj+1,
                              x_mean, xj_mean, x_diff, dms))
                    else:
                        print("|x_{} - x_{}| = |{} - {}| = {} <= {}".format(x+1, xj+1,
                              x_mean, xj_mean, x_diff, dms))
                else:
                    fratio = ss.f.ppf((1 - sign), (len(args) - 1), (len(np.concatenate(args)) - len(args)))
                    dms = (((1 / len(args[x])) + (1 / len(args[xj]))) * cme * fratio)**0.5
                    x_mean = np.mean(args[x])
                    xj_mean = np.mean(args[xj])
                    x_diff = abs(x_mean - xj_mean)
                    if (x_diff > dms):
                        print("|x_{} - x_{}| = |{} - {}| = {} > {}".format(x+1, xj+1,
                              x_mean, xj_mean, x_diff, dms))
                    else:
                        print("|x_{} - x_{}| = |{} - {}| = {} <= {}".format(x+1, xj+1,
                              x_mean, xj_mean, x_diff, dms))

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


from math import cos, sin, pi
import numpy as np
from numpy.linalg import inv

def sph_to_cart(epsilon, alpha, r):
    """
    Transform sensor readings to Cartesian coordinates in the sensor frames.
    """
    p = np.zeros(3)  # Position vector

    # Your code here
    p[0] = r * cos(alpha) * cos(epsilon)
    p[1] = r * sin(alpha) * cos(epsilon)
    p[2] = r * sin(epsilon)

    return p

def estimate_params(P):
    """
    Estimate parameters from sensor readings in the Cartesian frame.
    Each row in the P matrix contains a single measurement.
    """
    param_est = np.zeros(3)
    A = np.ones_like(P)
    b = np.zeros((P.shape[0], 1))

      # Your code here
    for i, meas in enumerate(P):
      A[i, 1], A[i, 2], b[i] = meas

    params = inv(A.T @ A) @ A.T @ b
    param_est[0] = params[0, 0]
    param_est[1] = params[1, 0]
    param_est[2] = params[2, 0]

    return param_est

meas = np.array([[pi/3, 0, 5],
                 [pi/4, pi/4, 7],
                 [pi/6, pi/2, 4],
                 [pi/5, 3*pi/4, 6],
                 [pi/8, pi, 3]])
P = np.array([sph_to_cart(*row) for row in meas])
print(estimate_params(P))


import numpy as np
from scipy.optimize import minimize

def mse(x, beacon_locations, beacon_distances):
    calculated_distances = np.array(
        [np.linalg.norm(x - beacon_locations[i]) for i in range(len(beacon_locations))])
    return np.sum(np.power(calculated_distances - beacon_distances, 2)) / len(beacon_distances)

def trilaterate(beacon_locations, beacon_distances, initial_location=np.array([0, 0, 0])):

    result = minimize(
        mse,
        initial_location,
        args=(beacon_locations, beacon_distances),
        method='L-BFGS-B',
        options={
            'ftol': 1e-5,
            'maxiter': 1e+7
        })

    return result.x

def main():
    # Testing the code
    beacon_locations = np.array(
        [[0, 0, 0], [10, 0, 0], [10, 10, 0], [0, 10, 0]])
    # Actual location of point is at [7,4,3]
    beacon_distances = np.array([8.6, 5.83, 7.35, 9.69])
    # Result generated is [ 6.99620322  4.00005823 -2.99673958]
    print(trilaterate(beacon_locations, beacon_distances))

if __name__ == "__main__":
    main()


'''
Binary classification:
Spiral pattern
'''

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

num_samples = 100
np.random.seed(1)

index = np.array(range(num_samples))
r = index/num_samples * 5
t1 = 1.75 * index/num_samples * 2 * math.pi
t2 = 1.75 * index/num_samples * 2 * math.pi + math.pi

x1 = r * np.sin(t1) + np.random.uniform(-0.5, 0.5, num_samples)
y1 = r * np.cos(t1) + np.random.uniform(-0.5, 0.5, num_samples)
z1 = [0] * num_samples

x2 = r * np.sin(t2) + np.random.uniform(-0.5, 0.5, num_samples)
y2 = r * np.cos(t2) + np.random.uniform(-0.5, 0.5, num_samples)
z2 = [1] * num_samples

x = np.hstack((x1, x2))
y = np.hstack((y1, y2))
z = np.hstack((z1, z2))

x = pd.DataFrame(x, columns=['x'])
y = pd.DataFrame(y, columns=['y'])
z = pd.DataFrame(z, columns=['labels'])
data = pd.concat([x, y, z], axis=1)
data = data.sample(frac=1)
#data.to_csv('../toy_data_5.csv', sep=' ', index=False)

fig = plt.figure(figsize=(4,4), dpi=100)
plt.scatter(x['x'], y['y'], c=z['labels'], cmap=plt.cm.brg)
plt.axis('off')
plt.savefig('../images/toy_data_5.png', transparent=True)
#plt.show()


import numpy as np
from numpy.polynomial.polynomial import Polynomial

def vandermond_integration(x, y, int_start, int_end):
    # Compute vector a
    vandermonde_inv = np.linalg.inv(np.vander(x, increasing=True))
    a_coeff = vandermonde_inv @ y

    # Generate the matrix that integrates polynomials.
    integral_matrix = np.vstack([np.zeros(x.size), np.diag(1 / np.arange(1, x.size+1))])

    # Integrate the polynomial a
    a_int_coeff = integral_matrix@a_coeff
    a_int = Polynomial(a_int_coeff)

    return a_int(int_end) - a_int(int_start)

# Define vector x
x1 = np.array([-1, -0.5, 0.5, 1])
x2 = np.array([-0.2, -0.1, 0.1, 0.2])
x3 = np.array([-np.sqrt(3/7 + 2/7 * np.sqrt(6/5)), -np.sqrt(3/7 - 2/7 * np.sqrt(6/5)), np.sqrt(3/7 - 2/7 * np.sqrt(6/5)), np.sqrt(3/7 + 2/7 * np.sqrt(6/5))])

print(f"Interpolated integral with x1 is: {vandermond_integration(x1, np.exp(x1), -1, 1)}")
print(f"Interpolated integral with x2 is: {vandermond_integration(x2, np.exp(x2), -1, 1)}")
print(f"Interpolated integral with x3 is: {vandermond_integration(x3, np.exp(x3), -1, 1)}")
print(f"integral should be {np.e - 1/np.e}")


#This problem was asked by Google.

#The edit distance between two strings refers to the minimum number of character insertions, deletions, and substitutions required to change one string to the other.
#For example, the edit distance between “kitten” and “sitting” is three: substitute the “k” for “s”, substitute the “e” for “i”, and append a “g”.

#Given two strings, compute the edit distance between them.

#solution 1: By recursion
ins_cost = 1
del_cost = 1
sub_cost = 2
def edit_distance_recurse(seq1, seq2, operations=[]):
    """Returns the Edit Distance between the provided two sequences."""

    if len(seq2) == 0:
        operations = operations + ([f"Delete `{seq1}` from sequence1."] if len(seq1) else [])
        return len(seq1), operations

    if len(seq1) == 0:
        operations = operations + ([f"Insert `{seq2}` into sequence1."] if len(seq2) else [])
        return len(seq2), operations

    if seq1[0] == seq2[0]:
        operations = operations + [f"Make no change for character `{seq1[0]}`."]
        return edit_distance_recurse(seq1[1: ], seq2[1: ], operations)

    # calculate cost if insertion was made
    ins_operations = operations + [f"Insert `{seq2[0]}` in sequence1."]
    insertion, ins_operations = edit_distance_recurse(seq1, seq2[1: ], ins_operations)

    # calculate cost if deletion was done
    del_operations = operations + [f"Delete `{seq1[0]}` from sequence1."]
    deletion, del_operations = edit_distance_recurse(seq1[1: ], seq2, del_operations)

    # calculate cost if substitution was done
    sub_operations = operations + [f"Replace `{seq1[0]}` in sequence1 with `{seq2[0]}`."]
    substitution, sub_operations = edit_distance_recurse(seq1[1: ], seq2[1: ], sub_operations)

    # calculate minimum cost
    min_cost = min(insertion + ins_cost, deletion + del_cost, substitution + sub_cost)

    if min_cost == (substitution + sub_cost):
        return min_cost, sub_operations
    elif min_cost == deletion + del_cost:
        return min_cost, del_operations
    else:
        return min_cost, ins_operations
seq1 = "kitten"
seq2 = "sitting"
score, operations = edit_distance_recurse(seq1, seq2)
print(f"Edit Distance between `{seq1}` & `{seq2}` is: {score}")
print("\nOperations performed are:\n")
for operation in operations:
    print(operation)
#Edit Distance between `kitten` & `sitting` is: 5

#Operations performed are:

#Replace `k` in sequence1 with `s`.
#Make no change for character `i`.
#Make no change for character `t`.
#Make no change for character `t`.
#Replace `e` in sequence1 with `i`.
#Make no change for character `n`.
#Insert `g` into sequence1.

# - - - - - -
#Solution2 by DP:
import numpy as np
ins_cost = 1
del_cost = 1
sub_cost = 2
def min_cost_path(cost, operations):

    # operation at the last cell
    path = [operations[cost.shape[0]-1][cost.shape[1]-1]]

    # cost at the last cell
    min_cost = cost[cost.shape[0]-1][cost.shape[1]-1]

    row = cost.shape[0]-1
    col = cost.shape[1]-1

    while row >0 and col > 0:

        if cost[row-1][col-1] <= cost[row-1][col] and cost[row-1][col-1] <= cost[row][col-1]:
            path.append(operations[row-1][col-1])
            row -= 1
            col -= 1
        elif cost[row-1][col] <= cost[row-1][col-1] and cost[row-1][col] <= cost[row][col-1]:
            path.append(operations[row-1][col])
            row -= 1
        else:
            path.append(operations[row][col-1])
            col -= 1

    return "".join(path[::-1][1:])

def edit_distance_dp(seq1, seq2):

    # create an empty 2D matrix to store cost
    cost = np.zeros((len(seq1)+1, len(seq2)+1))

    # fill the first row
    cost[0] = [i for i in range(len(seq2)+1)]

    # fill the first column
    cost[:, 0] = [i for i in range(len(seq1)+1)]

    # to store the operations made
    operations = np.asarray([['-' for j in range(len(seq2)+1)] \
                                 for i in range(len(seq1)+1)])

    # fill the first row by insertion
    operations[0] = ['I' for i in range(len(seq2)+1)]

    # fill the first column by insertion operation (D)
    operations[:, 0] = ['D' for i in range(len(seq1)+1)]

    operations[0, 0] = '-'

    # now, iterate over earch row and column
    for row in range(1, len(seq1)+1):

        for col in range(1, len(seq2)+1):

            # if both the characters are same then the cost will be same as
            # the cost of the previous sub-sequence
            if seq1[row-1] == seq2[col-1]:
                cost[row][col] = cost[row-1][col-1]
            else:

                insertion_cost = cost[row][col-1] + ins_cost
                deletion_cost = cost[row-1][col] + del_cost
                substitution_cost = cost[row-1][col-1] + sub_cost

                # calculate the minimum cost
                cost[row][col] = min(insertion_cost, deletion_cost, substitution_cost)

                # get the operation
                if cost[row][col] == substitution_cost:
                    operations[row][col] = 'S'

                elif cost[row][col] == ins_cost:
                    operations[row][col] = 'I'
                else:
                    operations[row][col] = 'D'

    return cost[len(seq1), len(seq2)], min_cost_path(cost, operations)
seq1 = "kitten"
seq2 = "sitting"
score, operations = edit_distance_dp("kitten", "sitting")
print(f"Edit Distance between `{seq1}` & `{seq2}` is: {score}")
print("\nOperations performed are:\n")
for operation in operations:
    if operation == '-':
        print('No Change.')
    elif operation == 'I':
        print('Insertion')
    elif operation == 'D':
        print('Deletion')
    else:
        print('Substitution')
#Edit Distance between `kitten` & `sitting` is: 5.0

#Operations performed are:

#Substitution
#No Change.
#No Change.
#No Change.
#Substitution
#No Change.
#Deletion


import numpy as np
from scipy.linalg import eigh

def decompose_kernel(M):
    """
    Decompose a symmetric matrix into sorted eigenvalues and corresponding eigenvectors
    :param M: input square np.array
    :return vals, vecs: vector of sorted eigenvalues, matrix of corresponding eigenvectors
    """
    vals, vecs = np.linalg.eigh(M)
    vals = np.real(vals)
    vecs = np.real(vecs)
    order = np.argsort(vals)
    return vals[order], vecs[:, order]


import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes

# old font defaults
mpl.rcParams['mathtext.fontset'] = 'cm'
mpl.rcParams['mathtext.rm'] = 'serif'

x_smooth = np.linspace(0.0, np.pi, 500)
f_smooth = np.sin(x_smooth)

x = np.linspace(0.0, np.pi, 10)
f = np.sin(x)

i = 5
x_0 = x[i]
f_0 = f[i]

plt.plot(x_smooth, f_smooth)
plt.scatter(x, f)

plt.scatter(x_0, np.sin(x_0), color="r", zorder=10)

dx = x[1] - x[0]
x_d = np.linspace(x[i]-1.5*dx, x[i]+1.5*dx, 2)

d_exact = np.cos(x_0)

d_l = (np.sin(x[i]) - np.sin(x[i-1]))/dx
d_r = (np.sin(x[i+1]) - np.sin(x[i]))/dx
d_c = 0.5*(np.sin(x[i+1]) - np.sin(x[i-1]))/dx

# plot a line showing dx
dx = x[2] - x[1]
ann = plt.annotate('', xy=(x[1], f[1]-0.5*dx),  xycoords='data',
                   xytext=(x[2], f[1]-0.5*dx), textcoords='data',
                   arrowprops=dict(arrowstyle="<->",
                                   connectionstyle="bar,fraction=-0.4",
                                   ec="k",
                                   shrinkA=5, shrinkB=5,
                   ))

plt.text(0.5*(x[1]+x[2]), f[1]-0.8*dx, "$\Delta x$",
         horizontalalignment="center")

plt.plot([x[1], x[1]], [f[1]-0.5*dx, f[2]+0.5*dx], ls=":", color="0.5")
plt.plot([x[2], x[2]], [f[1]-0.5*dx, f[2]+0.5*dx], ls=":", color="0.5")

plt.plot(x_d, d_exact*(x_d - x_0) + f_0, color="0.5", lw=2, ls=":", label="exact")
plt.plot(x_d, d_l*(x_d - x_0) + f_0, label="left-sided")
plt.plot(x_d, d_r*(x_d - x_0) + f_0, label="right-sided")
plt.plot(x_d, d_c*(x_d - x_0) + f_0, label="centered")

ax = plt.gca()

# origin of the axes through 0
ax.spines['left'].set_position('zero')
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['top'].set_color('none')
ax.spines['left'].set_smart_bounds(True)
ax.spines['bottom'].set_smart_bounds(True)
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')

plt.legend(frameon=False, loc="best")

plt.ylim(-0.1, 1.1)

plt.tight_layout()

plt.savefig("derivs.pdf")


import os
import numpy as np
from matplotlib import pyplot
from mpl_toolkits.mplot3d import Axes3D
from time import sleep

def warm_up_exercise():
    A = np.eye(5)

    return A

def plot_data(x, y):
    figure = pyplot.figure()
    pyplot.plot(x, y, 'ro', ms=10, mec='k')
    pyplot.ylabel('Profit in $10,000s')
    pyplot.xlabel('Population of City in 10,000s')

def plot_cost_hist(J_hist, rate):
    pyplot.plot(np.arange(len(J_hist)), J_hist, '-', ms=10, mec='k')
    pyplot.ylabel('Cost J')
    pyplot.xlabel('Iterations')

    pyplot.plot(np.arange(len(J_hist)), J_hist, '-')

def compute_cost(X, y, theta):
    m = y.shape[0]
    J = (1/(2*m)) * np.sum( np.square((np.dot(X, theta) - y)) )
    return J

def gradient_descent(X, y, theta, alpha, num_iters):
    m = y.shape[0]
    adjusted_theta = theta.copy()
    J_hist = []
    for i in range(0, num_iters):
        adjusted_theta -= (alpha / m) * (np.dot(X, adjusted_theta) - y).dot(X)
        J_hist.append(compute_cost(X, y, adjusted_theta))
    return J_hist, adjusted_theta

def predict(X, theta):
    pred = np.dot(X, theta)
    print("For population =", X[1]*10000, ' we predict a profit of {:.2f}\n'.format(pred*10000))

def visualize_cost(X, y, trained_theta):
    # Step over theta0 values in range -10,10 with 100 steps
    # theta0_vals => (100 x 1)
    theta0_vals = np.linspace(-10,10,100)
    # Step over theta1 values in range -4 4 with 100 steps
    # thteta1_vals => (100 x 1)
    theta1_vals = np.linspace(-1,4,100)
    # Create a matrix of costs at different values of theta0 and theta1
    # J_vals => (100 x 100)
    J_vals = np.zeros((theta0_vals.shape[0], theta1_vals.shape[0]))

    for i, theta0 in enumerate(theta0_vals):
        for j, theta1 in enumerate(theta1_vals):
            J_vals[i, j] = compute_cost(X, y, [theta0, theta1])
    J_vals = J_vals.T

    figure = pyplot.figure(figsize=(12, 5))
    # First parameter controls position in sub plot
    # projection controls 3d-ness
    axis = figure.add_subplot(121, projection='3d')
    # cmap='viridis' makes it colorful
    axis.plot_surface(theta0_vals, theta1_vals, J_vals, cmap='viridis')
    pyplot.xlabel('theta0')
    pyplot.ylabel('theta1')
    axis.set_zlabel('Cost J')
    pyplot.title('Cost at different thetas')

    axis = pyplot.subplot(122)
    # Levels controls number and positions of the contour lines, should be int or array-like object
    pyplot.contour(theta0_vals, theta1_vals, J_vals, linewidths=2, cmap='viridis', levels=np.logspace(-2, 3, 20))
    axis.set_xlabel('theta0')
    axis.set_ylabel('theta1')
    pyplot.plot(trained_theta[0], trained_theta[1], 'ro', ms=10, lw=2)
    pyplot.title('Minimum value of cost J')

def normalizeFeatures(X):
    # X => (m x n) : m = num. examples, n = num. features
    X_norm = X.copy()
    m = X_norm.shape[0]
    # X.shape => (m x n)
    # np.zeros(X.shape[1]) => np.zeros(n) => (n x 1)
    mu = np.zeros(X.shape[1])
    sigma = np.zeros(X.shape[1])

    for feature in range(X.shape[1]):
        mu[feature] = np.mean(X[:, feature])
        sigma[feature] = np.std(X[:, feature])
    X_norm = (X - mu) / sigma

    return X_norm, mu, sigma

def normal_equation(X, y):
    first = np.dot(X.T,X)
    inverse = None
    try:
        inverse = np.linalg.inv(first)
        X_terms = np.dot(inverse,X.T)
        return np.dot(X_terms, y)
    except np.linalg.LinAlgError:
        print('ERROR: Matrix is non-invertible')
        return -1

def part1():
    print('='*50)
    print('\tBegin Part 1')
    print('='*50)
    data = np.loadtxt(os.path.join('Data', 'ex1data1.txt'), delimiter=',')
    X, y = data[:, 0], data[:, 1]
    m = y.size

    plot_data(X, y)
    pyplot.show()

    X = np.stack([np.ones(m), X], axis=1)
    theta = np.array([0.0, 0.0])
    print("Compute cost for theta: ", theta)
    print(compute_cost(X, y, theta))
    print("Compute cost for theta: ", [-1,2])
    print(compute_cost(X, y, np.array([-1, 2])))

def part2():
    print('='*50)
    print('\tBegin Part 2')
    print('='*50)
    data = np.loadtxt(os.path.join('Data', 'ex1data1.txt'), delimiter=',')
    X, y = data[:, 0], data[:, 1]
    m = y.size
    iterations = 1500
    alpha = 0.01
    learning_rates = [0.01, 0.02, 0.0001, 0.0002]
    X = np.stack([np.ones(m), X], axis=1)

    str_rates = []
    figure = pyplot.figure()
    for rate in learning_rates:
        J_hist, theta = gradient_descent(X, y, [0, 0], rate, iterations)
        plot_cost_hist(J_hist, rate)
        str_rates.append("{:.4f}".format(rate))
    pyplot.legend(str_rates)
    pyplot.show()

    theta = np.array([0.0, 0.0])
    J_hist, theta = gradient_descent(X, y, theta, alpha, iterations)

    plot_data(X[:, 1], y)
    pyplot.plot(X[:, 1], np.dot(X, theta), '-')
    pyplot.legend(['Training data', 'Linear regression'])
    pyplot.show()
    predict([1, 3.5], theta)
    predict([1, 7], theta)

    visualize_cost(X, y, theta)
    pyplot.show()

def part3():
    print('='*50)
    print('\tBegin Part 3 (Optional)')
    print('='*50)
    data = np.loadtxt(os.path.join('Data', 'ex1data2.txt'), delimiter=',')
    # Grabs all rows and all columns from [start, 2) exclusive of index 2
    X = data[:, :2]
    # Grabs all rows and column at index 2
    y = data[:, 2]
    m = y.shape[0]
    print('{:>8s}{:>8s}{:>10s}'.format('X[:,0]', 'X[:,1]', 'y'))
    for i in range(10):
        print('{:8.0f}{:8.0f}{:10.0f}'.format(X[i, 0], X[i, 1], y[i]))

    X_norm, mu, sigma = normalizeFeatures(X)
    print('Computed mean:\t', mu)
    print('Computed STD:\t', sigma)
    # Concatenate a vector of 1s at the beginning of X_norm
    # Axis controls whether you concatenate by row or col, row is 0, col is 1
    X = np.concatenate([np.ones((m,1)), X_norm], axis=1)
    theta = np.array([0.0, 0.0, 0.0])
    cost = compute_cost(X, y, theta)
    print(cost)
    iterations = 400
    alpha = 0.1
    J_hist, theta = gradient_descent(X, y, theta, alpha, iterations)
    print(theta)
    cost = compute_cost(X, y, theta)
    test_case = [1, 1650, 3]
    test_case[1:3] = (test_case[1:3] - mu) / sigma
    cost = np.dot(test_case,theta)
    print(cost)
    norm_eq_theta = normal_equation(X, y)
    cost = np.dot(test_case, norm_eq_theta)
    print(cost)

def main():
    part1()
    part2()
    part3()

if __name__ == '__main__':
    main()


################################################################################
##############              21-241 Term Project                   ##############
##############                Topic: Page Rank                    ##############
##############        andrewIDs: nnishika and kratij              ##############
##############                                                    ##############
##############       Sections:                                    ##############
##############   1. Setup                                         ##############
##############   2. Method 1: Using eigenvectors                  ##############
##############   3. Method 2: Using iteration to find convergence ##############
##############   3. Helper Functions alphabetically arranged      ##############
##############   4. Testing the Program                           ##############
################################################################################

################################################################################
# The primary function which the user must call is rank(markov) which takes in a
# markov chain called 'markov' which is a list of lists (a 2D list) and returns
# a tuple of two lists with pages in descending order of 'importance'. The first
# list corresponds to the ranking from method 1 and the second to the ranking
# from method 2.
################################################################################

################################################################################
############                         Setup                        ##############
################################################################################

def rank(markov):
    # input: markov chain: a list of list that maps pages (index of inner
    # lists) to destinations (inner lists)
    # output: a tuple with two lists each of which is a list with pages in
    # descending order of 'importance'. The first list corresponds to the
    # ranking from method 1 and the second to the ranking from method 2.
    # epsilon that defines 'almost the same' to identify convergence
    epsilon = 0.000000000001
    # damp Weight
    dampWt = 0.15
    # fixing end nodes of the input markov chain
    fixEndNodes(markov)
    # method 1: using eigenvectors
    ans1 = limitToRank(limitVetor1(markov, dampWt))
    # method 2: iterative multiplication until convergence
    ans2 = limitToRank(limitVector2(markov, epsilon, dampWt))
    return ans1, ans2

def fixEndNodes(markov):
    # fixes cases when one node in the graph has no outgoing edges
    # i.e., if one column is a zero vector, we replace it with all entries
    # having equal probability. This function makes the change in the markov
    # chain itself because it is easier to check for an element of the chain
    # being an empty list than a column in a matrix being the zero vector
    for listOfDestinations in markov:
        if len(listOfDestinations) == 0:
            for page in range(len(markov)):
                listOfDestinations.append(page)

def limitToRank(limitVector):
    # given a limit vector which lists the probabilities of being
    # at any page in the form limitVector[i] is the probability of being on
    # page i, this function returns a ranking of the pages in descending order
    # of probability
    pagesWithProbability = {}
    for page in range(len(limitVector)):
        pagesWithProbability[page] = limitVector[page]
        # creates a new dictionary assignment: page --> probability
    rank = []
    while len(pagesWithProbability) > 0:
        maxPage = maxFromDictionary(pagesWithProbability)
        rank.append(maxPage)
        pagesWithProbability.pop(maxPage)
    return rank

################################################################################
############             Method 1: Using eigenvectors             ##############
################################################################################

def limitVector1(markov, dampWt):
    # this function outputs the limit vector according to method 1
    evecs = findEvecs(createPageRankMatrixWithDamping(markov, dampWt))
    for vector in evecs:
    # finds the first and only vector that can be scaled to a probability vector
        if isLimitVector(vector):
            convertToPositive(vector)
            return vector

def isLimitVector(vector):
    # function checks if given vector has all positive / all negative entries
    isPositive = None
    for entry in vector:
        if isPositive == None: # this checks if we are on the first entry
            if entry < 0:
                isPositive = False
            elif entry > 0:
                isPositive = True # sets to + or - based on the first entry
        else:
            if entry < 0 and isPositive or entry > 0 and not isPositive:
                return False # if future entry doesn't match sign of first entry
    return True

def createPageRankMatrixWithDamping(markov, dampWt):
    # adds the markov transition matrix and damping matrix according to input
    # damp weight, dampWt
    P0 = createPageRankMatrix(markov)
    B = createDampingMatrix(markov)
    # initialize a zero matrix of same dimensions as A and B
    P = [[0]*len(P0) for i in range(len(P0))]
    # taking an element wise weighted average of A and B based on dampWt
    for i in range(len(P0)):
        for j in range(len(P)):
            P[i][j] = (1 - dampWt) * P0[i][j] + dampWt * B[i][j]
    return P

def createPageRankMatrix(markov):
    # creates a markov transition matrix corresponding to the given markov chain
    m = len(markov)
    P0 = [[0]*m for index in range(m)]
    for page in range(m):
        denominator = len(markov[page])
        for destination in range(m):
            if destination in markov[page]:
                P0[destination][page] = 1/denominator
            else:
                P0[destination][page] = 0
    return P0

def createDampingMatrix(markov):
    # creates a transition matrix corresponding to a markov chain of the same
    # length as the given markov chain, where all nodes point to all nodes
    # (including self)
    m = len(markov)
    B = [[1/m]*m for index in range(m)]
    return B

################################################################################
###########      Method 2: Using iteration to find convergence       ###########
################################################################################

def limitVector2(markov, epsilon, dampWt):
    # returns the limit vector according to method 2
    markovTranspose, denominators = findMarkovTranspose(markov)
    vec0 = startVector(markov)
    vecA = findNextVector(markovTranspose, denominators, vec0, dampWt)
    vecB = findNextVector(markovTranspose, denominators, vecA, dampWt)
    while not withinEpsilon(vecA, vecB, epsilon):
        vecA = vecB
        vecB = findNextVector(markovTranspose, denominators, vecA, dampWt)
    return vecB

def findMarkovTranspose(markov):
    # given markov, returns markovTranspose as explained in the paper
    markovTranspose = [[] for i in range(len(markov))]
    denominators = []
    for j in range(len(markov)):
        for i in markov[j]:
            markovTranspose[i].append(j)
        denominators.append(len(markov[j]))
    return markovTranspose, denominators

def findNextVector(markovTranspose, denominators, currentVector, dampWt):
    # given the current vector and information derived from markov,
    # this functions returns the next vector (the probabilities at the next
    # step of the random walk)
    nextVector = []
    m = len(currentVector) # now m is the number of pages
    for i in range(m): # go through all pages in markovTranspose
        newPageProbability = 0
        for j in markovTranspose[i]:
            newPageProbability += (1 - dampWt) * currentVector[j]/denominators[j]
        # now we add the damping matrix part. The value of the damping matrix
        # times a probability vector is a constant: 1/m where m is the number
        # of pages
        newPageProbability += dampWt/m
        nextVector.append(newPageProbability)
    return nextVector

################################################################################
###############     Helper Functions alphabetically arranged     ###############
################################################################################

def convertToPositive(vector):
    # returns a vector where every element is the absolute value of the
    # corresponding element in the input vector
    for i in range(len(vector)):
        vector[i] = abs(vector[i])

import numpy
def findEvecs(matrix):
    # returns a list of eigen vectors of a matrix A
    return transpose(numpy.linalg.eig(matrix)[1].real.tolist())
    # .tolist() converts the numpy array to a 2D list and the transpose function
    # is used because the eigenvectors form the columns not the rows of the
    # returned matrix. In a 2D list representation, we have a list of the rows
    # and so to have a list of the eigenvectors it must be the case that the
    # eigenvectors are the rows and not the columns of the matrix.

def maxFromDictionary(dictionary):
    # takes in a dictionary that maps keys to values and returns the key in the
    # dictionary which is associated with the maximum value
    # this is different from a standard python max function because here we
    # want to return the key associated with the max value and not the max key
    maxKey = None
    maxValue = 0
    for key in dictionary:
        if maxKey == None or maxValue < dictionary[key]:
            maxKey = key
            maxValue = dictionary[key]
    return maxKey

def startVector(markov):
    # calculates a start vector for a given markov chain: where all pages have
    # equal probability
    denominator = len(markov)
    v = [1/denominator] * len(markov)
    return v

def transpose(matrix):
    # returns the transpose of the input matrix
    matrixTranspose = [[0]*len(matrix) for i in range(len(matrix))]
    for i in range(len(matrix)):
        for j in range(len(matrix)):
            matrixTranspose[j][i] = matrix[i][j]
    return matrixTranspose

def withinEpsilon(vec1, vec2, epsilon):
    # checks if two vectors are 'almost' equal to each other by seeing if each
    # element of vector 1 is within epsilon distance from the corresponding
    # element of vector 2 for a given epsilon
    assert(len(vec1) == len(vec2))
    for i in range(len(vec1)):
        if not -epsilon <= vec1[i] - vec2[i] <= epsilon:
            return False
    return True

################################################################################
###############              Testing the Program             ###################
################################################################################

def testPageRankProgram():
    # the results expected are in the form of a 2-tuple because we return
    # answers from both methods

    # test case 1
    markov = [[1, 3, 4], [0, 2, 3], [1, 2], [1, 3, 4], [0]]
    assert(rank(markov) == ([0, 1, 3, 2, 4], [0, 1, 3, 2, 4]))

    # test case 2
    markov = [[1], [2], [1]]
    assert(rank(markov) == ([1, 2, 0], [1, 2, 0]))

    # test case 3
    markov = [[1, 3, 4], [0, 2, 4], [3, 6], [2, 4, 6], [5, 8], [4, 6, 8],
                                    [0, 7, 9], [0, 6, 8], [2, 9], [0, 2, 8]]
    assert(rank(markov) == ([2, 6, 8, 0, 4, 3, 9, 5, 7, 1],
                                                [2, 6, 8, 0, 4, 3, 9, 5, 7, 1]))

    # test case 4
    markov = [[1, 5], [2, 5], [1, 3, 5], [4], [1, 5], [2, 6], [0, 1]]
    assert(rank(markov) == ([5, 2, 1, 6, 4, 3, 0], [5, 2, 1, 6, 4, 3, 0]))
    print('Yay!')

testPageRankProgram()

#### Credit:
#### Used numpy to find eigenvectors


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

# # CE-40717: Machine Learning

# ## HW7-Regression

# ### Please fill this part
#
#
# 1.   Full Name: Amir Pourmand
# 2.   Student Number: 99210259

# In[2]:

# You are not allowed to import other packages.

import numpy as np
import pandas as pd

from matplotlib import pyplot as plt
from sklearn.metrics import mean_squared_error

# ### Part1:
# Consider the dataset which has been given to you. Split the dataset into the `training dataset` and `test dataset`. You can use `80%` of the data for training and the rest for testing.

# In[45]:

dataset = pd.read_csv("ML2021S-HW7-data.csv")

df = dataset.to_numpy()
# implement the split part
# name them: x_train, x_test, y_train, y_test

train_count = int(0.8 * len(dataset))
x_train =df[:train_count,1]
y_train = df[:train_count,-1]

x_test = df[train_count:,1]
y_test =df[train_count:,-1]

# ### Part2:
# Assume there is simple linear relationship between X and Y. Train a simple `Linear Regression` on the training dataset and report its accuracy on the training and testing dataset, separately.

# In[197]:

# here you can implement via sgd or use least square solution. we suggest the second one inv(A^T A) * A^T y.
# consider m as the returned line --> m[0] = the slope , m[1] = y intercept, also don't forget to use numpy for calculating m.

bias=np.ones((x_train.shape[0],1))
biased_train = np.append( x_train[:,None],bias,axis=1)

m = np.linalg.inv(biased_train.T @ biased_train)@ (biased_train.T @ y_train)
m

# In[116]:

predicted_train_y = m[0] * x_train + m[1]
predicted_test_y = m[0] * x_test + m[1]

train_error = mean_squared_error(predicted_train_y,y_train)
test_error = mean_squared_error(predicted_test_y,y_test)

print(f"MSE train error: {train_error}\nMSE test error: {test_error}")

# ### Part3:
# Draw the line you've found in the last part, on train and test points (each separately).

# In[117]:

plt.scatter(x_train, y_train)
plt.scatter(x_train, m[0]*x_train + m[1], s=1)
plt.title("Train dataset")

# In[118]:

plt.scatter(x_test, y_test)
plt.scatter(x_test, m[0]*x_test + m[1], s=1)
plt.title("Test dataset")

# ### Part4:
# Were the reported accuracies acceptable?
#
# What about the line you drew in the last part?
#
# Was it appropriate for the dataset?
#

# Explain what are the main problems here.

#
# No, The error is too high.
# The Line is best line that fits but we should also try other models.
# The model is clearly not suitable for this dataset.
#
# Main problem is that a single line can't fit sin or cos or anything like that.

# ### Part5:
# Now, consider the `Locally Weighted Linear Regression` approach for this problem. Do expect it performs better than simple `Linear Regression` for this dataset?

# Yes, I expect that it would work better

# ### Part6:
# Here, we want to implement the `suggested model`. You can implement any weight function you want, the below implementation is just a sample:
#

# \begin{equation*}
#     w^{(i)}=exp\left(-\frac{(\mathbf x^{(i)}-\mathbf x)^\top(\mathbf x^{(i)}-\mathbf x)}{2\tau^2}\right)
# \end{equation*}

# In[145]:

def get_weight_matrix(the_query_point, train_dataset, hyperparameter):
    l = train_dataset.shape[0]
    W = np.mat(np.eye(l))
    for i, xi in enumerate(train_dataset):
        vector = xi - the_query_point
        W[i, i] = np.exp(-1/(2*hyperparameter**2) * (vector @ vector.T))
    return W

# Now implement the `closed form solution` based on the following formula:
#

# \begin{equation*}
#     \boldsymbol\theta=(X^\top WX)^{-1}(X^\top WY)
# \end{equation*}
#

# Of course if you use another form, implement your own.

# In[163]:

def predict(train_X, train_Y, query_x, hyperparameter):
    l = train_X.shape[0]
    X = np.hstack((train_X[:,None], np.ones((l, 1))))
    qx = np.mat([query_x, 1])
    W = get_weight_matrix(qx, X, hyperparameter)

    theta = np.linalg.pinv( X.T @ W @ X) @ (X.T @ W @ train_Y).T

    return np.dot(qx, theta).item(0)

# ### Part7:
# Report accuracy of this model on the test dataset. Also, draw a plot that contains (x_test, y_test) pairs and (x_test, predicted_y) pairs that differ in color.

# In[165]:

predicted_test = [predict(x_train, y_train, query, hyperparameter=0.10) for query in x_test]
error = mean_squared_error(predicted_test,y_test)
print(f"MSE test error: {error}")

# In[166]:

plt.scatter(x_test, y_test, c="blue")
plt.scatter(x_test, predicted_test, c="yellow")

# ### Part8:
# Is the performance of the new model better than the previous one? `Explain`.

# Yes, performance is clearly better. It's actually 1/3 of error in normal linear regression. The reason is that we put weight to the input values and this way we can fit more flexible models.

# ### Part9:
# Now, we want to consider the `KNN` approach for this problem; Do you think it will perform well?

# Yes, KNN has prooven to show very good results. Why not test it?!
#
#

# ### Part10:
# Implementing the `KNN model`.

# In[176]:

class KNN:
    def __init__(self, number_neighbours, X, Y):
        self.k = number_neighbours
        self.X = X
        self.Y = Y

    def distance(self, x1, x2):
        distance = np.abs(x1-x2) # implement the distance function
        return distance

    def return_nn_ids(self, x):
        distances = self.distance(self.X,x) # find indices of k nearest neighbours to x
        indices = np.argsort(distances)[:self.k]
        return indices

    def predict(self, x):
        neareest_indices = self.return_nn_ids(x)
        predicted_y=0
        for item in neareest_indices:
            predicted_y += self.Y[item]
        predicted_y = predicted_y / self.k
        return predicted_y

# In[177]:

def evaluation(k, x_train, y_train, x_test, y_test):
    model = KNN(k, x_train, y_train)
    predicted_y = [model.predict(x) for x in x_test]
    error = mean_squared_error(predicted_y,y_test)
    return error, predicted_y

# In[178]:

k = 3
error_knn, predicted_y = evaluation(k, x_train, y_train, x_test, y_test)
print(f"MSE error of KNN for test dataset: {error_knn}")

# In[179]:

plt.scatter(x_test, y_test, c="blue")
plt.scatter(x_test, predicted_y, c="yellow")

# To find best K for this algorithm we suggest to use cross-validation.
#

# Did this model perform well (due to the plot and test accuracy)?

# Yes, It actually performs very well. The good point is that with default parameters, i.e. k=3 , it performed very well!
# we can also see that the plot shows very good fit to data.

# In[196]:

error_array = []
limit =100

min_error,min_k = None,None

for k in range(1,limit):
    error_knn, predicted_y = evaluation(k, x_train, y_train, x_test, y_test)
    error_array.append(error_knn)

    if k == 1:
        min_error = error_knn
        min_k = k

    if error_knn < min_error:
        min_error = error_knn
        min_k = k

plt.plot(np.arange(1,limit),error_array)
plt.ylim(0.096,0.12)
print('min of k would be:', min_k,'Min of error would be',min_error )

# In[ ]:

# In[ ]:


import odespy
import numpy as np
import scitools.std as plt

def simulate_drag(
    beta=0.9,                 # dimensionless elasticity parameter
    gamma=0,                  # dimensionless drag parameter
    Theta=30,                 # initial angle in degrees
    epsilon=0,                # initial stretch of wire
    num_periods=6,            # simulate for num_periods
    time_steps_per_period=60, # time step resolution
    plot=True,                # make plots or not
    ):
    from math import sin, cos, pi
    Theta = Theta*np.pi/180  # convert to radians
    # Initial position and velocity
    # (we order the equations such that Euler-Cromer in odespy
    # can be used, i.e., vx, x, vy, y)
    ic = [0,                              # x'=vx
          (1 + epsilon)*sin(Theta),       # x
          0,                              # y'=vy
          1 - (1 + epsilon)*cos(Theta),   # y
          ]

    def f(u, t, beta, gamma):
        vx, x, vy, y = u
        L = np.sqrt(x**2 + (y-1)**2)
        v = np.sqrt(vx**2 + vy**2)
        h1 = beta/(1-beta)*(1 - beta/L)  # help factor
        h2 = gamma/beta*v
        return [-h2*vx - h1*x, vx, -h2*vy - h1*(y-1) - beta, vy]

    # Non-elastic pendulum (scaled similarly in the limit beta=1)
    # solution Theta*cos(t)
    P = 2*pi
    dt = P/time_steps_per_period
    T = num_periods*P
    omega = 2*pi/P

    time_points = np.linspace(
        0, T, num_periods*time_steps_per_period+1)

    solver = odespy.EulerCromer(f, f_args=(beta, gamma))
    solver.set_initial_condition(ic)
    u, t = solver.solve(time_points)
    x = u[:,1]
    y = u[:,3]
    theta = np.arctan(x/(1-y))

    if plot:
        plt.figure()
        plt.plot(x, y, 'b-', title='Pendulum motion',
                 daspect=[1,1,1], daspectmode='equal',
                 axis=[x.min(), x.max(), 1.3*y.min(), 1])
        plt.savefig('tmp_xy.png')
        plt.savefig('tmp_xy.pdf')
        # Plot theta in degrees
        plt.figure()
        plt.plot(t, theta*180/np.pi, 'b-',
                 title='Angular displacement in degrees')
        plt.savefig('tmp_theta.png')
        plt.savefig('tmp_theta.pdf')
        if abs(Theta) < 10*pi/180:
            # Compare theta and theta_e for small angles (<10 degrees)
            theta_e = Theta*np.cos(omega*t)  # non-elastic scaled sol.
            plt.figure()
            plt.plot(t, theta, t, theta_e,
                     legend=['theta elastic', 'theta non-elastic'],
                     title='Elastic vs non-elastic pendulum, '\
                            'beta=%g' % beta)
            plt.savefig('tmp_compare.png')
            plt.savefig('tmp_compare.pdf')
        # Plot y vs x (the real physical motion)
    return x, y, theta, t

def test_equilibrium():
    """Test that starting from rest makes x=y=theta=0."""
    x, y, theta, t = simulate_drag(
        beta=0.9, gamma=0, Theta=0, epsilon=0,
        num_periods=6, time_steps_per_period=10, plot=False)
    tol = 1E-14
    assert np.abs(x.max()) < tol
    assert np.abs(y.max()) < tol
    assert np.abs(theta.max()) < tol

def test_vertical_motion():
    beta = 0.9
    omega = np.sqrt(beta/(1-beta))
    # Find num_periods. Recall that P=2*pi for scaled pendulum
    # oscillations, while here we don't have gravity driven
    # oscillations, but elastic oscillations with frequency omega.
    period = 2*np.pi/omega
    # We want T = N*period
    N = 5
    # simulate function has T = 2*pi*num_periods
    num_periods = 5/omega
    n = 600
    time_steps_per_period = omega*n

    y_exact = lambda t: -0.1*np.cos(omega*t)
    x, y, theta, t = simulate_drag(
        beta=beta, gamma=0, Theta=0, epsilon=0.1,
        num_periods=num_periods,
        time_steps_per_period=time_steps_per_period,
        plot=False)

    tol = 0.00055 # ok tolerance for the above resolution
    # No motion in x direction is epxected
    assert np.abs(x.max()) < tol
    # Check motion in y direction
    y_e = y_exact(t)
    diff = np.abs(y_e - y).max()
    if diff > tol: # plot
        plt.plot(t, y, t, y_e, legend=['y', 'exact'])
        raw_input('Type CR:')
    assert diff < tol, 'diff=%g' % diff

def demo(beta=0.999, gamma=0, Theta=40, num_periods=3):
    x, y, theta, t = simulate_drag(
        beta=beta, gamma=gamma, Theta=Theta, epsilon=0,
        num_periods=num_periods, time_steps_per_period=600,
        plot=True)

if __name__ == '__main__':
    test_equilibrium()
    test_vertical_motion()
    demo(beta=0.999, gamma=1, num_periods=3)
    raw_input('Type CR: ')


"""
This example will walk the user through implementing DCF fits to data sets with
turning points and inflection points. It builds on the details in the
'Simple Example Code' and uses the 'constraints' keyword argument introduced
there. The 'constraints' keyword argument is used to adjust the type of DCF that
is being fitted. Recall that by default ``maxsmooth`` implements a Maximally
Smooth Function or MSF with constraints=2 i.e. derivatives of order :math:`{m \geq 2}`
constrained so that they do not cross zero. This allows for turning points in the
DCF as illustrated below.

We start by generating some noisy data that we know will include a turning point
and defining the order of the DCF we would like to fit.

.. code:: bash

    import numpy as np

    x = np.linspace(-10, 10, 100)
    noise = np.random.normal(0, 0.02, 100)
    y = x**(2) + noise

    N = 10

"""
import numpy as np

x = np.linspace(-10, 10, 100)
noise = np.random.normal(0, 0.02, 100)
y = x**(2) + noise

N = 10

"""

We can go ahead and plot this data just to double check it features a turning
point.

.. code:: bash

    import matplotlib.pyplot as plt

    plt.plot(x, y)
    plt.xlabel('x', fontsize=12)
    plt.ylabel('y', fontsize=12)
    plt.show()

"""

import matplotlib.pyplot as plt

plt.plot(x, y)
plt.xlabel('x', fontsize=12)
plt.ylabel('y', fontsize=12)
plt.show()

"""
As already stated ``maxsmooth`` does not constrain the first derivative of the
DCF by default so we can go ahead and fit the data.

.. code:: bash

    from maxsmooth.DCF import smooth

    res = smooth(x, y, N)

"""

from maxsmooth.DCF import smooth

res = smooth(x, y, N)

"""
If we than plot the resultant residuals we will see that despite the data
having a turning point present we have recovered the Gaussian noise.

.. code:: bash

    plt.plot(x, y- res.y_fit, label='Recovered Noise')
    plt.plot(x, noise, label='Actual Noise')
    plt.ylabel(r'$\delta y$', fontsize=12)
    plt.xlabel('x', fontsize=12)
    plt.legend()
    plt.show()

"""

plt.plot(x, y- res.y_fit, label='Recovered Noise')
plt.plot(x, noise, label='Actual Noise')
plt.ylabel(r'$\delta y$', fontsize=12)
plt.xlabel('x', fontsize=12)
plt.legend()
plt.show()

"""
To illustrate what happens when there is an inflection point in the data we can
define some sinusoidal data as so.

.. code:: bash

    x = np.linspace(1, 5, 100)
    noise = np.random.normal(0, 0.02, 100)
    y = np.sin(x) + noise

    N = 10

    plt.plot(x, y)
    plt.xlabel('x', fontsize=12)
    plt.ylabel('y', fontsize=12)
    plt.show()

"""
x = np.linspace(1, 5, 100)
noise = np.random.normal(0, 0.02, 100)
y = np.sin(x) + noise

N = 10

plt.plot(x, y)
plt.xlabel('x', fontsize=12)
plt.ylabel('y', fontsize=12)
plt.show()

"""
If we proceed to fit this with smooth() in its default settings we will get a
poor fit as by default the second derivative is constrained. We need to lift this
constraint to allow for the prominent inflection point to be modelled. We do this
by setting the keyword argument constraints=3 creating a Partially Smooth Function
or PSF.

.. code:: bash

    res_msf = smooth(x, y, N)
    res_psf = smooth(x, y, N, constraints=3)

    plt.plot(x, y, label='Data')
    plt.plot(x, res_msf.y_fit, label=r'MSF fit, $m \geq 2$')
    plt.plot(x, res_psf.y_fit, label=r'PSF fit, $m \geq 3$')
    plt.xlabel('x', fontsize=12)
    plt.ylabel('y', fontsize=12)
    plt.legend()
    plt.show()

"""

res_msf = smooth(x, y, N)
res_psf = smooth(x, y, N, constraints=3)

plt.plot(x, y, label='Data')
plt.plot(x, res_msf.y_fit, label=r'MSF fit, $m \geq 2$')
plt.plot(x, res_psf.y_fit, label=r'PSF fit, $m \geq 3$')
plt.xlabel('x', fontsize=12)
plt.ylabel('y', fontsize=12)
plt.legend()
plt.show()

"""
Finally, we can plot the residuals to further see that by lifting the constraint on the
second derivative we have allowed an inflection point in the data.

.. code::

    plt.plot(x, y- res_psf.y_fit, label='Recovered Noise')
    plt.plot(x, noise, label='Actual Noise')
    plt.ylabel(r'$\delta y$', fontsize=12)
    plt.xlabel('x', fontsize=12)
    plt.legend()
    plt.show()

"""

plt.plot(x, y - res_psf.y_fit, label='Recovered Noise')
plt.plot(x, noise, label='Actual Noise')
plt.ylabel(r'$\delta y$', fontsize=12)
plt.xlabel('x', fontsize=12)
plt.legend()
plt.show()


import numpy as np

# problem 1
class Precise(object):
    def __init__(self, val=0, exp=0):
        self.val = val
        # note, I'm storing the power of the last significant digit.
        self.sig = exp - len(str(self.val)) +1

    def exp(self):
        # get the exponent from the sig value.
        return len(str(self.val)) - 1 + self.sig

    def pyfloat(self):
        # return a python float from the floating point object.
        return self.val * 10**(self.sig)

    def copy(self):
        # make a copy of the floating point object.
        new = precise(val=self.val)
        new.sig = self.sig
        return new

    def __repr__(self):
        # print method
        s = str(self.val)
        return s[0] + '.' + s[1:] + " x 10^" + str(self.exp())

    def __add__(self, other):
        # addition
        if self.sig < other.sig:
            new = self.copy()
            new.val *= 10**(other.sig - self.sig)
            new.sig = self.sig
            new.val += other.val
        elif other.sig < self.sig:
            new = other.copy()
            new.val *= 10**(self.sig - other.sig)
            new.sig = other.sig
            new.val += self.val
        else:
            new = other.copy()
            new.val += self.val
        return new

    def __mul__(self, other):
        # multiplication
        new = self.copy()
        new.sig += other.sig
        new.val *= other.val
        return new

    def __sub__(self, other):
        # subtraction
        new = other.copy()
        new.val *= -1
        return self + new

    def truncate(self, newsig):
        # truncation
        if self.sig < newsig:
            self.val = int(str(self.val)[:(self.sig-newsig)])
            self.sig = newsig

class Significant(object):
    # most of these methods are based on the previous class
    def __init__(self, val=Precise(), err=Precise()):
        self.val = val
        # note: error should ALWAYS be positive
        self.err = err

    def pyfloat(self):
        return self.val.pyfloat()

    def copy(self):
        new = significant()
        new.val = self.val.copy()
        new.err = self.err.copy()
        return new

    def __repr__(self):
        return str(self.val) + " +- " + str(self.err)

    def __add__(self, other):
        new = self.copy()
        new.val += other.val
        new.err += other.err
        return new

    def __sub__(self, other):
        new = self.copy()
        new.val -= other.val
        new.err += other.err
        return new

    def __mul__(self, other):
        new = self.copy()
        new.val *= other.val
        new.err  = self.err*other.val + self.val*other.err + self.err*other.err
        return new

    def truncate(self):
        if self.err.val == 1:
            self.err.sig = self.err.exp()
            self.val.truncate(self.err.sig)
        elif self.err.val != 0:
            self.err.sig = self.err.exp() + 1
            self.err.val = 1
            self.val.truncate(self.err.sig)

# problem 2
def sqrt64(A, reps):
    Ac = A.copy()
    I = Ac.view(dtype=np.int64)
    I >>= 1
    I += (1<<61) - (1<<51)
    for i in xrange(reps):
        Ac = .5 *(Ac + A / Ac)
    return Ac

# For the analysis, it is system dependent, but the sqrt here will probably be faster for low accuracy.
# Again, for the fast inverse square root, it is system dependent, but the cython version here
# should still be moderately faster than the version using the sqrt function in C for up to 4 iterations
# which is just as accurate.


# K-Means Clustering
# This chapter introduces a different clustering algorithm - k-means clustering - and its implementation in SciPy. K-means clustering overcomes the biggest drawback of hierarchical clustering that was discussed in the last chapter. As dendrograms are specific to hierarchical clustering, this chapter discusses one method to find the number of clusters before running k-means clustering. The chapter concludes with a discussion on the limitations of k-means clustering and discusses considerations while using this algorithm.

# K-means clustering: first exercise
# This exercise will familiarize you with the usage of k-means clustering on a dataset. Let us use the Comic Con dataset and check how k-means clustering works on it.

# Recall the two steps of k-means clustering:

# Define cluster centers through kmeans() function. It has two required arguments: observations and number of clusters.
# Assign cluster labels through the vq() function. It has two required arguments: observations and cluster centers.
# The data is stored in a Pandas data frame, comic_con. x_scaled and y_scaled are the column names of the standardized X and Y coordinates of people at a given point in time.

# Import the kmeans and vq functions
from scipy.cluster.vq import kmeans, vq

# Generate cluster centers
cluster_centers, distortion = kmeans(comic_con[['x_scaled','y_scaled']],2)

# Assign cluster labels
comic_con['cluster_labels'], distortion_list = vq(comic_con[['x_scaled', 'y_scaled']], cluster_centers)

# Plot clusters
sns.scatterplot(x='x_scaled', y='y_scaled',
                hue='cluster_labels', data = comic_con)
plt.show()

# Elbow method on distinct clusters
# Let us use the comic con data set to see how the elbow plot looks on a data set with distinct, well-defined clusters. You may want to display the data points before proceeding with the exercise.

# The data is stored in a Pandas data frame, comic_con. x_scaled and y_scaled are the column names of the standardized X and Y coordinates of people at a given point in time.

distortions = []
num_clusters = range(1, 7)

# Create a list of distortions from the kmeans function
for i in num_clusters:
    cluster_centers, distortion = kmeans(comic_con[['x_scaled', 'y_scaled']], i)
    distortions.append(distortion)

# Create a data frame with two lists - num_clusters, distortions
elbow_plot = pd.DataFrame({'num_clusters': num_clusters, 'distortions': distortions})

# Creat a line plot of num_clusters and distortions
sns.lineplot(x='num_clusters', y='distortions', data = elbow_plot)
plt.xticks(num_clusters)
plt.show()

# Elbow method on uniform data
# In the earlier exercise, you constructed an elbow plot on data with well-defined clusters. Let us now see how the elbow plot looks on a data set with uniformly distributed points. You may want to display the data points on the console before proceeding with the exercise.

# The data is stored in a Pandas data frame, uniform_data. x_scaled and y_scaled are the column names of the standardized X and Y coordinates of points.

distortions = []
num_clusters = range(2, 7)

# Create a list of distortions from the kmeans function
for i in num_clusters:
    cluster_centers, distortion = kmeans(uniform_data[['x_scaled','y_scaled']],i)
    distortions.append(distortion)

# Create a data frame with two lists - number of clusters and distortions
elbow_plot = pd.DataFrame({'num_clusters': num_clusters, 'distortions': distortions})

# Creat a line plot of num_clusters and distortions
sns.lineplot(x='num_clusters', y='distortions', data=elbow_plot)
plt.xticks(num_clusters)
plt.show()

# Impact of seeds on distinct clusters
# You noticed the impact of seeds on a dataset that did not have well-defined groups of clusters. In this exercise, you will explore whether seeds impact the clusters in the Comic Con data, where the clusters are well-defined.

# The data is stored in a Pandas data frame, comic_con. x_scaled and y_scaled are the column names of the standardized X and Y coordinates of people at a given point in time.

# Import random class
from numpy import random

# Initialize seed
random.seed(0)

# Run kmeans clustering
cluster_centers, distortion = kmeans(comic_con[['x_scaled', 'y_scaled']], 2)
comic_con['cluster_labels'], distortion_list = vq(comic_con[['x_scaled', 'y_scaled']], cluster_centers)

# Plot the scatterplot
sns.scatterplot(x='x_scaled', y='y_scaled',
                hue='cluster_labels', data = comic_con)
plt.show()

# Uniform clustering patterns
# Now that you are familiar with the impact of seeds, let us look at the bias in k-means clustering towards the formation of uniform clusters.

# Let us use a mouse-like dataset for our next exercise. A mouse-like dataset is a group of points that resemble the head of a mouse: it has three clusters of points arranged in circles, one each for the face and two ears of a mouse.

# Here is how a typical mouse-like dataset looks like (Source).

# The data is stored in a Pandas data frame, mouse. x_scaled and y_scaled are the column names of the standardized X and Y coordinates of the data points.

# Import the kmeans and vq functions
from scipy.cluster.vq import kmeans,vq

# Generate cluster centers
cluster_centers, distortion = kmeans(mouse[['x_scaled','y_scaled']],3)

# Assign cluster labels
mouse['cluster_labels'], distortion_list = vq(mouse[['x_scaled','y_scaled']],cluster_centers)

# Plot clusters
sns.scatterplot(x='x_scaled', y='y_scaled',
                hue='cluster_labels', data = mouse)
plt.show()

# FIFA 18: defenders revisited
# In the FIFA 18 dataset, various attributes of players are present. Two such attributes are:

# defending: a number which signifies the defending attributes of a player
# physical: a number which signifies the physical attributes of a player
# These are typically defense-minded players. In this exercise, you will perform clustering based on these attributes in the data.

# The following modules have been pre-loaded: kmeans, vq from scipy.cluster.vq, matplotlib.pyplot as plt, seaborn as sns. The data for this exercise is stored in a Pandas dataframe, fifa. The scaled variables are scaled_def and scaled_phy

# Set up a random seed in numpy
random.seed([1000,2000])

Cluster Analysis in Python/K-Means_Clustering.py


#!/usr/bin/env python

"""
Author  : Bhekimpilo Ndhlela
Author  : 18998712
module  : TW244 Applied Mathematics
task    : src code for Assignment04
"""
from scipy.special import lambertw
from scipy.optimize import fsolve
from scipy.linalg import eig, expm
import matplotlib.pyplot as plt
import numpy as np

def plot_question3(x, y, point=None, point_plot=False):
    plt.title("IVP solution Plot:")
    plt.plot(x, y[:,0], 'k-', lw=4, label='x1')
    plt.plot(x, y[:,1], 'r-', lw=4, label='x2')
    plt.ylabel('y')
    if point_plot != False:
        plt.plot(10, point[0], 'ro', lw=4, label='x1(10)')
        plt.plot(10, point[1], 'ko', lw=4, label='x2(10)')
        plt.xlabel('0 <= t <= 10')
        plt.xlim(0, 10)
    else:
        plt.xlabel('0 <= t <= 2')
        plt.xlim(0, 2)
    plt.legend(loc='best')
    plt.grid(True)
    plt.show()

def f(R):
    sind = lambda theta_radians: np.degrees(np.sin(theta_radians))
    cosd = lambda theta_radians: np.degrees(np.cos(theta_radians))
    g, r, theta, v0 = 9.81, 0.14, 45.0, 44.7 # constants
    print (g/r + v0*sind(theta))*(R/(v0*cosd(theta))) + (g/r**2)*np.log(1-(r*R)/(v0*cosd(theta)))
    return (g/r + v0*sind(theta))*(R/(v0*cosd(theta))) + (g/r**2)*np.log(1-(r*R)/(v0*cosd(theta)))

if __name__ == '__main__':
    # question 1
    #a.)
    z = fsolve(lambda z: z*np.exp(z) - 1.0, 0.0)
    #print z
    #b.)    i.
    w = lambertw(1).real
    #print w
    #b.)    ii.
    wn = w *np.exp(w)
    #print wn

    # question 2
    print '{:.17f}'.format(1.7356e-14 )
    R = fsolve(f, )
    print 'R = ', R

    # question 3
    x0 = np.array([2, -0.5])
    A, time = np.array([[-0.50, 3.0], [0.75, -2.0]]), np.linspace(0, 2, 1000)
    eigvals, eigvects = eig(A) #eigenvaluse and eigen vectors of: A
    eigvals = eigvals.real #take the real part of the eigvalues
    c = np.linalg.solve(eigvects, x0)   #z = inverse(eigenvectors)b

    x1 = lambda t: c[0]*np.exp(eigvals[0]*t)*eigvects[0][0] + c[1]*np.exp(eigvals[1]*t)*eigvects[0][1]
    x2 = lambda t: c[0]*np.exp(eigvals[0]*t)*eigvects[1][0] + c[1]*np.exp(eigvals[1]*t)*eigvects[1][1]

    x = np.array([(x1(t), x2(t)) for t in time])
    #plot_question3(time, x, point=None)

    eA = np.dot(expm(10 *A), x0) # get the matrix exponintiation and multiply by x(0)
    time = np.linspace(0, 10, 1000)
    x = np.array([(x1(t), x2(t)) for t in time])
    #plot_question3(time, x, point=eA, point_plot=True)

    #question 4

else:
    import sys
    sys.exit('USAGE: python assignment04.py')


# coding: utf-8

# # Function dctmatrix
#
# ## Synopse
#
# Compute the Kernel matrix for the DCT Transform.
#
# - **A = dctmatrix(N)**
#
#   - **A**:output: DCT matrix NxN.
#   - **N**:input: matrix size (NxN).

# In[1]:

import numpy as np

def dctmatrix(N):
    x = np.resize(range(N), (len(range(N)), len(range(N)))) #matrix with columns index
    u = np.transpose(np.resize(range(N), (len(range(N)), len(range(N))))) #matrix with rows index
    alpha = np.ones((N,N)) * np.sqrt(2./N) # 1/sqrt(2/N)
    alpha[0,:] = np.sqrt(1./N) # alpha(u,x)
    A = alpha * np.cos((2*x+1)*u*np.pi / (2.*N)) # Cn(u,x)
    return A

# ## Examples

# In[1]:

testing = (__name__ == "__main__")
if testing:
    get_ipython().system(' jupyter nbconvert --to python dctmatrix.ipynb')
    import numpy as np
    import sys,os
    ea979path = os.path.abspath('../../')
    if ea979path not in sys.path:
        sys.path.append(ea979path)
    import ea979.src as ia

# ### Example 1

# In[3]:

if testing:
    np.set_printoptions(suppress=True, precision=4)
    A = ia.dctmatrix(4)
    print('Visualiza matriz DCT 4x4:\n',A)
    B = np.dot(A,np.transpose(A))

    print("\nVisualiza propriedade A*A'= I:\n", B)

# ### Example 2

# In[2]:

if testing:
    get_ipython().magic('matplotlib inline')
    import matplotlib.pyplot as plt
    import matplotlib.image as mpimg
    import sys,os
    ea979path = os.path.abspath('../../')
    if ea979path not in sys.path:
        sys.path.append(ea979path)
    import ea979.src as ia
    A = ia.dctmatrix(128)
    ia.adshow(ia.normalize(A,[0,255]),'DCT 128x128')

# In[ ]:

# In[ ]:


import numpy as np
from myutil.probability_statistics_util import cal_R2

def calibrate_alpha_beta(t,X):
    '''
    Use Least Square method to applying "point estimate" on alpha and beta,
    where X[k]=alpha*L*t[k]+beta
    '''
    cal_t_count=t.shape[0]
    _,H,W=X.shape
    t=t.reshape(-1,1)
    A=np.concatenate([t,np.ones((cal_t_count,1))],1)
    ls_estimation=np.linalg.inv(A.T@A)@A.T@X.reshape(cal_t_count,-1)
    ls_estimation=ls_estimation.reshape(2,H,W)
    alpha_L=ls_estimation[0]
    beta=ls_estimation[1]
    # calculate the Coefficient_of_determination
    X_hat=alpha_L.reshape(1,H,W)*t.reshape(cal_t_count,1,1)+beta.reshape(1,H,W)
    R2=cal_R2(X,X_hat)
    alpha=alpha_L
    return alpha,beta,R2


import math
import numpy as np

def to_binary(x_i):
    #this can probably also be done with round()
    return 1 if x_i > 0.5 else 0

def hypothesis(theta, x_i):
    z = np.dot(theta, x_i)
    sigmoid = 1.0 / (1.0 + math.exp(-1.0*z))
    return 0.9999999999999 if sigmoid == 1 else sigmoid

def determine_correct_guesses(X, Y, theta, m):
    determined_Y = [np.dot(theta, X[ii]) for ii in range(m)]
    determined_Y_binary = [to_binary(elem) for elem in determined_Y]
    correct = 0
    for ii in range(0,m):
        if determined_Y_binary[ii] == Y[ii]:
            correct+=1
    return correct

def gradient_descent(X, Y, theta, alpha, m, number_of_iterations=1000):
    for iter in range(0,number_of_iterations):
        cost = (-1.0/m)*sum([Y[ii]*math.log(hypothesis(theta, X[ii]))+(1-Y[ii])*math.log(1-hypothesis(theta, X[ii])) for ii in range(m)])
        grad = (-1.0/m)*sum([X[ii]*(Y[ii]-hypothesis(theta, X[ii])) for ii in range(m)])
        theta = theta - alpha * grad
        correct = determine_correct_guesses(X, Y, theta, m)
        print "iteration %s : cost %s : correct_guesses %s / %s" % (iter, cost, correct, len(Y))
    return theta


import scipy.optimize
import numpy as np
import sys

def pay_monthly_on_loan(initial_value, apr, duration_years, monthly_payment):
    principal = {0:initial_value}
    for year in np.arange(1,duration_years):
        principal[year] = principal[year-1] * apr - 12 * monthly_payment
    return principal[year]

def find_monthly_payment(loan_value, apr, duration_years):
    principal_remaining = lambda monthly_payment : pay_monthly_on_loan(loan_value, apr, duration_years, monthly_payment)
    # Use a Brent gradient approach method of solving roots to determine the proper value
    payment_needed = scipy.optimize.brentq(principal_remaining, 0, loan_value * (apr**duration_years))
    return round(payment_needed)

def pay_yearly_on_loan(initial_value, apr, duration_years, yearly_payment):
    principal = {0:initial_value}
    for year in np.arange(1,duration_years):
        principal[year] = principal[year-1] * apr - yearly_payment
    return principal[year]

def find_yearly_payment(loan_value, apr, duration_years):
    principal_remaining = lambda yearly_payment : pay_yearly_on_loan(loan_value, apr, duration_years, yearly_payment)
    # Use a Brent gradient approach method of solving roots to determine the proper value
    payment_needed = scipy.optimize.brentq(principal_remaining, 0, loan_value * (apr**duration_years))
    return round(payment_needed)

if __name__ is '__main__':
	loan_value = 400 * 1000
	apr = 1.05
	monthly_30 = find_monthly_payment(loan_value, apr, 30)
	monthly_15 = find_monthly_payment(loan_value, apr, 15)
	paid_30 = monthly_30 * 12 * 30
	paid_15 = monthly_15 * 12 * 15
	print(f'For {loan_value}@{apr}, the 30-year payment is {monthly_30} while it\'s {monthly_15} for 15, totalling {paid_30} and {paid_15} respectively')


# --------------
#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
#create sample of data
data_sample = data.sample(n = sample_size , random_state = 0)

sample_mean = data_sample['installment'].mean()
population_std = data['installment'].std()
margin_of_error = z_critical * (population_std/math.sqrt(sample_size))
confidence_interval = (sample_mean - margin_of_error,
                       sample_mean + margin_of_error)
true_mean = data['installment'].mean()
print(confidence_interval)
print(true_mean)

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

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

#running loop to iterate over rows
for i in range(len(sample_size)):
    m = []
    #loop to implement number of samples
    for j in range(1000):
        mean = data['installment'].sample(sample_size[i]).mean()
        #mean = j +1
        m.append(mean)
    #convert list to series
    mean_series = pd.Series(m)
    axes[i].hist(mean_series, normed = True)
#plt.show()

#Small busiess interests
data["int.rate"] = data["int.rate"].map(lambda x: str(x)[:-1])
print(data["int.rate"].head())
data["int.rate"] = data["int.rate"].astype(float)/100
z_statistic_1, p_value_1 = ztest(data[data['purpose'] == 'small_business']['int.rate'], value=data['int.rate'].mean(), alternative = 'larger')

print(('z-statistic_1 is :{}'.format(z_statistic_1)))
print(('p_value_1 is :{}'.format(p_value_1)))

#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 is :{}'.format(z_statistic_2)))
print(('p_value_2 is :{}'.format(p_value_2)))

#Chi2 test
critical_value = stats.chi2.ppf(q=0.95, df = 6)
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'])
print(observed)

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

print (critical_value, chi2)

import numpy,import scipy,from scipy,from statsmodels