from mpl_toolkits.mplot3d import Axes3D
from matplotlib.ticker import LinearLocator, FormatStrFormatter
from matplotlib import cm
import numpy as np
from scipy import linalg
import matplotlib.pyplot as plt
import time

# Variance
def var(f_model):
    n = np.size(f_model)
    f_model_mean = np.sum(f_model)/n
    #f_model_mean = np.mean(f_model)
    return np.sum((f_model-f_model_mean)**2)/n

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

# Bias
def bias(f_true,f_model):
    n = np.size(f_model)
    #f_model_mean = np.sum(f_model)/n
    f_model_mean = np.mean(f_model)
    return np.sum((f_true-f_model_mean)**2)/n

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

# MSE
def MSE(f_true,f_model):
    n = np.size(f_model)
    return np.sum((f_true-f_model)**2)/n

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

# Extra term
def extra_term(f_true,f_model):
    n = np.size(f_model)
    f_model_mean = np.mean(f_model)
    return 2.0/n*np.sum((f_model_mean-f_true)*(f_model-f_model_mean))

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

# SVD invert
def SVDinv(A):
    ''' Takes as input a numpy matrix A and returns inv(A) based on singular value decomposition (SVD).
    SVD is numerically more stable (at least in our case) than the inversion algorithms provided by
    numpy and scipy.linalg at the cost of being slower.
    '''
    U, s, VT = linalg.svd(A)
    D = np.zeros((len(U),len(VT)))
    for i in range(0,len(VT)):
        D[i,i]=s[i]
    UT = np.transpose(U); V = np.transpose(VT); invD = np.linalg.inv(D)
    return np.matmul(V,np.matmul(invD,UT))

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

# R2 score
def R2(x_true,x_predict):
    n = np.size(x_true)
    x_avg = np.sum(x_true)/n
    enumerator = np.sum ((x_true-x_predict)**2)
    denominator = np.sum((x_true-x_avg)**2)
    return 1.0 - enumerator/denominator

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

## Mean
#def mean(x):
#    n = np.size(x)
#    x_avg = np.sum(x)/n
#    return x_avg

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

# get sub-entries of matrix A
def get_subset(A,indices):
    '''given an indexing set "indices", return the vector consisting of 
    entries A[i,j] where (i,j) is an entry in indices.'''
    N = len(indices)
    B = np.zeros(N)
    for k in range(0,N):
        i = indices[k][0]
        j = indices[k][1]
        B[k] = A[j,i]
    return B


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

class k_cross_validation:
    '''An k-cross validation object is initialized by passing to it data of the type linreg,
    and a paritition of the data. The class function R2 calculates the mean R2 scores
    of test and training data for the given model. The function MSE calculates the mean MSE, bias,
    variance and error terms of the test data for the given model. These quantities are stored
    as self variables.'''

    def __init__(self, data, partition,*args):
        self.data = data; self.partition = partition; self.args = args;
        #f = data.f; X = data.X; z = data.z; correspondence = data.correspondence; 
        self.k = len(partition)
        self.test_R2, self.test_var, self.test_bias, self.test_MSE, self.test_extra_terms = 0, 0, 0, 0, 0
        self.train_R2 = 0
        
        #self.train_var, self.train_bias, self.train_MSE, self.train_extra_terms = 0, 0, 0, 0

    def R2(self):
        data = self.data
        f = data.f; X = data.X; z = data.z; correspondence = data.correspondence; partition = self.partition
        k = self.k
        args = self.args
        
        test_R2, train_R2 = 0, 0

        for i, test_data in enumerate(partition):
            train_data = [x for j,x in enumerate(partition) if j!=i]
            train_data = sum(train_data, [])
            beta = data.get_beta(X[train_data],z[train_data],*args)
            freg = data.model(beta)
            test_data = [correspondence[j] for j in test_data]
            train_data = [correspondence[j] for j in train_data]

            # test errors:
            ftest = get_subset(f,test_data); fregtest = get_subset(freg,test_data)
            test_R2 +=  R2(ftest,fregtest)

            #training errors:
            ftrain = get_subset(f,train_data); fregtrain = get_subset(freg,train_data)
            train_R2 +=  R2(ftrain,fregtrain)

        # self variables
        self.test_R2 = test_R2/k
        self.train_R2 = train_R2/k

    def MSE(self):
        data = self.data
        f = data.f; X = data.X; z = data.z; correspondence = data.correspondence; partition = self.partition
        k = self.k
        args = self.args

        test_var, test_bias, test_MSE, test_extra_terms = 0, 0, 0, 0
        #train_var, train_bias, train_MSE, train_extra_terms = 0, 0, 0, 0

        for i, test_data in enumerate(partition):
            train_data = [x for j,x in enumerate(partition) if j!=i]
            train_data = sum(train_data, [])
            beta = data.get_beta(X[train_data],z[train_data],*args)
            freg = data.model(beta)
            test_data = [correspondence[j] for j in test_data]
            # train_data = [correspondence[j] for j in train_data]

            # test errors:
            ftest = get_subset(f,test_data); fregtest = get_subset(freg,test_data)
            test_var += var(fregtest) 
            test_bias += bias(ftest,fregtest)
            test_MSE += MSE(ftest,fregtest)
            test_extra_terms += extra_term(ftest,fregtest)

            ##training errors:
            #ftrain = get_subset(f,train_data); fregtrain = get_subset(freg,train_data)
            #train_var += var(fregtrain) 
            #train_bias += bias(ftrain,fregtrain)
            #train_MSE += MSE(ftrain,fregtrain)
            #train_extra_terms += extra_term(ftrain,fregtrain)

        # self variables
        self.test_var = test_var/k
        self.test_bias = test_bias/k
        self.test_MSE = test_MSE/k
        self.test_extra_terms = test_extra_terms/k

        #self.train_var = train_var/k
        #self.train_bias = train_bias/k
        #self.train_MSE = train_MSE/k
        #self.train_extra_terms = train_extra_terms/k

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

