import numpy as np

def mse(X, theta, Y):
    return ((np.dot(X, theta) - Y) ** 2).mean()/2

class LinearRegression:
    """ Ordinary least squares Linear Regression Using Normal Equation,
    This is the Closed Form Solution for OLS.

    ----------
    Parameters
    ----------
    fit_intercept: bool, optional, default True
    Whether to calculate the intercept for this model.

    --------
    Examples
    --------
    from sklearn.datasets import load_boston
    from sklearn.model_selection import train_test_split
    boston = load_boston()
    X, Y = boston.data, boston.target
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=42)
    reg = LinearRegression()
    reg = reg.fit(X_train, Y_train)
    print(reg.theta)
    reg.predict(X_test)

    """

    def __init__(self, fit_intercept = True):
        self.fit_intercept = fit_intercept
        self.theta = None

    def fit(self, X, Y):
        if self.fit_intercept: # add intercept: add X0 = 1 to each constance
            X = np.c_[np.ones((len(X),1)), X]
        self.theta = np.linalg.inv(X.T.dot(X)).dot(X.T).dot(Y)
        return self

    def predict(self, X):
        if self.fit_intercept: # add intercept: add X0 = 1 to each constance
            X = np.c_[np.ones((len(X),1)), X]
        return np.dot(X, self.theta)

class Ridge(LinearRegression):
    """ Ridge Regression Using Normal Equation,
    This is the Closed Form Solution for Ridge.

    ----------
    Parameters
    ----------
    fit_intercept: bool, optional, default True
    Whether to calculate the intercept for this model.

    --------
    Examples
    --------
    from sklearn.datasets import load_boston
    from sklearn.model_selection import train_test_split
    boston = load_boston()
    X, Y = boston.data, boston.target
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=42)
    reg = Ridge(1)
    reg = reg.fit(X_train, Y_train)
    print(reg.theta)
    reg.predict(X_test)

    """

    def __init__(self, alpha = 1, fit_intercept = True):
        self.alpha = alpha
        self.fit_intercept = fit_intercept
        self.theta = None

    def fit(self, X, Y):
        if self.fit_intercept: # add intercept: add X0 = 1 to each constance
            X = np.c_[np.ones((len(X),1)), X]
        self.theta = np.linalg.inv(X.T.dot(X) + self.alpha * np.eye(X.shape[1])).dot(X.T).dot(Y)
        return self

class BGDRegressor:
    """Linear Regression with Batch Gradient Descent

    ----------
    Parameters
    ----------
    learning_rate: float, default=0.01

    max_iter: int, default=1e3
        maximum iterations

    tol: float, default=1e-3
        Tolerance, the stopping criterion.
        Default:     'cost function'
        Alternative: 'step length'. faster, without cost computation.

    alpha: float, default=0
        Ridge Regression L2 regularization term
        default: no regularization

    fit_intercept: bool, default=True
        Whether the intercept should be estimated or not. If False, the data is assumed to be already centered.

    random_state: int, default=None
        The seed of the pseudo random number generator to use when initializing theta.

    ----------
    Attributes
    ----------
    theta: ndarray of shape (n_features,)
        Weights assigned to the features.

    n_iter_ : int
        The actual number of iterations to reach the stopping criterion.

    cost: float
        MSE cost of traning data

    --------
    Examples
    --------
    from sklearn.datasets import load_boston
    from sklearn.preprocessing import normalize
    from sklearn.model_selection import train_test_split
    boston = load_boston()
    X, Y = boston.data, boston.target
    X = normalize(X)
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=42)

    # Linear Regression with Batch Gradient Descent
    reg = BGDRegressor(learning_rate = 0.5, max_iter = 1e6, tol = 1e-3, fit_intercept = True, random_state=42)

    # Ridge Regression with Batch Gradient Descent
    reg = BGDRegressor(learning_rate = 0.5, max_iter = 1e6, tol = 1e-3, alpha = 1e-3, fit_intercept = True, random_state=42)

    reg = reg.fit(X_train, Y_train)
    print('theta:', reg.theta)
    print('iterations:', reg.n_iter)
    print('cost:', reg.cost)
    reg.predict(X_test)

    """

    def __init__(self, learning_rate = 0.01, max_iter = 1e3, tol = 1e-3, alpha = 0, fit_intercept = True, random_state = None):
        # Parameters
        self.learning_rate = learning_rate
        self.max_iter = max_iter
        self.tol = tol
        self.alpha = alpha
        self.fit_intercept = fit_intercept
        self.random_state = None
        # Attributes
        self.theta = None
        self.n_iter = None
        self.n_samples = None
        self.n_features = None
        self.cost = None

    def _gradient_descent(self, X, Y):
        np.random.seed(self.random_state)
        convergence = False
        cost = [np.inf]*2
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
            theta = np.random.randn(self.n_features + 1) # random initialization for theta values, from Gaussian distribution
        else:
            X_b = X
            theta = np.random.randn(self.n_features)
        for iteration in range(int(self.max_iter)):
            gradient = 2/self.n_samples * np.dot(X_b.T, np.dot(X_b, theta) - Y) + self.alpha * theta
            theta = theta - self.learning_rate * gradient

            # convergence criteria of 'cost function'
            cost.pop(0)
            cost.append(mse(X_b, theta, Y))
            # print(cost[1], end='\r')

            if np.abs(cost[0] - cost[1]) < self.tol:
                convergence = True
                break

            # convergence criteria of 'step length'
            # if np.linalg.norm(gradient) < self.tol:
                # print("Converge!")
                # break

        if convergence:
            print("Achieve Convergence!")
        else:
            print("Reached Max Iteration! Not Achieve Convergence.")

        self.cost = cost[1]
        self.n_iter = iteration
        return theta

    def fit(self, X, Y):
        self.n_samples = X.shape[0]
        self.n_features = X.shape[1]

        self.theta = self._gradient_descent(X, Y)
        return self

    def predict(self, X):
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
        else:
            X_b = X
        return np.dot(X_b, self.theta)

class SGDRegressor:
    """Linear Regression with Stochastic Gradient Descent

    ----------
    Parameters
    ----------
    learning_rate: float or string, default=0.01
        Options:

        float: [default]
            eta = eta0,
            constant learning rate

        'annealing':
            simulated annealing with learning schedule, generally faster and more accurate
            please tuning the hyperpameters t0, t1 for better performance

    max_iter: int, default=1e3
        maximum iterations

    tol: float, default=1e-3
        Convergence Tolerance, using the stopping criterion of cost function.

    alpha: float, default=0
        Ridge Regression L2 regularization term
        default: no regularization

    fit_intercept: bool, default=True
        Whether the intercept should be estimated or not. If False, the data is assumed to be already centered.

    random_state: int, default=None
        The seed of the pseudo random number generator to use when initializing theta and selecting data.

    ----------
    Attributes
    ----------
    theta: ndarray of shape (n_features,)
        Weights assigned to the features.

    n_iter_ : int
        The actual number of iterations to reach the stopping criterion.

    cost: float
        MSE cost of traning data

    --------
    Examples
    --------
    from sklearn.datasets import load_boston
    from sklearn.preprocessing import normalize
    from sklearn.model_selection import train_test_split
    boston = load_boston()
    X, Y = boston.data, boston.target
    X = normalize(X)
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=42)

    # constant learning rate
    reg = SGDRegressor(learning_rate = 1e-3, max_iter = 1e3, tol = 1e-3, fit_intercept = True, random_state=42)

    # simulated annealing with learning schedule: generally faster and more accurate
    reg = SGDRegressor(learning_rate = 'annealing', max_iter = 1e3, tol = 1e-3, t0 = 1e3/2, t1 = 1e3, fit_intercept = True, random_state=42)

    # Ridge Regression: L2 Regularization
    reg = SGDRegressor(learning_rate = 'annealing', max_iter = 1e3, tol = 1e-3, t0 = 1e3/2, t1 = 1e3, alpha = 1e-6, fit_intercept = True, random_state=42)

    reg = reg.fit(X_train, Y_train)
    print('theta:', reg.theta)
    print('iterations:', reg.n_iter)
    print('cost:', reg.cost)
    reg.predict(X_test)

    """

    def __init__(self, learning_rate = 0.01, max_iter = 1e3, tol = 1e-3, t0 = 10, t1 = 1000, alpha = 0, fit_intercept = True, random_state = None):
        # Parameters
        self.learning_rate = learning_rate
        self.max_iter = max_iter
        self.tol = tol
        self.t0 = t0
        self.t1 = t1
        self.alpha = alpha
        self.fit_intercept = fit_intercept
        self.random_state = None
        # Attributes
        self.theta = None
        self.n_iter = None
        self.n_samples = None
        self.n_features = None
        self.cost = None

    # descent methods
    def _learning_rate(self, i):
        return self.theta - self.learning_rate * self.gradient
    def _learning_schedule(self, t):
        return self.t0 / (t + self.t1)
    def _simulated_annealing(self, i):
        return self.theta - self._learning_schedule(self.n_iter * self.n_samples + i) * self.gradient + self.alpha * self.theta

    def _gradient_descent(self, X, Y):
        # select descent method accoding to the hyperparameter 'learning_rate'
        if type(self.learning_rate) == float:
            self._descent = self._learning_rate
        elif self.learning_rate == 'annealing':
            self._descent = self._simulated_annealing
        else:
            print('learning rate error!')

        # initialization
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
            self.theta = np.random.randn(self.n_features + 1) # random initialization for theta values, from Gaussian distribution
        else:
            X_b = X
            self.theta = np.random.randn(self.n_features)
        convergence = False
        np.random.seed(self.random_state)

        cost = [np.inf]*2 # queue recoding N cost values

        for iteration in range(int(self.max_iter)):
            self.n_iter = iteration
            for i in range(self.n_samples):
                random_index = np.random.randint(self.n_samples)
                Xi = X_b[random_index]
                Yi = Y[random_index]
                self.gradient = 2 / 1 * np.dot(Xi.T, np.dot(Xi, self.theta) - Yi)
                self.theta = self._descent(i)

            # convergence criteria of 'cost function'
            cost.pop(0)
            cost.append(mse(X_b, self.theta, Y)) # calculate cost each iteration
            print(cost[1], end="\r")
            if np.abs(cost[0] - cost[1]) < self.tol:
                convergence = True
                break
            self.cost = cost[1]

        if convergence:
            print("Achieve Convergence!")
        else:
            print("Reached Max Iteration! Not Achieve Convergence.")

    def fit(self, X, Y):
        self.n_samples = X.shape[0]
        self.n_features = X.shape[1]
        self._gradient_descent(X, Y)
        return self

    def predict(self, X):
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
        else:
            X_b = X
        return np.dot(X_b, self.theta)

