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

# # Some Variations of Banach's Matchbox Problem

# Banach's matchbox problem is a good entry point into stochastic stopping problems. A man buys two matchbooks and puts one in each of his two pockets. He then selects a matchbox at np.random.random from either pocket, uses a single match, and then returns the matchbox to the same pocket. This problem is slightly different from the classic matchbox problem in that when the last match is taken, the sequence ends. In the classic problem, discovery of the empty box happens only when the empty box is selected **after** being emptied.
#
# The question is: **What is the probability of $k$ matches in the remaining matchbox? **
#
# The problem is given two matchbooks containing $n$ matches each and with a matchbook placed in both the left and right pockets, if a person reaches into one or the other pocket at np.random.random with equal probability, what is the probability of there being $k$ matches in the other pocket when the matchbook in the selected pocket is found to be empty?  That is, the person keeps reaching alternatively and at np.random.random to sample a match from either pocket until one of the pockets is exhausted of matches.
#
# This is easy to code up in Python using a generator:

# [1]

from __future__ import division
from collections import Counter, OrderedDict
import pandas as pd
import numpy as np
from scipy.special import comb
#np.random.random.seed(12345)

# [2]

def step(n=4):
    'keep track of remaining matches in each matchbook'
    a = b = n
    while a > 0 and b > 0:
        if np.random.random.np.random.randint(0, 1):
            a -= 1
        else:
            b -= 1
        yield (a, b)

# Thus, suppose there are $n=4$ matches in each matchbook, then a valid sequence of draws from the `(left,right`) pocket is the following:
#
# `[(4, 3), (4, 2), (3, 2), (3, 1), (3, 0)]`
#
# This np.means that the first draw is from the right pocket leaving `3` matches there and `4` matches in the left pocket. The next draw again
# samples a match from the right pocket leaving `2` matches there and `4` in the left pocket. For the following draw, the left pocket is chosen leaving `3` matches there and `2` matches in the right pocket. This continues until the right pocket is emptied `(3,0)`. We can draw this sequence using the following code:

# [3]

import numpy as np
import matplotlib.pyplot as plt

def draw_grid(n=4):
    'draw square grid of `n` dimensions'
    fig, ax = plt.subplots()
    i, j = np.mgrid[0:n + 1, 0:n + 1]
    ax.scatter(i.flat, j.flat, alpha=.8, color='black')
    ax.set_aspect(1)
    ax.plot(0, 0, 'ow', mec='w')  # remove origin
    ax.plot([0, ] * 4, range(1, 5), 'or', mec='r', ms=10)
    ax.plot(range(1, 5), [0, ] * 4, 'or', mec='r', ms=10)
    return ax

def draw_path(seq, ax, color='gray', alpha=0.5):
    x, y = zip(*seq)
    n = max(seq[0])
    ax.plot((n,) + x, (n,) + y, marker='o', markersize=20,
            alpha=alpha, color=color, lw=5)
    ax.set_title('Sequence Length=%d' % (len(x)))

ax = draw_grid()
draw_path([(4, 3), (4, 2), (3, 2), (3, 1), (3, 0)], ax)

#  the figure above, the red circles indicate the termination points where one of the pockets has been emptied. The `(4,4)` point is the starting point with incremental steps moving down and to the left until one of the red circles is encountered. The length of the sequence is indicated in the title. In this case it took five draws in total to exhaust one of the matchbooks and terminate the sequence.
#
# The classical matchbox problem is to find the probability of termination at a particular circle. For example, what is the probability that the sequence terminates with one match remaining in the other matchbook? In the figure above, this np.means terminating at `(1,0)` or `(0,1)`.
#
# Specifically, termination at `(1,0)` np.means accumulating four steps down and three steps left in any sequence. This is the same as the $n$ *choose* $k$ binomial coefficient $\texttt{Binom}(n,k)$. We can compute this using `scipy` as the following with $n=7,k=3$:

# [4]

print(comb(7, 3, exact=True))

# The problem with this approach is that we can accidentally count paths that would have terminated earlier. For example,

# [5]

ax = draw_grid()
draw_path([(3, 4), (3, 3), (2, 3), (2, 2), (1, 2), (1, 1), (1, 0)], ax)
draw_path([(4, 3), (4, 2), (4, 1), (4, 0),
           (3, 0), (2, 0), (1, 0)], ax, 'blue', .2)

# The blue path would never have gotten so long because it would have encountered the termination point at `(4,0)`. Thus, this straight-forward counting scheme would over-count by including these paths. The following figure shows these valid paths that terminate at `(1,0)`:

# [6]

paths = [[(3, 4), (2, 4), (1, 4), (1, 3), (1, 2), (1, 1), (1, 0)],
         [(3, 4), (2, 4), (2, 3), (1, 3), (1, 2), (1, 1), (1, 0)],
         [(3, 4), (2, 4), (2, 3), (2, 2), (1, 2), (1, 1), (1, 0)],
         [(3, 4), (2, 4), (2, 3), (2, 2), (2, 1), (1, 1), (1, 0)],
         [(3, 4), (3, 3), (2, 3), (1, 3), (1, 2), (1, 1), (1, 0)],
         [(3, 4), (3, 3), (2, 3), (2, 2), (1, 2), (1, 1), (1, 0)],
         [(3, 4), (3, 3), (2, 3), (2, 2), (2, 1), (1, 1), (1, 0)],
         [(3, 4), (3, 3), (3, 2), (2, 2), (1, 2), (1, 1), (1, 0)],
         [(3, 4), (3, 3), (3, 2), (2, 2), (2, 1), (1, 1), (1, 0)],
         [(3, 4), (3, 3), (3, 2), (3, 1), (2, 1), (1, 1), (1, 0)],
         [(4, 3), (3, 3), (2, 3), (1, 3), (1, 2), (1, 1), (1, 0)],
         [(4, 3), (3, 3), (2, 3), (2, 2), (1, 2), (1, 1), (1, 0)],
         [(4, 3), (3, 3), (2, 3), (2, 2), (2, 1), (1, 1), (1, 0)],
         [(4, 3), (3, 3), (3, 2), (2, 2), (1, 2), (1, 1), (1, 0)],
         [(4, 3), (3, 3), (3, 2), (2, 2), (2, 1), (1, 1), (1, 0)],
         [(4, 3), (3, 3), (3, 2), (3, 1), (2, 1), (1, 1), (1, 0)],
         [(4, 3), (4, 2), (3, 2), (2, 2), (1, 2), (1, 1), (1, 0)],
         [(4, 3), (4, 2), (3, 2), (2, 2), (2, 1), (1, 1), (1, 0)],
         [(4, 3), (4, 2), (3, 2), (3, 1), (2, 1), (1, 1), (1, 0)],
         [(4, 3), (4, 2), (4, 1), (3, 1), (2, 1), (1, 1), (1, 0)]]

ax = draw_grid()
for i in paths:
    draw_path(i, ax, alpha=.1)

# Let's change our perspective slightly. Instead let's examine the probability of a sequence of a certain length and see if we can use that to answer the classic question. To start with, the following figure shows valid four-long sequences. The diagonal elements are indicated in green and these are the termination points for all four-long sequences. There are $2^4=16$ such sequences.

# [7]

ax = draw_grid()
for i in zip(range(5)[::-1], range(5)):
    ax.plot(i[0], i[1], 'sg', ms=15, alpha=.3)
draw_path([(3, 4), (2, 4), (1, 4), (0, 4)], ax)
draw_path([(4, 3), (4, 2), (4, 1), (4, 0)], ax)

# Because only two of the sixteen valid  sequences result in termination, the probability of termination with a four-long sequence is $P_4 = \frac{2}{16}= \frac{1}{8}$. Now let's examine the diagonal elements with the following labels.

# [8]

ax = draw_grid()
for i in zip(range(1, 4)[::-1], range(1, 4)):
    ax.text(i[0], i[1], '(%d,%d)' % i, fontsize=13, color='g')

# We can use the labels to compute the number of paths that terminate at each of the diagonal elements.
#
# | Label | Number of Paths|
# |-------|----------------|
# | (1,3) | $\texttt{binom}(4,1)=4$|
# | (2,2) | $\texttt{binom}(4,2)=6$|
# | (3,1) | $\texttt{binom}(4,1)=4$|
#
# This np.means that there are `16-2=4+6+4=14` paths out of the initial group of `16` that have yet to terminate. Now, let's consider sequences of length five. The following figure shows the migration of paths to the next lower diagonal corresponding to the five-long sequences.

# [9]

# construct matrix of indices for convenience
i, j = np.meshgrid(range(5), range(4, -1, -1))
m = i + 1j * j

ax = draw_grid()
for i in np.diagonal(m, -1):
    ax.plot(i.real, i.imag, 'sg', ms=15, alpha=.3)

for i in np.diagonal(m, 0):
    if i.real == 0 or i.imag == 0:
        continue
    ax.text(i.real, i.imag, '(%d,%d)' %
            (i.real, i.imag), fontsize=13, color='g')

# add arrow markers
heads = map(lambda i: (i.real, i.imag), np.diagonal(m, 0))[1:-1]

for i, j in heads:
    t = (i - 1, j)
    ax.annotate("", xy=(i - 1, j), xytext=(i, j),
                arrowprops=dict(width=.5, color='green'),
                )
    ax.annotate("", xy=(i, j - 1), xytext=(i, j),
                arrowprops=dict(width=.5, color='green'),
                )

# Note that two arrows end up at red termini. There are `28=2*14` paths in total (because each diagonal element can go either down or right). Of these `28`, eight terminate on a red circle. Thus, under these conditions, the probability of a terminating five-long sequence is therefore $p_5 = \frac{8}{28}=\frac{2}{7}$. Next, we can draw the next layer corresponding to six-long sequences.

# [10]

ax = draw_grid()
for i in np.diagonal(m, -2):
    #     if i.real==0 or i.imag==0: continue
    ax.plot(i.real, i.imag, 'sg', ms=15, alpha=.3)

for i in np.diagonal(m, -1):
    if i.real == 0 or i.imag == 0:
        continue
    ax.text(i.real, i.imag, '(%d,%d)' %
            (i.real, i.imag), fontsize=13, color='g')

for i in np.diagonal(m, -2):
    #     if i.real==0 or i.imag==0: continue
    ax.text(i.real - 0.5, i.imag - 0.5, '(%d,%d)' %
            (i.real, i.imag), fontsize=13, color='blue')

# add arrow markers
heads = map(lambda i: (i.real, i.imag), np.diagonal(m, -1))[1:-1]

for i, j in heads:
    t = (i - 1, j)
    ax.annotate("", xy=(i - 1, j), xytext=(i, j),
                arrowprops=dict(width=.5, color='green'),
                )
    ax.annotate("", xy=(i, j - 1), xytext=(i, j),
                arrowprops=dict(width=.5, color='green'),
                )

#  the last stage, there were `8` out of `28` paths that terminated. That leaves `20` paths still in play and therefore `40` paths on the indicated diagonal.  In terms of our earlier accounting, we have
#
# | Label | Number of Paths|
# |-------|----------------|
# | (1,2) | 10|
# | (2,1) | 10|
#
#
# For the next layer, we have the following:
#
# | Label | Number of Paths|
# |-------|----------------|
# | (0,2) | 10|
# | (2,0) | 10|
# | (1,1)  | 20|
#
# With this accounting, the probability of termination with a six-long
# sequence is $p_6=\frac{20}{40}=\frac{1}{2}$. Finally, because all paths going through `(1,1)` terminate at either `(1,0)` or `(0,1)`, we have $p_7=1$.
#
# To assemble all these results, recall we have
# $$ P_4=\frac{1}{8}$$
#
# and also, because $p_4 = P(T_5| \hat{T}_4)$ where $T_5$ is the probability of termination in five steps and $\hat{T}_4$ is the probability of not terminating in four steps, we have
#
# $$ P_5 = p_5 (1-P_4) = \frac{2}{7} \frac{7}{8}=\frac{1}{4}$$
#
# Likewise for $P_6$,
#
# $$ P_6 = p_6 (1-P_4)(1-p_5) = \frac{1}{2}\frac{7}{8}\frac{5}{7} =\frac{5}{16}$$
#
# Finally, for $P_7$, we have the following:
#
# $$ P_7 = p_7 (1-P_4)(1-p_5)(1-p_6) = 1\frac{7}{8}\frac{5}{7}\frac{1}{2} =\frac{5}{16}$$
#
# Note that
#
# $$ P_4+P_5+P_6+P_7 = 1 $$
#
# A quick simulation can bear this out.