class regdata:
    def __init__(self, f, degree):
        # initializing variables
        m = len(f[0,:]); n = len(f);  mn = m*n; 
        x = np.linspace(0, 1, m); y = np.linspace(0, 1, n); z = np.zeros(mn); xy = np.zeros((mn,2)); 

        # initializing some self variables
        self.f = f; self.degree = degree; self.xm, self.ym = np.meshgrid(x,y); self.n=n;self.m=m;  self.mn = mn; self.correspondence = []
        
        # Making a sequence xy containing the pairs (x_i,y_j) for i,j=0,...,n, and a sequence z with matching pairs z_ij = f(x_i, y_j)
        counter = 0
        for i in range(0,m):
            for j in range(0,n):
                z[counter]=f[j,i] #wtf
                xy[counter,:] = [x[i],y[j]]
                self.correspondence.append([i,j]) #Saves the 1-1 correspondence: {counter} <-> {(i,j)} for later
                counter+=1
        self.z = z

        # Make X
        number_basis_elts=int((degree+2)*(degree+1)/2) #(degree+1)th triangular number (number of basis elements for R[x,y] of degree <= degree)
        X = np.zeros((mn,number_basis_elts))
        powers = []
        for i in range(0,mn):
            counter = 0
            for j in range(0,degree+1):
                k = 0
                while j+k <= degree:
                    xi = xy[i,0]
                    yi = xy[i,1]
                    X[i,counter]= (xi**j)*(yi**k)
                    powers.append([j , k])
                    k+=1
                    counter+=1
        self.X = X
        self.powers = powers
        self.number_basis_elts = number_basis_elts
        self.invXTX = linalg.inv(np.matmul(np.transpose(X),X))

    # Regression
    def get_reg(self, *args):
        '''Returns the polynomial fit as a numpy array. If *args is empty the fit is based on an ordinary least square.
        If *args contains a number LAMBDA, then the fit is found using Ridge for the given bias LAMBDA. If *args contains
        two numbers LAMBDA and epsilon, then the fit is found using lasso. See the function " __get_beta" for more details.'''

        X=self.X; z=self.z #relabeling self variables
        beta = self.get_beta(X,z,*args) #obtaining beta
        reg = self.model(beta) #obtaining model from coefficients beta
        return reg

    # Get beta (given X and z)
    def get_beta(self, X, z,*args):
        '''Returns coefficients for a given beta as a numpy array, found using either ordinary least square,
        Ridge or Lasso regression depending on the arguments. If *args is empty, then beta is found using
        ordinary least square. If *args contains a number it will be treated as a bias LAMBDA for a Ridge regression.
        If *args contains two numbers, then the first will count as a LAMBDA and the second as a tolerance epsilon.
        In this case beta is found using a shooting algorithm that runs until it converges up to the set tolerance.
        '''

        XT = np.transpose(X)
        beta = np.matmul(XT,X)                   
        if len(args) >= 1: #Ridge parameter LAMBDA
            LAMBDA = args[0] 
            beta[np.diag_indices_from(beta)]+=LAMBDA
        beta = SVDinv(beta)
        beta = np.matmul(beta,XT)
        beta = np.matmul(beta,z)

        #Shooting algorithm for Lasso
        if len(args)>=2:
            epsilon = args[1]
            D = self.number_basis_elts
            ints = np.arange(0,D,1)
            beta_old = 0.0
            while np.linalg.norm(beta-beta_old)>=epsilon:
                beta_old = np.copy(beta)
                for j in range(0,D):
                    aj = 2*np.sum(X[:,j]**2)
                    no_j = ints[np.arange(D)!=j]
                    cj = 2*np.sum(np.multiply(X[:,j],(z-np.matmul(X[:,no_j],beta[no_j]))))
                    if cj<-LAMBDA:
                        beta[j]=(cj+LAMBDA)/aj
                    elif cj > LAMBDA:
                        beta[j]=(cj-LAMBDA)/aj
                    else:
                        beta[j]=0.0
        return beta

    # Get model given beta
    def model(self,beta):
        '''Returns heigh values based on the coefficients beta as a matrix
        that matches the grid xm, ym. The degree of the polynomial equals self.degree.
        '''
        xm = self.xm; ym = self.ym; degree = self.degree #relabeling self variables
        s=0
        counter = 0
        # loop that adds terms of the form beta*x^j*y^k such that j+k<=5
        for j in range(0,degree + 1):
            k = 0
            while j+k <= degree: 
                s+= beta[counter]*(xm**j)*(ym**k)
                counter +=1
                k+=1
        return s

    def get_data_partition(self,k):
        ''' Creates a random partition of k (almost) equally sized parts of the array
        {1,2,...,mn}. This can be used to make training/testing data.
        '''
        mn =  self.mn; correspondence = self.correspondence
        indices = np.arange(mn)
        indices_shuffle = np.arange(mn)
        np.random.shuffle(indices_shuffle)
        partition = []
        for step in range(0,k):
            part = list(indices_shuffle[step:mn:k])
            #part = [correspondence[i] for i in part]
            partition.append(part) 
        return partition

    def bootstrap_step(self, samplesize, *args):
        '''Finds and returns the coefficient that determines a model (ols, Ridge or Lasso),
        depending on args*.
        '''
        mn =  self.mn; X = self.X; z = self.z;  #relabeling self variables
        integers = np.random.randint(low=0, high=mn-1, size=samplesize)
        znew =  z[integers]
        Xnew = X[integers,:]
        betanew = self.get_beta(Xnew,znew,*args)
        return betanew

    # Variance/ covariance matrix
    def var_covar_matrix(self,reg):
        ''' Returns the variance/covariance matrix for beta based on the given data.
        This matrix is derived from a statistical viewpoint, where one assumes beta to
        have a normal distribution.
        '''
        p = self.number_basis_elts; invXTX = self.invXTX; N = self.mn; f = self.f # Relabeling self variables
        sigma2=1.0/(N-p-1)*np.sum((f-reg)*(f-reg))
        return sigma2*invXTX # OBS! Based on matrix inversion. Inaccurate for  N,p>>0.

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

