# -*- coding: utf-8 -*-


# Import necessary libraries
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import accuracy_score, roc_auc_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.decomposition import PCA
from sklearn.cross_decomposition import PLSRegression
from sklearn.model_selection import permutation_test_score
import matplotlib.pyplot as plt
from sklearn.metrics import r2_score
from sklearn.utils import shuffle
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.offline as pyo
from .cross_validation import CrossValidation
#import plotting
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

import warnings
warnings.filterwarnings('ignore')

from pyChemometrics import ChemometricsScaler

import os

__auther__ = "aeiwz"


class opls_da:
    
    
        
    # Import necessary libraries
    import numpy as np
    import pandas as pd
    from sklearn.model_selection import train_test_split, cross_val_score
    from sklearn.metrics import accuracy_score, roc_auc_score
    from sklearn.preprocessing import StandardScaler
    from sklearn.pipeline import Pipeline
    from sklearn.decomposition import PCA
    from sklearn.cross_decomposition import PLSRegression
    from sklearn.model_selection import permutation_test_score
    import matplotlib.pyplot as plt
    from sklearn.metrics import r2_score
    from sklearn.utils import shuffle
    import plotly.express as px
    import plotly.graph_objects as go
    from plotly.subplots import make_subplots
    import plotly.offline as pyo
    from .cross_validation import CrossValidation
    #import plotting
    from sklearn.linear_model import LinearRegression
    from sklearn.metrics import mean_squared_error

    import warnings
    warnings.filterwarnings('ignore')

    from pyChemometrics import ChemometricsScaler

    import os




    '''
    OPLS-DA model
    
    Parameters
    ----------
    X : array-like, shape (n_samples, n_features)
        Training data, where n_samples is the number of samples and n_features is the number of features.
    y : array-like, shape (n_samples,)
        Target data, where n_samples is the number of samples.
    n_components : int, default=2
        Number of components to keep.
    scale : str, default='par'
        Method of scaling. 'par' for pareto scaling, 'mc' for mean centering, 'uv' for unitvarian scaling.
    cv : int, default=5
        Number of cross-validation folds.
    n_permutations : int, default=1000
        Number of permutations for permutation test.
    random_state : int, default=42
        Random state for permutation test.
    kfold : int, default=3
        Number of cross-validation folds.


    Examples:
    ----------
    
        ## Import package into python


        from metbit import opls_da, pca
        import pandas as pd
        import numpy as np


        ## Load dataset
        For example dataset are generated by random


        data = pd.DataFrame(np.random.rand(500, 50000))
        class_ = pd.Series(np.random.choice(['A', 'B'], 500), name='Group')

        datasets = pd.concat([class_, data], axis=1)


        # Assign X and target
        X = datasets.iloc[:, 2:]
        y = datasets['Group']
        time = datasets['Time point']
        features_name = list(X.columns.astype(float))

        ## Perform OPLS-DA model


        opls_da_mod = opls_da(X=X, y=y,features_name=features_name, n_components=2, scale='pareto', kfold=3, estimator='opls', random_state=42):
                
        opls_da.fit()

        opls.permutation_test(n_permutataion=1000,cv=3, n_jobs=-1, verbose=10)

        opls_da.vip_scores()


        ## Isualiseation of OPLs-DA model



        opls_da_model.plot_oplsda_scores()

        opls_da_model.vip_plot()

        opls_da_model.plot_hist()

        opls_da_model.plot_s_scores()

        opls_da_model.plot_loading()


        

    '''
    

        
    
    def __init__(self, X, y,features_name=None, n_components=2, scale='pareto', kfold=3, estimator='opls', random_state=42):
        


        #check X and y must be dataframe or array
        if not isinstance(X, (pd.DataFrame, np.ndarray)):
            raise ValueError('X must be a dataframe or array')
        if not isinstance(y, (pd.Series, np.ndarray)):
            raise ValueError('y must be a series or array')
        if X.shape[0] != y.shape[0]:
            raise ValueError('X and y must have the same number of samples')
        if not isinstance(n_components, int):
            raise ValueError('n_components must be an integer')
        if not isinstance(scale, str):
            raise ValueError('scale must be a string')
        if not isinstance(kfold, int):
            raise ValueError('kfold must be an integer')
        if not isinstance(estimator, str):
            raise ValueError('estimator must be a string')
        if not isinstance(random_state, int):
            raise ValueError('random_state must be an integer')
        if features_name is not None:
            if not isinstance(features_name, (pd.Series, np.ndarray, list)):
                raise ValueError('features_name must be a series, list or 1D array')
            if len(features_name) != X.shape[1]:
                raise ValueError('features_name must have the same number of features as X')
            
            
            
        #check unique values in y
        if isinstance(y, pd.Series):
            if len(y.unique()) < 2:
                raise ValueError('OPLS-DA requires at least 2 group comparisons')
        if isinstance(y, np.ndarray):
            if len(np.unique(y)) < 2:
                raise ValueError('OPLS-DA requires at least 2 group comparisons')
        if isinstance(y, list):
            if len(np.unique(y)) < 2:
                raise ValueError('OPLS-DA requires at least 2 group comparisons')        
            
        #check unique values in y
        if isinstance(y, pd.Series):
            if len(y.unique()) > 2:
                raise ValueError('OPLS-DA requires only 2 group comparisons')
        if isinstance(y, np.ndarray):
            if len(np.unique(y)) < 2:
                raise ValueError('OPLS-DA requires only 2 group comparisons')
        if isinstance(y, list):
            if len(np.unique(y)) < 2:
                raise ValueError('OPLS-DA requires only 2 group comparisons')



        self.features_name = features_name
        if features_name is None:
            if isinstance(X, pd.DataFrame):
                self.features_name = X.columns
            else:
                self.features_name = np.arange(X.shape[1])
        else:
            self.features_name = features_name
            
        if isinstance(X, pd.DataFrame):
            self.X = X.values
        else:
            self.X = X 

        #fill nan with 0
        self.X = np.nan_to_num(self.X)
                     
        self.X = X
        self.y = y
        self.n_components = n_components
        self.scale = scale
        self.random_state = random_state
        self.opls_model = None
        self.opls_cv = None
        self.opls_permutation_cv = None
        self.opls_permutation_cv_scores = None
        self.opls_permutation_cv_score = None
        self.opls_permutation_cv_score_std = None
        self.kfold = kfold
        self.estimator = estimator
        
        
    def fit(self):
        
        X = self.X
        y = self.y
        features_name = self.features_name
        n_components = self.n_components
        scale = self.scale

        random_state = self.random_state
        kfold = self.kfold
        estimator = self.estimator
        
        if scale == 'pareto':
            scale_power = 0.5
        elif scale == 'mean':
            scale_power = 0
        elif scale == 'uv':
            scale_power = 1
        elif scale == 'minmax':
            scale_power = 0
            
        self.scale = scale
            
            
        # Create a pipeline with data preprocessing and OPLS-DA model
        pipeline = Pipeline([
                                ('scale', ChemometricsScaler(scale_power=scale_power)),
                                ('oplsda', PLSRegression(n_components=n_components)),
                                ('opls', CrossValidation(kfold=kfold, estimator=estimator, scaler=scale))
                            ])

        self.pipeline = pipeline

        oplsda = pipeline.named_steps['oplsda']
        cv = pipeline.named_steps['opls']
        cv.fit(X.values, y)

        oplsda.fit(X, pd.Categorical(y).codes)
        
        s_df_scores_ = pd.DataFrame({'correlation': cv.correlation,'covariance': cv.covariance}, index=features_name)
        df_opls_scores = pd.DataFrame({'t_scores': cv.scores, 't_ortho': cv.orthogonal_score, 't_pred': cv.predictive_score, 'Group': y})


        R2Xcorr = cv.R2Xcorr
        R2y = cv.R2y
        q2 = cv.q2

        self.R2Xcorr = R2Xcorr
        self.R2y = R2y
        self.q2 = q2

        self.s_df_scores_ = s_df_scores_
        self.df_opls_scores = df_opls_scores
        
        self.oplsda = oplsda
        self.cv = cv
    


    def permutation_test(self, n_permutations=500, cv=3, n_jobs=-1, verbose=10):


        from sklearn.pipeline import Pipeline
        
        self.cv = cv
        self.n_permutations = n_permutations
        oplsda = self.oplsda
        X = self.X
        y = self.y
        pipeline = self.pipeline
        randomstate = self.random_state
        self.n_jobs = n_jobs
        self.verbose = verbose
        
        oplsda.fit(X, pd.Categorical(y).codes)

        # Permutation test to assess the significance of the model
        acc_score, permutation_scores, p_value = permutation_test_score(
        pipeline.named_steps['oplsda'], X, pd.Categorical(y).codes, cv=3, n_permutations=n_permutations, n_jobs=n_jobs, random_state=randomstate, verbose=verbose)


        self.acc_score = acc_score
        self.permutation_scores = permutation_scores
        self.p_value = p_value
        
        
    
    def vip_scores(self, model=None, features_name = None):
        
        
        if model is None:   
            model = self.oplsda
        else:
            model = model
            
        
        features_name = self.features_name



        t = model.x_scores_
        w = model.x_weights_
        q = model.y_loadings_
        p, h = w.shape
        vips = np.zeros((p,))
        s = np.diag(t.T @ t @ q.T @ q).reshape(h, -1)
        total_s = np.sum(s)
        for i in range(p):
            weight = np.array([ (w[i,j] / np.linalg.norm(w[:,j]))**2 for j in range(h) ])
            vips[i] = np.sqrt(p*(s.T @ weight)/total_s)
       
        if features_name is not None:
            vips = pd.DataFrame(vips, columns = ['VIP'])
            vips['Features'] = features_name
        else:
            vips = pd.DataFrame(vips, columns = ['VIP'])
            vips['Features'] = vips.index



        self.vips = vips

        return

    def get_vip_scores(self):
        vips = self.vips
        return vips



    def vip_plot(self, x_range = 9, threshold = 2, size = 12, width = 1000, height = 500, filter_ = False):
        
        
        # add scatter plot of VIP score
        import plotly.express as px
        vips = self.vips

        #add threshold column to define cutoff for VIP score if >= treschold then 1 else 0
        vips['threshold'] = np.where(vips['VIP'] >= threshold, "Pass", "Under cut off")

        if filter_ == True:
            vips = vips[vips['VIP'] >= threshold]

        fig = px.scatter(vips, x='Features', y='VIP', 
        text='Features', 
        color='threshold', color_discrete_map={'Pass':'#FF7961', 'Under cut off':'#ECECEC'}, 
        height=height, width=width, 
        title='VIP score')

        fig.update_traces(marker=dict(size=size))
        fig.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_layout(paper_bgcolor='rgba(0,0,0,0)',plot_bgcolor='rgba(0,0,0,0)')

        # tickformat of y-axis to 2 decimal places and x-axis to 3 decimal places
        fig.update_yaxes(tickformat=".2f")
        fig.update_xaxes(tickformat=".3f")

        
        fig.update_layout(
            title={
                'y':1,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=20))

        # reverse the x-axis
        fig.update_xaxes(autorange="reversed")
        
        # add dashed line for threshold
        fig.add_shape(type="line",
                    x0=0, y0=threshold, x1=x_range, y1=threshold,
                    line=dict(color="red",width=2))
                    
        fig.update_layout(showlegend=False)
        
        return fig





    def plot_oplsda_scores(self, color_dict = None, symbol = None, symbol_dict = None, fig_height = 900, fig_width = 1300,
                    marker_size = 35, marker_opacity = 0.7):
        
        #Visualise
        #check symbol dimension must be equal to y
        if symbol is not None:
            if len(symbol) != len(self.y):
                raise ValueError('symbol must have the same number of samples as y')

        #check symbol_dict must be a dictionary
        if symbol_dict is not None:
            if not isinstance(symbol_dict, dict):
                raise ValueError('symbol_dict must be a dictionary')
        else:
            symbol_dict = None

        #check color_dict must be a dictionary
        if color_dict is not None:
            if not isinstance(color_dict, dict):
                raise ValueError('color_dict must be a dictionary')
        else:
            color_dict = None

        df_opls_scores = self.df_opls_scores


        
        from .pca_ellipse import confidence_ellipse
        fig = px.scatter(df_opls_scores, x='t_scores', y='t_ortho', symbol=symbol, 
                    
                        symbol_map=symbol_dict,
                    
                        color='Group', 
                        color_discrete_map=color_dict, 
                        title='<b>OPLS-DA Scores Plot<b>', 
                        height=fig_height, width=fig_width,
                        labels={
                            't_pred': 't<sub>predict</sub>',
                            't_ortho': 't<sub>orthogonal</sub>',
                            't_scores': 't<sub>scores</sub>',
                            'Group': 'Group'}
        )

        fig.update_traces(marker=dict(size=marker_size, 
                            opacity=marker_opacity, 
                            line=dict(width=2, color='DarkSlateGrey')))


        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=0,
                                y=1.04+0.05,
                                showarrow=False,
                                text='<b>R<sup>2</sup>X: {}%<b>'.format(np.round(self.R2Xcorr*100, decimals=2)),
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")


        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=0,
                                y=1.0+0.05,
                                showarrow=False,
                                text='<b>R<sup>2</sup>Y: {}%<b>'.format(np.round(self.R2y*100, decimals=2)),
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")


        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=0,
                                y=1.08+0.05,
                                showarrow=False,
                                text='<b>Q<sup>2</sup>: {}%<b>'.format(np.round(self.q2*100, decimals=2)),
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")

                        
        fig.add_shape(type='path',
                path=confidence_ellipse(df_opls_scores['t_scores'], df_opls_scores['t_ortho']))


        fig.update_yaxes(tickformat=",.0")
        fig.update_xaxes(tickformat=",.0")

        fig.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_layout(
            title={
                'y':1,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=20))
        fig.update_layout(paper_bgcolor='rgba(0,0,0,0)',plot_bgcolor='rgba(0,0,0,0)')

        return fig



    def plot_hist(self, nbins_=50, height_=500, width_=1000):

        #check permutation model must be fitted
        if self.permutation_scores is None:
            raise ValueError('Permutation test must be performed first')

        permutation_scores = self.permutation_scores


        #Histrogram
        #Plot histogram of permutation scores
        fig = px.histogram(permutation_scores, nbins=nbins_, height=height_, width=width_, 
                        title='<b>Permutation scores<b>',
                        labels={'value': 'Accuracy score', 
                                'count': 'Frequency'})



        fig.add_shape(type='line', yref='paper', y0=0, y1=1, 
                        xref='x', x0=self.acc_score, x1=self.acc_score, 
                        line=dict(dash='dash', color='red', width=3))



        fig.add_annotation(dict(font=dict(color="black",size=14),
                                #x=x_loc,
                                x=0,
                                y=1.25,
                                #y=1.18,
                                showarrow=False,
                                text='Number of permutation: {}'.format(self.n_permutations),
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")

        fig.add_annotation(dict(font=dict(color="black",size=14),
                                #x=x_loc,
                                x=0,
                                y=1.18,
                                showarrow=False,
                                text='Accuracy score: {}'.format(np.round(self.acc_score, decimals=3)),
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")
        fig.add_annotation(dict(font=dict(color="black",size=14),
                                #x=x_loc,
                                x=0,
                                y=1.11,
                                showarrow=False,
                                text='<i>p-value</i>: {}'.format(np.round(self.p_value, decimals=6)),
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")


        
        fig.update_layout(showlegend=False)

        fig.update_layout(title_x=0.5)

        return fig



    def plot_s_scores(self, height_=900, width_=2000, range_color_=[-0.05,0.05], color_continuous_scale_='jet'):

        s_df_scores_ = self.s_df_scores_


        fig = px.scatter(s_df_scores_, x='covariance', y='correlation', color='covariance', range_color=range_color_,
                        color_continuous_scale=color_continuous_scale_, text=s_df_scores_.index, height=height_, width=width_,)
        fig.update_layout(title='<b>S-plot</b>', xaxis_title='Covariance', yaxis_title='Correlation')


        
        #add line of axis and set color to black and line width to 2 pixel
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')

        #Add tick width to 2 pixel
        fig.update_xaxes(tickwidth=2)
        fig.update_yaxes(tickwidth=2)
        fig.update_layout(paper_bgcolor='rgba(0,0,0,0)',plot_bgcolor='rgba(0,0,0,0)')
        fig.update_yaxes(tickformat=",.0")
        #fig.update_xaxes(tickformat=",.0")
        fig.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_layout(
            title={
                'y':1,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=20))

        fig.update_traces(marker=dict(size=14))

        return fig


    
    def plot_loading(self, height_=900, width_=2000, range_color_=[-0.05,0.05], color_continuous_scale_='jet'):

        s_df_scores_ = self.s_df_scores_

        def median_corr(X):
            X_corr = np.median(X, axis=0)
            X_corr = X_corr * np.sign(s_df_scores_['correlation'])
            return X_corr

        X2 = median_corr(self.X)

        s_df_scores_ = self.s_df_scores_
        features_name = self.features_name

        fig = px.scatter(s_df_scores_, x=features_name, y=X2, 
                            color='covariance', color_continuous_scale=color_continuous_scale_, range_color=range_color_,
                            text=s_df_scores_.index, 
                            height=height_, width=width_)

        fig.update_traces(marker=dict(size=5))
        fig.update_xaxes(autorange="reversed")
        fig.update_layout(title='<b>Loading spectra</b>', xaxis_title='𝛿<sub>H</sub> in ppm', yaxis_title='Correlation')
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')

        fig.update_layout(paper_bgcolor='rgba(0,0,0,0)',plot_bgcolor='rgba(0,0,0,0)')
        fig.update_yaxes(tickformat=",.0")
        #fig.update_xaxes(tickformat=",.0")
        fig.update_layout(
            title={
                'y':1,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=20))

        return fig




class pca:

    import numpy as np

    from sklearn import preprocessing
    import pandas as pd
    import matplotlib.pyplot as plt

    from pyChemometrics.ChemometricsPCA import ChemometricsPCA
    from pyChemometrics.ChemometricsScaler import ChemometricsScaler

    # Use to obtain same values as in the text


    import os
    import plotly.express as px
    import plotly.graph_objects as go

    from sklearn import decomposition
    from sklearn.preprocessing import scale
    from .pca_ellipse import confidence_ellipse

    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import StandardScaler
    from sklearn.decomposition import PCA
    from sklearn.metrics import r2_score
    from lingress import unipair


    def __init__(self, X, label, features_name=None, n_components=2, scale='pareto', random_state=42, test_size=0.3):

        '''
        

        PCA model
        
        Parameters
        ----------
        X : array-like, shape (n_samples, n_features)
            Training data, where n_samples is the number of samples and n_features is the number of features.
        label : array-like, shape (n_samples,)
            Target data, where n_samples is the number of samples.
        features_name : array-like, shape (n_features,), default=None
            Name of features.
        n_components : int, default=2
            Number of components to keep.
        scale : str, default='pareto'
            Method of scaling. 'pareto' for pareto scaling, 'mean' for mean centering, 'uv' for unitvarian scaling.
        random_state : int, default=42
            Random state for permutation test.
        test_size : float, default=0.3
            Size of test set.

        Examples:
        ----------
        import pandas as pd
        import numpy as np
        from metbit import pca

        # Create a dataset
        data = pd.DataFrame(np.random.rand(500, 50000))
        class_ = pd.Series(np.random.choice(['A', 'B', 'C'], 500), name='Group')
        time = pd.Series(np.random.choice(['1-wk', '2-wk', '3-wk', '4-wk'], 500), name='Time point')


        # Assign X and target
        X = datasets.iloc[:, 2:]
        y = datasets['Group']
        time = datasets['Time point']
        features_name = list(X.columns.astype(float))

        ## Perform PCA model


        pca_mod = pca(X = X, label = y, features_name=features_name, n_components=2, scale='pareto', random_state=42, test_size=0.3)
        pca_mod.fit()


        # Visualisation of PCA model
        pca_mod.plot_observe_variance()

        pca_mod.plot_cumulative_observed()

        shape_ = {'1-wk': 'circle', '2-wk': 'square', '3-wk': 'diamond', '4-wk': 'cross'}

        pca_mod.plot_pca_scores(symbol=time, symbol_dict=shape_)

        pca_mod.plot_loading_()

        pca_mod.plot_pca_trajectory(time_=time, time_order={'1-wk': 0, '2-wk': 1, '3-wk': 2, '4-wk': 3}, color_dict={'A': '#636EFA', 'B': '#EF553B', 'C': '#00CC96'}, symbol_dict=shape_)




        '''


        if features_name is not None:
            if not isinstance(features_name, (pd.Series, np.ndarray, list)):
                raise ValueError('features_name must be a series, list or 1D array')
            if len(features_name) != X.shape[1]:
                raise ValueError('features_name must have the same number of features as X')

        if not isinstance(X, (pd.DataFrame, np.ndarray)):
            raise ValueError('X must be a dataframe or array')

        if not isinstance(n_components, int):
            raise ValueError('n_components must be an integer')

        if not isinstance(scale, str):
            raise ValueError('scale must be a string')

        if not isinstance(random_state, int):
            raise ValueError('random_state must be an integer')

        if not isinstance(label, (pd.Series, np.ndarray, list)):
            raise ValueError('label must be a series, list or array')
        if len(label) != X.shape[0]:
            raise ValueError('X and label must have the same number of samples')

        self.features_name = features_name
        if features_name is None:
            if isinstance(X, pd.DataFrame):
                self.features_name = X.columns
            else:
                self.features_name = np.arange(X.shape[1])
        else:
            self.features_name = features_name

        
        # Check missing values in X
        if isinstance(X, pd.DataFrame):
            if X.isnull().sum().sum() > 0:
                raise ValueError('X contains missing values')
        else:
            if np.isnan(X).sum().sum() > 0:
                raise ValueError('X contains missing values')

        self.X = X
        self.label = label
        self.n_components = n_components
        self.scale = scale
        self.random_state = random_state

        if scale == 'pareto':
            scale_power_ = 0.5
        elif scale == 'mean':
            scale_power_ = 0
        elif scale == 'uv':
            scale_power_ = 1
        elif scale == 'minmax':
            scale_power_ = 0

        self.scale_power_ = scale_power_
        self.test_size = test_size



    
    def fit(self):

        import numpy as np

        from sklearn import preprocessing
        import pandas as pd
        import matplotlib.pyplot as plt

        from pyChemometrics.ChemometricsPCA import ChemometricsPCA
        from pyChemometrics.ChemometricsScaler import ChemometricsScaler

        # Use to obtain same values as in the text


        import os
        import plotly.express as px
        import plotly.graph_objects as go

        from sklearn import decomposition
        from sklearn.preprocessing import scale
        from .pca_ellipse import confidence_ellipse

        from sklearn.model_selection import train_test_split
        from sklearn.preprocessing import StandardScaler
        from sklearn.decomposition import PCA
        from sklearn.metrics import r2_score
        from lingress import unipair

        test_size=self.test_size

        X = self.X
        label = self.label
        n_components = self.n_components
        scale = self.scale
        random_state = self.random_state
        features_name = self.features_name
        Y = pd.Categorical(label).codes
        scale_power_ = self.scale_power_ 


        model_scaler = ChemometricsScaler(scale_power=scale_power_)
        model_scaler.fit(X)
        model_X = model_scaler.transform(X)



        pca_model = decomposition.PCA(n_components=n_components)
        pca_model.fit(model_X)

        self.scores_ = pca_model.transform(model_X)
        self.loadings_ = pca_model.components_.T
        

        #Create dataframe for scores depending on the number of components
        for i in range(n_components):
            if i == 0:
                df_scores_ = pd.DataFrame(self.scores_[:,i], columns=['PC{}'.format(i+1)])
            else:
                df_scores_['PC{}'.format(i+1)] = self.scores_[:,i]

        df_scores_['Group'] = label

        self.df_scores_ = df_scores_

        #Create dataframe for loadings depending on the number of components
        for i in range(n_components):
            if i == 0:
                df_loadings_ = pd.DataFrame(self.loadings_[:,i], index=features_name, columns=['PC{}'.format(i+1)])
            else:
                df_loadings_['PC{}'.format(i+1)] = self.loadings_[:,i]

        df_loadings_['Features'] = features_name

        self.df_loadings_ = df_loadings_

        explained_variance_ = pca_model.explained_variance_ratio_
        explained_variance_ = np.insert(explained_variance_, 0, 0)
        cumulative_variance_ = np.cumsum(explained_variance_)


        r2_index = ['']
        for i in range(n_components):
            r2_index.append('PC{}'.format(i+1))

        df_explained_variance_ = pd.DataFrame(r2_index, columns=['PC'])
        df_explained_variance_['Explained variance'] = explained_variance_
        df_explained_variance_['Cumulative variance'] = cumulative_variance_



        X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=test_size, random_state=random_state)
        X_test = model_scaler.transform(X_test)
        X_test_pca = pca_model.transform(X_test)

            # Inverse transform the test set from the PCA space
        X_test_reconstructed = pca_model.inverse_transform(X_test_pca)


        # Calculate Q2 score for the test set
        q2_test = r2_score(X_test, X_test_reconstructed)
        

        self.q2_test = q2_test
        self.explained_variance_ = explained_variance_  
        self.cumulative_variance_ = cumulative_variance_
        self.df_explained_variance_ = df_explained_variance_
        self.pca_model = pca_model
        self.model_scaler = model_scaler
        self.X_test = X_test
        self.y_test = y_test
        self.X_test_reconstructed = X_test_reconstructed
        self.X_test_pca = X_test_pca
        self.df_scores_
        self.df_loadings_

        return pca_model

    def get_explained_variance(self):
        df_explained_variance_ = self.df_explained_variance_
        return df_explained_variance_

    def get_scores(self):
        df_scores_ = self.df_scores_
        return df_scores_

    def get_loadings(self):
        df_loadings_ = self.df_loadings_
        return df_loadings_

    def get_q2_test(self):
        q2_test = self.q2_test
        return q2_test

    def plot_observe_variance(self):

        scale = self.scale
        
        df_explained_variance_ = self.df_explained_variance_
        
        fig = px.bar(df_explained_variance_, 
                x='PC', y=df_explained_variance_['Explained variance'],
                text=np.round(df_explained_variance_['Explained variance'], decimals=3),
                width=800, height=600,
                title='Explained Variance ({} scaling)'.format(scale))
        fig.update_layout(
            title={
                'y':0.9,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=15))
        fig.update_traces(texttemplate='%{text:.3f}', textposition='outside')
        return fig


    def plot_cumulative_observed(self):
        
        df_explained_variance_ = self.df_explained_variance_

        fig = go.Figure()

        fig.add_trace(
            go.Scatter(
                x=df_explained_variance_['PC'],
                y=df_explained_variance_['Cumulative variance'],
                marker=dict(size=15, color="LightSeaGreen"),
                name='R<sup>2</sup>X (Cum)'
            ))

        fig.add_trace(
            go.Bar(
                x=df_explained_variance_['PC'],
                y=df_explained_variance_['Explained variance'],
                marker=dict(color="RoyalBlue"),
                name='R<sup>2</sup>X',
                text=np.round(df_explained_variance_['Explained variance'], decimals=3)
            ))
        fig.update_layout(width=800, height=600,
                        title='Explained Variance and Cumulative Variance')
        fig.update_layout(
            title={
                'y':0.9,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'})

        return fig



    def plot_pca_scores(self, pc=['PC1', 'PC2'], 
                        color_=None, color_dict=None, 
                        symbol=None, symbol_dict=None, 
                        fig_height=900, fig_width=1300,
                        marker_size=35, marker_opacity=0.7,
                        text_ = None):

        from .pca_ellipse import confidence_ellipse

        scale = self.scale
        df_scores_ = self.df_scores_
        r2 = self.df_explained_variance_
        q2_test = self.q2_test
        
        if color_ is not None:
            if color_ not in self.label:
                raise ValueError('color must be in y')
        if symbol is not None:
            if len(symbol) != len(self.label):
                raise ValueError('symbol must have the same number of samples as y')

        if color_ is None:
            color_ = df_scores_['Group']
        else:
            color_ = color_
            




        #check symbol dimension must be equal to y
        if symbol is not None:
            if len(symbol) != len(self.label):
                raise ValueError('symbol must have the same number of samples as y')

        #check symbol_dict must be a dictionary
        if symbol_dict is not None:
            if not isinstance(symbol_dict, dict):
                raise ValueError('symbol_dict must be a dictionary')
        else:
            symbol_dict = None

        #check color_dict must be a dictionary
        if color_dict is not None:
            if not isinstance(color_dict, dict):
                raise ValueError('color_dict must be a dictionary')
        else:
            color_dict = None

        # pc must be a list of 2
        if not isinstance(pc, list):
            raise ValueError("pc must be a list of string \n Example: pc=['PC1', 'PC2']")
        if len(pc) != 2:
            raise ValueError('pc must be a list of 2')
        # pc must be match with columns of df_scores_
        if pc[0] not in self.df_scores_.columns:
            raise ValueError("pc must be in df_scores_ columns \n Example: pc=['PC1', 'PC2']")
        if pc[1] not in self.df_scores_.columns:
            raise ValueError("pc must be in df_scores_ columns \n Example: pc=[\'PC1\', \'PC2\']")

        

        df_scores_ = self.df_scores_
        r2 = self.df_explained_variance_
        q2_test = self.q2_test



        fig = px.scatter(df_scores_, x=pc[0], y=pc[1], color=color_,
                        symbol=symbol, 
                        color_discrete_map=color_dict, 
                        symbol_map=symbol_dict, 
                        title=f'<b>PCA Scores Plot<b> {scale} scaling', 
                        height=fig_height, width=fig_width,
                        labels={'Group': 'Group',
                                pc[0]: "{} R<sup>2</sup>X: {}%".format(pc[0], np.round(r2.loc[r2.loc[r2['PC']==pc[0]].index, 'Explained variance'].values[0]*100, decimals=2)),
                                pc[1]: "{} R<sup>2</sup>X: {}%".format(pc[1], np.round(r2.loc[r2.loc[r2['PC']==pc[1]].index, 'Explained variance'].values[0]*100, decimals=2))},
                        text=text_)

        fig.update_traces(marker=dict(size=marker_size, 
                            opacity=marker_opacity, 
                            line=dict(width=2, color='DarkSlateGrey')))


        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=1.0,
                                y=0.05,
                                showarrow=False,
                                text=f"<b>R<sup>2</sup>X (Cum): {np.round(r2.loc[r2.loc[r2['PC']==pc[1]].index, 'Cumulative variance'].values[0]*100, decimals=2)}%<b>",
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")

        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=1.0,
                                y=0.01,
                                showarrow=False,
                                text=f"<b>Q<sup>2</sup>X (Cum): {np.round(q2_test*100, decimals=2)}%<b>",
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")

        fig.add_shape(type='path',
                path=confidence_ellipse(df_scores_[pc[0]], df_scores_[pc[1]]))



        fig.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_layout(
            title={
                'y':1,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=20))
        fig.update_layout(paper_bgcolor='rgba(0,0,0,0)',plot_bgcolor='rgba(0,0,0,0)')

        return fig



    def plot_loading_(self, pc=['PC1', 'PC2'], height_=600, width_=1800):

        df_loadings_ = self.df_loadings_

        loadings_label = self.features_name


        fig = px.line(df_loadings_, x=loadings_label, y=pc,
                                height=height_, width=width_,
                                title='Loadings plot',
                                text=loadings_label)

        fig.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_layout(title={'y':0.95,
                                'x':0.5,
                                'xanchor': 'center',
                                'yanchor': 'top'},
                        font=dict(size=20))
        
        fig.update_layout(scene={'xaxis': {'autorange': 'reversed'}})
                
        fig.update_traces(marker=dict(size=1))
        fig.update_layout(xaxis_title="𝛿<sub>H</sub> in ppm")
        
        
        return fig






    def plot_pca_trajectory(self,time_, time_order, stat_ = ['mean', 'sem'], pc=['PC1', 'PC2'],
                            color_dict = None, symbol_dict = None, 
                            height_=900, width_=1300,
                            marker_size=35, marker_opacity=0.7, ):


        from .pca_ellipse import confidence_ellipse


        #check time_order must be a dictionary
        if not isinstance(time_order, dict):
            raise ValueError("time_order must be a dictionary \n Example: time_order = {'Day 1': 0, 'Day 2': 1, 'Day 3': 2}")



        #check time are not missing
        if time_ is None:
            raise ValueError('time_ must be provided fot time trajectory analysis')

        #check time_ must be pandas series, list or array
        if not isinstance(time_, (pd.Series, np.ndarray, list)):
            raise ValueError('time_ must be a series, list or array')
        if len(time_) != len(self.label):
            raise ValueError('time_ must have the same number of samples as group')

        


        df_scores_ = self.df_scores_
        r2 = self.df_explained_variance_
        q2_test = self.q2_test
        df_scores_['Time point'] = time_
        



        #check stat_[0] must be mean or median
        if stat_[0] not in ['mean', 'median']:
            raise ValueError('stat_[0] must be mean or median')
        #check stat_[1] must be sem, std
        if stat_[1] not in ['sem','std']:
            raise ValueError('stat_[1] must be sem or std')

        if stat_[0] == 'mean':
            df_scores_point = df_scores_.groupby(['Group', 'Time point']).mean()
        if stat_[0] == 'median':
            df_scores_point = df_scores_.groupby(['Group', 'Time point']).median()

        if stat_[1] == 'sem':
            err_df = df_scores_.groupby(['Group', 'Time point']).sem()
        if stat_[1] == 'std':
            err_df = df_scores_.groupby(['Group', 'Time point']).std()


        df_scores_point.reset_index(inplace=True)
        df_scores_point['Time order'] = df_scores_point['Time point'].map(time_order)
        df_scores_point.sort_values(by=['Group', 'Time order'], inplace=True)

        err_df.reset_index(inplace=True)
        err_df['Time order'] = err_df['Time point'].map(time_order)
        err_df.sort_values(by=['Group', 'Time order'], inplace=True)



        #check color_dict must be a dictionary
        if color_dict is not None:
            if not isinstance(color_dict, dict):
                raise ValueError('color_dict must be a dictionary')
        else:
            color_dict = None


        if symbol_dict is not None:
            if not isinstance(symbol_dict, dict):
                raise ValueError('symbol_dict must be a dictionary')
        else:
            symbol_dict = None
        


        #If user not input color_dict then get unique of label and create color_dict
        if color_dict is None:
            color_dict = {i: px.colors.qualitative.Plotly[i] for i in range(len(df_scores_point['Group'].unique()))}

        #Change {0: '#636EFA', 1: '#EF553B', 2: '#00CC96'} to {'Group1': '#636EFA', 'Group2': '#EF553B', 'Group3': '#00CC96'}
        color_dict = {df_scores_point['Group'].unique()[i]: list(color_dict.values())[i] for i in range(len(df_scores_point['Group'].unique()))}
        
        n_group = df_scores_point['Group'].unique()



        fig = px.line(df_scores_point, x=pc[0], y=pc[1], line_group='Time point', 
                        error_x=err_df[pc[0]], error_y=err_df[pc[1]],
                        color='Group', color_discrete_map=color_dict,
                        symbol='Time point', symbol_map=symbol_dict,
                        title='<b>Principle component analysis ({})<b>'.format(self.scale), 
                        height=height_, width=width_,
                        labels={
                            pc[0]: "{} R<sup>2</sup>X: {} %".format(pc[0], np.round(r2.loc[r2.loc[r2['PC']==pc[0]].index, 'Explained variance'].values[0]*100, decimals=2)),
                            pc[1]: "{} R<sup>2</sup>X: {} %".format(pc[1], np.round(r2.loc[r2.loc[r2['PC']==pc[1]].index, 'Explained variance'].values[0]*100, decimals=2)),
                            'Group': 'Group'
                            })


        



        for i in range(len(n_group)):
            # create a new trace for the connecting line
            fig.add_trace(go.Scatter(
                x=df_scores_point.loc[list(df_scores_point.loc[df_scores_point['Group'] == df_scores_point['Group'].unique()[i]].index), pc[0]], # x-coordinates of the line
                y=df_scores_point.loc[list(df_scores_point.loc[df_scores_point['Group'] == df_scores_point['Group'].unique()[i]].index), pc[1]], # y-coordinates of the line
                mode='lines', # specify the trace type as lines
                line=dict(color=color_dict[n_group[i]], width=2), # set the color and width of the line
                showlegend=False # hide the trace from the legend
            ))



        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=1.0,
                                y=0.05,
                                showarrow=False,
                                text=f"<b>R<sup>2</sup>X (Cum): {np.round(r2.loc[r2.loc[r2['PC']==pc[1]].index, 'Cumulative variance'].values[0]*100, decimals=2)}%<b>",
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")

        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=1.0,
                                y=0.01,
                                showarrow=False,
                                text=f"<b>Q<sup>2</sup>X (Cum): {np.round(q2_test*100, decimals=2)}%<b>",
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")

        fig.add_shape(type='path',
                path=confidence_ellipse(df_scores_point[pc[0]], df_scores_point[pc[1]]))


                    #update axis as scitifics
        fig.update_xaxes(tickformat=".1e")
        fig.update_yaxes(tickformat=".1e")



        fig.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_layout(
            title={
                'y':0.95,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=20))


        fig.update_traces(marker=dict(size=marker_size, opacity=marker_opacity, line=dict(width=2, color='DarkSlateGrey')))

        fig.update_layout(paper_bgcolor='rgba(0,0,0,0)',plot_bgcolor='rgba(0,0,0,0)')
        
        
        return fig