# [11]

Counter([len(tuple(step())) for i in range(1000)])

# We can automate this process in the following code

# [12]

from scipy.misc import comb
import numpy as np

# [13]

n = 4
t = np.np.array([comb(n, i, exact=True) for i in range(n + 1)], dtype=int)
print(t)
t[[0, -1]].sum() / t.sum()
# split
(t[1], t[-2])

# [14]

from numpy.lib.stride_tricks import as_strided
np.np.hstack([t[1], as_strided(t[1:-1], (2, 3 - 2 + 1),
                               (t.itemsize,) * 2).sum(axis=1), t[-2]])

# [15]

from scipy.misc import comb

# [16]

x = [comb(4, i, exact=True) for i in range(5)]
print(x)
print(x[0] * 2 / sum(x))

# [17]

a = x.pop(0)
b = x.pop()
x = [x[0]] + [sum(x[slice(i, i + 2)]) for i in range(len(x) - 1)] + [x[-1]]

# [18]

print(2 * x[0] / sum(x))

# [19]

def probstop(n=4):
    o = OrderedDict()
    k = n
    x = [comb(n, i, exact=True) for i in range(n + 1)]
    o[k] = 2 * x[0] / sum(x)
    while len(x) > 2:
        a = x.pop(0)
        b = x.pop()
        x = [x[0]] + [sum(x[slice(i, i + 2)])
                      for i in range(len(x) - 1)] + [x[-1]]
        k += 1
        o[k] = 2 * x[0] / sum(x)
        assert o[k] > 0
    p = OrderedDict()
    factor = 1
    for k, v in o.iteritems():
        p[k] = factor * o[k]
        factor = factor * (1 - v)
    return o, p

# [27]

o, p = probstop(30)

# [28]

p

# [35]

sum(p.values())

# [36]

fig, ax = plt.subplots()
ax.plot(p.keys(), p.values(), '-o')

# [37]

w = pd.Series(Counter([len(tuple(step(30))) for i in range(5000)]))
(w / 5000.).plot(ax=ax, marker='s')

# [38]

fig

# [56]

fig, ax = plt.subplots()
ax.plot(p.keys(), np.cumsum(p.values()), '-o')
ax.grid()

# [103]

fig, ax = plt.subplots()
fig.set_size_inches((10, 5))
for i in range(150):
    x, y = zip(*tuple(step(100)))
    ax.plot(x, y, 'o-', color='gray', alpha=.1 / 5.)
    ax.plot(x[-1], y[-1], 'sg', alpha=.3)
ax.set_aspect(1)
ax.axis(xmin=-1, ymin=-1)

# [ ]

# [ ]

# [ ]


# --------------
# Importing header files
import numpy as np
import warnings

warnings.filterwarnings('ignore')

#New record
new_record=np.array([[50,  9,  4,  1,  0,  0, 40,  0]])

#Reading file
data = np.genfromtxt(path, delimiter=",", skip_header=1)
data = np.array(data)
print(data.shape)
#print(data)
#Code starts here
census = np.concatenate((data,new_record))
print(census.shape)
#step 2
age = census[:,0]
#print(age)
print(age.shape)
max_age = np.max(age)
print('max age is : ' + str(max_age))
min_age = np.min(age)
print('min age is : ' + str(min_age))
age_mean = age.mean()
print('mean_age age is : ' + str(age_mean))
age_std = np.std(age)
print('std age is : ' + str(age_std))

#step 3
race = census[:,2]

race_0 = np.extract(race == 0, race)
race_1 = np.extract(race == 1, race)
race_2 = np.extract(race == 2, race)
race_3 = np.extract(race == 3, race)
race_4 = np.extract(race == 4, race)
len_0 = len(race_0)
len_1 = len(race_1)
len_2 = len(race_2)
len_3 = len(race_3)
len_4 = len(race_4)
number_of_citizens = [len_0,len_1,len_2,len_3,len_4]
min_citizen = min(number_of_citizens)
print(min_citizen)
minority_race = number_of_citizens.index(6)
print(minority_race)

# step_4

senior_citizens = census[census[:,0]>60]
#print(senior_citizens)
senior_citizens_len = len(senior_citizens)
working_hours_sum = sum(senior_citizens[:,6])
# print(working_hours_sum)
avg_working_hours = working_hours_sum / senior_citizens_len
print(avg_working_hours)
#step 5
high = census[census[:,1]>10]
print(type(high))
avg_pay_high = high.mean(axis=0)[7]
low = census[census[:,1]<=10]
#print(low)
avg_pay_low = low.mean(axis=0)[7]
print(avg_pay_high)
print(avg_pay_low)


import numpy as np
import matplotlib.pyplot as plt
import scipy.optimize
import time

def lin_reg(theta,x,y):
    """
    Arguments:
    theta - A vector containing the parameter values to optimize.
    X - The examples stored in a matrix.
        X(i,j) is the i'th coordinate of the j'th example.
    y - The target value for each example.  y(j) is the target for example j.

    A basic function computing cost and gradient for the given arguments.
    """

    no_of_ex = x.shape[1]
    grad = np.zeros(theta.shape)
    cost = 0
    for i in range(no_of_ex):
        val = (np.sum(theta[:]*x[:,i]) - y[i])
        cost = cost + pow(val,2)
        grad = grad + x[:,i]*val
    cost = cost/2
    return cost,grad

def lin_reg_vec(theta,x,y):
    """
    An optimized function to compute the cost and gradient for the given arguments
    """
    val = np.dot(theta,x) - y
    grad = np.transpose(np.dot(x,np.transpose(val)))
    cost = np.sum(val**2)/2
    return cost,grad

def cost_fun(theta,x,y):
    """
    Function to calculate cost
    """
    val = np.dot(theta,x) - y
    cost = np.sum(val**2)/2
    return cost

def grad_fun(theta,x,y):
    """
    Function to calculate gradient
    """
    val = np.dot(theta,x) - y
    grad = np.transpose(np.dot(x,np.transpose(val)))
    return grad

def rms_error(theta,x,y):
    """
    Function to calculate RMS error
    """
    val = np.dot(theta,x) - y
    error = np.sqrt(np.mean(val ** 2))
    return error

data = np.loadtxt("housing.data")  #specify the path of data file
data = np.insert(data,0,1,axis=1)
np.random.shuffle(data)

train_data = data[:400,:-1]
train_labels = data[:400,-1]

test_data = data[400:,:-1]
test_labels = data[400:,-1]

train_data = np.transpose(train_data)
test_data = np.transpose(test_data)

j_hist = []

t0 = time.time()
res = scipy.optimize.minimize(
    fun=cost_fun,
    x0=np.random.rand(train_data.shape[0]),
    args=(train_data, train_labels),
    method='bfgs',
    jac=grad_fun,
    options={'maxiter': 200, 'disp': True},
    callback=lambda x: j_hist.append(cost_fun(x, train_data, train_labels)),
)
t1 = time.time()
optimal_theta = res.x
print ("Optimization took %r seconds" %(t1-t0))

plt.plot(j_hist, marker='o')
plt.xlabel('Iterations')
plt.ylabel('J(theta)')

plt.figure(figsize=(10, 8))
plt.scatter(np.arange(test_labels.size), sorted(test_labels), c='b', edgecolor='None', alpha=0.5, label='actual')
plt.scatter(np.arange(test_labels.size), sorted(optimal_theta.dot(test_data)), c='g', edgecolor='None', alpha=0.5, label='predicted')
plt.legend(loc='upper left')
plt.ylabel('House price ($1000s)')
plt.xlabel('House #')

print ("training rms error = %r" %(rms_error(optimal_theta,train_data,train_labels)))
print ("testing rms error = %r" %(rms_error(optimal_theta,test_data,test_labels)))


"""
first_order_lagrangian_algorithm.py

Returns the minimizer of the function
X0 - initial guess for X;
Lambda0 - initial guess for Lambda;
func - anonimous function (objective function)
Dy - anonimous function gradient (objective function)
Dh - anonimous function gradient (equality constraints function)
Dg - anonimous function gradient (nonequality constraints function)
h - anonimous function (equality constraints function)
g - anonimous function (nonequality constraints function)
"""

import numpy as np

def first_order_lagrangian_algorithm(X0, Lambda0, Mu0, func, Dy, Dh, Dg, h, g, options):

    epsilon = 10e-3;
    alpha = 0.01; # step size for X
    beta = 0.01; # step size for Lambda
    gamma = 0.01 # step size for Mu
    report = {};
    N_iter_max = options['N_iter_max'];
    tolerance_x = options['tolerance_x'];
    tolerance_y = options['tolerance_y'];
    X_lower = options['x_lower'];
    X_upper = options['x_upper'];
    progress_x = np.zeros((X0.size, N_iter_max + 1));
    progress_y = np.zeros((1, N_iter_max + 1));
    progress_x[:, [0]] = X0;
    progress_y[0, [0]] = func(X0);
    m = Lambda0.shape[0];
    n = X0.shape[0];
    X_old = X0;
    Lambda_old = Lambda0;
    Mu_old = Mu0;
    W_old = np.concatenate((X0, Lambda0, Mu0), axis=0);

    for iter_no in range(1, N_iter_max + 1):

        dX = -(Dy(X_old) + Dh(X_old) @ Lambda_old + Dg(X_old) @ Mu_old); # direction for X
        dLambda = h(X_old); # direction for Lambda
        dMu = g(X_old); # direction for Mu

        X = X_old + alpha * dX;
        Lambda = Lambda_old + beta * dLambda;
        Mu = Mu_old + gamma * dMu;
        indx_limits = (Mu < 0);
        Mu[indx_limits] = 0;

        # # # Projection onto the box constraints of X (causes numerical instability):
        # indx_limits = (X < X_lower);
        # X[indx_limits] = X_lower[indx_limits];
        # indx_limits = (X > X_upper);
        # X[indx_limits] = X_upper[indx_limits];

        W = np.concatenate((X, Lambda, Mu), axis=0);
        progress_x[:, [iter_no]] = X;
        progress_y[0, [iter_no]] = func(X);

        if (np.linalg.norm(h(X)) < epsilon and np.linalg.norm(g(X)) < epsilon):
            print('Tolerance in h(X) and g(X) is reached in %d iterations, exit..' % (iter_no));
            break;

        # if (np.linalg.norm(W - W_old) < tolerance_x * np.linalg.norm(W_old)):
        #     print('Tolerance in X is reached in %d iterations, exit..' % (iter_no));
        #     break;

        # if (np.abs(progress_y[0, [iter_no]] - progress_y[0, [iter_no - 1]]) < tolerance_y * np.abs(progress_y[0, [iter_no - 1]])):
        #     print('Tolerance in Y is reached in %d iterations, exit..' % (iter_no));
        #     break;

        X_old = X;
        Lambda_old = Lambda;
        Mu_old = Mu;
        W_old = W;

    report = {'N_iter_max' : N_iter_max, 'iter_no' : iter_no, 'X0' : X0, 'X' : X, 'progress_x' : progress_x, 'progress_y' : progress_y};
    return (X, report);


# this is an experimental script to determine where the constants come from

import numpy as np

def weights(n):
    # Double Exponential Formulas for Numerical Integration By Hidetosi TAKAHASI* and Masatake MORI (3-3)
    # Integration(f(x), -1, 1) ~=
    # pi/2 * h * sum(f(tanh(pi/2 * sinh(n*h)))*(cosh(n*h)/ (cosh(pi/2 * sinh(n*h)))^2 ), n, -oo, oo)

    # so the ABCISSAS are:
    # tanh(pi/2 * sinh(n*h)

    # and the WEIGHTS are:
    # pi/2 * h *(cosh(n*h)/ (cosh(pi/2 * sinh(n*h)))^2

    # John D. Cook uses -oo = -3 and oo = 3

    u = np.linspace(-3, 3, n)
    h = u[1] - u[0]
    x = np.tanh(np.pi / 2 * np.sinh(u))
    w = np.pi / 2.0 * h * (np.cosh(u) / ((np.cosh(np.pi / 2 * np.sinh(u))) ** 2))
    return x, w

# and 1st layer has 7 fn cals so
n = 7
print weights(n)[0][n / 2:]
print weights(n)[1][n / 2:]

# and 2st layer has 6 fn cals more
n += 6
print weights(n)[0][n / 2:]
print weights(n)[1][n / 2:]

# and 3st layer has 12 fn cals more
n += 12
print weights(n)[0][n / 2:]
print weights(n)[1][n / 2:]