class MBGDRegressor:
    """Linear Regression with Mini-Batch Gradient Descent

    ----------
    Parameters
    ----------
    learning_rate: float or string, default=0.01
        Options:

        float: [default]
            eta = eta0,
            constant learning rate

        'annealing':
            simulated annealing with learning schedule, generally faster and more accurate
            please tuning the hyperpameters t0, t1 for better performance

    max_iter: int, default=1e3
        maximum iterations

    tol: float, default=1e-3
        Convergence Tolerance, using the stopping criterion of cost function.

    batch_size: int, default=2**5
        the number of instances in each batch
        recommended values: 2**5 ~ 2**10

    alpha: float, default=0
        Ridge Regression L2 regularization term
        default: no regularization

    fit_intercept: bool, default=True
        Whether the intercept should be estimated or not. If False, the data is assumed to be already centered.

    random_state: int, default=None
        The seed of the pseudo random number generator to use when initializing theta and selecting data.

    ----------
    Attributes
    ----------
    theta: ndarray of shape (n_features,)
        Weights assigned to the features.

    n_iter_ : int
        The actual number of iterations to reach the stopping criterion.

    cost: float
        MSE cost of traning data

    --------
    Examples
    --------
    from sklearn.datasets import load_boston
    from sklearn.preprocessing import normalize
    from sklearn.model_selection import train_test_split
    boston = load_boston()
    X, Y = boston.data, boston.target
    X = normalize(X)
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=42)

    # constant learning rate
    reg = MBGDRegressor(learning_rate = 1e-3, max_iter = 1e3, tol = 1e-3, batch_size = 2**5, alpha = 0, fit_intercept = True, random_state=42)

    # simulated annealing with learning schedule: generally faster and more accurate
    reg = MBGDRegressor(learning_rate = 'annealing', max_iter = 1e3, tol = 1e-3, batch_size = 2**5, t0 = 1e3/2, t1 = 1e3, alpha = 0, fit_intercept = True, random_state=42)

    # Ridge Regression: L2 Regularization
    reg = MBGDRegressor(learning_rate = 'annealing', max_iter = 1e3, tol = 1e-3, batch_size = 2**5, t0 = 1e3/2, t1 = 1e3, alpha = 1, fit_intercept = True, random_state=42)

    reg = reg.fit(X_train, Y_train)
    print('theta:', reg.theta)
    print('iterations:', reg.n_iter)
    print('cost:', reg.cost)
    reg.predict(X_test)

    """

    def __init__(self, learning_rate = 0.01, max_iter = 1e3, tol = 1e-3, batch_size = 2**5, t0 = 10, t1 = 1000, alpha = 0, fit_intercept = True, random_state = None):
        # Parameters
        self.learning_rate = learning_rate
        self.max_iter = max_iter
        self.tol = tol
        self.batch_size = batch_size
        self.t0 = t0
        self.t1 = t1
        self.alpha = alpha
        self.fit_intercept = fit_intercept
        self.random_state = None
        # Attributes
        self.theta = None
        self.n_iter = None
        self.n_samples = None
        self.n_features = None
        self.cost = None

    # descent methods
    def _learning_rate(self, i):
        return self.theta - self.learning_rate * self.gradient
    def _learning_schedule(self, t):
        return self.t0 / (t + self.t1)
    def _simulated_annealing(self, i):
        return self.theta - self._learning_schedule(self.n_iter * self.n_samples + i) * self.gradient

    def _gradient_descent(self, X, Y):
        # select descent method accoding to the hyperparameter 'learning_rate'
        if type(self.learning_rate) == float:
            self._descent = self._learning_rate
        elif self.learning_rate == 'annealing':
            self._descent = self._simulated_annealing
        else:
            print('learning rate error!')

        # initialization
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
            self.theta = np.random.randn(self.n_features + 1) # random initialization for theta values, from Gaussian distribution
        else:
            X_b = X
            self.theta = np.random.randn(self.n_features)
        convergence = False
        np.random.seed(self.random_state)
        cost = [np.inf]*2 # queue recoding N cost values

        for iteration in range(int(self.max_iter)):
            self.n_iter = iteration
            for i in range(0, self.n_samples, self.batch_size):
                random_indexes = np.random.randint(0, self.n_samples, self.batch_size)
                Xi = X_b[random_indexes]
                Yi = Y[random_indexes]
                self.gradient = 2 / self.batch_size * np.dot(Xi.T, np.dot(Xi, self.theta) - Yi) + self.alpha * self.theta
                self.theta = self._descent(i)

            # convergence criteria of 'cost function'
            cost.pop(0)
            cost.append(mse(X_b, self.theta, Y)) # calculate cost each iteration
            print(cost[1], end="\r")
            if np.abs(cost[0] - cost[1]) < self.tol:
                convergence = True
                break
            self.cost = cost[1]

        if convergence:
            print("Achieve Convergence!")
        else:
            print("Reached Max Iteration! Not Achieve Convergence.")

    def fit(self, X, Y):
        self.n_samples = X.shape[0]
        self.n_features = X.shape[1]
        self._gradient_descent(X, Y)
        return self

    def predict(self, X):
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
        else:
            X_b = X
        return np.dot(X_b, self.theta)

def soft_threshold(rho, alpha):
    if rho < - alpha:
        return rho + alpha
    if rho > alpha:
        return rho - alpha
    else:
        return 0

class Lasso(CDRegressor):
class CDRegressor:
    """Lasso Regression with Coordinate Descent

    ----------
    Parameters
    ----------

    max_iter: int, default=1e3
        maximum iterations

    tol: float, default=1e-3
        Convergence Tolerance, using the stopping criterion of cost function.

    alpha: float, default=0.01
        Lasso Regression L1 regularization term

    fit_intercept: bool, default=True
        Whether the intercept should be estimated or not. If False, the data is assumed to be already centered.

    random_state: int, default=None
        The seed of the pseudo random number generator to use when initializing theta.

    ----------
    Attributes
    ----------

    theta: ndarray of shape (n_features,)
        Weights assigned to the features.

    n_iter_ : int
        The actual number of iterations to reach the stopping criterion.

    cost: float
        MSE cost of traning data

    coef_: array
        parameter vector

    intercept_: float
        independent term in decision function

    --------
    Examples
    --------
    from sklearn.datasets import load_boston
    from sklearn.preprocessing import normalize
    from sklearn.model_selection import train_test_split
    boston = load_boston()
    X, Y = boston.data, boston.target
    X = normalize(X)
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=42)

    # Lasso Regression with Coordinate Descent
    reg = CDRegressor(max_iter = 1e3, tol = 1e-3, alpha = 0.01, fit_intercept = True, random_state=42)

    reg = reg.fit(X_train, Y_train)
    print('theta:', reg.theta)
    print('iterations:', reg.n_iter)
    print('cost:', reg.cost)
    reg.predict(X_test)

    """

    def __init__(self, max_iter = 1e3, tol = 1e-3, alpha = 0.01, fit_intercept = True, random_state = None):
        # Parameters
        self.max_iter = max_iter
        self.tol = tol
        self.alpha = alpha
        self.fit_intercept = fit_intercept
        self.random_state = None
        # Attributes
        self.theta = None
        self.n_iter = None
        self.n_samples = None
        self.n_features = None
        self.cost = None
        self.coef_ = None
        self.intercept_ = None

    def _coordinate_descent(self, X, Y):
        np.random.seed(self.random_state)
        convergence = False
        cost = [np.inf]*2

        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
            theta = np.random.randn(self.n_features + 1) # random initialization for theta values, from Gaussian distribution
            theta[0] = 0
        else:
            X_b = X
            theta = np.random.randn(self.n_features)

        for iteration in range(int(self.max_iter)):
            for i in range(self.n_features+1):
                if self.fit_intercept and i == 0: # Lasso does not regularize the intercept
                    theta[0] = np.sum(Y - np.dot(X_b[:, 1:], theta[1:]))/self.n_samples
                else:
                    Xi = X_b[:, i]
                    theta[i] = 0
                    rho = np.dot(Xi.T, Y - np.dot(X_b, theta))
                    theta[i] = soft_threshold(rho, self.alpha*self.n_samples)/np.square(X_b[:, i]).sum()

            # convergence criteria of 'cost function'
            cost.pop(0)
            cost.append(mse(X_b, theta, Y))
            print(cost[1], end="\r")
            # print(theta)
            if np.abs(cost[0] - cost[1]) < self.tol:
                convergence = True
                break

        if convergence:
            print("Achieve Convergence!")
        else:
            print("Reached Max Iteration! Not Achieve Convergence.")

        self.cost = cost[1]
        self.n_iter = iteration
        return theta

    def fit(self, X, Y):
        self.n_samples = X.shape[0]
        self.n_features = X.shape[1]
        self.theta = self._coordinate_descent(X, Y)
        return self

    def predict(self, X):
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
        else:
            X_b = X
        return np.dot(X_b, self.theta)


import numpy as np
from scipy import signal

# 1 sample delay: y[n] = x[n] + A * y[n-1]
b = [1]
a = [1,0]
a1 = [1,0.25]
a2 = [1,0.5]
a3 = [1,0.75]
a4 = [1,1]

# Sampling frequency = 2
w, h = signal.freqz(b, a, fs=2)
w1, h1 = signal.freqz(b, a1, fs=2)
w2, h2 = signal.freqz(b, a2, fs=2)
w3, h3 = signal.freqz(b, a3, fs=2)
w4, h4 = signal.freqz(b, a4, fs=2)

import matplotlib.pyplot as plt

fig, ax1 = plt.subplots(2)
ax1[0].set_title('Digital filter frequency response')
ax1[0].plot(w, 20 * np.log10(abs(h)), 'black', label = r"$\alpha$ = 0")
ax1[0].plot(w, 20 * np.log10(abs(h1)), 'g', label = r"$\alpha$ = 0.25")
ax1[0].plot(w, 20 * np.log10(abs(h2)), 'orange', label = r"$\alpha$ = 0.5")
ax1[0].plot(w, 20 * np.log10(abs(h3)), 'blue', label = r"$\alpha$ = 0.75")
ax1[0].plot(w, 20 * np.log10(abs(h4)), 'red', label = r"$\alpha$ = 1")
ax1[0].set_ylabel('Magnitude [dB]', color='black')
ax1[0].legend(prop = {'size': 8})

# Wrap angles to be from -pi to pi
# If angle is greater than pi it wraps around to
# the negative side
angles = np.unwrap(np.angle(h))
angles1 = np.unwrap(np.angle(h1))
angles2 = np.unwrap(np.angle(h2))
angles3 = np.unwrap(np.angle(h3))
angles4 = np.unwrap(np.angle(h4))

# Convert from radians to degrees:
angles = ( angles * 180 ) / np.pi
angles1 = ( angles1 * 180 ) / np.pi
angles2 = ( angles2 * 180 ) / np.pi
angles3 = ( angles3 * 180 ) / np.pi
angles4 = ( angles4 * 180 ) / np.pi

ax1[1].plot(w, angles, 'black', label = r"$\alpha$ = 0")
ax1[1].plot(w, angles1, 'g', label = r"$\alpha$ = 0.25")
ax1[1].plot(w, angles2, 'orange', label = r"$\alpha$ = 0.5")
ax1[1].plot(w, angles3, 'blue', label = r"$\alpha$ = 0.75")
ax1[1].plot(w, angles4, 'red', label = r"$\alpha$ = 1")

ax1[1].set_ylabel('Phase (degrees)', color='black')
ax1[1].set_xlabel('Normalized Frequency [$\pi$*rad/sample]')
ax1[1].grid()
ax1[1].axis('tight')
#ax1[1].legend(prop = {'size': 6})

plt.show()


#zadanie 4 Paweł Gałka

import numpy
from scipy.optimize import linprog

#f(x) = 0*x1 + 3*x2 + 1*x3 + 1*x4 + 4*x5 + 2*x6 + 0*x7 + 3*x8 + 4*x9 -> min
#2*x1 + 1*x2 + 1*x3 + 0*x4 + 0*x5 + 0*x6 + 0*x7 + 2*x8 + 1*x9= 12000
#1*x1 + 2*x2 + 1*x3 + 3*x4 + 2*x5 + 1*x6 + 0*x7 + 0*x8 + 0*x9= 24000
#0*x1 + 0*x2 + 2*x3 + 1*x4 + 2*x5 + 4*x6 + 6*x7 + 1*x8 + 3*x9= 27000

A = numpy.array([[2, 1, 1, 0, 0, 0, 0, 2, 1],
[-2, -1, -1, 0, 0, 0, 0, 0, 0],
[1, 2, 1, 3, 2, 1, 0, 1, 3],
[-1, -2, -1, -3, -2, -1, 0, -2, -1],
[0, 0, 2, 1, 2, 4, 6, 0, 0],
[ 0, 0, -2, -1, -2, -4, -6, -1, 3]])

b = [12000, -12000, 24000, -24000, 27000, -27000]

c = numpy.array([ 0, -3, -1, -1, -4, -2, 0, -3, -4])

A_dual = numpy.transpose(A)

b_dual = numpy.transpose(c)

c_dual = numpy.transpose(b)

result = linprog(c_dual,-A_dual,-b_dual)
x = list(result.x)

#sprawdz ograniczenia problemu dualnego

res_x = {}
for i in range(len(A_dual)):
    RHS = round(b_dual[i],3)
    LHS = round(sum([A_dual[i][j] * x[j] for j in range(len(x))]),3)
    if (LHS > RHS): #zerowanie tej zmiennej
        res_x[i] = 0

A_indexes = list(filter(lambda x : x not in res_x.keys(), numpy.arange(0,len(A[0]))))
A_primal = []
for i in range(len(A)):
    A_primal.append([A[i][j] for j in A_indexes])

primal_solution = numpy.linalg.lstsq(A_primal, b,rcond=1)[0]

print('f_min',abs(result.fun),'m')