def plot_3D(f,plottitle):
    ''' Simple function to create 3d plot of the given data f,
    with plotitle.
    '''

    m = len(f[0,:]); n = len(f);
    x = np.linspace(0, 1, m)
    y = np.linspace(0, 1, n);
    xm, ym = np.meshgrid(x,y)

    # Plot f
    fig = plt.figure()
    ax = fig.gca(projection="3d")
    surf = ax.plot_surface(xm, ym, f, cmap=cm.coolwarm, linewidth=0, antialiased=False)

    # Customize the z axis.
    ax.zaxis.set_major_locator(LinearLocator(10))
    ax.zaxis.set_major_formatter(FormatStrFormatter("%.02f"))
    ax.text2D(0.05, 0.95, plottitle, transform=ax.transAxes)
    ax.view_init(30, 60)

    # Add a color bar which maps values to colors.
    fig.colorbar(surf, shrink=0.5, aspect=5)
    plt.show(block=False)

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

def numerical_error(data,LAMBDA):
    '''Rough numerical analysis of matrix inversions for this problem. Comparison of error and time usage
    of SVD (singular values decomposition) for matrix inversion against scipy.linalg inversion algorithm.
    Printing results to terminal.
    '''
    return_items = []

    degree = data.degree; m = data.m; n = data.n
    # Study numerical error and time for SVD
    print("Polynomial fit of FrankeFunction in x, y of degree ", degree," with grid size ", (m,n)," analysis:")
    print("")
    X = data.X; XT = np.transpose(X); XTX = np.matmul(XT,X) #Obtaining XTX
    start_time = time.time() # start meassuring time
    inv_XTX = linalg.inv(XTX) # inversion using scipi.linalg
    end_time = time.time()
    print("Inverting XTX without SVD", "--- %s seconds ---" % (end_time - start_time)); return_items.append(end_time - start_time)
    inv_XTX_ = np.copy(inv_XTX) # storing inversion of XTX for later
    start_time = time.time()
    inv_XTX = SVDinv(XTX)
    end_time = time.time()
    print("Inverting XTX with SVD", "--- %s seconds ---" % (end_time - start_time)); return_items.append(end_time - start_time)
    print(' ')
    I_approx_ = np.matmul(inv_XTX_,XTX); # approximate I (no SVD)
    I = np.identity(len(I_approx_)); # obtaining analytical I
    output = np.linalg.norm(I_approx_-I)
    print("|(X^TX)^-1(X^TX)-I| = ",output, " (no SVD)"); return_items.append(output)
    I_approx = np.matmul(inv_XTX,XTX) # approximate I (SVD)
    output = np.linalg.norm(I_approx-I)
    print("|(X^TX)^-1(X^TX)-I| = ",np.linalg.norm(I_approx-I), " (SVD)"); return_items.append(output)
    XTX[np.diag_indices_from(XTX)]+=LAMBDA
    inv_XTX = linalg.inv(XTX)
    I_approx_ = np.matmul(inv_XTX,XTX) # approximate I (no SVD)
    output = np.linalg.norm(I_approx_-I)
    print("|(X^TX + I LAMBDA)^-1(X^TX + I LAMBDA)-I| = ",output , ", LAMBDA = ", LAMBDA, " (no SVD)"); return_items.append(output)
    inv_XTX = SVDinv(XTX)
    I_approx = np.matmul(inv_XTX,XTX)
    output = np.linalg.norm(I_approx-I)
    print("|(X^TX + I LAMBDA)^-1(X^TX + I LAMBDA)-I| = ",output, ", LAMBDA = ", LAMBDA, " (SVD)"); return_items.append(output)
    print(' ')
    
    return return_items

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