def test_with_sympy(n):
    import sympy
    a, b, c, x = sympy.symbols("a, b, c, x")
    f = a * x ** 2 + b * x + c
    print sum([w * f.subs(x, xi) for xi, w in zip(*weights(n))])
    print sympy.integrate(f, (x, -1, 1))

for i in range(1, 5):
    n = 2 ** i + 1
    o_x = weights(n)[0][n / 2:]
    o_w = weights(n)[1][n / 2:]
    print "o_x", n, o_x
    print "o_w", n, o_w
    n = 2 ** (i + 1) + 1
    n_x = weights(n)[0][n / 2:]
    n_w = weights(n)[1][n / 2:]
    print "n_x", n, n_x
    print "n_w", n, n_w
    on_x = n_x[::2]
    on_w = n_w[::2]
    print "on_x", n, on_x
    print "on_w", n, on_w
    print "on_w/o_w", n, on_w / o_w
    # on_w/o_w is a constant so we do not need to hold the old f calls
    print


def bubble_sort(data):
    data_len = data.shape[0]
    for i in range(data_len - 1):
        for j in range(data_len - i - 1):
            if data[j] > data[j + 1]:
                data[j], data[j + 1] = data[j + 1], data[j]
            yield data

def selection_sort(data):
    data_len = data.shape[0]
    for i in range(data_len - 1):
        min_idx = i
        for j in range(i + 1, data_len):
            if data[j] < data[min_idx]:
                min_idx = j
        data[i], data[min_idx] = data[min_idx], data[i]
        yield data

def insertion_sort(data):
    data_len = data.shape[0]
    for i in range(1, data_len):
        anchor = data[i]
        j = i - 1
        while j >= 0 and anchor < data[j]:
            data[j + 1] = data[j]
            j -= 1
            yield data
        data[j + 1] = anchor
        yield data

def shell_sort(data):
    data_len = data.shape[0]
    groups = data_len // 2
    while groups != 0:
        for group_idx in range(groups):
            # group insertion sort
            for i in range(group_idx + groups, data_len, groups):
                anchor = data[i]
                j = i - groups
                while j >= group_idx and anchor < data[j]:
                    data[j + groups] = data[j]
                    j -= groups
                    yield data
                data[j + groups] = anchor
                yield data
        groups //= 2

def merge_cost(data):
    data_len = data.shape[0]
    data_tmp = data.copy()
    interval = 1
    while interval < data_len:
        for start in range(0, data_len, interval * 2):
            low = start
            mid = min(start + interval, data_len)
            end = min(start + 2 * interval, data_len)
            point_left = low
            point_right = mid
            while point_left < mid and point_right < end:
                if data[point_left] < data[point_right]:
                    data_tmp[low] = data[point_left]
                    point_left += 1
                else:
                    data_tmp[low] = data[point_right]
                    point_right += 1
                low += 1

            while point_left < mid:
                data_tmp[low] = data[point_left]
                point_left += 1
                low += 1

            while point_right < end:
                data_tmp[low] = data[point_right]
                point_right += 1
                low += 1

        data = data_tmp.copy()
        interval *= 2

        yield data

def quick_sort(data):
    data_len = data.shape[0]
    range_stack = [range(0, data_len)]
    while range_stack:
        unsort_range = range_stack.pop()
        if len(unsort_range) <= 1:
            continue
        start, end = unsort_range[0], unsort_range[-1]
        left, right = start+1, end
        anchor = data[start]
        while left < right:
            while left < right and data[right] >= anchor:
                right -= 1
            while left < right and data[left] < anchor:
                left += 1
            data[left], data[right] = data[right], data[left]
        if data[left] < data[start]:
            data[left], data[start] = data[start], data[left]

        yield data
        range_stack.append(range(start, left))
        range_stack.append(range(left + 1, end + 1))

if __name__ == "__main__":
    import numpy as np

    data = np.array([0, 1, 2, -1, -5, -3, 5, 2, 7])
    res = quick_sort(data)
    print(data)
    for each in res:
        print(each)


'''
Utility functions for "Data Mining for Business Analytics: Concepts, Techniques, and
Applications in Python"

(c) 2019 Galit Shmueli, Peter C. Bruce, Peter Gedeck
'''
import math
import numpy as np
from sklearn.metrics import r2_score, mean_squared_error
from sklearn.metrics import confusion_matrix, accuracy_score

def adjusted_r2_score(y_true, y_pred, model):
    """ calculate adjusted R2
    Input:
        y_true: actual values
        y_pred: predicted values
        model: predictive model
    """
    n = len(y_pred)
    p = len(model.coef_)
    if p >= n - 1:
        return 0
    r2 = r2_score(y_true, y_pred)
    return 1 - (1 - r2) * (n - 1) / (n - p - 1)

def AIC_score(y_true, y_pred, model=None, df=None):
    """ calculate Akaike Information Criterion (AIC)
    Input:
        y_true: actual values
        y_pred: predicted values
        model (optional): predictive model
        df (optional): degrees of freedom of model

    One of model or df is requried
    """
    if df is None and model is None:
        raise ValueError('You need to provide either model or df')
    n = len(y_pred)
    p = len(model.coef_) + 1 if df is None else df
    resid = np.array(y_true) - np.array(y_pred)
    sse = np.sum(resid ** 2)
    constant = n + n * np.log(2 * np.pi)
    return n * math.log(sse / n) + constant + 2 * (p + 1)

def BIC_score(y_true, y_pred, model=None, df=None):
    """ calculate Schwartz's Bayesian Information Criterion (AIC)
    Input:
        y_true: actual values
        y_pred: predicted values
        model: predictive model
        df (optional): degrees of freedom of model
    """
    aic = AIC_score(y_true, y_pred, model=model, df=df)
    p = len(model.coef_) + 1 if df is None else df
    n = len(y_pred)
    return aic - 2 * (p + 1) + math.log(n) * (p + 1)

def regressionSummary(y_true, y_pred):
    """ print regression performance metrics

    Input:
        y_true: actual values
        y_pred: predicted values
    """
    y_true = _toArray(y_true)
    y_pred = _toArray(y_pred)
    y_res = y_true - y_pred
    metrics = [
        ('Mean Error (ME)', sum(y_res) / len(y_res)),
        ('Root Mean Squared Error (RMSE)', math.sqrt(mean_squared_error(y_true, y_pred))),
        ('Mean Absolute Error (MAE)', sum(abs(y_res)) / len(y_res)),
    ]
    if all(yt != 0 for yt in y_true):
        metrics.extend([
            ('Mean Percentage Error (MPE)', 100 * sum(y_res / y_true) / len(y_res)),
            ('Mean Absolute Percentage Error (MAPE)', 100 * sum(abs(y_res / y_true) / len(y_res))),
        ])
    fmt1 = '{{:>{}}} : {{:.4f}}'.format(max(len(m[0]) for m in metrics))
    print('\nRegression statistics\n')
    for metric, value in metrics:
        print(fmt1.format(metric, value))

def _toArray(y):
    y = np.asarray(y)
    if len(y.shape) == 2 and y.shape[1] == 1:
        y = y.ravel()
    return y

def classificationSummary(y_true, y_pred, class_names=None):
    """ Print a summary of classification performance

    Input:
        y_true: actual values
        y_pred: predicted values
        class_names (optional): list of class names
    """
    confusionMatrix = confusion_matrix(y_true, y_pred)
    accuracy = accuracy_score(y_true, y_pred)

    print('Confusion Matrix (Accuracy {:.4f})\n'.format(accuracy))

    # Pretty-print confusion matrix
    cm = confusionMatrix

    labels = class_names
    if labels is None:
        labels = [str(i) for i in range(len(cm))]

    # Convert the confusion matrix and labels to strings
    cm = [[str(i) for i in row] for row in cm]
    labels = [str(i) for i in labels]

    # Determine the width for the first label column and the individual cells
    prediction = 'Prediction'
    actual = 'Actual'
    labelWidth = max(len(s) for s in labels)
    cmWidth = max(max(len(s) for row in cm for s in row), labelWidth) + 1
    labelWidth = max(labelWidth, len(actual))

    # Construct the format statements
    fmt1 = '{{:>{}}}'.format(labelWidth)
    fmt2 = '{{:>{}}}'.format(cmWidth) * len(labels)

    # And print the confusion matrix
    print(fmt1.format(' ') + ' ' + prediction)
    print(fmt1.format(actual), end='')
    print(fmt2.format(*labels))

    for cls, row in zip(labels, cm):
        print(fmt1.format(cls), end='')
        print(fmt2.format(*row))


"""
    Code to simulate the Monty Hall Problem (MHP) (https://en.wikipedia.org/wiki/Monty_Hall_problem) and verify winning probabilities
    Creator: Aditya Basu

    Run the code with: python3 MHP.py <NUMBER OF SIMULATIONS>
    Recommended NUMBER OF SIMULATIONS: 2500-3000 (although the more the merrier)
    Approximate run-time for 2500-3000 simulations: 1 minute

    We also verify the following facts (using a plot):
    (i) probability of winning if you switch = 2/3
    (ii) probability of winning if you stick = 1/3

    Mathematical insight:
    Assume (#winning ratio after switching) as a Bernoulli random variable with p = 2/3
    or,(#winning ratio after sticking) as a Bernoulli random variable with p = 1/3
    The results we get are in accordance with the Law of Large Numbers
    i.e with very high number of simulations, the winning ratio does converge to its expected value
"""

from numpy import random
import matplotlib.pyplot as plt
from tqdm import tqdm
import sys

def DidYouWin(prizeIn: int, yourChoice: int, switch: bool):
    """
        Returns: 1 if you win the game
        Returns: 0 if you lose the game
    """
    # list of all doors
    ALL = list(range(1, 4))
    # canShow contains the doors which can be shown
    canShow = [num for num in ALL if (num != prizeIn) and (num != yourChoice)]
    show = random.choice(canShow)  # Door which is shown
    # If you switch (choose the other door)
    if switch:
        newChoice = [num for num in ALL if (num != show) and (num != yourChoice)][0]
        return int(newChoice == prizeIn)
    # If you stick (remain with the same door)
    else:
        return int(yourChoice == prizeIn)

def simulate_switch(num_sims: int):
    """
        Runs the simulation <num_sims> times where we always switch
        Returns: the ratio of simulations where we won after switching
    """
    # Running the simulation and counting the number of wins after switching
    won: int = 0
    for i in range(num_sims):
        # The door which contains the prize
        prizeIn: int = random.randint(1, 4)
        # You choose a door
        yourChoice: int = random.randint(1, 4)
        won += DidYouWin(prizeIn, yourChoice, True)
    win_ratio = won/num_sims
    return win_ratio

def simulate_stick(num_sims: int):
    """
        Runs the simulation <num_sims> times where we always stick
        Returns: the ratio of simulations where we won after sticking
    """
    # Running the simulation and counting the number of wins after sticking
    won: int = 0
    for i in range(num_sims):
        # The door which contains the prize
        prizeIn: int = random.randint(1, 4)
        # You choose a door
        yourChoice: int = random.randint(1, 4)
        won += DidYouWin(prizeIn, yourChoice, False)
    win_ratio = won/num_sims
    return win_ratio

if __name__ == "__main__":
    assert len(sys.argv) == 2, "Run with python3 MHP.py <NUMBER OF SIMULATIONS>"

    num_sims = int(sys.argv[1])
    switch = dict()
    stick = dict()
    # Simulating
    for i in tqdm(range(1, num_sims+1)):
        switch[i] = simulate_switch(i)
        stick[i] = simulate_stick(i)
    # Plotting
    keys = list(range(1, num_sims+1))
    switchlist = list(switch.values())
    sticklist = list(stick.values())

    plt.figure(figsize=(20, 6))
    plt.plot(keys, switchlist, label = "After switching")
    plt.plot(keys, sticklist, label = "After sticking")
    plt.axhline(y = 1/3, color = 'k', linestyle = '-')
    plt.axhline(y = 2/3, color = 'k', linestyle = '-')
    plt.ylabel("Winning probability",fontweight = 'bold', fontsize = 14, fontname = 'Arial')
    plt.xlabel("Number of simulations",fontweight = 'bold', fontsize = 14, fontname = 'Arial')
    plt.suptitle("Monty Hall Problem - Winning Probability Convergence", fontweight = 'bold', fontsize = 20, fontname = 'Arial')
    plt.legend(loc = 'upper right')
    plt.show()


import numpy as np