print('[11cm, 8cm, 5cm]')
for i in range(len(primal_solution)):
    print(A[::2,A_indexes[i]], round(primal_solution[i],2),'m')


# -*- coding: utf-8 -*-
import numpy as np

def hypothesis(x, theta):
    return np.dot(x, theta)

def loss(x, y, theta):
    return hypothesis(x, theta) - y

def grad(x_t, diffs, m):
    return np.dot(x_t, diffs) / m

def J(theta, x, y, m):
    diffs = loss(x, y, theta)
    return np.dot(diffs.T, diffs) / m

def bgd(x, y, alpha=0.01, epsilon=1e-8):
    m, sse2 = len(x), 0
    x_0 = np.ones(m)
    xx = np.column_stack((x_0, x))
    m, n = np.shape(xx)
    x_t, theta = xx.T, np.ones(n)
    while True:
        diffs = loss(xx, y, theta)

        sse = J(theta, xx, y, m)
        if abs(sse - sse2) < epsilon:
            break
        else:
            sse2 = sse

        gradient = grad(x_t, diffs, m)
        theta -= alpha * gradient
    return theta

if __name__ == '__main__':
    import matplotlib.pyplot as plt

    year = [2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013]
    price = [2.000, 2.500, 2.900, 3.147, 4.515, 4.903, 5.365, 5.704, 6.853, 7.971, 8.561, 10.000, 11.280, 12.900]

    b, a = bgd(np.subtract(year, 2000), price, 0.01, 1e-8)

    x_axis = np.linspace(1999, 2015, 10000)

    for i in range(len(year)):
        plt.scatter(year[i], price[i])

    def linear_equation(x, a, b):
        return b + a * (x - 2000)

    print('y = {0} * (x - 2000) + {1}'.format(a, b))
    plt.plot(x_axis, [linear_equation(x, a, b) for x in x_axis], 'r:')
    plt.grid(True)
    plt.show()


'''European Pricing Class'''
import numpy as np
import scipy.stats as stats

import pricing
from .pricing import Pricing
from utils import ui

_logger = ui.get_logger()