def plot_R2_scores(data,Nstart,Nstop,name, epsilon = 0.001):
    ''' This function makes a plot of the R2 scores vs Lambda of the different regression methods,
    for a given dataset.'''

    degree = data.degree; f = data.f # obtaining class data
    N = Nstop-Nstart # number of lambdas
    lambdas = np.zeros(N)
    R2_ols = np.zeros(N)
    R2_Ridge = np.zeros(N)
    R2_Lasso = np.zeros(N)
    for i in range(0,N):
        LAMBDA = 10**(Nstart+i)
        lambdas[i]=LAMBDA
        R2_ols[i]=R2(f, data.get_reg())
        R2_Ridge[i]=R2(f, data.get_reg(LAMBDA))
        R2_Lasso[i]=R2(f, data.get_reg(LAMBDA,epsilon))
        print("Completed lambda: ", LAMBDA, " Completion: {:.1%}".format(float(i)/(N-1)))
    plotitle = '$R^2$ score of degree {} polynomial fit on {}'.format(degree,name)
    plt.figure()
    plt.plot(np.log10(lambdas),R2_ols)
    plt.plot(np.log10(lambdas),R2_Ridge)
    plt.plot(np.log10(lambdas),R2_Lasso,'--')
    plt.axis([Nstart, N+Nstart-1, 0, 1])
    plt.xlabel('log $\lambda$')
    plt.ylabel('$R^2$ score')
    plt.legend(('Ordinary least square','Ridge','Lasso'))
    plt.title(plotitle)
    plt.grid(True)
    plt.show(block=False)

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