def Distance(X,Y):
    i = 0
    total = 0
    while(i < len(X)):
        total = total + pow((X[i] - Y[i]),2)
        i = i + 1
    return total**2

def KNN_test(X_train,Y_train,X_test,Y_test,K):
    i = 0
    all_distances = []
    #This gets all the distances and stores it into all_distances
    while (i < len(X_test)):
        j = 0
        distances = []
        while (j < len(X_train)):
            distances.append([Distance(X_train[j],X_test[i]),j])
            j = j + 1
        distances.sort()
        all_distances.append(distances[0:K])
        i = i + 1

    i = 0
    all_labels = []
    #This gets all the labels and puts them in all_labels
    while(i < len(X_test)):
        j = 0
        label = 0
        while (j <  K):
            label = label  + Y_train[all_distances[i][j][1]]
            j = j +1
        if(label != 0):
            label = label/(abs(label))
            all_labels.append(label)
        else :
            all_labels.append(1)
        i = i + 1

    i = 0
    num_Correct = 0
    while(i < len(X_test)):
        if (all_labels[i] == Y_test[i] ):
            num_Correct = num_Correct + 1
        i = i + 1
    accuracy = num_Correct/(len(X_test))
    return accuracy

def choose_K(X_train,Y_train,X_val,Y_val):
    best_acc = 0
    k = 0
    i = 1
    while(i < len(X_train)):
        acc = KNN_test(X_train,Y_train,X_val,Y_val, i)
        if(acc > best_acc):
            best_acc = acc
            k = i
        i = i + 2
    return k, best_acc

if __name__ == '__main__':

  X_train = np.array([[1,5], [2,6], [2,7], [3,7], [3,8], [4,8],[5,1], [5,9], [6,2], [7,2], [7,3], [8,3], [8,4], [9,5]])

  Y_train = np.array([[-1],[-1],[1],[-1],[1],[-1],[1],[-1],[1],[-1], [1],[-1], [1],[1]])

  X_test = np.array([[1,1], [2,1], [0,10], [10,10], [5,5], [3,10], [9,4], [6,2], [2,2], [8,7]])
  Y_test = np.array([[1], [-1], [1], [-1], [1], [-1], [1], [-1], [1], [-1]])
  # print(Y_test)

  # acc = KNN_test(X_train,Y_train,X_test,Y_test,5)
  # print("The accuracy is: ", acc*100, "%")
  K1, acc1 = choose_K(X_train,Y_train, X_test, Y_test)
  print("For this data, the best K is: ", K1, "with accuracy ", acc1*100, "%")


import math
import warnings

import numpy
import numpy as np

def haar_transform(x: numpy.ndarray, h: numpy.ndarray = None, orthogonal=True, shape_conform_mode='rpad', shape_conform_pad_value='median'):
    """
    Perform 1-d discrete Haar wavelet transform.
    :param x: input signal with shape (n_signals, n_samples_per_signal)
    :param h: (optional) Haar transformation matrix. Provide cached matrix for performance.
    :param orthogonal: orthogonal transformation is likely slower (default True)
    :param shape_conform_mode: how to handle data lengths that aren't powers of two. Can be 'rtrim', 'ltrim', 'lrtrim', 'rpad', 'lpad', 'lrpad' (default 'rpad')
    :param shape_conform_pad_value: what value to use if shape_conform_mode is '*pad'. Can be 'median', 'mean', or 'constant' (use zeros). (default 'median')
    :return:
        y - transformed data
        h - haar matrix used for transformation
    """

    x = np.asarray(x, dtype=np.float64)

    n_samples = x.shape[1]

    # Ensure n_samples is power of two by padding data
    if not _is_power_of_two(n_samples):
        x = _conform_shape_to_pow2(x, axis=1, mode=shape_conform_mode, pad_with=shape_conform_pad_value)
        n_samples = x.shape[1]

    # Calculate Haar matrix
    if h is None:
        h = haar_matrix(n_samples, orthogonal)

    y = x @ h.T

    return y, h

def haar_matrix(n=2, orthogonal=True):
    """
    Creates an n x n (optionally orthonormal) Haar transformation matrix. Where n is a power of 2.

    k = log2(n)

    H(k=1) = [[1, 1]
              [1, -1]]

    H(k+1) = [H(k) kron [1, 1]
              2**(k/2) * I(2**k) kron [1, -1]]

    For an orthogonal matrix, H(k) then needs to be normalized by column Euclidean norms.

    Otherwise no normalization is needed and the scaling term 2**(k/2) can be omitted.
    This is equivalent to omitting division by sqrt(2) after calculating pairwise sums and diffs.

    From:
        Steeb, W. H., Hardy, Y., & Stoop, R. (2003). Discrete wavelets and perturbation theory.
        JOURNAL OF PHYSICS-LONDON-A MATHEMATICAL AND GENERAL, 36(24), 6807-6812.
    :param n: matrix size, expected to be power of 2. Otherwise n = 2**np.floor(np.log2(n))
    :param orthogonal: True to request an orthogonal Haar matrix (slower). (default True)
    :return:
    nxn Haar matrix.
    """
    k = int(math.log2(n))
    h = _haar_matrix_recursive(k, orthogonal)

    # Normalize columns (divide by column Euclidean norm)
    if orthogonal:
        h /= np.linalg.norm(h, axis=0)
    return h

def _haar_matrix_recursive(k, orthogonal=True):
    if k > 1:
        h = _haar_matrix_recursive(k - 1, orthogonal)
        # Calculate the top and bottom parts
        h_t = np.kron(h, [1, 1])
        h_b = np.kron(np.eye(2 ** (k - 1)), [1, -1])
        # The orthogonal Haar matrix requires scaling the bottom half
        if orthogonal:
            h_b *= 2 ** ((k - 1) / 2)
        h = np.vstack((h_t, h_b))
    else:
        h = np.asarray([[1, 1], [1, -1]])

    return h

def _is_power_of_two(n: int):
    return n != 0 and (n & (n - 1) == 0)

def _conform_shape_to_pow2(data: np.ndarray, axis=0, mode='lrtrim', pad_with='median'):
    """
    Ensure data length is power of two along a specific axis.

    :param data: input data
    :param axis: (default 0)
    :param mode: 'rtrim', 'ltrim', 'lrtrim', 'rpad', 'lpad', 'lrpad' (default 'lrtrim')
    :param pad_with: 'median', 'mean', or 'constant' (use zeros). See numpy.pad() (default 'median')
    :return:
    View of sliced array (trim mode) or copy of padded array (pad mode)
    """

    data_length = data.shape[axis]
    if data_length <= 1:
        raise ValueError(f"Data length expected to be 2 or higher, is {data_length}")
    elif _is_power_of_two(data_length):
        return data
    else:
        if mode in ('rtrim', 'ltrim', 'lrtrim'):
            desired_length = 2 ** math.floor(math.log2(data_length))
        elif mode in ('lpad', 'rpad', 'lrpad'):
            desired_length = 2 ** math.ceil(math.log2(data_length))
        else:
            raise ValueError(
                f"Unrecognized mode: '{mode}', expected 'rtrim', 'ltrim', 'lrtrim', 'lpad', 'rpad', or 'lrpad'")

    if mode == 'rtrim':
        data = data.take(range(0, desired_length), axis=axis)
    elif mode == 'ltrim':
        data = data.take(range(data_length - desired_length, data_length), axis=axis)
    elif mode == 'lrtrim':
        n_ltrim = (data_length - desired_length) // 2
        n_rtrim = data_length - desired_length - n_ltrim
        data = data.take(range(n_ltrim, data_length - n_rtrim), axis=axis)
    elif mode == 'rpad':
        pw = [(0, 0)] * data.ndim
        pw[axis] = (0, desired_length - data_length)
        data = np.pad(data, pad_width=pw, mode=pad_with)
    elif mode == 'lpad':
        pw = [(0, 0)] * data.ndim
        pw[axis] = (desired_length - data_length, 0)
        data = np.pad(data, pad_width=pw, mode=pad_with)
    elif mode == 'lrpad':
        n_lpad = (desired_length - data_length) // 2
        n_rpad = desired_length - data_length - n_lpad
        pw = [(0, 0)] * data.ndim
        pw[axis] = (n_lpad, n_rpad)
        data = np.pad(data, pad_width=pw, mode=pad_with)

    assert data.shape[axis] == desired_length
    return data


from sage.all import var, sin, Matrix

from ..riemannian import RiemChart

def sphere_polar(n: int, variables: list=None) -> RiemChart:
    """
    :param n: dimension
    :param variables: Coordinates to use, optional, defaults to r, t1, t2, ... t{n-1}
    :return: a Riemmanian chart for a round sphere in polar coordinates
    :raises IndexError: if the provided list of variables has wrong length

    Create a Riemannian Chart for an n-dimensional radius 1 sphere in polar coordinates,
    misses a codimension 2 totally geodesic sphere
    """

    if variables:
        if len(variables) != n:
            raise IndexError('Number of variables is not the same as the dimension')
    else:
        var_names = ['r'] + [f't{i}' for i in range(1, n)]
        variables = var(' '.join(var_names))

    metric = [[0 for _ in range(n)] for _ in range(n)]

    for i in range(n):
        gii = 1
        for j in range(i):
            gii *= sin(variables[j]) ** 2
        metric[i][i] = gii

    return RiemChart(n, variables, metric)

def sphere_conformal(n: int, variables: list=None) -> RiemChart:
    """
    :param n: dimension
    :param variables: Coordinates to use, optional, defaults to x1, x2, ...
    :return: a Riemannian chart for a unit sphere in conformal coordinates
    :raises IndexError: if the provided list of variables has wrong length

    Create a Riemannian Chart for an n-dimensional radius 1 sphere in conformal coordinates,
    misses one point on the sphere
    """

    if variables:
        if len(variables) != n:
            raise IndexError('Number of variables is not the same as the dimension')
    else:
        var_names = [f'x{i + 1}' for i in range(n)]
        variables = var(' '.join(var_names))

    # noinspection PyUnresolvedReferences
    metric = Matrix.identity(n) * (4 / (1 + sum(v ** 2 for v in variables)) ** 2)

    return RiemChart(n, variables, metric)

def hyperbolic_half_space(n: int, variables: list=None) -> RiemChart:
    """
    :param n: dimension
    :param variables: Coordinates to use, optional, defaults to x1, x2, ...
    :return: a Riemannian chart for the upper half-space model of a hyperbolic space
    :raises IndexError: if the provided list of variables has wrong length

    Create a Riemannian Chart for an n-dimensional hyperbolic space with sectional curvature -1,
    uses the half-space model, with the last coordinate positive.
    """
    if variables:
        if len(variables) != n:
            raise IndexError('Number of variables is not the same as the dimension')
    else:
        var_names = [f'x{i + 1}' for i in range(n)]
        variables = var(' '.join(var_names))

    # noinspection PyUnresolvedReferences
    metric = Matrix.identity(n) * (1 / variables[-1] ** 2)

    return RiemChart(n, variables, metric)


import numpy as np
import pandas as pd
import sys
np.set_printoptions(threshold=sys.maxsize)

def calculate_loss(x, y, w):
    y_pred = np.sign(np.matmul(x, w))
    zeros = np.zeros(shape = (len(y_pred), 1))
    pred_hadamard_label = np.multiply(-y,y_pred)
    loss = (np.sum(np.maximum(zeros, pred_hadamard_label)))/len(y)
    return loss

def online_perceptron(x_train, y_train, x_val, y_val, n_iters):
    y_train = y_train.reshape(len(y_training),1)
    weight_array = []
    training_loss_array = []
    val_loss_array = []

    w = np.zeros(shape = (x_train.shape[1], 1))

    for i in range(n_iters + 1):
        for x, y in zip(x_train, y_train):
            x = x.reshape(1, x_training.shape[1])
            y = y.reshape(1,1)
            y_pred = np.matmul(x,w)
            # print(y_pred.shape)
            if -(y_pred * y) <= 0:
                w = w + np.matmul(y,x).T
                # print(w)
        training_loss = calculate_loss(x_train, y_train, w)
        training_loss_array.append(training_loss)
        print("Iterations " + str(i)+ " training loss " + str(training_loss))

        val_loss = calculate_loss(x_val, y_val, w)
        val_loss_array.append(val_loss)
        print("Iterations " + str(i)+ " validation loss " + str(val_loss))

        weight_array.append(w)

    return weight_array, training_loss_array, val_loss_array