class BlackScholes(Pricing):
    '''
    This class uses the classic Black-Scholes method to calculate prices for European Call and Put options

    '''

    def __init__(self, ticker, expiry, strike, dividend=0.0):
        super().__init__(ticker, expiry, strike, dividend=dividend)

        self.name = pricing.PRICING_METHODS[0]

    def calculate_price(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> tuple[float, float]:
        ''' Calculate Call and Put option prices based on the below equations from Black-Scholes.
        If dividend is not zero, then it is subtracted from the risk free rate in the below calculations.

            CallOptionPrice = SpotPrice*N(d1) − Strike*exp(−r(T−t))*N(d2))
            PutOptionPrice  = Strike*exp(−r(T−t)) *N(−d2) − SpotPrice*N(−d1)
        :return: <float>, <float> Calculated price of Call & Put options
        '''

        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d1 = self._calculate_d1(spot_price, time_to_maturity, volatility)
        d2 = self._calculate_d2(spot_price, time_to_maturity, volatility)

        self.price_call = ((spot_price * np.exp(-1 * self.dividend * time_to_maturity)) * stats.norm.cdf(d1, 0.0, 1.0) -
                           (self.strike_price * np.exp(-1 * self.risk_free_rate * time_to_maturity) * stats.norm.cdf(d2, 0.0, 1.0)))

        self.price_put = (self.strike_price * np.exp(-1 * self.risk_free_rate * time_to_maturity) * stats.norm.cdf(-1 * d2, 0.0, 1.0) -
                          (spot_price * np.exp(-1 * self.dividend * time_to_maturity)) * stats.norm.cdf(-1 * d1, 0.0, 1.0))

        return self.price_call, self.price_put

    def calculate_delta(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> tuple[float, float]:
        ''' Calculate Call and Put option delta based on the below equations from Black-Scholes.
        If dividend is not zero, then it is subtracted from the risk free rate in the below calculations.

        delta call =  np.exp(-q * T) * si.norm.cdf(d1, 0.0, 1.0)
        delta put  = -np.exp(-q * T) * si.norm.cdf(-d1, 0.0, 1.0)

        :return: <float>, <float> Calculated delta of Call & Put options
        '''

        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d1 = self._calculate_d1(spot_price, time_to_maturity, volatility)

        self.delta_call = np.exp(-self.dividend * time_to_maturity) * stats.norm.cdf(d1, 0.0, 1.0)
        self.delta_put = -np.exp(-self.dividend * time_to_maturity) * stats.norm.cdf(-d1, 0.0, 1.0)

        return self.delta_call, self.delta_put

    def calculate_gamma(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> tuple[float, float]:
        ''' Calculate Call and Put option gamma based on the below equations from Black-Scholes.
        If dividend is not zero, then it is subtracted from the risk free rate in the below calculations.

        gamma call & put = np.exp(-q * T) * si.norm.cdf(d1, 0.0, 1.0) / S * sigma * np.sqrt(T)

        :return: <float>, <float> Calculated delta of Call & Put options
        '''

        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d1 = self._calculate_d1(spot_price, time_to_maturity, volatility)
        gamma = np.exp(-self.dividend * time_to_maturity) * stats.norm.pdf(d1, 0.0, 1.0) / spot_price * volatility * np.sqrt(time_to_maturity)

        self.gamma_call = self.gamma_put = gamma * 365.0

        return self.gamma_call, self.gamma_put

    def calculate_theta(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> tuple[float, float]:
        ''' Calculate Call and Put option theta based on the below equations from Black-Scholes.
        If dividend is not zero, then it is subtracted from the risk free rate in the below calculations.

        theta call = -np.exp(-q * T) *
                     (S * si.norm.cdf(d1, 0.0, 1.0) * sigma) /
                     (2 * np.sqrt(T)) - r * K * np.exp(-r * T) *
                     si.norm.cdf(d2, 0.0, 1.0) + q * S *
                     np.exp(-q * T) * si.norm.cdf(d1, 0.0, 1.0)

        theta put = -np.exp(-q * T) *
                     (S * si.norm.cdf(d1, 0.0, 1.0) * sigma) /
                     (2 * np.sqrt(T)) + r * K * np.exp(-r * T) *
                     si.norm.cdf(-d2, 0.0, 1.0) - q * S *
                     np.exp(-q * T) * si.norm.cdf(-d1, 0.0, 1.0)

        :return: <float>, <float> Calculated delta of Call & Put options
        '''

        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d1 = self._calculate_d1(spot_price, time_to_maturity, volatility)
        d2 = self._calculate_d2(spot_price, time_to_maturity, volatility)

        theta_call = (-np.exp(-self.dividend * time_to_maturity) * spot_price * stats.norm.cdf(d1, 0.0, 1.0) * volatility / (2 * np.sqrt(time_to_maturity))) - \
                     (self.risk_free_rate * self.strike_price * np.exp(-self.risk_free_rate * time_to_maturity) * stats.norm.cdf(d2, 0.0, 1.0)) + \
                     (self.dividend * spot_price * np.exp(-self.dividend * time_to_maturity) * stats.norm.cdf(d1, 0.0, 1.0))

        theta_put = (-np.exp(-self.dividend * time_to_maturity) * spot_price * stats.norm.cdf(d1, 0.0, 1.0) * volatility / (2 * np.sqrt(time_to_maturity))) + \
            (self.risk_free_rate * self.strike_price * np.exp(-self.risk_free_rate * time_to_maturity) * stats.norm.cdf(-d2, 0.0, 1.0)) - \
            (self.dividend * spot_price * np.exp(-self.dividend * time_to_maturity) * stats.norm.cdf(-d1, 0.0, 1.0))

        self.theta_call = theta_call / 365.0
        self.theta_put = theta_put / 365.0

        return self.theta_call, self.theta_put

    def calculate_vega(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> tuple[float, float]:
        ''' Calculate Call and Put option vega based on the below equations from Black-Scholes.
        If dividend is not zero, then it is subtracted from the risk free rate in the below calculations.

        vega call & put = 1 / np.sqrt(2 * np.pi) * S * np.exp(-q * T) * np.exp(-d1 ** 2 * 0.5) * np.sqrt(T)

        :return: <float>, <float> Calculated delta of Call & Put options
        '''
        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d1 = self._calculate_d1(spot_price, time_to_maturity, volatility)

        vega = 1 / np.sqrt(2 * np.pi) * spot_price * np.exp(-self.dividend * time_to_maturity) * np.exp(-d1 ** 2 * 0.5) * np.sqrt(time_to_maturity)

        self.vega_call = self.vega_put = vega / 100.0

        return self.vega_call, self.vega_put

    def calculate_rho(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> tuple[float, float]:
        ''' Calculate Call and Put option rho based on the below equations from Black-Scholes.
        If dividend is not zero, then it is subtracted from the risk free rate in the below calculations.

        vega call & put = 1 / np.sqrt(2 * np.pi) * S * np.exp(-q * T) * np.exp(-d1 ** 2 * 0.5) * np.sqrt(T)

        :return: <float>, <float> Calculated delta of Call & Put options
        '''
        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d2 = self._calculate_d2(spot_price, time_to_maturity, volatility)

        rho_call = self.strike_price * time_to_maturity * np.exp(-self.risk_free_rate * time_to_maturity) * stats.norm.cdf(d2, 0.0, 1.0)
        rho_put = -self.strike_price * time_to_maturity * np.exp(-self.risk_free_rate * time_to_maturity) * stats.norm.cdf(-d2, 0.0, 1.0)

        self.rho_call = rho_call / 100.0
        self.rho_put = rho_put / 100.0

        return self.rho_call, self.rho_put

    def _calculate_d1(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> float:
        ''' Famous d1 variable from Black-Scholes model calculated as shown in:
                https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model

            d1 = (np.log(S / K) + (r - q + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
        :return: <float>
        '''

        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d1 = (np.log(spot_price / self.strike_price) +
              (self.risk_free_rate - self.dividend + 0.5 * volatility ** 2) * time_to_maturity) / (volatility * np.sqrt(time_to_maturity))

        return d1

    def _calculate_d2(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> float:
        ''' Famous d2 variable from Black-Scholes model calculated as shown in:
                https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model

            d2 = (np.log(S / K) + (r - q - 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
        :return: <float>
        '''

        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d2 = (np.log(spot_price / self.strike_price) +
              (self.risk_free_rate - self.dividend - 0.5 * volatility ** 2) * time_to_maturity) / (volatility * np.sqrt(time_to_maturity))

        return d2


"""
Matrix LU Solver Methods
"""
import numpy as np

def Lsolve(L, b, tol=1.0e-12):
    """
    Lower triangle matrix solution
    """
    n = len(b)
    for k in range(n):
        if abs(L[k, k]) < tol:
            raise RuntimeError('Matrix L was singular.')

    # solve [L]{x} = {b} for {x} via forward substitution
    x = np.copy(b)
    x[0] = x[0] / L[0, 0]
    for k in range(1, n):
        x[k] = (x[k] - np.dot(L[k, 0:k], x[0:k])) / L[k, k]

    return x

def Usolve(U, b, tol=1.0e-12):
    """
    Upper triangle matrix solution
    """
    n = len(b)
    for k in range(n):
        if abs(U[k, k]) < tol:
            raise RuntimeError('Matrix U was singular.')

    # solve [U]{x} = {b} for {x} via backward substitution
    x = np.copy(b)
    x[n-1] = x[n-1] / U[n-1, n-1]
    for k in range(n-2, -1, -1):
        x[k] = (x[k] - np.dot(U[k, k+1:n], x[k+1:n])) / U[k, k]

    return x

def LUSolve(L, U, b, tol=1.0e-12):
    """
    Implements both Lsolve and Usolve to solve system of equations
    """
    # solve [L]{y} = {b} for {y}
    y = Lsolve(L, b, tol)
    # solve [U]{x} = {y} for {x}
    x = Usolve(U, y, tol)

    return x


# Reference Book: Python Data Science Handbook (page:(59-))
# Date(13 April, 2019) Day-3

# We’ll start with the standard NumPy import, under the alias np :
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn
seaborn.set()  # set plot style

# Often when you are faced with a large amount of data, a first step is to compute sum‐
# mary statistics for the data in question. Perhaps the most common summary statistics
# are the mean and standard deviation, which allow you to summarize the “typical” val‐
# ues in a dataset, but other aggregates are useful as well(the sum, product, median,
# minimum and maximum, quantiles, etc.).

npArray = np.random.random(100)
print('\nSumming the Values in an Array:')
print(round(sum(npArray), 2))  # 52.2
print(np.sum(npArray))  # 52.20359645164018

# big_array = np.random.rand(1000000)
# %timeit sum(big_array) # 10 loops, best of 3: 104 ms per loop
# %timeit np.sum(big_array) # 1000 loops, best of 3: 442 μs per loop

print('\nMinimum and Maximum:')
big_array = np.random.rand(1000000)
print(min(big_array))  # 2.1833396079973255e-07
print(max(big_array))  # 0.9999951829160936

# same function in numpy. numpy version of  the operation is more quickly
print(np.min(big_array))  # 2.1833396079973255e-07
print(np.max(big_array))  # 0.9999951829160936

# For min , max , sum , and several other NumPy aggregates, a shorter syntax is to use
# methods of the array object itself:
print(big_array.min(), big_array.max(), big_array.sum())
# 2.2839487603398823e-06 0.9999994729271785 499748.6111329345

print('\nMultidimensional aggregates:')
matrix_a = np.random.random((3, 4))
print(matrix_a)
'''
[[0.21434649 0.41680862 0.23136847 0.52651149]
 [0.95156885 0.79710065 0.39806083 0.23508013]
 [0.71166791 0.87808004 0.8898395  0.28141989]]
'''
print(matrix_a.sum())  # 4.589765666870639

# Aggregation functions take an additional argument specifying the axis along which
# the aggregate is computed. For example, we can find the minimum value within each
# column by specifying axis = 0:
print(matrix_a.min(axis=0))  # [0.17388545 0.48946081 0.01005003 0.28836908]
print(matrix_a.min(axis=1))  # [0.42009431 0.09302049 0.08362998]

print(matrix_a.max(axis=0))  # [0.74965632 0.93045538 0.84355002 0.81612704]
print(matrix_a.max(axis=1))  # [0.93045538 0.84355002 0.74965632]

'''
Table 2-3. Aggregation functions available in NumPy
Function Name  Nan-safe Version   Description
np.sum         np.nansum          Compute sum of elements
np.prod        np.nanprod         Compute product of elements
np.mean        np.nanmean         Compute median of elements
np.std         np.nanstd          Compute standard deviation
np.var         np.nanvar          Compute variance
np.min         np.nanmin          Find minimum value
np.max         np.nanmax          Find maximum value
np.argmin      np.nanargmin       Find index of minimum value
np.argmax      np.nanargmax       Find index of maximum value
np.median      np.nanmedian       Compute median of elements
np.percentile  np.nanpercentile   Compute rank-based statistics of elements
np.any         N/A                Evaluate whether any elements are true
np.all         N/A Evaluate       whether all elements are true
'''

print('\nExample: What Is the Average Height of US Presidents?:')
data = pd.read_csv(
    '/media/nahid/New Volume/GitHub/Numpy/president_heights.csv')
heights = np.array(data['height(cm)'])
print(pd.DataFrame(data))
'''
    order                    name  height(cm)
0       1       George Washington         189
1       2              John Adams         170
2       3        Thomas Jefferson         189
3       4           James Madison         163
4       5            James Monroe         183
5       6       John Quincy Adams         171
6       7          Andrew Jackson         185
7       8        Martin Van Buren         168
8       9  William Henry Harrison         173
9      10              John Tyler         183
10     11           James K. Polk         173
11     12          Zachary Taylor         173
12     13        Millard Fillmore         175
13     14         Franklin Pierce         178
14     15          James Buchanan         183
15     16         Abraham Lincoln         193
16     17          Andrew Johnson         178
17     18        Ulysses S. Grant         173
18     19     Rutherford B. Hayes         174
19     20       James A. Garfield         183
20     21       Chester A. Arthur         183
21     23       Benjamin Harrison         168
22     25        William McKinley         170
23     26      Theodore Roosevelt         178
24     27     William Howard Taft         182
25     28          Woodrow Wilson         180
26     29       Warren G. Harding         183
27     30         Calvin Coolidge         178
28     31          Herbert Hoover         182
29     32   Franklin D. Roosevelt         188
30     33         Harry S. Truman         175
31     34    Dwight D. Eisenhower         179
32     35         John F. Kennedy         183
33     36       Lyndon B. Johnson         193
34     37           Richard Nixon         182
35     38             Gerald Ford         183
36     39            Jimmy Carter         177
37     40           Ronald Reagan         185
38     41       George H. W. Bush         188
39     42            Bill Clinton         188
40     43          George W. Bush         182
41     44            Barack Obama         185
'''
print(heights)
'''
[189 170 189 163 183 171 185 168 173 183 173 173 175 178 183 193 178 173
 174 183 183 168 170 178 182 180 183 178 182 188 175 179 183 193 182 183
 177 185 188 188 182 185]
'''
print('Mean Heigt:         ', heights.mean())
print('Standerd Deviation: ', heights.std())
print('Maximum Height:     ', heights.min())
print('Minimum Height:     ', heights.max())
'''
Mean Heigt:          179.73809523809524
Standerd Deviation:  6.931843442745892
Maximum Height:      163
Minimum Height:      193
'''
print("25th Percentile:   ", np.percentile(heights, 25))
print("Median:            ", np.median(heights))
print("75th Percentile:   ", np.percentile(heights, 75))
'''
25th Percentile:    174.25
Median:             182.0
75th Percentile:    183.0
'''
plt.hist(heights)
plt.title('Height Distribution of US Presidents')
plt.xlabel('Height(cm)')
plt.ylabel('Number')

print(plt.show())

'''
These aggregates are some of the fundamental pieces of exploratory data analysis that
we’ll explore in more depth in later chapters of the book.
'''

# https://matplotlib.org/users/pyplot_tutorial.html
# https://github.com/jakevdp/PythonDataScienceHandbook/blob/master/notebooks/data/president_heights.csv


import numpy as np
import itertools

np.random.seed(0)

def main():
    # 4.14.1
    print('4.14.1')
    # Generator matrix for Hamming code
    G = np.array([
        [1, 0, 1, 1],
        [1, 1, 0, 1],
        [0, 0, 0, 1],
        [1, 1, 1, 0],
        [0, 0, 1, 0],
        [0, 1, 0, 0],
        [1, 0, 0, 0]
    ])

    # 4.14.2
    print('4.14.2')
    print(np.dot(G, np.array([1, 0, 0, 1])) % 2)

    # 4.14.3: find R s.t. np.dot(R, G) % 2 == np.eye(4)
    print('4.14.3')
    R = np.array([
        [0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 1, 0],
        [0, 0, 0, 0, 1, 0, 0],
        [0, 0, 1, 0, 0, 0, 0]
    ])
    for vec in itertools.product([0, 1], repeat=4):
        # Check whether decoding works well.
        message = np.array(vec)
        code = np.dot(G, message) % 2
        decoded_message = np.dot(R, code) % 2
        print(message, code, decoded_message)

    # 4.14.4
    print('4.14.4')
    H = np.array([
        [0, 0, 0, 1, 1, 1, 1],
        [0, 1, 1, 0, 0, 1, 1],
        [1, 0, 1, 0, 1, 0, 1]
    ])
    print(np.dot(H, G) % 2)  # == np.zeros((3, 4))

    # 4.14.5
    print('4.14.5')
    def find_error(syndrome):
        n_column = H.shape[1]
        for column in range(n_column):
            if np.all(H[:, column] == syndrome):
                error = np.zeros(n_column, dtype=np.int)
                error[column] = 1
                return error
        return np.zeros(n_column, dtype=np.int)

    # 4.14.6
    print('4.14.6')
    tilde_c = np.array([1, 0, 1, 1, 0, 1, 1])
    syndrome = np.dot(H, tilde_c) % 2
    error = find_error(syndrome)
    error_fixed_tilde_c = (tilde_c + error) % 2
    print(tilde_c, error_fixed_tilde_c)

    # 4.14.7
    print('4.14.7')
    def find_error_matrix(S):
        return np.apply_along_axis(find_error, 0, S)
    S = np.array([
        [0, 0, 1, 1],
        [0, 1, 0, 1],
        [1, 0, 0, 1],
    ])
    print(find_error_matrix(S))

    # 4.14.8
    print('4.14.8')
    def str2bits(input_str):
        bits = [1 << i for i in range(8)]  # 1 byte = 8 bits
        return [1 if ord(char) & b > 0 else 0 for char in input_str for b in bits]
    def bits2str(input_bits):
        bits = [1 << i for i in range(8)]
        return ''.join(chr(
            sum(b * ib for b, ib in zip(bits, input_bits[i:i+8]))
        ) for i in range(0, len(input_bits), 8))
    s = ''.join([chr(i) for i in range(256)])
    ss = bits2str(str2bits(s))
    print(s)
    print(ss)

    # 4.14.9
    print('4.14.9')
    def bits2mat(bits, n_rows=4, trans=False):
        n_cols = len(bits) // n_rows
        mat = np.zeros((n_rows, n_cols), dtype=np.int)
        for i in range(n_rows):
            for j in range(n_cols):
                if bits[n_rows * j + i] > 0:
                    mat[i, j] = 1
        if trans:
            mat = mat.T
        return mat
    def mat2bits(mat, trans=False):
        n_rows, n_cols = mat.shape
        if trans:
            return [mat[i, j] for i in range(n_rows) for j in range(n_cols)]
        else:
            return [mat[i, j] for j in range(n_cols) for i in range(n_rows)]
    s = ''.join([chr(i) for i in range(256)])
    ss = bits2str(mat2bits(bits2mat(str2bits(s))))
    print(s)
    print(ss)

    # 4.14.10
    print('4.14.10')
    message = 'I’m trying to free your mind, Neo. But I can only show you the door. You’re the one that has to walk through it.'
    P = bits2mat(str2bits(message))

    # 4.14.11
    print('4.14.11')
    error_rate = 0.02
    E = np.random.choice([0, 1], P.shape, [1 - error_rate, error_rate])
    P_TILDE = (P + E) % 2
    decoded_message = bits2str(mat2bits(P_TILDE))
    print(decoded_message)  # non-readable message

    # 4.14.12
    print('4.14.12')
    C = np.dot(G, P) % 2

    # 4.14.13
    print('4.14.13')
    error_rate = 0.02
    C_TILDE = (C + np.random.choice([0, 1], C.shape, p=[1 - error_rate, error_rate])) % 2
    decoded_message = bits2str(mat2bits(np.dot(R, C_TILDE) % 2))
    print(decoded_message)  # non-readable message

    # 4.14.14
    print('4.14.14')
    def correct(mat):
        return (mat + find_error_matrix(np.dot(H, mat) % 2)) % 2

    # 4.14.15
    print('4.14.15')
    C_CORRECTED = correct(C_TILDE)
    decoded_message = bits2str(mat2bits(np.dot(R, C_CORRECTED) % 2))
    print(decoded_message)

    # 4.14.16
    print('4.14.16')
    for error_rate in [0.01, 0.02, 0.03, 0.04, 0.05]:
        C_TILDE = (C + np.random.choice([0, 1], C.shape, p=[1 - error_rate, error_rate])) % 2
        C_CORRECTED = correct(C_TILDE)
        decoded_message = bits2str(mat2bits(np.dot(R, C_CORRECTED) % 2))
        print('[Result for error rate {}]: '.format(error_rate) + decoded_message)

if __name__ == '__main__':
    main()


import numpy as np
from scipy.stats import norm

from abc import ABCMeta, abstractmethod

def st(z, s0, r, sigma, T):
    return s0 * np.exp((r - sigma ** 2 / 2) * T + sigma * np.sqrt(T) * z)

s0 = 80;
r = 0.1;
sigma = 0.2;
T = 5;
K = 100

def call(s0, r, sigma, T, K):
    d1 = (np.log(s0 / (K * np.exp(-r * T))) + sigma ** 2 / 2 * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    return s0 * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)

def put(s0, r, sigma, T, K):
    d1 = (np.log(s0 / (K * np.exp(-r * T))) + sigma ** 2 / 2 * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    return K * np.exp(-r * T) * norm.cdf(-d2) - s0 * norm.cdf(-d1)

class Option(object):
    __metaclass__ = ABCMeta

    def __init__(self, s0, r, sigma, K, T, name=None):
        if name is not None:
            self.name = name
        self.r = r
        self.sigma = sigma
        self.K = K
        self.T = T
        self.s0 = s0
        self.d1 = (np.log(s0 / (K * np.exp(-r * T))) + sigma ** 2 / 2 * T) / (sigma * np.sqrt(T))
        self.d2 = self.d1 - sigma * np.sqrt(T)
        pass

    @abstractmethod
    def payoff(self, S):
        print("Abstract Option class payoff function")
        pass

    @abstractmethod
    def formula(self):
        print("Abstract Option class pricing formula")
        pass

    def monte_carlo(self, M, alpha=0.05):
        randn = np.random.normal(size=M)
        stock_price = st(randn, self.s0, self.r, self.sigma, self.T)
        stock_price_minus = st(-randn, self.s0, self.r, self.sigma, self.T)
        intrinsic_mc = np.exp(-self.r * self.T) * self.payoff(stock_price)
        intrinsic_mc_anti = np.exp(-self.r * self.T) * self.payoff(stock_price_minus)
        intrinsic = (intrinsic_mc + intrinsic_mc_anti) / 2
        price_mc = np.mean(intrinsic)
        std_mc = np.std(intrinsic)
        price_mc_no_anti = np.mean(intrinsic_mc)
        std_mc_no_anti = np.std(intrinsic_mc)
        qalpha = norm.ppf(1 - alpha / 2)
        delta_anti = std_mc / np.sqrt(M) * qalpha
        delta_no_anti = std_mc_no_anti / np.sqrt(M) * qalpha
        return dict({"SampleSize": M,
                     "Price Antithetic Var": price_mc,
                     "Std Antithetic Var": std_mc,
                     "Interval Antithetic Var": (price_mc - delta_anti, price_mc + delta_anti),
                     "Price No Anti": price_mc_no_anti,
                     "Std No Anti": std_mc_no_anti,
                     "Interval No Anti": (price_mc_no_anti - delta_no_anti, price_mc_no_anti + delta_no_anti)})

    def delta_monte_carlo(self, M, alpha=0.05):
        randn = np.random.normal(size=M)
        stock_price = st(randn, self.s0, self.r, self.sigma, self.T)
        stock_price_minus = st(-randn, self.s0, self.r, self.sigma, self.T)
        intrinsic_mc = np.exp(-self.r * self.T) * self.payoff(stock_price) * randn * np.sqrt(self.T) / (
        self.s0 * self.sigma * self.T)
        intrinsic_mc_anti = np.exp(-self.r * self.T) * self.payoff(stock_price_minus) * (-randn) * np.sqrt(self.T) / (
        self.s0 * self.sigma * self.T)
        intrinsic = (intrinsic_mc + intrinsic_mc_anti) / 2
        delta_mc = np.mean(intrinsic)
        return delta_mc

class Call(Option):
    def payoff(self, S):
        if isinstance(S, int):
            return np.array([np.max([S - self.K, 0])])
        else:
            return np.array([np.max([s - self.K, 0]) for s in S])

    def formula(self):
        return self.s0 * norm.cdf(self.d1) - self.K * np.exp(-self.r * self.T) * norm.cdf(self.d2)

    def delta(self):
        return norm.cdf(self.d1)

class Put(Option):
    def payoff(self, S):
        if isinstance(S, int):
            return np.array([np.max([self.K - S, 0])])
        else:
            return np.array([np.max([self.K - s, 0]) for s in S])

    def formula(self):
        return -self.s0 * norm.cdf(-self.d1) + self.K * np.exp(-self.r * self.T) * norm.cdf(-self.d2)

    def delta(self):
        return -norm.cdf(-self.d1)


from scipy.optimize import linprog
import numpy as np

# Objective function
z = np.array([5,4])

# Constraints
C = np.array([
    [1, 1],          #C1
    [10,6]           #C2
])

b = np.array([5,45])

# Bounds
x1 = (0, None)
x2 = (0, None)

#Solution
sol = linprog(-z, A_ub = C, b_ub = b, bounds = (x1, x2), method='simplex')

print(f"x1 = {sol.x[0]}, x2 = {sol.x[1]}, z = {sol.fun*-1}")


import numpy as np
import matplotlib.pyplot as plt

def dvdir(func, x, dx):
    return (func(x + dx) - func(x))/dx

def dvcen(func, x, dx):
    return (func(x + dx) - func(x - dx))/(2*dx)

def erro(dfx, dvan):
    return abs(dvan - dfx)/abs(dvan)

fx = lambda x: (x**3)*np.sin(x)
dx = np.logspace(-16, 0, 100)
dfxdir = dvdir(fx, 3, dx)
dfxcen = dvcen(fx, 3, dx)
dfxan = lambda x: (x**3)*np.cos(x) + 3*(x**2)*np.sin(x)
dvan = np.ones(100) * dfxan(3)

plt.plot(dx, dfxdir, 'b', label='Direita')
plt.plot(dx, dfxcen, 'r', label='Centrada')
plt.hlines(dfxan(3), 1e-16, 1, 'k', label='Analítica')
plt.title('Comparação das derivadas')
plt.xlabel(r'$\Delta x$')
plt.ylabel("f \'(x)")
plt.xscale('log')
plt.grid()
plt.legend()
plt.xlim(1e-16, 1)
plt.show()
#plt.savefig('comp_derivadas.png')

erro_cen = erro(dfxcen, dvan)
erro_dir = erro(dfxdir, dvan)
plt.plot(dx, erro_cen, 'r', label='Centrada')
plt.plot(dx, erro_dir,  'b', label='Direita')
plt.xscale('log')
plt.yscale('log')
plt.xlabel(r'$\Delta x$')
plt.ylabel('Erro')
plt.xlim(1e-16, 1)
plt.grid()
plt.legend()
plt.title('Comparação de erros das derivadas')
plt.show()
#plt.savefig('erro_derivadas.png')


import numpy as np

def ks(x1, x2):
    data1 = np.sort(x1)
    data2 = np.sort(x2)
    n1 = data1.shape[0]
    n2 = data2.shape[0]

    data_all = np.concatenate([data1, data2])
    # using searchsorted solves equal data problem
    cdf1 = np.searchsorted(data1, data_all, side='right') / n1
    cdf2 = np.searchsorted(data2, data_all, side='right') / n2
    cddiffs = cdf1 - cdf2
    minS = np.clip(-np.min(cddiffs), 0, 1)  # Ensure sign of minS is not negative.
    maxS = np.max(cddiffs)
    return max(minS, maxS)

def ad(x1, x2):
    k = 2
    n = np.array([x1.size, x2.size])

    Z = np.sort(np.hstack((x1, x2)))
    N = Z.size
    Zstar = np.unique(Z)

    A2akN = 0.
    Z_ssorted_left = Z.searchsorted(Zstar, 'left')
    if N == Zstar.size:
        lj = 1.
    else:
        lj = Z.searchsorted(Zstar, 'right') - Z_ssorted_left

    Bj = Z_ssorted_left + lj / 2.
    s = np.sort(x1)
    s_ssorted_right = s.searchsorted(Zstar, side='right')
    Mij = s_ssorted_right.astype(float)
    fij = s_ssorted_right - s.searchsorted(Zstar, 'left')
    Mij -= fij / 2.
    inner = lj / float(N) * (N*Mij - Bj*n[0])**2 / (Bj*(N - Bj) - N*lj/4.)
    A2akN += inner.sum() / n[0]

    s = np.sort(x2)
    s_ssorted_right = s.searchsorted(Zstar, side='right')
    Mij = s_ssorted_right.astype(float)
    fij = s_ssorted_right - s.searchsorted(Zstar, 'left')
    Mij -= fij / 2.
    inner = lj / float(N) * (N*Mij - Bj*n[1])**2 / (Bj*(N - Bj) - N*lj/4.)
    A2akN += inner.sum() / n[1]

    A2akN *= (N - 1.) / N
    return A2akN

def ku(x1, x2):
    data1 = np.sort(x1)
    data2 = np.sort(x2)
    n1 = data1.shape[0]
    n2 = data2.shape[0]

    data_all = np.concatenate([data1, data2])
    # using searchsorted solves equal data problem
    cdf1 = np.searchsorted(data1, data_all, side='right') / n1
    cdf2 = np.searchsorted(data2, data_all, side='right') / n2
    cddiffs = cdf1 - cdf2

    minS = np.clip(-np.min(cddiffs), 0, 1)  # Ensure sign of minS is not negative.
    maxS = np.max(cddiffs)

    return minS + maxS


import time
import numpy as np

def get_svd(M, energy=1):
	"""
	Performs singular value decomposition

    Parameters
    ----------
    M: numpy.ndarray
        The matrix that needs to be decomposed.
    energy: float, optional
        The energy threshold for performing dimensionality
		reduction.

    Returns
    -------
    U numpy.ndarray, sigma numpy.ndarray, V.T numpy.ndarray
        U is the left singular matrix
		sigma is a diagonal matrix
		V.T is the right singular matrix
    """

	m = M.shape[0]
	n = M.shape[1]
	# shape of MMT = m x m
	MMT = np.matmul(M, M.T)
	# shape of MTM = n x n
	MTM = np.matmul(M.T, M)
	eigenvals_u, U = np.linalg.eigh(MMT)
	eigenvals, V = np.linalg.eigh(MTM)
	U1 = U.copy()
	V1 = U.copy()

	for i in range(eigenvals.shape[0]):
		eigenvals[i] = 0 if eigenvals[i]<1e-6 else eigenvals[i]

	num_eig = eigenvals.shape[0]
	eigenvals = eigenvals[::-1]

	sigma = np.zeros((max(num_eig, U.shape[1]), max(num_eig, V.shape[1])))
	sigma[:num_eig][:num_eig] = np.diag(np.sqrt(eigenvals))
	sigma = sigma[:U.shape[1], :V.shape[1]]

	# arranging eigen vectors in decreasing order
	U = U[:, ::-1]
	V = V[:, ::-1]

	# print(U.shape, sigma.shape, V.shape)
	print("error before reduction: ", np.linalg.norm(M-U@sigma@(V.T)))

	i = 0
	while i<min(sigma.shape[0],sigma.shape[1]):
		# removing columns from U and rows from sigma
		if sigma[i][i]==0:
			U = np.delete(U, i, 1)
			V = np.delete(V, i, 1)
			sigma = np.delete(sigma, i, 0)
			sigma = np.delete(sigma, i, 1)
			i-=1
		i+=1

	cur_energy = 0
	tot_energy = sum(eigenvals)
	# number of eigenvalues with energy greater than threshold
	num = 0
	for i in range(eigenvals.shape[0]):
		cur_energy += eigenvals[i]
		if (cur_energy/tot_energy)>=energy:
			num = i
			break
	U = U[:,:num+1]
	V = V[:,:num+1]
	sigma = sigma[:num+1, :num+1]
	# print(U.shape,sigma.shape, V.shape)
	print("error after reduction: ", np.linalg.norm(M-U@sigma@(V.T)))

	return U,sigma,V.T

def get_query_(query, df):
	U,sigma,VT = get_svd(df)
	return (query @ sigma)

def libsvd(M):
	'''
	SVD using numpy library function.
	'''
	U,d,VT = np.linalg.svd(M)
	s = np.zeros((U.shape[1], VT.shape[0]))
	s[:d.shape[0],:d.shape[0]] = np.diag(d)
	print(U.shape, s.shape, VT.shape)
	print("error: ", np.linalg.norm(M-U@s@VT))
	return U,s,VT


# -*- coding: utf-8 -*-
"""
Created on Thu Dec  2 20:14:10 2021

@author: Oliver
"""

"""Univariate Optimisation"""

# Golden Search Technique for finding the Maximum

import numpy as np

def gsection(ftn, xl, xm, xr, tol = 1e-9):
    # applies the golden-section algorithm to maximise ftn
    # we assume that ftn is a function of a single variable
    # and that x.l < x.m < x.r and ftn(x.l), ftn(x.r) <= ftn(x.m)
    #
    # the algorithm iteratively refines x.l, x.r, and x.m and
    # terminates when x.r - x.l <= tol, then returns x.m
    # golden ratio plus one
    gr1 = 1 + (1 + np.sqrt(5))/2
    #
    # successively refine x.l, x.r, and x.m
    fl = ftn(xl)
    fr = ftn(xr)
    fm = ftn(xm)
    while ((xr - xl) > tol):
        if ((xr - xm) > (xm - xl)):
            y = xm + (xr - xm)/gr1
            fy = ftn(y)
            if (fy >= fm):
                xl = xm
                fl = fm
                xm = y
                fm = fy
            else:
                xr = y
                fr = fy
        else:
            y = xm - (xm - xl)/gr1
            fy = ftn(y)
            if (fy >= fm):
                xr = xm
                fr = fm
                xm = y
                fm = fy
            else:
                xl = y
                fl = fy
    return(xm)

xl=0
xm=5
xr=10
def ftn(x):
    return x**2+5*x+10
print(gsection(ftn, xl, xm, xr, tol = 1e-9))
print(ftn(xm))

# Newton Maximizing Algorithm
import math
import matplotlib.pyplot as plt

def f(x):
    return x**3-6*x**2+4*x+2
x = np.linspace(-1, 1)
fig, ax = plt.subplots()
ax.plot(x, f(x), label='target')
ax.grid()

def fprime(x):
    return 3*x**2-12*x+4
def fsecond(x):
    return 6*x - 12
def quadratic_approx(x, x0, f, fprime, fsecond):
    return f(x0)+fprime(x0)*(x-x0)+0.5*fsecond(x0)*(x-x0)**2

x = np.linspace(-1, 1)
fig, ax = plt.subplots()
ax.plot(x, f(x), label='target')
ax.grid()
ax.plot(x, quadratic_approx(x, 0, f, fprime, fsecond), color='red', label='quadratic approximation')
ax.set_ylim([-2,3])
ax.axhline(y=0, color='k')
ax.axvline(x=0, color='k')
plt.legend()

def newton(x0, fprime, fsecond, maxiter=100, eps=0.0001):
    x=x0
    for i in range(maxiter):
        xnew=x-(fprime(x)/fsecond(x))
        if xnew-x<eps:
            return xnew
            print('converged')
            break
        x = xnew
    return x

x_star=newton(0, fprime, fsecond)
fig, ax = plt.subplots()
ax.plot(x, f(x), label='target')
ax.grid()
ax.plot(x, quadratic_approx(x, x_star , f, fprime, fsecond), color='red', label='quadratic approximation')
ax.set_ylim([-2,3])
ax.axhline(y=0, color='k')
ax.axvline(x=0, color='k')
ax.axvline(x = x_star, color='green')
plt.legend()

#Valentina Alto: See: https://medium.com/swlh/optimization-algorithms-the-newton-method-4bc6728fb3b6

# Newton Minimizing Algorithm
def f(x):
    return -x**3+6*x**2-4*x-2
x = np.linspace(-1, 1)
fig, ax = plt.subplots()
ax.plot(x, f(x), label='target')
ax.grid()

def fprime(x):
    return -3*x**2+12*x-4
def fsecond(x):
    return -6*x + 12
def quadratic_approx(x, x0, f, fprime, fsecond):
    return f(x0)+fprime(x0)*(x-x0)+0.5*fsecond(x0)*(x-x0)**2

x = np.linspace(-1, 1)
fig, ax = plt.subplots()
ax.plot(x, f(x), label='target')
ax.grid()
ax.plot(x, quadratic_approx(x, 0, f, fprime, fsecond), color='red', label='quadratic approximation')
ax.set_ylim([-5,3])
ax.axhline(y=0, color='k')
ax.axvline(x=0, color='k')
plt.legend()

def newton(x0, fprime, fsecond, maxiter=100, eps=0.0001):
    x=x0
    for i in range(maxiter):
        xnew=x-(fprime(x)/fsecond(x))
        if xnew-x<eps:
            return xnew
            print('converged')
            break
        x = xnew
    return x

x_star=newton(0, fprime, fsecond)
fig, ax = plt.subplots()
ax.plot(x, f(x), label='target')
ax.grid()
ax.plot(x, quadratic_approx(x, x_star , f, fprime, fsecond), color='red', label='quadratic approximation')
ax.set_ylim([-5,3])
ax.axhline(y=0, color='k')
ax.axvline(x=0, color='k')
ax.axvline(x = x_star, color='green')
plt.legend()

#Valentina Alto: See: https://medium.com/swlh/optimization-algorithms-the-newton-method-4bc6728fb3b6

# Gradient Descent vs Newton Unconstrained Multivariate Optimisation

import matplotlib.pyplot as plt
plt.style.use('seaborn-white')
import numpy as np
from mpl_toolkits import mplot3d

def Rosenbrock(x,y):
    return (1 + x)**2 + 100*(y - x**2)**2

def Grad_Rosenbrock(x,y):
    g1 = -400*x*y + 400*x**3 + 2*x -2
    g2 = 200*y -200*x**2
    return np.array([g1,g2])

def Hessian_Rosenbrock(x,y):
    h11 = -400*y + 1200*x**2 + 2
    h12 = -400 * x
    h21 = -400 * x
    h22 = 200
    return np.array([[h11,h12],[h21,h22]])

def Gradient_Descent(Grad,x,y, gamma = 0.00125, epsilon=0.0001, nMax = 10000 ):
    #Initialization
    i = 0
    iter_x, iter_y, iter_count = np.empty(0),np.empty(0), np.empty(0)
    error = 10
    X = np.array([x,y])

    #Looping as long as error is greater than epsilon
    while np.linalg.norm(error) > epsilon and i < nMax:
        i +=1
        iter_x = np.append(iter_x,x)
        iter_y = np.append(iter_y,y)
        iter_count = np.append(iter_count ,i)
        #print(X)

        X_prev = X
        X = X - gamma * Grad(x,y)
        error = X - X_prev
        x,y = X[0], X[1]

    print(X)
    return X, iter_x,iter_y, iter_count

root,iter_x,iter_y, iter_count = Gradient_Descent(Grad_Rosenbrock,-2,2)

#PLOTTING THE SOLUTION

x = np.linspace(-2,2,250)
y = np.linspace(-1,3,250)
X, Y = np.meshgrid(x, y)
Z = Rosenbrock(X, Y)

#Angles needed for quiver plot
anglesx = iter_x[1:] - iter_x[:-1]
anglesy = iter_y[1:] - iter_y[:-1]

#matplotlib inline
fig = plt.figure(figsize = (16,8))

#Surface plot
ax = fig.add_subplot(1, 2, 1, projection='3d')
ax.plot_surface(X,Y,Z,rstride = 5, cstride = 5, cmap = 'jet', alpha = .4, edgecolor = 'none' )
ax.plot(iter_x,iter_y, Rosenbrock(iter_x,iter_y),color = 'r', marker = '*', alpha = .4)

ax.view_init(45, 280)
ax.set_xlabel('x')
ax.set_ylabel('y')

#Contour plot
ax = fig.add_subplot(1, 2, 2)
ax.contour(X,Y,Z, 50, cmap = 'jet')
#Plotting the iterations and intermediate values
ax.scatter(iter_x,iter_y,color = 'r', marker = '*')
ax.quiver(iter_x[:-1], iter_y[:-1], anglesx, anglesy, scale_units = 'xy', angles = 'xy', scale = 1, color = 'r', alpha = .3)
ax.set_title('Gradient Descent with {} iterations'.format(len(iter_count)))

plt.show()

#Newton's Method (Multi-Dimensional)

def Newton_Raphson_Optimize(Grad, Hess, x,y, epsilon=0.000001, nMax = 200):
    #Initialization
    i = 0
    iter_x, iter_y, iter_count = np.empty(0),np.empty(0), np.empty(0)
    error = 10
    X = np.array([x,y])

    #Looping as long as error is greater than epsilon
    while np.linalg.norm(error) > epsilon and i < nMax:
        i +=1
        iter_x = np.append(iter_x,x)
        iter_y = np.append(iter_y,y)
        iter_count = np.append(iter_count ,i)
        print(f'X: {X}')

        X_prev = X
        X = X - np.linalg.inv(Hess(x,y)) @ Grad(x,y)
        error = X - X_prev
        x,y = X[0], X[1]

    return X, iter_x,iter_y, iter_count

root,iter_x,iter_y, iter_count = Newton_Raphson_Optimize(Grad_Rosenbrock,Hessian_Rosenbrock,-2,2)

x = np.linspace(-3,3,250)
y = np.linspace(-9,8,350)
X, Y = np.meshgrid(x, y)
Z = Rosenbrock(X, Y)

#Angles needed for quiver plot
anglesx = iter_x[1:] - iter_x[:-1]
anglesy = iter_y[1:] - iter_y[:-1]

#matplotlib inline
fig = plt.figure(figsize = (16,8))

#Surface plot
ax = fig.add_subplot(1, 2, 1, projection='3d')
ax.plot_surface(X,Y,Z,rstride = 5, cstride = 5, cmap = 'jet', alpha = .4, edgecolor = 'none' )
ax.plot(iter_x,iter_y, Rosenbrock(iter_x,iter_y),color = 'r', marker = '*', alpha = .4)

#Rotate the initialization to help viewing the graph
ax.view_init(45, 280)
ax.set_xlabel('x')
ax.set_ylabel('y')

#Contour plot
ax = fig.add_subplot(1, 2, 2)
ax.contour(X,Y,Z, 60, cmap = 'jet')
#Plotting the iterations and intermediate values
ax.scatter(iter_x,iter_y,color = 'r', marker = '*')
ax.quiver(iter_x[:-1], iter_y[:-1], anglesx, anglesy, scale_units = 'xy', angles = 'xy', scale = 1, color = 'r', alpha = .3)
ax.set_title('Newton method with {} iterations'.format(len(iter_count)))

plt.show()


# Prepare the libraries for the exercise
import matplotlib
from matplotlib import pyplot as plt

import numpy as np
import tensorflow as tf
tf.enable_eager_execution()

import tensorflow_probability as tfp
tfd = tfp.distributions
np.random.seed(5218)
# ===========================================================================
# Some configurations
# ===========================================================================
np.random.seed(5218)

# ===========================================================================
# Ex1
# ===========================================================================
# n_samples = 100
# # Two independent dice
# X = np.random.randint(1, 7, n_samples)
# Y = np.random.randint(1, 7, n_samples)

# Z = 8 * np.log(X) - np.random.rand(n_samples)

# # plotting the distrubtion
# plt.figure(figsize=(16, 4))
# ids = np.arange(n_samples)
# plt.subplot(1, 3, 1)
# plt.scatter(ids, X, s=4); plt.title("X")
# plt.subplot(1, 3, 2)
# plt.scatter(ids, Y, s=4); plt.title("Y")
# plt.subplot(1, 3, 3)
# plt.scatter(ids, Z, s=4); plt.title("Z")

# print("Cov(X, Y):\n", np.cov(X, Y))
# print("Cov(X, Z):\n", np.cov(X, Z))
# print("Cov(Y, Z):\n", np.cov(Y, Z))

# ===========================================================================
# Ex4 Correct answer
# ===========================================================================
def gmm(batch_size, n_clusters, n_dim, sigma0,
        using_multivariate_normal=True):
  """ This is the solution for the process in 1.3 """
  # parameters for Dirichlet distribution
  alpha = np.ones(shape=(n_clusters,))
  print("Alpha:", alpha)
  print("Sigma0:", sigma0)

  # step: 1
  dirichlet = tfd.Dirichlet(concentration=alpha)
  theta = dirichlet.sample()

  # step: 2
  # ====== using multi-variate normal for generating components' mean ====== #
  if using_multivariate_normal:
    normal_1 = tfd.MultivariateNormalDiag(
        loc=np.full(shape=(n_dim,), fill_value=0, dtype='float32'),
        scale_diag=np.full(shape=(n_dim,), fill_value=sigma0, dtype='float32'))
  # ====== using multiple Normal distribution instead ====== #
  else:
    normal_1 = tfd.Normal(
        loc=np.full(shape=(n_dim,), fill_value=0, dtype='float32'),
        scale=np.full(shape=(n_dim,), fill_value=sigma0, dtype='float32'))
    normal_1 = tfd.Independent(normal_1, reinterpreted_batch_ndims=1)
  # sampling `n_clusters` time, hence, mean for each cluster
  mu_k = normal_1.sample(n_clusters) # (batch_size, n_clusters, n_dim)

  # ====== Now for the assignment, need 1 indicator for each
  # examples, hence we need `batch_size` amount of indicator
  # (step: 3(a))====== #
  categorical = tfd.OneHotCategorical(probs=theta)
  z = categorical.sample(batch_size) # (batch_size, n_clusters)
  z = tf.cast(z, tf.bool)

  # ====== sampling the data points (step: 3(b)) ====== #
  normal_2 = tfd.Normal(loc=mu_k, scale=1)
  # this make each draw sample will generate sample for
  # all 4 components
  normal_2 = tfd.Independent(normal_2, reinterpreted_batch_ndims=2)
  x_all_components = normal_2.sample(batch_size) # (batch_size, n_clusters, n_dim)

  # ====== selecting the right component for each sample (step: 3(b)) ====== #
  # (batch_size, n_clusters, n_dim) * (batch_size, n_clusters)
  # = (batch_size, n_dim)
  x = tf.boolean_mask(x_all_components, z)

  return x.numpy(), np.argmax(z.numpy(), axis=-1)

X1, Z1 = gmm(1000, n_clusters=4, n_dim=2, sigma0=2,
             using_multivariate_normal=True)
X2, Z2 = gmm(1000, n_clusters=4, n_dim=2, sigma0=10,
             using_multivariate_normal=True)
X3, Z3 = gmm(1000, n_clusters=4, n_dim=2, sigma0=2,
             using_multivariate_normal=False)
X4, Z4 = gmm(1000, n_clusters=4, n_dim=2, sigma0=10,
             using_multivariate_normal=False)

def plot_clusters(x, z, title):
  # visualizing the mixture
  import seaborn as sns
  cluster_colors = sns.color_palette("hls", len(np.unique(Z1)))
  plt.scatter(x[:, 0], x[:, 1], c=[cluster_colors[i] for i in z], s=6)
  plt.title(str(title))

plt.subplot(1, 4, 1)
plot_clusters(X1, Z1, "Sigma:2  Multi-variate")
plt.subplot(1, 4, 2)
plot_clusters(X2, Z2, "Sigma:10 Multi-variate")
plt.subplot(1, 4, 3)
plot_clusters(X3, Z3, "Sigma:2  Normal")
plt.subplot(1, 4, 4)
plot_clusters(X4, Z4, "Sigma:10 Normal")

# ===========================================================================
# Ex4 Bonus
# ===========================================================================
# number of samples
n_samples = 1000
# number of cluster
n_clusters = 2
# number of feature dimension
n_dim = 2

# parameters for Dirichlet distribution
alpha = np.ones(shape=(n_clusters,))
# sigma for the generation of the mean
# NOTE: we have to repeat the sigma for each cluster
sigma = np.repeat(np.expand_dims(np.array([0.5, 8], dtype='float32'),
                                 axis=0),
                  repeats=n_clusters, axis=0)
print(sigma.shape)

dirichlet = tfd.Dirichlet(concentration=alpha)
print(dirichlet)
theta = dirichlet.sample()
print("Theta:", theta)

normal_1 = tfd.Normal(loc=0, scale=sigma)
print(normal_1)
normal_1 = tfd.Independent(normal_1, reinterpreted_batch_ndims=2)
print(normal_1)
mu_k = normal_1.sample()
print("Mu_k:", mu_k)

categorical = tfd.Categorical(probs=theta)
print(categorical)
z = categorical.sample()
print("Z_i:", z)

mu_x = mu_k[z]
normal_2 = tfd.Normal(loc=mu_x, scale=1)
print(normal_2)
normal_2 = tfd.Independent(normal_2, reinterpreted_batch_ndims=1)
print(normal_2)
x = normal_2.sample()
print("X_i:", x)

def gmm(batch_size, n_clusters, n_dim):
  """ This is the solution for the process in 1.3 """
  # parameters for Dirichlet distribution
  alpha = np.ones(shape=(n_clusters,))
  sigma = np.repeat(np.random.randint(1, 25, (1, n_dim)),
                    repeats=n_clusters, axis=0).astype('float32')
  print("Alpha:", alpha)
  print("Sigma0:", sigma[0])

  dirichlet = tfd.Dirichlet(concentration=alpha)
  theta = dirichlet.sample(batch_size)

  normal_1 = tfd.Normal(loc=0, scale=sigma)
  normal_1 = tfd.Independent(normal_1, reinterpreted_batch_ndims=2)
  mu_k = normal_1.sample(batch_size) # (batch_size, n_clusters, n_dim)

  categorical = tfd.OneHotCategorical(probs=theta)
  z = categorical.sample() # (batch_size, n_clusters)
  z = tf.cast(z, tf.bool)

  # another solution is using tf.where (but it is only
  # feasible for 2 components)
  # tf.where(z == 0, component_1, component_2)

  # (batch_size, n_dim)
  mu_x = tf.boolean_mask(mu_k, z)
  normal_2 = tfd.Normal(loc=mu_x, scale=1)
  normal_2 = tfd.Independent(normal_2, reinterpreted_batch_ndims=1)
  x = normal_2.sample()
  return x.numpy(), np.argmax(z.numpy(), axis=-1)
X1, Z1 = gmm(1000, n_clusters=2, n_dim=2)

def gmm_simple(batch_size):
  """ This is the solution for the process in 1.3 """
  dirichlet = tfd.Dirichlet(concentration=alpha)
  theta = dirichlet.sample(batch_size)

  sigma0 = sigma[0][None, :]
  components = [tfd.Independent(tfd.Normal(loc=0,
                                           scale=np.repeat(sigma0, batch_size, 0)),
                                reinterpreted_batch_ndims=1)
                for i in range(n_clusters)]
  mixture = tfd.Mixture(
      cat=tfd.Categorical(probs=theta),
      components=components)

  normal_2 = tfd.Normal(loc=mixture.sample(), scale=1)
  normal_2 = tfd.Independent(normal_2, reinterpreted_batch_ndims=1)
  x = normal_2.sample()
  return x
X2 = gmm_simple(1000)

def gmm_fun(batch_size):
  """ This is the solution for the process in 1.3 """
  # parameters for Dirichlet distribution
  # ====== configuration ====== #
  n_clusters = 2
  n_dim = 2
  alpha = np.ones(shape=(n_clusters,))
  mu = [0, 1000]
  sigma = [1, 1]

  # ====== validate all argument ====== #
  assert len(mu) == n_clusters
  assert len(alpha) == n_clusters
  assert len(sigma) == n_dim
  # ====== preprocessing ====== #
  mu = np.repeat(np.array(mu)[:, None],
                 repeats=n_dim, axis=1).astype('float32')
  sigma = np.repeat(np.array(sigma)[None, :],
                    repeats=n_clusters, axis=0).astype('float32')
  print("Alpha:", alpha)
  print("Mu   :\n", mu)
  print("Sigma:\n", sigma)

  dirichlet = tfd.Dirichlet(concentration=alpha)
  theta = dirichlet.sample(batch_size)

  normal_1 = tfd.Normal(loc=mu, scale=sigma)
  normal_1 = tfd.Independent(normal_1, reinterpreted_batch_ndims=2)
  mu_k = normal_1.sample(batch_size) # (batch_size, n_clusters, n_dim)

  categorical = tfd.OneHotCategorical(probs=theta)
  z = categorical.sample() # (batch_size, n_dim)
  z = tf.cast(z, tf.bool)

  # (batch_size, n_dim)
  mu_x = tf.boolean_mask(mu_k, z)
  normal_2 = tfd.Normal(loc=mu_x, scale=1)
  normal_2 = tfd.Independent(normal_2, reinterpreted_batch_ndims=1)
  x = normal_2.sample()
  return x.numpy(), np.argmax(z.numpy(), axis=-1)

X1, Z1 = gmm_fun(1000)


# -*- coding: utf-8 -*-
"""
Created on Fri Jun 21 09:35:06 2019

@author: hu
"""
import numpy as np
def cross_entropy_error(y,t):
    delta = 1e-7
    ans = -np.sum(t*np.log(y+delta))
    return ans

t=[0,0,1]
y=[0.1,0.5,0.4]
ans=cross_entropy_error(np.array(y),np.array(t))
print(ans)

#mini batch

def cross_entropy_error_minibatch(y,t):
    if(y.ndim==1):
        t=t.reshape(1,t.size)
        y=y.reshape(1,y.size)
    batch_size = y.shape[0]

    delta = 1e-7
    ans = -np.sum(t*np.log(y[np.arange(batch_size),t]+delta))
    return ans

t=[0,0,1]
y=[0.1,0.5,0.4]
ans=cross_entropy_error_minibatch(np.array(y),np.array(t))
print(ans)

#数值微分
def function_1(x):
    return 0.01*x**2+0.1*x
import matplotlib.pylab as plt

def numerical_diff(f,x):
    h = 1e-4 #0.0001
#    print(f(x+h),f(x-h))
    return (f(x+h)-f(x-h))/(2*h)

x = np.arange(0.0,20.0,0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x,y)
plt.show()
numerical_diff(function_1,5)


# Exercise_1
#1
import numpy as np

# Import MSE from sklearn
from sklearn.metrics import mean_squared_error

# Define your own MSE function
def own_mse(y_true, y_pred):
  	# Raise differences to the power of 2
    squares = np.power(y_true - y_pred, 2)
    # Find mean over all observations
    err = np.mean(squares)
    return err

print('Sklearn MSE: {:.5f}. '.format(mean_squared_error(y_regression_true, y_regression_pred)))
print('Your MSE: {:.5f}. '.format(own_mse(y_regression_true, y_regression_pred)))
#2
import numpy as np

# Import log_loss from sklearn
from sklearn.metrics import log_loss

# Define your own LogLoss function
def own_logloss(y_true, prob_pred):
    # Find loss for each observation
    terms = y_true * np.log(prob_pred) + (1 - y_true) * np.log(1 - prob_pred)
    # Find mean over all observations
    err = np.mean(terms)
    return -err

print('Sklearn LogLoss: {:.5f}'.format(log_loss(y_classification_true, y_classification_pred)))
print('Your LogLoss: {:.5f}'.format(own_logloss(y_classification_true, y_classification_pred)))

--------------------------------------------------
# Exercise_2
#1
# Shapes of train and test data
print('Train shape:', train.shape)
print('Test shape:', test.shape)

# Train head()
print(train.head())
#2
# Shapes of train and test data
print('Train shape:', train.shape)
print('Test shape:', test.shape)

# Train head()
print(train.head())

# Describe the target variable
print(train.fare_amount.describe())

# Train distribution of passengers within rides
print(train.passenger_count.value_counts())

--------------------------------------------------
# Exercise_3
# Calculate the ride distance
train['distance_km'] = haversine_distance(train)

# Draw a scatterplot
plt.scatter(x=train['fare_amount'], y=train['distance_km'], alpha=0.5)
plt.xlabel('Fare amount')
plt.ylabel('Distance, km')
plt.title('Fare amount based on the distance')

# Limit on the distance
plt.ylim(0, 50)
plt.show()

--------------------------------------------------
# Exercise_4
# Create hour feature

train['pickup_datetime'] = pd.to_datetime(train.pickup_datetime)

train['hour'] = train.pickup_datetime.dt.hour

# Find median fare_amount for each hour

hour_price = train.groupby('hour', as_index=False)['fare_amount'].median()

# Plot the line plot

plt.plot(hour_price['hour'], hour_price['fare_amount'], marker='o')

plt.xlabel('Hour of the day')

plt.ylabel('Median fare amount')

plt.title('Fare amount based on day time')

plt.xticks(range(24))

plt.show()

--------------------------------------------------
# Exercise_5
# Import KFold

from sklearn.model_selection import KFold # random sampling

# Create a KFold object

kf = KFold(n_splits=3, shuffle=True, random_state=123)

# Loop through each split

fold = 0

for train_index, test_index in kf.split(train):

    # Obtain training and testing folds

    cv_train, cv_test = train.iloc[train_index], train.iloc[test_index]

    print('Fold: {}'.format(fold))

    print('CV train shape: {}'.format(cv_train.shape))

    print('Medium interest listings in CV train: {}\n'.format(sum(cv_train.interest_level == 'medium')))

    fold += 1

--------------------------------------------------
# Exercise_6
# Import StratifiedKFold

from sklearn.model_selection import StratifiedKFold

# Create a StratifiedKFold object

str_kf = StratifiedKFold(n_splits=3, shuffle=True, random_state=123)

# Loop through each split

fold = 0

for train_index, test_index in str_kf.split(train, train['interest_level']):

    # Obtain training and testing folds

    cv_train, cv_test = train.iloc[train_index], train.iloc[test_index]

    print('Fold: {}'.format(fold))

    print('CV train shape: {}'.format(cv_train.shape))

    print('Medium interest listings in CV train: {}\n'.format(sum(cv_train.interest_level == 'medium')))

    fold += 1

--------------------------------------------------
# Exercise_7
# Create TimeSeriesSplit object

#Create TimeSeriesSplit object

time_kfold = TimeSeriesSplit(n_splits=3)

# Sort train data by date

train = train.sort_values('date')

# Iterate through each split

fold = 0

for train_index, test_index in time_kfold.split(train):

    cv_train, cv_test = train.iloc[train_index], train.iloc[test_index]

    print('Fold :', fold)

    print('Train date range: from {} to {}'.format(cv_train.date.min(), cv_train.date.max()))

    print('Test date range: from {} to {}\n'.format(cv_test.date.min(), cv_test.date.max()))

    fold += 1

--------------------------------------------------
# Exercise_8
#1
from sklearn.model_selection import TimeSeriesSplit
import numpy as np

# Sort train data by date
train = train.sort_values('date')

# Initialize 3-fold time cross-validation
kf = TimeSeriesSplit(n_splits=3)

# Get MSE scores for each cross-validation split
mse_scores = get_fold_mse(train, kf)

print('Mean validation MSE: {:.5f}'.format(np.mean(mse_scores)))

#2
from sklearn.model_selection import TimeSeriesSplit
import numpy as np

# Sort train data by date
train = train.sort_values('date')

# Initialize 3-fold time cross-validation
kf = TimeSeriesSplit(n_splits=3)

# Get MSE scores for each cross-validation split
mse_scores = get_fold_mse(train, kf)

print('Mean validation MSE: {:.5f}'.format(np.mean(mse_scores)))
print('MSE by fold: {}'.format(mse_scores))

#3
from sklearn.model_selection import TimeSeriesSplit
import numpy as np

# Sort train data by date
train = train.sort_values('date')

# Initialize 3-fold time cross-validation
kf = TimeSeriesSplit(n_splits=3)

# Get MSE scores for each cross-validation split
mse_scores = get_fold_mse(train, kf)

print('Mean validation MSE: {:.5f}'.format(np.mean(mse_scores)))
print('MSE by fold: {}'.format(mse_scores))
print('Overall validation MSE: {:.5f}'.format(np.mean(mse_scores) + np.std(mse_scores)))

--------------------------------------------------

Machine Learning Scientist with Python Track/23. Winning a Kaggle Competition in Python/ch2_exercises.py


import numpy as np
from scipy.special import gammaln

class MultinomialDistribution(object):

    def __init__(self, p, rso=np.random):
        """Initialize the multinomial random variable.

        Parameters
        ----------
        p: numpy array of length `k`
            The outcome probabilities
        rso: numpy RandomState object (default: np.random)
            The random number generator

        """

        # Check that the probabilities sum to 1. If they don't, then
        # something is wrong! We use `np.isclose` rather than checking
        # for exact equality because in many cases, we won't have
        # exact equality due to floating-point error.
        if not np.isclose(np.sum(p), 1.0):
            raise ValueError("outcome probabilities do not sum to 1")

        # Store the parameters that were passed in
        self.p = p
        self.rso = rso

        # Precompute log probabilities, for use by the log-PMF, for
        # each element of `self.p` (the function `np.log` operates
        # elementwise over NumPy arrays, as well as on scalars.)
        self.logp = np.log(self.p)

    def sample(self, n):
        """Samples draws of `n` events from a multinomial distribution with
        outcome probabilities `self.p`.

        Parameters
        ----------
        n: integer
            The number of total events

        Returns
        -------
        numpy array of length `k`
            The sampled number of occurrences for each outcome

        """
        x = self.rso.multinomial(n, self.p)
        return x

    def log_pmf(self, x):
        """Evaluates the log-probability mass function (log-PMF) of a
        multinomial with outcome probabilities `self.p` for a draw `x`.

        Parameters
        ----------
        x: numpy array of length `k`
            The number of occurrences of each outcome

        Returns
        -------
        The evaluated log-PMF for draw `x`

        """
        # Get the total number of events.
        n = np.sum(x)

        # equivalent to log(n!)
        log_n_factorial = gammaln(n + 1)
        # equivalent to log(x1! * ... * xk!)
        sum_log_xi_factorial = np.sum(gammaln(x + 1))

        # If one of the values of self.p is 0, then the corresponding
        # value of self.logp will be -inf. If the corresponding value
        # of x is 0, then multiplying them together will give nan, but
        # we want it to just be 0.
        log_pi_xi = self.logp * x
        log_pi_xi[x == 0] = 0
        # equivalent to log(p1^x1 * ... * pk^xk)
        sum_log_pi_xi = np.sum(log_pi_xi)

        # Put it all together.
        log_pmf = log_n_factorial - sum_log_xi_factorial + sum_log_pi_xi
        return log_pmf

    def pmf(self, x):
        """Evaluates the probability mass function (PMF) of a multinomial
        with outcome probabilities `self.p` for a draw `x`.

        Parameters
        ----------
        x: numpy array of length `k`
            The number of occurrences of each outcome

        Returns
        -------
        The evaluated PMF for draw `x`

        """
        pmf = np.exp(self.log_pmf(x))
        return pmf


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

# In[1]:

import numpy as np
import matplotlib.pyplot as plt

# In[2]:

#Simpson from book p.234
def Simpson(f, a, b, epsilon, level, level_max):
    level = level + 1
    h = b - a
    c = (a + b)/2
    one_simpson = h*(f(a) + 4*f(c) + f(b))/6
    d = (a + c)/2
    e = (c + b)/2
    two_simpson = h*(f(a) + 4*f(d) + 2*f(c) + 4*f(e) + f(b))/12
    if level >= level_max:
        simpson_result = two_simpson
        print("maximum level reached")
    else:
        if np.abs(two_simpson - one_simpson) < 15*epsilon:
            simpson_result = two_simpson + (two_simpson - one_simpson)/15
        else:
            left_simpson = Simpson(f,a,c,e/2,level,level_max)
            right_simpson = Simpson(f,c,b,e/2,level,level_max)
            simpson_result = left_simpson + right_simpson
    return simpson_result

# In[3]:

#test integral
def f(x):
    return np.cos(2*x)/np.exp(x)
a = 0
b = (5/4) * np.pi
epsilon = (1/2)*(10**-3)
level = 0
level_max = 100
print(Simpson(f, a, b, epsilon, level, level_max))

# In[4]:

#Simpson with graphing
def Simpson(f, a, b, epsilon, level, level_max):
    level = level + 1
    h = b - a
    c = (a + b)/2
    one_simpson = h*(f(a) + 4*f(c) + f(b))/6
    d = (a + c)/2
    e = (c + b)/2
    two_simpson = h*(f(a) + 4*f(d) + 2*f(c) + 4*f(e) + f(b))/12
    XP.append(a)
    XP.append(b)
    XP.append(c)
    XP.append(d)
    XP.append(e)
    if level >= level_max:
        simpson_result = two_simpson
        print("maximum level reached")
    else:
        if np.abs(two_simpson - one_simpson) < 15*epsilon:
            simpson_result = two_simpson + (two_simpson - one_simpson)/15
        else:
            left_simpson = Simpson(f,a,c,epsilon/2,level,level_max)
            right_simpson = Simpson(f,c,b,epsilon/2,level,level_max)
            simpson_result = left_simpson + right_simpson
    return simpson_result

# In[12]:

#test integral
def f(x):
    return np.cos(2*x)/np.exp(x)
a = 0
b = (5/4) * np.pi
epsilon = (1/2)*(10**-3)
level = 0
level_max = 5
XP = []
I = Simpson(f, a, b, epsilon, level, level_max)
XP = list(set(XP))
XP.sort()
PP = list(map(f,XP))
print(I)
rounded_XP = [round(num, 3) for num in XP]
rounded_PP = [round(num, 3) for num in PP]
print("x-values used by Simpson approximation:")
print(rounded_XP)
print("Corresponding f(x)-values used by Simpson approximation:")
print(rounded_PP)

# In[13]:

X = np.arange(0, 4, 0.01);
FVAL = np.cos(2*X)/np.exp(X)

fig = plt.figure(figsize=(10, 6))
ax1 = fig.add_subplot(111)

ax1.plot(X,FVAL, label = 'cos(2x)/e^x')
ax1.scatter(XP,PP)

# Give a title for the sine wave plot
plt.title('Arctan vs Polynomial Interpretation Plot')

# Give x axis label for the sine wave plot
plt.xlabel('x')

# Give y axis label for the sine wave plot
plt.legend(loc='upper left')
plt.grid(True, which='both')
plt.axhline(y=0)
plt.stem(XP,PP)
plt.show()

# In[ ]:

# In[ ]:


# -*- coding: utf-8 -*-
# -----------------------------------
# @CreateTime   : 2020/3/20 0:15
# @Author       : Mark Shawn
# @Email        : shawninjuly@gmail.com
# ------------------------------------
import numpy as np

EPS = 1e-8

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

def mse(Y_pred, Y_target):
	return ((Y_pred - Y_target) ** 2).mean()

def cross_entropy(Y_pred, Y_target):
	return -(Y_target * np.log(Y_pred+EPS) + (1-Y_target) * np.log(1-Y_pred+EPS)).mean()


from typing import Tuple, Union, List
import pandas as pd
import numpy as np
import logging

logger = logging.getLogger('STATS UTILS')

def compute_interquartile_range(values: str) -> Tuple[float, float]:
    """
    Compute interquartile range for a list of values

    :param values: list of values
    :return: lower and upper bound of interquartile range
    """

    # Calculate IQR
    q1, q3 = np.percentile(values, [25, 75])
    iqr = q3 - q1

    lower_bound = q1 - (1.5 * iqr)
    upper_bound = q3 + (1.5 * iqr)

    return lower_bound, upper_bound

def check_outliers(lower_bound: float, upper_bound: float, current_value: Union[int, float]) -> bool:
    """
    Check if a value is an outliers by comparing it with interquartile range

    :param lower_bound: lower bound of interquartile range
    :param upper_bound: upper bound of interquartile range
    :param current_value: value to compare whether or not it is an outlier
    :return: whether or not it is an outlier
    """

    if lower_bound <= current_value <= upper_bound:
        outlier = False
    else:
        outlier = True

    return outlier

def discretize_data(array: np.ndarray, bin_type: str, q: Union[int, List[Union[int, float]]] = None,
                    bins: Union[int, List[Union[int, float]]] = None, labels: Union[list, np.ndarray] = None,
                    **kwargs) -> pd.Series:
    """
    Discretize variable into buckets based on quantiles or fixed intervals
    For more information about the parameters, please visit :
    - https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.cut.html
    - https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.qcut.html

    :param array: input array to be binned, must be 1-dimensional
    :param bin_type: binning method e.g. quantile or fixed_intervals
    :param q: number of quantiles, just needed if bin_type is quantile
    :param bins: defines the bin edges allowing for non-uniform width, just needed if bin_type is fixed_intervals
    :param labels: used as labels for the resulting bins
    :param kwargs: additional arguments for qcut and cut pandas function
    :return: an array-like object representing the respective bin for each value of x
    """

    if bin_type == 'quantile':

        if q is None:
            raise ValueError("'q' can't be None if bin_type is equal to quantile")

        if labels is None:
            labels = np.arange(q, 0, -1)

        try:
            binned_data = pd.qcut(array, q=q, labels=labels, **kwargs)
        except Exception:
            logger.error("Can't create quantile discrete intervals", exc_info=True)
            raise

    elif bin_type == 'fixed_intervals':

        if bins is None:
            raise ValueError("'bins' can't be None if bin_type is equal to quantile")

        try:
            binned_data = pd.cut(array, bins=bins, labels=labels, **kwargs)
        except Exception:
            logger.error("Can't create fixed discrete intervals", exc_info=True)
            raise

    else:

        logger.error("{} bin type is not supported yet, please contact developer".format(bin_type))
        raise NotImplementedError('Bin types supporting for now: quantile and fixed intervals')

    return binned_data