def plot_R2_scores_k_cross_validation(data,Nstart,Nstop,k,name, epsilon = 0.001):
    ''' This function makes a plot of the R2 scores vs LAMBDA of the best iteration from a k-fold cross validation on 
    the data set from the given data. Best in the sense that the fit had the highest R2 score on testing data. The same 
    partition of the data set is used for each lambda, and each time we select the best training data on which we base the model.
    See "k_cross_validation" for more details.'''

    degree = data.degree; f = data.f # obtaining class data
    N = Nstop-Nstart # number of lambdas

    # Comparing R2 scores, regression with fixed degree, variable LAMBDA
    lambdas = np.zeros(N)
    partition = data.get_data_partition(k)
    kval = k_cross_validation(data,partition)
    kval.R2()
    
    R2_Lasso_test_data = np.zeros(N)
    R2_Lasso_training_data = np.zeros(N)
    R2_Ridge_test_data = np.zeros(N)
    R2_Ridge_training_data = np.zeros(N)

    # OLS R2 score
    R2score_ols_test, R2score_ols_train = kval.test_R2, kval.train_R2
    R2_ols_test_data = np.ones(N)*R2score_ols_test
    R2_ols_training_data = np.ones(N)*R2score_ols_train

    for i in range(0,N): 
        LAMBDA = 10**(Nstart+i)
        lambdas[i]=LAMBDA
        kval = k_cross_validation(data,partition,LAMBDA)
        kval.R2()

        # Ridge R2 score
        R2score_ridge_test, R2score_ridge_train = kval.test_R2, kval.train_R2
        R2_Ridge_test_data[i] = R2score_ridge_test
        R2_Ridge_training_data[i] = R2score_ridge_train

        kval = k_cross_validation(data,partition,LAMBDA,epsilon)
        kval.R2()

        # Lasso R2 score
        R2score_lasso_test, R2score_lasso_train = kval.test_R2, kval.train_R2
        R2_Lasso_test_data[i] = R2score_lasso_test
        R2_Lasso_training_data[i] = R2score_lasso_train

        print("Completed lambda: ", LAMBDA, " Completion: {:.1%}".format(float(i)/(N-1)))

    plotitle = '$R^2$ scores of degree {} polynomial fit on {}, $k=${}'.format(degree,name,k)
    plt.figure()
    plt.plot(np.log10(lambdas),R2_ols_test_data)
    plt.plot(np.log10(lambdas),R2_ols_training_data,'--')
    plt.plot(np.log10(lambdas),R2_Ridge_test_data)
    plt.plot(np.log10(lambdas),R2_Ridge_training_data,'--')
    plt.plot(np.log10(lambdas),R2_Lasso_test_data)
    plt.plot(np.log10(lambdas),R2_Lasso_training_data,'--')
    plt.axis([Nstart, Nstart+N-2, 0, 1])
    plt.xlabel('log $\lambda$')
    plt.ylabel('$R^2$ score')
    if (np.amax(R2_ols_test_data)> 0 and np.amax(R2_ols_training_data)> 0):
        plt.legend(('OLS: test data', 'OLS: training data','Ridge: test data', 'Ridge: training data','Lasso: test data', 'Lasso: training data'))
    elif (np.amax(R2_ols_test_data)<= 0 and np.amax(R2_ols_training_data)> 0):
        plt.legend(('OLS: test data (negative)', 'OLS: training data','Ridge: test data', 'Ridge: training data','Lasso: test data', 'Lasso: training data'))
    elif (np.amax(R2_ols_test_data)> 0 and np.amax(R2_ols_training_data)<= 0):
        plt.legend(('OLS: test data', 'OLS: training data (negative)','Ridge: test data', 'Ridge: training data','Lasso: test data', 'Lasso: training data'))
    elif (np.amax(R2_ols_test_data)<= 0 and np.amax(R2_ols_training_data)<= 0):
        plt.legend(('OLS: test data (negative)', 'OLS: training data (negative)','Ridge: test data', 'Ridge: training data','Lasso: test data', 'Lasso: training data'))
    plt.title(plotitle)
    plt.grid(True)
    plt.show(block=False)

    #return ols_best, ridge_best, lasso_best

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