x_training = np.load('/nfs/stak/users/lehoang/Downloads/CS534_I2/data/interim/x_training.npy')
y_training = np.load('/nfs/stak/users/lehoang/Downloads/CS534_I2/data/interim/y_training.npy')
x_val = np.load('/nfs/stak/users/lehoang/Downloads/CS534_I2/data/interim/x_val.npy')
y_val = np.load('/nfs/stak/users/lehoang/Downloads/CS534_I2/data/interim/y_val.npy')

n_iters = 15

# w, training_loss, val_loss = online_perceptron(x_training, y_training, x_val, y_val, n_iters)
# x_training = np.load('/nfs/stak/users/lehoang/Downloads/CS534_I2/data/interim/x_training.npy')
x_training[1]

def predict(x, w):
    y = np.sign(np.matmul(x,w))
    return y


import numpy as np
from scipy import linalg
A = np.array([[2,1],
              [3,-1],
              [0,4]])
v = np.array([-2,1])

# 1
print "Question 1"
# a
print "A.v = " + str(A.dot(v))

# b
print "{}.{} = {}".format(A[1:2,:], v, A[1:2,:].dot(v))

# c
# print A[:, 2:4].dot(v)

# 2
print ""
print "Question 2"
i2 = np.identity(2)
v2 = v
print "{}.{} = {}".format(i2, v2, i2.dot(v2))

# 3
print ""
print "Question 3"
t3 = np.array([[1,0,0],[0,1,0],[0,0,0]])
v3 = np.array([1,2,3])
print "{}.{} = {}".format(t3, v3, t3.dot(v3))

# 4
print ""
print "Question 4"
t4 = np.array([[0,1,0,0],[1,0,0,0],[0,0,1,0],[0,0,0,1]])
v4 = np.array([1,2,3,4])
print "{}.{} = {}".format(t4, v4, t4.dot(v4))

# 8
print ""
print "Question 8"
t8 = np.array([[3,0,0],[0,3,0],[0,0,3]])
v8 = np.array([2,2,2])
print "{}.{} = {}".format(t8, v8, t8.dot(v8))

# 9
print ""
print "Question 9"
t9 = np.array([[3,0,0],[0,5,0],[0,0,1]])
v9 = np.array([2,2,2])
print "{}.{} = {}".format(t9, v9, t9.dot(v9))

# 10
print ""
print "Question 10"
t10 = np.array([[0,1,0],[-1,0,0],[0,0,1]])
v10_1 = np.array([1,0,0])
v10_2 = np.array([0,1,0])
v10_3 = np.array([0,0,1])

print "{}.{} = {}".format(t10, v10_1, t10.dot(v10_1))
print "{}.{} = {}".format(t10, v10_2, t10.dot(v10_2))
print "{}.{} = {}".format(t10, v10_3, t10.dot(v10_3))
print "This rotation matrix is rotating the x and y elements by 90 degrees"

# 11
print ""
print "Question 11"
t11 = np.array([[1,0,0],
                [0,0,-1],
                [0,1,0]])
v11_1 = np.array([1,0,0])
v11_2 = np.array([0,1,0])
v11_3 = np.array([0,0,1])

print "{}.{} = {}".format(t11, v11_1, t11.dot(v11_1))
print "{}.{} = {}".format(t11, v11_2, t11.dot(v11_2))
print "{}.{} = {}".format(t11, v11_3, t11.dot(v11_3))

# 12, 13, 14, 15, 16
print ""
print "Question 12, 13, 14, 15, 16"
angleDeg12 = 30
q12 = angleDeg12 * (np.pi / 180) #30 degrees to radians
t12 = np.array([[np.cos(q12),np.sin(q12),0],[-np.sin(q12),np.cos(q12),0],[0,0,1]])
v12_1 = np.array([1,0,0])
dot12 = t12.dot(v12_1)
mag12 = (dot12[0]**2 + dot12[1]**2 + dot12[2]**2)**(1/2)
print "Angle degrees, q12 = {}".format(angleDeg12)
print "{}.{} = {}".format(t12, v12_1, dot12)
print "This rotation matrix is rotating the x and y elements by q12 degrees"
print "magnitude of vector = {} = {}".format("SQRT(a^2 + b^2 + c^2)",mag12)
print "this matrix will rotate in x, y by given number of degrees in the negative direction"

# 17
print ""
print "Question 17"
A17 = np.array([[-2,4],
                [0,3]])
B17 = np.array([[5,-3],
                [-1,-1]])
C17 = A17.dot(B17)
print "{}\t{}\t= {}".format(A17[0], B17[0], C17[0])
print "{}\t{}\t= {}".format(A17[1], B17[1], C17[1])

# 18
print ""
print "Question 18"
A18 = np.array([[-2,4],
                [0,3]])
B18 = np.array([[5,-3],
                [-1,-1]])
C18 = B18.dot(A18)
print "{}\t  {} = {}".format(B18[0], A18[0], C18[0])
print "{}\t. {}   = {}".format(B18[1], A18[1], C18[1])

# 17
print ""
print "Question 17"
A19 = np.array([[-2,4],
                [0,3]])
B19 = np.array([[5,-3],
                [-1,-1]])
C19 = np.array([[-5,-1],
                [-3,2]])
D19 = A19.dot(B19+C19)
print "{}\t   | {}\t \t{} | = {}".format(A19[0], B19[0], C19[0], D19[0])
print "{}  \t . | {}\t+\t{} | = {}".format(A19[1], B19[1], C19[1], D19[1])

# 22
print ""
print "Question 22"
angleDeg22 = 30
q22 = angleDeg22 * (np.pi / 180) #30 degrees to radians
t22 = np.array([[np.cos(q22),np.sin(q22),0],[-np.sin(q22),np.cos(q22),0],[0,0,1]])
v22_1 = np.array([1,0,0])
dot22 = t22.dot(v22_1)
print "Angle degrees, q22 = {}".format(angleDeg22)
print "{}.{} = {}".format(t22, v22_1, dot22)
print ""

angleDeg22b = -30
q22b = angleDeg22b * (np.pi / 180) #30 degrees to radians
t22b = np.array([[np.cos(q22b),np.sin(q22b),0],[-np.sin(q22b),np.cos(q22b),0],[0,0,1]])
dot22b = t22b.dot(dot22)
print "Angle degrees, q22b = {}".format(angleDeg22b)
print "{}.{} = {}".format(t22b, dot22, dot22b)
print ""

dot22c = t22.dot(t22b)
print "{}\t {} = {}".format(t22b[0], t22b[0], dot22c[0])
print "{}\t {} = {}".format(t22b[1], t22b[1], dot22c[1])
print "{}\t\t\t\t.{}\t\t\t    = {}".format(t22b[2], t22b[2], dot22c[2])

print "rotation matrix * rotation backwards = identity matrix"


import numpy as np

print(np.sum([1, 2, 3, 4, 5]))
print()

idades = np.random.randint(24, 50, size=30)
print(idades)
print()

# Media: soma todos e divide pela qtd de elementos
print(int(idades.mean()))

# Mediana: ordena a lista e pega o valor do meio.
# Se a lista conter qtd impar de elementos, pega os dois do meio, soma e divide por 2
print(np.median(idades))
print()

# Operações vetorizadas
idades1 = np.random.randint(24, 50, size=31).astype(np.int)
idades2 = np.random.randint(24, 50, size=31).astype(np.int)

diff = idades1 - idades2
print(diff)

# Media de idade da subtração de idades1 - idades2
# Sabemos que a diferença da media de idades é
print(np.round(diff.mean(), 3))
print()

# Normal: gera números aleatórios com a mesma probabilidade de aparecerem
idades3 = np.random.normal(30, 3, size=10)
print(np.round(idades3, 3))
print()

# Gera array de boleanos com considerando a regra para cada posição
array = np.random.randint(0, 50, size=30).astype(np.int)
v = array > 15
print(v)
print()

# Retorna so os itens que são true
print(array[v])
print()


"""
Suppose a casino invents a new game that you must pay $250 to play. The game
works like this: The casino draws random numbers between 0 and 1, from a
uniform distribution. It adds them together until their sum is greater than 1,
at which time it stops drawing new numbers. You get a payout of $100 each time
a new number is drawn.

For example, suppose the casino draws 0.4 and then 0.7. Since the sum is
greater than 1, it will stop after these two draws, and you receive $200. If
instead it draws 0.2, 0.3, 0.3, and then 0.6, it will stop after the fourth
draw and you will receive $400. Given the $250 entrance fee, should you play
the game?

Specifically, what is the expected value of your winnings?

From:
    http://fivethirtyeight.com/features/
        should-you-pay-250-to-play-this-casino-game
"""

import numpy as np

def trial():
    total = 0
    spins = 0
    while total < 1:
        total += np.random.random()
        spins += 1
    return spins

def main():
    n = 10000000
    dollar_return = (np.mean([trial() for _ in range(n)]))
    return_on_stake = 100 * dollar_return
    print(return_on_stake)

if __name__ == '__main__':
    main()


# Created by YongHua | 12 April 2021 23:10
# Email: yht1e20@soton.ac.uk

import numpy as np
from math import pi, sin, cos, sqrt

def stress_transformation(exx, eyy, exy, theta):
    """Stress transformation for a 2-D stress tensor matrix.

    Takes four input parameters:
    `exx` >>> the exx stress component.
    `eyy` >>> the eyy stress component.
    `exy` >>> the exy stress component.
    `theta` >>> (in degrees) which is the angle of the stress element to be rotated.

    Returns `exx'`, `eyy'` and `exy'`

    * You can choose to include or omit prefixes e.g. MPa which is 1E6, as always take
      care of units and magnitudes!
    """

    # Convert to radians for the sin and cos functions
    thetar = theta / 180 * pi

    rotation_matrix = np.array([
        [cos(thetar)**2, sin(thetar)**2, sin(2*thetar)],
        [sin(thetar)**2, cos(thetar)**2, -sin(2*thetar)],
        [-cos(thetar)*sin(thetar), cos(thetar)*sin(thetar), cos(2*thetar)]
    ])

    old_stress = np.array([exx, eyy, exy])

    new_stress = np.dot(rotation_matrix, old_stress)

    return new_stress

def find_principle(exx, eyy, exy):
    """Finds the principle values `eI` and `eII` of a 2-D stress element.
    Takes three input parameters: `exx`, `eyy` and `exy`. Returns a list of
    principal stresses"""

    invariant_1 = exx + eyy  # trace
    invariant_2 = exx * eyy - exy ** 2  # determinant

    eI = 0.5 * (invariant_1 + sqrt(invariant_1 ** 2 - 4 * invariant_2))
    eII = 0.5 * (invariant_1 - sqrt(invariant_1 ** 2 - 4 * invariant_2))

    return [eI, eII]  # returns a list

def find_maxshear(exx, eyy, exy):
    """Find the maximum shear stress `exy` in 2-D stress element subject to
    planar rotational transformation. Numerical implementation of Mohr's circle.
    Takes three input parameters: `exx`, `eyy` and `exy`. Returns a list of
    coordinates of maximum shear stresses, in the form [centre, ±radius]"""
    eI, eII = find_principle(exx, eyy, exy)
    centre = (eI + eII) / 2  # centre of Mohr's circle, 0.5tr(e)
    radius = abs(eI - centre)  # radius of Mohr's circle
    assert abs(eI - centre) == abs(eII - centre)
    maxshear = [[centre, radius], [centre, -radius]]
    return maxshear

if __name__ == '__main__':
    # example
    help(stress_transformation)
    print(stress_transformation(40, 860, 375, 30))

    help(find_principle)
    print(find_principle(-25, 75, -56))

    help(find_maxshear)
    print(find_maxshear(-60, -40, 35))


import numpy as np

def gauss_elimination(A, B, triflag=False):
    '''
    gauss_elimination.m

    Use Gauss Elimination to solve a set of linear equations A*x = b
    Uses partial pivoting to avoid singularities

    @arg
    A       - n x n numpy.ndarray
              Matrix that represents the A matrix. Must be square
    B       - n x m numpy.ndarray
              Matrix that represents the set of B vectors
    triflag - bool (optional)
              Flag to return the tridiagonal matrix of A. Default to false

    @return
    x_soln  - n x o numpy.ndarray
              Solution matrix to the system of linear equations
    A_aug   - n x n numpy.ndarray
              Upper Triangular Augmented form of input A

    @dependencies
    python 3.6.0
    numpy

    @author: Matt Marti
    @date: 2019-06-05
    '''

    # Raise Type Error if input is not a numpy array
    if type(A) != np.ndarray:
        raise TypeError("Argument 'A' is not of type numpy.ndarray");
    #
    if type(B) != np.ndarray:
        raise TypeError("Argument 'B' is not of type numpy.ndarray");
    #

    # Dimension of linear system
    n = A.shape[0]; # Number of A rows
    assert A.shape[0] == A.shape[1], "Argument A is not square matrix";
    assert B.shape[0] == A.shape[1], \
        "Argument 'B' dimension does not match dimension of argument 'A'";
    #

    # Copy A and B to local memory
    A_aug = A.copy() + 0.0; # TODO: Figure out how to convert to float
    B_aug = B.copy() + 0.0; # TODO: Figure out how to convert to float

    # Gauss Elimination Main Loop
    for i in range( 0, n-1 ): # Iterate by rows

        # Pivot zero-valued diagonal elements
        if not A_aug[i,i]:
            k = i + 1;
            successflag = False;
            for k in range( i+1, n ):
                if A[k,i]:
                    Aktemp = A_aug[k,:].copy();
                    A_aug[k,:] = A_aug[i,:];
                    A_aug[i,:] = Aktemp.copy();
                    Bktemp = B_aug[k,:].copy();
                    B_aug[k,:] = B_aug[i,:];
                    B_aug[i,:] = Bktemp.copy();
                    successflag = True;
                    break;
                #
            #
            assert successflag, 'Argument A is singular matrix';
        #

        # Perform gauss elimination on this row
        for j in range( i+1, n ): # Iterate by rows
            if A_aug[j,i]: # If there is a non-zero element
                ajioveraii = A_aug[j,i] / A_aug[i,i];
                A_aug[j,:] = A_aug[j,:] - ajioveraii * A_aug[i,:];
                B_aug[j,:] = B_aug[j,:] - ajioveraii * B_aug[i,:];
                #
            #
        #
    #

    # Back-substitution loop
    x_soln = np.zeros((n,B.shape[1]));
    for k in range( 0, B.shape[1] ):
        for i in range( n-1, -1, -1 ):
            sum = 0;
            for j in range( i+1, n ):
                sum += A_aug[i,j] * x_soln[j,k];
            #
            x_soln[i,k] = ( B_aug[i,k] - sum ) / A_aug[i,i];
        #
    #

    if triflag:
        return x_soln, A_aug;
    else:
        return x_soln;
    #
#


from manimlib.imports import *
import numpy as np
import bats
# slide scene
from manim_reveal import SlideScene
# manim interface with bats
from manimtda import *

import matplotlib

from scipy.spatial import ConvexHull

def gen_circle2(n, r=1.0):
    theta = np.linspace(0, 2*np.pi*(1 - 1./n), n)
    pts = np.array([r*np.cos(theta), r*np.sin(theta)],  dtype=np.float)
    return pts.T

def gen_point_cloud(n, scale=1.0):
	"""
	sample points on the unit square
	"""
	return np.random.uniform(low=-scale, high=scale, size=(n,2))

def get_dimension_filt(C):
	"""
	get dimension filtration data from bats complex
	"""
	spxs = []
	ts = []
	for d in range(C.maxdim() + 1):
		ts.extend([d for _ in range(C.ncells(d))])
		spxs.extend(C.get_simplices(d))

	return spxs, ts

def dimension_filtration_from_bats(C, pts, **kwargs):
	"""
	get manimtda filtration from bats filtration
	"""
	spxs, ts = get_dimension_filt(C)
	return SimplicialFiltration(pts, spxs, ts, **kwargs)

def get_convex_hull(s, pts, color=BLUE, **kwargs):
	"""
	get convex hull of pts[s] as a Manim polygon
	"""
	spts = np.array([pts[i] for i in s])
	hull_2d = ConvexHull(spts[:,:-1])
	P = Polygon(*[spts[i] for i in hull_2d.vertices],
				color=color,
				**kwargs
				)
	P.set_fill(color, opacity=0.2)
	#P.round_corners(0.05)
	#P.scale(1.3)
	return P

def get_convex_hull_barycenter(s, pts):
	"""
	get barycenter of convex hull
	"""
	spts = np.array([pts[i] for i in s])
	hull_2d = ConvexHull(spts[:,:-1])
	hull_pts = spts[hull_2d.vertices]
	return np.mean(hull_pts, axis=0)

class Nerve(SlideScene):
	CONFIG={
        "camera_config":{"background_color":"#F0F1EB"},
        "video_slides_dir":"../video_slides"
    }
	def construct(self):
		# pts = gen_circle2(20, r=2.5)
		# pts = pts + np.random.normal(scale=0.2, size=pts.shape)
		pts = gen_point_cloud(100, scale=2.5)

		# put points into bats
		pbats = bats.DataSet(bats.Matrix(pts))
		# get landmarks
		lbats = bats.greedy_landmarks(pbats, 3, bats.Euclidean(), 0)
		lms = np.array(lbats.data()) # numpy array
		# get cover based on landmarks - assign each point to nearest landmarks
		cover = bats.landmark_eps_cover(pbats, lbats, bats.LInfDist(), 0.7)

		# construct the nerve
		N = bats.Nerve(cover, 2)

		# Fbats = WeakAlphaFiltration(pts)
		Fbats = bats.RipsFiltration(pbats, bats.Euclidean(), 0.0, 2)
		RFC = bats.ReducedFilteredChainComplex(Fbats, bats.F2())

		pts = np.hstack((pts, np.zeros((pts.shape[0], 1))))
		lms = np.hstack((lms, np.zeros((lms.shape[0], 1))))
		bcs = np.array([get_convex_hull_barycenter(s, pts) for s in cover])
		print([len(s) for s in cover])
		print(bcs)

		# this is just for point cloud
		F = filtration_from_bats(Fbats, pts, color=BLACK)
		F.shift(3*RIGHT)

		# construct filtration on Nerve
		FN = dimension_filtration_from_bats(N, bcs, color=RED, tri_opacity=0.4)
		FN.shift(3*RIGHT)

		# this just generates the point cloud
		title = TexMobject(r"\text{Nerve of a Cover }\mathcal{N}(\mathcal{U})", color=BLACK).shift(3*UP)
		t = 0.0
		anim = [FadeIn(title)]
		Ft = F.step_to(t)
		anim.append(FadeIn(Ft))
		self.play(*anim)
		self.slide_break()

		# show the open sets
		line0 = TexMobject(r"\mathcal{U}", r"=\{U_0, U_1, U_2\}", color=BLACK).shift(3*LEFT + 1*UP)
		anim = [FadeIn(line0)]
		hulls = [get_convex_hull(cover[i], pts).shift(3*RIGHT) for i in range(len(cover))]
		anim.extend([FadeInFrom(h, DOWN) for h in hulls])
		self.play(*anim)
		self.slide_break()

		# now let's highlight the barycenters
		line1 = TexMobject(r"U_0, U_1, U_2 \in \mathcal{N}(\mathcal{U})_0", color=BLACK)
		line1.next_to(line0, DOWN)
		anim = [FadeIn(line1)]
		Nt = FN.step_to(0)
		anim.append(FadeIn(Nt))
		self.play(*anim)
		self.slide_break()

		# add 1 simplices of Nerve
		line2 = TexMobject(r"(U_i, U_j) \in \mathcal{N}(\mathcal{U})_1", color=BLACK)
		line2a = TexMobject(r"\text{if }~U_i \cap U_j \ne \emptyset", color=BLACK)
		line2.next_to(line0, DOWN)
		line2a.next_to(line2, DOWN)
		anim = [Transform(line1, line2), FadeIn(line2a)]
		Nt = FN.step_to(1)
		anim.append(FadeIn(Nt))
		self.play(*anim)
		self.slide_break()

		# add 2-simplices of nerve
		line3 = TexMobject(r"(U_i, U_j, U_k) \in \mathcal{N}(\mathcal{U})_2", color=BLACK)
		line3a = TexMobject(r"\text{if }~U_i \cap U_j \cap U_k \ne \emptyset", color=BLACK)
		line3.next_to(line0, DOWN)
		line3a.next_to(line3, DOWN)
		anim = [Transform(line1, line3), Transform(line2a, line3a)]
		Nt = FN.step_to(2)
		anim.append(FadeIn(Nt))
		self.play(*anim)
		self.slide_break()


import numpy as np
import matplotlib.pyplot as plt

# This function improves the solution Ax=b with one Gauss-Seifel step, with the
# result overwriting x. The matrix A is hardcoded and represents the Poisson equation.
def gaussseidel_step(x, b, N):
   #TODO: fill in your code
   #x[i,j] = ...

# This function calculates the resdiuum vector res = b - Ax, for input vectors
# of length N. The output is stored in res.
def calc_residuum(x, b, N, res):
    #TODO: fill in your code
    #res[i,j] = ...

# This function calculates the norm of the vector of length N,
# defined as the usual quadratic vector norm.
def norm_of_residual(res, N):
    sum = np.sum(res**2)
    return np.sqrt(sum)

#This function restricts the NxN mesh stored in 'fine[ ]' to the NNxNN mesh stored in 'coarse[ ]'
def do_restrict(N, fine, NN, coarse):
    #TODO: fill in your code
    #coarse[i,j] = ...

# This function interpolates the the NNxNN mesh stored in 'coarse[ ]' to NxN mesh stored in 'fine[ ]'
def do_prolong(NN, coarse, N, fine):
    #TODO: fill in your code
    #fine[i,j] = ...

# This function carries out a V-cycle using the multigrid approach, down to N=4.
def do_v_cycle(x, b, N):
    gaussseidel_step(x, b, N)

    if N > 4:
        NN = N / 2;

        #allocate some storage for the residual and error on the current and a coarsened mesh */
        res = np.zeros((N,N))
        err = np.zeros((N,N))
        res_coarse = np.zeros((NN,NN))
        err_coarse = np.zeros((NN, NN))

        #calculate the residuum
        calc_residuum(x, b, N, res)

        #restrict the residuum
        do_restrict(N, res, NN, res_coarse)

        #now multiply the residuum on the coarser mesh (our b)
        #with a factor of 4 to take care of the (h) -> (2h) change, and the fact that we do not change A
        res_coarse[:,:] *= 4

        #call the V-cycle again, recursively
        do_v_cycle(err_coarse, res_coarse, NN)

        #now interpolate the error to the finer mesh
        do_prolong(NN, err_coarse, N, err)

        #finally add the error to the current solution
        x[:,:] += err

    gaussseidel_step(x, b, N)

N = 256
steps = 2000
L = 1.0
h = L / N
eta = 0.1 * L
rho0 = 10.0

res = np.zeros((N,N))

#now set-up the density field
x = (np.arange(-N/2,N/2)+0.5)*h
mesh = np.meshgrid(x,x)
r2 = mesh[0] * mesh[0] + mesh[1] * mesh[1]

rho = rho0* np.exp(-r2/(2*eta**2))
sum = np.sum(rho)

#initialize the starting values for phi[] and b[]
rho -= sum/(N**2)
b = 4 * np.pi*h**2*rho
phi = np.zeros((N,N))

#open a file for outputting the residuum values, and then do 2000 Jacobi steps
residuals = np.zeros(steps)
f = file("res_multigrid.txt", "w")
f.write("%d\n"%steps)

for i in np.arange(steps):
    do_v_cycle(phi, b, N)

    calc_residuum(phi, b, N, res)

    r = norm_of_residual(res, N)

    print("iter=%d:  residual=%g\n"%(i, r))
    f.write("%g\n"%r)
    residuals[i] = r

f.close()

plt.semilogy(residuals, label="multigrid")
plt.show()


# A bee walks around on a honeycomb, an infinite tessalating hexagonal grid,
# starting at a fixed hexagon. At each step, the bee moves to one of the six adjacent
# hexagons with equal probability. We'll assume adjacent hexagons are always a
# distance of one unit away from each other.

# directions = [N, NE, SE, S, SW, NW]

import random
import numpy as np
import pandas as pd

def bee(steps):
    distance = 0
    change = 0

    for step in range(1, steps + 1):
        #print("step: {}".format(step))
        if distance == 0: # Movement from the starting point
            change = 1
        else: # Steps outside the starting point
            change = random.choices([-1, 0, 1], [1/6, 2/6, 3/6])[0]
        #print("change: {}".format(change))
        distance += change
        #print("distance: {}".format(distance))
        #print("\n")
    return distance