def plot_R2_complexity(degstart,degend,degstep,f,name, LAMBDA = 0.00001, epsilon = 0.001):
    ''' Comparing R2 scores, regression with fixed LAMBDA, variable degree as well as variance and Bias
    Plotting the result.
    '''
    degrees = np.arange(degstart,degend+1,degstep)
    N = len(degrees)
    R2_ols, R2_Ridge, R2_Lasso = np.zeros(N), np.zeros(N), np.zeros(N)
    for i, degree in enumerate(degrees):
        data_f = regdata(f,degree)
        R2_ols[i]=R2(f, data_f.get_reg())
        R2_Ridge[i]=R2(f, data_f.get_reg(LAMBDA))
        R2_Lasso[i]=R2(f, data_f.get_reg(LAMBDA,epsilon))
        print("Completed degree: ", degree, " Completion: {:.1%}".format(float(i)/(N-1)))
    plotitle = '$R^2$ score of polynomial fit on {} with $\lambda=${}'.format(name,LAMBDA)
    plt.figure()
    plt.plot(degrees,R2_ols)
    plt.plot(degrees,R2_Ridge)
    plt.plot(degrees,R2_Lasso,'--')
    plt.xlabel('degree of fitting polynomial')
    plt.ylabel('$R^2$ score')
    plt.axis([degstart,degend, 0, 1])
    plt.legend(('Ordinary least square','Ridge','Lasso'))
    plt.title(plotitle)
    plt.grid(True)
    plt.show(block=False)

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

def plot_MSE_variance(degstart, degend, degstep, f, LAMBDA = 0.01, epsilon = 0.001, k=10):
    # Comparing MSE, bias, variance and additional terms as function of complexity.
    degrees = np.arange(degstart,degend+1,degstep)
    N = len(degrees)
    data = regdata(f,5)
    fvar = np.zeros(N); fbias = np.zeros(N); fMSE = np.zeros(N); fextra_terms = np.zeros(N)

    # function for plotting
    def makeplot(methodname, *args, partition = None):
        print(methodname)
        for i, degree in enumerate(degrees):
            data = regdata(f,degree)
            if partition == None:
                freg = data.get_reg(*args)
                fvar[i], fbias[i], fMSE[i], fextra_terms[i] =  var(freg), bias(f,freg), MSE(f,freg), extra_term(f,freg)
            else:
                kval = k_cross_validation(data, partition, *args)
                kval.MSE()
                fvar[i] = kval.test_var
                fbias[i] = kval.test_bias
                fMSE[i] = kval.test_MSE
                fextra_terms[i] =kval.test_extra_terms

                #fvar[i], fbias[i], fMSE[i], fextra_terms[i], train_var, train_bias, train_MSE, train_extra_terms 
            print("Completed degree: ", degree, " Completion: {:.1%}".format(float(degree-degstart)/(degend-degstart)))
        plt.figure() 
        plt.plot(degrees, fvar)
        plt.plot(degrees, fbias)
        plt.plot(degrees, fMSE,'--')
        plt.plot(degrees, fextra_terms)
        plt.xlabel('degree')
        plt.ylabel('Variance, bias, and MSE')
        plt.legend(('Variance','Bias','MSE','Additional term'))
        plt.grid(True)
        plt.show(block=False)

    #It is a good idea to comment out the plots that you dont need


    ## Ordinary least square plot
    #makeplot("Ordinary least squares")
    #plt.title("Error of  ordinary least squares")

    ## Ridge plot
    #makeplot("Ridge regression",LAMBDA)
    #plt.title("Error of Ridge regression, $\lambda=${}".format(LAMBDA))

    ## Lasso plot
    #makeplot("Lasso regression",LAMBDA,epsilon)
    #plt.title("Error of lasso regression, $\lambda=${}".format(LAMBDA))

    # k-cross validation
    partition_ = data.get_data_partition(k)

    # Ordinary least square plot
    # makeplot("Ordinary least squares {}-fold cross validation".format(k), partition = partition_)
    # plt.title("Error OLS using {}-fold cross validation".format(k))
    
    ## Ridge plot
    #makeplot("Ridge regression {}-fold cross validation".format(k), LAMBDA, partition=partition_)
    #plt.title("Error Ridge using {}-fold cross validation, $\lambda=${}".format(k,LAMBDA))

    # Lasso plot
    makeplot("Lasso regression {}-fold cross validation".format(k), LAMBDA, epsilon, partition_)
    plt.title("Error Lasso using {}-fold cross validation, $\lambda=${}".format(k,LAMBDA))