def simulation(steps, experiments, distquery, mincond):
    distances = []
    expdist = 0
    expdev = 0

    for exp in range(experiments):
        distances.append(bee(steps))
    expdist = np.mean(distances)
    devs = abs(expdist - distances)
    expdev = np.mean(devs)

    condtrues = 0
    querytrues = 0
    for distance in distances:
        if distance >= mincond:
            condtrues += 1
            if distance >= distquery:
                querytrues += 1

    if condtrues != 0:
        probquery = querytrues / condtrues

    print("Based on {} experiments, after {} steps, the expected value of the bee's distance from the starting point is {} with {} expected deviation. The probability that its distance is at least {}, given that it is at least {}, is {}."
    .format(experiments, steps, expdist, expdev, distquery, mincond, probquery))

# After 16 steps, what is the expected value of the bee's distance from the starting hexagon?
# After 16 steps, what is the expected value of the deviation of the bee's distance from the starting hexagon?

# After 64 steps, what is the expected value of the bee's distance from the starting hexagon?
# After 64 steps, what is the expected value of the deviation of the bee's distance from the starting hexagon?

simulation(16, 1000000, 8, 6)
print("\n")
simulation(64, 1000000, 24, 20)

# After 16 moves, what is the probability that the bee is at least A = 8 distance away from the starting hexagon, given it is at least B = 6 distance?
# After 16 moves, what is the probability that the bee is at least A = 24 distance away from the starting hexagon, given it is at least B = 20 distance?

CS fundamentals/Random Walk with Python/DI_random_walk_challenges/bee.py


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Program: Fit peaks with Lorentzian distribution
Version: 20201123
@author: Pranab Das (GitHub: @pranabdas)
data = suv.fit_lorentz(x, y, a='', x0='', gamma='', xmin='', xmax='')
"""

def fit_lorentz(x, y, a='', x0='', gamma='', xmin='', xmax='', num=1000):
    import numpy as np
    from scipy import optimize

    def lorentz(x, a, x0, gamma):
        return a*gamma**2/(np.pi*gamma*((x - x0)**2 + gamma**2))

    if xmin:
        xmin = float(xmin)
        xmin_id = np.where(abs(x-xmin)==min(abs(x-xmin)))[0][0]
        x = x[xmin_id:]
        y = y[xmin_id:]
    else:
        xmin = x[0]

    if xmax:
        xmax = float(xmax)
        xmax_id = np.where(abs(x-xmax)==min(abs(x-xmax)))[0][0]
        x = x[:xmax_id]
        y = y[:xmax_id]
    else:
        xmax = x[-1]

    if not a:
        a = max(y)

    if not x0:
        x0 =  x[np.where(y==max(y))][0]

    if not gamma:
        gamma = abs(x[-1] - x[0])/10

    params, params_covariance = optimize.curve_fit(lorentz, x, y, p0=[a, x0, gamma])

    print("a =", params[0],"\nx0 =", params[1], "\ngamma =", params[2], \
        "\nFWHM =", 2 * params[2])

    fit_x = np.linspace(xmin, xmax, num)
    fit_y = lorentz(fit_x, params[0], params[1], params[2])

    return fit_x, fit_y


import numpy as np
import time
from sklearn.naive_bayes import BernoulliNB
from sklearn.naive_bayes import MultinomialNB
from sklearn.svm import LinearSVC
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier

# You may use this function as you like.
error = lambda y, yhat: np.mean(y!=yhat)

class Question1(object):
    # The sequence in this problem is different from the one you saw in the jupyter notebook. This makes it easier to grade. Apologies for any inconvenience.
    def BernoulliNB_classifier(self, traindata, trainlabels, valdata, vallabels):
        """ Train and evaluate a BernoulliNB classifier using the given data.

        Parameters:
        1. traindata    (Nt, d) numpy ndarray. The features in the training set.
        2. trainlabels  (Nt, ) numpy ndarray. The labels in the training set.
        3. valdata      (Nv, d) numpy ndarray. The features in the validation set.
        4. vallabels    (Nv, ) numpy ndarray. The labels in the validation set.

        Outputs:
        1. classifier           The classifier already trained on the training data.
        2. trainingError        Float. The reported training error. It should be less than 1.
        3. validationError      Float. The reported validation error. It should be less than 1.
        4. fittingTime          Float. The time it takes to fit the classifier (i.e. time to perform xxx.fit(X,y)). This is not evaluated.
        5. valPredictingTime    Float. The time it takes to run the classifier on the validation data (i.e. time to perform xxx.predict(X,y)). This is not evaluated.

        You can ignore all errors, if any.
        """
        # Put your code below
        classifier = BernoulliNB()
        fitTimeStart = time.time()
        classifier.fit(traindata, trainlabels)
        fitTimeEnd = time.time()
        predictTimeStart = time.time()
        est_labels = classifier.predict(valdata)
        predictTimeEnd = time.time()
        trainingError = 1 - classifier.score(traindata, trainlabels)
        validationError = error(vallabels, est_labels)
        fittingTime = fitTimeEnd - fitTimeStart
        valPredictingTime = predictTimeEnd - predictTimeStart
        # Do not change this sequence!
        return (classifier, trainingError, validationError, fittingTime, valPredictingTime)

    def MultinomialNB_classifier(self, traindata, trainlabels, valdata, vallabels):
        """ Train and evaluate a MultinomialNB classifier using the given data.

        Parameters:
        1. traindata    (Nt, d) numpy ndarray. The features in the training set.
        2. trainlabels  (Nt, ) numpy ndarray. The labels in the training set.
        3. valdata      (Nv, d) numpy ndarray. The features in the validation set.
        4. vallabels    (Nv, ) numpy ndarray. The labels in the validation set.

        Outputs:
        1. classifier           The classifier already trained on the training data.
        2. trainingError        Float. The reported training error. It should be less than 1.
        3. validationError      Float. The reported validation error. It should be less than 1.
        4. fittingTime          Float. The time it takes to fit the classifier (i.e. time to perform xxx.fit(X,y)). This is not evaluated.
        5. valPredictingTime    Float. The time it takes to run the classifier on the validation data (i.e. time to perform xxx.predict(X,y)). This is not evaluated.

        You can ignore all errors, if any.
        """
        # Put your code below
        classifier = MultinomialNB()
        fitTimeStart = time.time()
        classifier.fit(traindata, trainlabels)
        fitTimeEnd = time.time()
        predictTimeStart = time.time()
        est_labels = classifier.predict(valdata)
        predictTimeEnd = time.time()
        trainingError = 1 - classifier.score(traindata, trainlabels)
        validationError = error(vallabels, est_labels)
        fittingTime = fitTimeEnd - fitTimeStart
        valPredictingTime = predictTimeEnd - predictTimeStart
        # Do not change this sequence!
        return (classifier, trainingError, validationError, fittingTime, valPredictingTime)

    def LinearSVC_classifier(self, traindata, trainlabels, valdata, vallabels):
        """ Train and evaluate a LinearSVC classifier using the given data.

        Parameters:
        1. traindata    (Nt, d) numpy ndarray. The features in the training set.
        2. trainlabels  (Nt, ) numpy ndarray. The labels in the training set.
        3. valdata      (Nv, d) numpy ndarray. The features in the validation set.
        4. vallabels    (Nv, ) numpy ndarray. The labels in the validation set.

        Outputs:
        1. classifier           The classifier already trained on the training data.
        2. trainingError        Float. The reported training error. It should be less than 1.
        3. validationError      Float. The reported validation error. It should be less than 1.
        4. fittingTime          Float. The time it takes to fit the classifier (i.e. time to perform xxx.fit(X,y)). This is not evaluated.
        5. valPredictingTime    Float. The time it takes to run the classifier on the validation data (i.e. time to perform xxx.predict(X,y)). This is not evaluated.

        You can ignore all errors, if any.
        """
        # Put your code below
        classifier = LinearSVC()
        fitTimeStart = time.time()
        classifier.fit(traindata, trainlabels)
        fitTimeEnd = time.time()
        predictTimeStart = time.time()
        est_labels = classifier.predict(valdata)
        predictTimeEnd = time.time()
        trainingError = 1 - classifier.score(traindata, trainlabels)
        validationError = error(vallabels, est_labels)
        fittingTime = fitTimeEnd - fitTimeStart
        valPredictingTime = predictTimeEnd - predictTimeStart
        # Do not change this sequence!
        return (classifier, trainingError, validationError, fittingTime, valPredictingTime)

    def LogisticRegression_classifier(self, traindata, trainlabels, valdata, vallabels):
        """ Train and evaluate a LogisticRegression classifier using the given data.

        Parameters:
        1. traindata    (Nt, d) numpy ndarray. The features in the training set.
        2. trainlabels  (Nt, ) numpy ndarray. The labels in the training set.
        3. valdata      (Nv, d) numpy ndarray. The features in the validation set.
        4. vallabels    (Nv, ) numpy ndarray. The labels in the validation set.

        Outputs:
        1. classifier           The classifier already trained on the training data.
        2. trainingError        Float. The reported training error. It should be less than 1.
        3. validationError      Float. The reported validation error. It should be less than 1.
        4. fittingTime          Float. The time it takes to fit the classifier (i.e. time to perform xxx.fit(X,y)). This is not evaluated.
        5. valPredictingTime    Float. The time it takes to run the classifier on the validation data (i.e. time to perform xxx.predict(X,y)). This is not evaluated.

        You can ignore all errors, if any.
        """
        # Put your code below
        classifier = LogisticRegression()
        fitTimeStart = time.time()
        classifier.fit(traindata, trainlabels)
        fitTimeEnd = time.time()
        predictTimeStart = time.time()
        est_labels = classifier.predict(valdata)
        predictTimeEnd = time.time()
        trainingError = 1 - classifier.score(traindata, trainlabels)
        validationError = error(vallabels, est_labels)
        fittingTime = fitTimeEnd - fitTimeStart
        valPredictingTime = predictTimeEnd - predictTimeStart
        # Do not change this sequence!
        return (classifier, trainingError, validationError, fittingTime, valPredictingTime)

    def NN_classifier(self, traindata, trainlabels, valdata, vallabels):
        """ Train and evaluate a Nearest Neighbor classifier using the given data.

        Make sure to modify the default parameter.

        Parameters:
        1. traindata            (Nt, d) numpy ndarray. The features in the training set.
        2. trainlabels          (Nt, ) numpy ndarray. The labels in the training set.
        3. valdata              (Nv, d) numpy ndarray. The features in the validation set.
        4. vallabels            (Nv, ) numpy ndarray. The labels in the validation set.

        Outputs:
        1. classifier           The classifier already trained on the training data.
        2. trainingError        Float. The reported training error. It should be less than 1.
        3. validationError      Float. The reported validation error. It should be less than 1.
        4. fittingTime          Float. The time it takes to fit the classifier (i.e. time to perform xxx.fit(X,y)). This is not evaluated.
        5. valPredictingTime    Float. The time it takes to run the classifier on the validation data (i.e. time to perform xxx.predict(X,y)). This is not evaluated.

        You can ignore all errors, if any.
        """
        # Put your code below
        classifier = KNeighborsClassifier(n_neighbors=1)
        fitTimeStart = time.time()
        classifier.fit(traindata, trainlabels)
        fitTimeEnd = time.time()
        predictTimeStart = time.time()
        est_labels = classifier.predict(valdata)
        predictTimeEnd = time.time()
        trainingError = 1 - classifier.score(traindata, trainlabels)
        validationError = error(vallabels, est_labels)
        fittingTime = fitTimeEnd - fitTimeStart
        valPredictingTime = predictTimeEnd - predictTimeStart
        # Do not change this sequence!
        return (classifier, trainingError, validationError, fittingTime, valPredictingTime)

    def confMatrix(self, truelabels, estimatedlabels):
        """ Write a function that calculates the confusion matrix (cf. Fig. 2.1 in the notes).

        You may wish to read Section 2.1.1 in the notes -- it may be helpful, but is not necessary to complete this problem.

        Parameters:
        1. truelabels           (Nv, ) numpy ndarray. The ground truth labels.
        2. estimatedlabels      (Nv, ) numpy ndarray. The estimated labels from the output of some classifier.

        Outputs:
        1. cm                   (2,2) numpy ndarray. The calculated confusion matrix.
        """
        cm = np.zeros((2,2))
        # Put your code below
        for i in range(len(truelabels)-1):
            if truelabels[i] == 1 and estimatedlabels[i] == 1:
                cm[0, 0] += 1
            if truelabels[i] == -1 and estimatedlabels[i] == 1:
                cm[0, 1] += 1
            if truelabels[i] == 1 and estimatedlabels[i] == -1:
                cm[1, 0] += 1
            if truelabels[i] == -1 and estimatedlabels[i] == -1:
                cm[1, 1] += 1
        return cm

    def classify(self, traindata, trainlabels, testdata, testlabels):
        """ Run the classifier you selected in the previous part of the problem on the test data.

        Parameters:
        1. traindata            (Nt, d) numpy ndarray. The features in the training set.
        2. trainlabels          (Nt, ) numpy ndarray. The labels in the training set.
        3. testdata             (Nte, d) numpy ndarray. The features in the test set.
        4. testlabels           (Nte, ) numpy ndarray. The labels in the test set.

        Outputs:
        1. classifier           The classifier already trained on the training data.
        2. testError            Float. The reported test error. It should be less than 1.
        3. confusionMatrix      (2,2) numpy ndarray. The resulting confusion matrix. This will not be graded.
        """
        # You can freely use the following line
        # confusionMatrix = self.confMatrix(testlabels, est_labels)
        # Put your code below
        classifier = LogisticRegression(max_iter=1000)
        classifier.fit(traindata, trainlabels)
        est_labels = classifier.predict(testdata)
        testError = error(testlabels, est_labels)
        confusionMatrix = self.confMatrix(testlabels, est_labels)
        # Do not change this sequence!
        return (classifier, testError, confusionMatrix)

class Question2(object):
    def crossValidationkNN(self, traindata, trainlabels, k):
        """ Write a function which implements 5-fold cross-validation to estimate the error of a classifier with cross-validation with the 0,1-loss for k-Nearest Neighbors (kNN).

        For this problem, take your folds to be 0:N/5, N/5:2N/5, ..., 4N/5:N for cross-validation.

        Parameters:
        1. traindata            (Nt, d) numpy ndarray. The features in the training set.
        2. trainlabels          (Nt, ) numpy ndarray. The labels in the training set.
        3. k                    Integer. The cross-validated error estimates will be outputted for 1,...,k.

        Outputs:
        1. err                  (k+1,) numpy ndarray. err[i] is the cross-validated estimate of using i neighbors (the zero-th component of the vector will be meaningless).
        """
        # Put your code below
        N, folds = traindata.shape[0], 5
        err = np.zeros(k+1)
        valDataList = [traindata[i: i+N//5] for i in range(0, N, N//5)]
        valLabelList = [trainlabels[i: i+N//5] for i in range(0, N, N//5)]
        trainDataList, trainLabelList = [], []
        for i in range(folds):
            tmpData, tmpLabel = [], []
            for j in range(folds):
                if i != j:
                    tmpData.extend(valDataList[j])
                    tmpLabel.extend(valLabelList[j])
            trainDataList.append(tmpData)
            trainLabelList.append(tmpLabel)
        for i in range(1, k+1):
            classifier = KNeighborsClassifier(n_neighbors=i)
            error = 0
            for j in range(folds):
                classifier.fit(trainDataList[j], trainLabelList[j])
                error += (1 - classifier.score(valDataList[j], valLabelList[j]))
            err[i] = error / folds
        return err

    def minimizer_K(self, traindata, trainlabels, k):
        """ Write a function that calls the above function and returns 1) the output from the previous function, 2) the number of neighbors within  1,...,k  that minimizes the cross-validation error, and 3) the correponding minimum error.

        Parameters:
        1. traindata            (Nt, d) numpy ndarray. The features in the training set.
        2. trainlabels          (Nt, ) numpy ndarray. The labels in the training set.
        3. k                    Integer. The cross-validated error estimates will be outputted for 1,...,k.

        Outputs:
        1. err                  (k+1,) numpy ndarray. The output from crossValidationkNN().
        2. k_min                Integer (np.int64 or int). The number of neighbors within  1,...,k  that minimizes the cross-validation error.
        3. err_min              Float. The correponding minimum error.
        """
        err = self.crossValidationkNN(traindata, trainlabels, k)
        # Put your code below
        k_min, err_min = float("inf"), float("inf")
        for i in range(1, len(err)):
            if err[i] <= err_min:
                k_min = i
                err_min = err[i]
        # Do not change this sequence!
        return (err, k_min, err_min)

    def classify(self, traindata, trainlabels, testdata, testlabels):
        """ Train a kNN model on the whole training data using the number of neighbors you found in the previous part of the question, and apply it to the test data.

        Parameters:
        1. traindata            (Nt, d) numpy ndarray. The features in the training set.
        2. trainlabels          (Nt, ) numpy ndarray. The labels in the training set.
        3. testdata             (Nte, d) numpy ndarray. The features in the test set.
        4. testlabels           (Nte, ) numpy ndarray. The labels in the test set.

        Outputs:
        1. classifier           The classifier already trained on the training data. Use the best k value that you choose.
        2. testError            Float. The reported test error. It should be less than 1.
        """
        # Put your code below
        _, k_min, _ = self.minimizer_K(traindata, trainlabels, 30)
        classifier = KNeighborsClassifier(n_neighbors=k_min)
        classifier.fit(traindata, trainlabels)
        est_labels = classifier.predict(testdata)
        testError = error(testlabels, est_labels)
        # Do not change this sequence!
        return (classifier, testError)

from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import GridSearchCV

class Question3(object):
    def LinearSVC_crossValidation(self, traindata, trainlabels):
        """ Use cross-validation to select a value of C for a linear SVM by varying C from 2^{-5},...,2^{15}.

        You should seaerch by hand.

        Parameters:
        1. traindata            (Nt, d) numpy ndarray. The features in the training set.
        2. trainlabels          (Nt, ) numpy ndarray. The labels in the training set.

        Outputs:
        1. C_min                Float. The hyper-parameter C that minimizes the validation error.
        2. min_err              Float. The correponding minimum error.
        """
        # Put your code below
        C_min, max_accuracy = float("nan"), 0
        for i in [2**i for i in range(-5, 16)]:
            classifier = LinearSVC(dual=False, C=i)
            score = cross_val_score(classifier, traindata, trainlabels, cv=10)
            accuracy = sum(score)/len(score)
            if max_accuracy < accuracy:
                max_accuracy = accuracy
                C_min = i
        min_err = 1 - max_accuracy
        # Do not change this sequence!
        return (C_min, min_err)

    def SVC_crossValidation(self, traindata, trainlabels):
        """ Use cross-validation to select a value of C for a linear SVM by varying C from 2^{-5},...,2^{15} and \gamma from 2^{-15},...,2^{3}.

        Use GridSearchCV to perform a grid search.

        Parameters:
        1. traindata            (Nt, d) numpy ndarray. The features in the training set.
        2. trainlabels          (Nt, ) numpy ndarray. The labels in the training set.

        Outputs:
        1. C_min                Float. The hyper-parameter C that minimizes the validation error.
        2. gamma_min            Float. The hyper-parameter \gamma that minimizes the validation error.
        3. min_err              Float. The correponding minimum error.
        """
        # Put your code below
        C_min, gamma_min, max_accuracy = float("nan"), float("nan"), 0
        for i in [2**i for i in range(-5, 16)]:
            for j in [2**i for i in range(-15, 4)]:
                classifier = SVC(C=i, gamma=j)
                score = cross_val_score(classifier, traindata, trainlabels, cv=10)
                accuracy = sum(score)/len(score)
                if max_accuracy < accuracy:
                    max_accuracy = accuracy
                    C_min = i
                    gamma_min = j
        min_err = 1 - max_accuracy
        # Do not change this sequence!
        return (C_min, gamma_min, min_err)

    def LogisticRegression_crossValidation(self, traindata, trainlabels):
        """ Use cross-validation to select a value of C for a linear SVM by varying C from 2^{-14},...,2^{14}.

        You may either use GridSearchCV or search by hand.

        Parameters:
        1. traindata            (Nt, d) numpy ndarray. The features in the training set.
        2. trainlabels          (Nt, ) numpy ndarray. The labels in the training set.

        Outputs:
        1. C_min                Float. The hyper-parameter C that minimizes the validation error.
        2. min_err              Float. The correponding minimum error.
        """
        # Put your code below
        # C_min, max_accuracy = float("nan"), 0
        # for i in [2**i for i in range(-14, 15)]:
        #     classifier = LogisticRegression(C=i)
        #     score = cross_val_score(classifier, traindata, trainlabels, cv=10)
        #     accuracy = sum(score)/len(score)
        #     if max_accuracy < accuracy:
        #         max_accuracy = accuracy
        #         C_min = i
        # min_err = 1 - max_accuracy
        # # Do not change this sequence!
        # return (C_min, min_err)

        fold = 10
        param_grid = [{'C':[2**(-14+i) for i in range(29)]}]
        classifier = GridSearchCV(LogisticRegression(), param_grid, scoring = 'accuracy', cv = fold)
        classifier.fit(traindata, trainlabels)
        C_min = classifier.best_params_['C']
        min_err = 1 - classifier.best_score_
        print("asshole")
        # Do not change this sequence!
        return (C_min, min_err)

    def classify(self, traindata, trainlabels, testdata, testlabels):
        """ Train the best classifier selected above on the whole training set.

        Parameters:
        1. traindata            (Nt, d) numpy ndarray. The features in the training set.
        2. trainlabels          (Nt, ) numpy ndarray. The labels in the training set.
        3. testdata             (Nte, d) numpy ndarray. The features in the test set.
        4. testlabels           (Nte, ) numpy ndarray. The labels in the test set.

        Outputs:
        1. classifier           The classifier already trained on the training data. Use the best classifier that you choose.
        2. testError            Float. The reported test error. It should be less than 1.
        """
        # Put your code below
        classifier = SVC(C = 8, gamma = 0.125)
        classifier.fit(traindata, trainlabels)
        est_labels = classifier.predict(testdata)
        testError = error(testlabels, est_labels)
        # Do not change this sequence!
        return (classifier, testError)


# A Sphere, brough to you by PharaohCola13

import mpl_toolkits.mplot3d.axes3d as p3
import matplotlib.pyplot as plt
from matplotlib import *
from numpy import *
from mpl_toolkits.mplot3d.art3d import *
from matplotlib.animation import *

name = "Roman Surface"

def shape(fig, alpha, color, edge_c, edge_w, grid, sides, edges, multi_pi, figcolor, rotation, rotmagt, rotmagp, save):
	plt.clf()
    # Definition of x
	def x_(u,v):
		    x = ((sqrt(2) * cos(2*u)*cos(v)**2) + (cos(u) * sin(2*v)))/(2 - a*sqrt(2) * sin(3*u)*sin(2*v))
		    return x

	# Definition of y
	def y_(u,v):
		    y = ((sqrt(2) * sin(2*u)*cos(v)**2) - (sin(u) * sin(2*v)))/(2 - a*sqrt(2) * sin(3*u)*sin(2*v))
		    return y

	# Definition of z
	def z_(u,v):
		    z  = (3 * cos(v)**2)/(2 - a*sqrt(2) * sin(3*u)*sin(2*v))
		    return z

	a = 0 # a = 0, Roman Surface. a = 1, Boys Surface
	# Values of the angles
	s = sides
	u = linspace(0, 2 * pi, s + 1)
	v = linspace(0, pi/2, edges)

	u, v = meshgrid(u, v)

	# Symbolic Representation
	x = x_(u,v)
	y = y_(u,v)
	z = z_(u,v)

	# Figure Properties
	ax = p3.Axes3D(fig)
	ax.set_facecolor(figcolor) # Figure background turns black

	# Axis Properties
	plt.axis(grid) # Turns off the axis grid
	plt.axis('equal')

	# Surface Plot
	roman = ax.plot_surface(x, y, z)

	roman.set_alpha(alpha) # Transparency of figure
	roman.set_edgecolor(edge_c) # Edge color of the lines on the figure
	roman.set_linewidth(edge_w) # Line width of the edges
	roman.set_facecolor(color) # General color of the figure

	def rot_on():
		def animate(i):
			ax.view_init(azim=rotmagt * i, elev=rotmagp * i)

		if save == "MP4":
			# Animate
			ani = FuncAnimation(fig, animate, frames=500,
								interval=100, save_count=50)  # frames=100)#, repeat=True)

			Writer = writers['ffmpeg']
			writer = Writer(fps=30, bitrate=1800)
			ani.save('{}.mp4'.format(name), writer=writer)
		else:
			#save = None
			# Animate
			ani = FuncAnimation(fig, animate,
								interval=1, save_count=50)  # frames=100)#, repeat=True)
			pass

		plt.ion()
		plt.show()
		time.sleep(0)
		plt.close()

	if rotation == "On":
		rot_on()
	elif rotation == "Off":
		pass