import pandas as pd
import numpy as np
import math
import scipy.stats as stats

import matplotlib.pyplot as plt
from matplotlib.ticker import FormatStrFormatter

from bluebelt.core.exceptions import *
import bluebelt.analysis.ci as ci
import bluebelt.helpers.boxplot
import bluebelt.helpers.text
import bluebelt.styles

from bluebelt.core.checks import check_kwargs

def index():
    df = pd.DataFrame([['discrete', 'discrete', '-', '-', '-', 'chi square test', ''],
                        ['discrete', 'continuous', '-', '-', '-', 'logistic regression', ''],
                        ['continuous', 'discrete', 'mean', '1', 'normal', '1 sample t-test', 'one_sample_t'],
                        ['continuous', 'discrete', 'mean', '1', 'non-normal', '1 sample Wilcoxon test', 'wilcoxon'],
                        ['continuous', 'discrete', 'mean', '2', 'normal', '2 sample t-test', 'two_sample_t'],
                        ['continuous', 'discrete', 'mean', '2', 'non-normal', 'Mann-Whitney test', 'mann_whitney'],
                        ['continuous', 'discrete', 'mean', '>2', 'normal', '1 way Anova', 'anova'],
                        ['continuous', 'discrete', 'mean', '>2', 'non-normal', 'Kruskal-Wallis test', 'kruskal'],
                        ['continuous', 'discrete', 'variance', '2', 'normal', 'F-test', 'f_test'],
                        ['continuous', 'discrete', 'variance', '2', 'non-normal', 'Levene’s test', 'levene'],
                        ['continuous', 'discrete', 'variance', '>2', 'normal', 'Bartlett’s test', 'bartlett'],
                        ['continuous', 'discrete', 'variance', '>2', 'non-normal', 'Levene’s test', 'levene'],
                        ['continuous', 'continuous', '', '-', '-', 'regression', '']],
                    columns=['Y', 'X', 'investigate', '# data groups', 'distribution', 'test', 'bluebelt'])
    print(df.to_string(index=False))

# equal means
class OneSampleT():

    def __init__(self, frame, columns=None, popmean=None, confidence=0.95, alpha=0.05, **kwargs):

        if isinstance(frame, pd.Series):
            frame = pd.DataFrame(frame)
        elif isinstance(columns, str):
            frame = frame[columns]
        elif isinstance(columns, list):
            frame = frame[columns[0]]
        else:
            frame = frame[:,:1]

        # check arguments
        ncols = 1
        check_kwargs(locals())
                        
        self.frame = frame
        self.columms = columns
        self.nrows = frame.shape[0]
        self.ncols = frame.shape[1]
        self.popmean = popmean
        self.confidence = confidence
        self.alpha = alpha
        self.test = "One Sample T"
        self.h0 = f'$H_0: \\bar{{X}} = \\mu'
        self.calculate(**kwargs)

    def calculate(self, **kwargs):
        self.statistic, self.p_value = stats.ttest_1samp(a=self.frame.iloc[:,0].values, popmean=self.popmean, **kwargs)
        self.ci_mean = [ci.ci_mean(self.frame.iloc[:,0])]
        self.passed = True if self.p_value > self.alpha else False

    def __str__(self):
        return ""
    
    def __repr__(self):
        return (f'{self.__class__.__name__}(n={self.nrows})')
    
    def result(self):
        print(self.statistic)
    
    def plot(self, **kwargs):
        return _equal_means_plot(self, **kwargs)

class Wilcoxon():
    """
    Calculate the Wilcoxon signed-rank test.

    The Wilcoxon signed-rank test tests the null hypothesis that two related paired samples come from the same distribution.
    In particular, it tests whether the distribution of the differences x - y is symmetric about zero.
    It is a non-parametric version of the paired T-test.
    This test is performed with the assumption that if a pandas Series is provided the series contains the differences between x and y.
    """
    def __init__(self, frame, columns=None, confidence=0.95, alpha=0.05, **kwargs):

        if isinstance(frame, pd.Series):
            frame = pd.DataFrame(frame)
        elif isinstance(columns, str):
            frame = frame[columns]
        elif isinstance(columns, list):
            frame = frame[columns[:2]]
        else:
            frame = frame[:,:2]

        # check arguments
        ncols = (1, min(frame.shape[1], 2))
        check_kwargs(locals())
        
        self.frame = frame
        self.columms = columns
        self.nrows = self.frame.shape[0]
        self.ncols = self.frame.shape[1]
        self.confidence = confidence
        self.alpha = alpha
        self.test = "Wilcoxon signed-rank test"
        self.h0 = f'difference between the pairs follows a symmetric distribution around zero'
        self.calculate(**kwargs)

    def calculate(self, **kwargs):
        self.array_a = self.frame.iloc[:,0]
        self.array_b = self.frame.iloc[:,1] if self.ncols == 2 else None
        self.statistic, self.p_value = stats.wilcoxon(x=self.array_a, y=self.array_b, **kwargs)
        self.passed = True if self.p_value > self.alpha else False

    def __str__(self):
        return ""
    
    def __repr__(self):
        return (f'{self.__class__.__name__}(n={self.n})')

    def plot(self, **kwargs):
        return _equal_means_plot(self, **kwargs)

class TwoSampleT():

    def __init__(self, frame, columns=None, related=False, confidence=0.95, alpha=0.05, **kwargs):
        
        if isinstance(columns, list):
            frame = frame[columns[:2]]
        else:
            frame = frame.iloc[:,:2]

        # check arguments
        ncols = 2
        check_kwargs(locals())
        
        self.frame = frame
        self.columms = columns
        self.nrows = self.frame.shape[0]
        self.ncols = self.frame.shape[1]
        self.related = related
        self.confidence = confidence
        self.alpha = alpha
        self.test = "Two Sample T Test"
        self.h0 = bluebelt.helpers.text.get_h0_equal_means(self.frame.columns)
        self.calculate(**kwargs)

    def calculate(self, **kwargs):
        self.array_a = self.frame.iloc[:,0]
        self.array_b = self.frame.iloc[:,1]
        
        self.mean_a = self.array_a.mean()
        self.mean_b = self.array_b.mean()
        
        self.max_a = self.array_a.max()
        self.max_b = self.array_b.max()
        
        self.min_a = self.array_a.min()
        self.min_b = self.array_b.min()
        
        self.ci_mean_a = ci.ci_mean(self.array_a)
        self.ci_mean_b = ci.ci_mean(self.array_b)

        # related or not
        if self.related:
            statistic, pvalue = stats.ttest_rel(self.array_a, self.array_b, **kwargs)
        else:
            # test equal variance
            equal_var = True if Levene(frame=self.frame, alpha=self.alpha).passed else False
            statistic, pvalue = stats.ttest_ind(self.array_a, self.array_b, equal_var=equal_var, **kwargs)

        self.statistic = statistic
        self.p_value = pvalue

    def __str__(self):
        return ""
    
    def __repr__(self):
        return (f'{self.__class__.__name__}(nrows={self.nrows}, mean=({self.mean_a}, {self.mean_b}))')
    
    def result(self):
        print(self.statistic)
    
    def plot(self, **kwargs):
        return _equal_means_plot(self, **kwargs)

class MannWhitney():
    """
    apply the Mann-Whitney test on a pandas dataframe with the columns as groups to compare
    Returns the scipy Mann-Whitney; statistic, pvalue
    pvalue >= alpha (default 0.05) : columns have equal means
    pvalue < alpha: columns don't have equal means
    """
    
    def __init__(self, frame, columns=None, confidence=0.95, alpha=0.05, **kwargs):
        
        # check arguments
        ncols = 2
        check_kwargs(locals())

        self.frame = frame if columns is None else frame[columns]
        self.columms = columns
        self.nrows = self.frame.shape[0]
        self.ncols = self.frame.shape[1]
        self.confidence = confidence
        self.alpha = alpha
        self.test = 'Mann-Whitney'
        self.h0 = bluebelt.helpers.text.get_h0_equal_means(self.frame.columns)
        self.calculate(**kwargs)

    def calculate(self, **kwargs):
        
        self.statistic, self.p_value = stats.mannwhitneyu(*self.frame.dropna().T.values)
        self.passed = True if self.p_value >= self.alpha else False

    def plot(self, **kwargs):
        return _equal_means_plot(self, **kwargs)

class Anova():
    """
    apply the one way ANOVA test on a pandas dataframe with the columns as groups to compare
    Returns the scipy F_onewayResult; statistic, pvalue
    pvalue >= alpha (default 0.05) : columns have equal means
    pvalue < alpha: columns don't have equal means
    """
    
    def __init__(self, frame, columns=None, confidence=0.95, alpha=0.05, **kwargs):
        
        # check arguments
        ncols = (2, None)
        check_kwargs(locals())

        self.frame = frame if columns is None else frame[columns]
        self.columms = columns
        self.nrows = self.frame.shape[0]
        self.ncols = self.frame.shape[1]
        self.confidence = confidence
        self.alpha = alpha
        self.test = "ANOVA"
        self.h0 = bluebelt.helpers.text.get_h0_equal_means(self.frame.columns)
        self.calculate(**kwargs)

    def calculate(self, **kwargs):
        
        self.statistic, self.p_value = stats.f_oneway(*self.frame.dropna().T.values)
        self.passed = True if self.p_value >= self.alpha else False

    def plot(self, **kwargs):
        return _equal_means_plot(self, **kwargs)

class KruskalWallis():
    """
    apply the Kruskal-Wallis test on a pandas dataframe with the columns as groups to compare
    Returns the scipy KruskalResult; statistic, pvalue
    pvalue >= alpha (default 0.05) : columns have equal means
    pvalue < alpha: columns don't have equal means
    """
    
    def __init__(self, frame, columns=None, confidence=0.95, alpha=0.05, **kwargs):
        
        # check arguments
        ncols = (2, None)
        check_kwargs(locals())


        self.frame = frame if columns is None else frame[columns]
        self.columms = columns
        self.nrows = self.frame.shape[0]
        self.ncols = self.frame.shape[1]
        self.confidence = confidence
        self.alpha = alpha
        self.test = 'Kruskal-Wallis'
        self.h0 = bluebelt.helpers.text.get_h0_equal_means(self.frame.columns)
        self.calculate(**kwargs)

    def calculate(self, **kwargs):
        
        self.statistic, self.p_value = stats.kruskal(*self.frame.dropna().T.values)
        self.passed = True if self.p_value >= self.alpha else False

    def plot(self, **kwargs):
        return _equal_means_plot(self, **kwargs)

class EqualMeans():
    """
    Test a dataframe for equal means over the dataframe columns.

    frame: a pandas DataFrame
    columns: string or list of strings with the column names to be tested
    alpha: test threshold

    applies ANOVA or Kruskal-Wallis test

    pvalue >= alpha (default 0.05) : columns have equal means
    pvalue < alpha: columns don't have equal means
    
    """
    
    def __init__(self, frame, columns=None, confidence=0.95, alpha=0.05, **kwargs):
        
       # check arguments
        ncols = (2, None)
        check_kwargs(locals())

        self.frame = frame if columns is None else frame[columns]
        self.columms = columns
        self.nrows = self.frame.shape[0]
        self.ncols = self.frame.shape[1]
        self.confidence = confidence
        self.alpha = alpha
        self.h0 = bluebelt.helpers.text.get_h0_equal_means(self.frame.columns)
        self.calculate(**kwargs)

    def calculate(self, **kwargs):

        # test for normal distribution; null hypothesis: values come from a normal distribution
        if NormalDistribution(frame=self.frame, alpha=self.alpha).passed and Levene(frame=self.frame, alpha=self.alpha).passed:
            # all columns in the dataframe come from a normal distribution AND have equal variances
            # do Anova
            test = Anova(frame=self.frame, alpha=self.alpha)
        else:
            # not all columns in the dataframe come from a normal distribution OR have equal variances
            # do Kruskal-Wallis
            test = KruskalWallis(frame=self.frame, alpha=self.alpha)
        
        self.test = test.test
        self.statistic = test.statistic
        self.p_value = test.p_value

    def plot(self, **kwargs):
        return _equal_means_plot(self, **kwargs)

def _equal_means_plot(plot_obj, **kwargs):

    style = kwargs.pop('style', bluebelt.styles.paper)
    path = kwargs.pop('path', None)
    digits = kwargs.pop('digits', 3)


    # prepare figure
    fig, axes = plt.subplots(nrows=plot_obj.ncols, ncols=1, gridspec_kw={'wspace': 0, 'hspace': 0}, **kwargs)

    for row in range(0, plot_obj.ncols):

        if plot_obj.ncols == 1:
            ax = axes
        else:
            ax = axes[row]
        
        # set right zorder
        ax.set_zorder((plot_obj.ncols-row)*10)
        # get the data
        array = plot_obj.frame.iloc[:,row]

        # 1. box plot ############################################
        boxplot = ax.boxplot(array, vert=False, widths=0.3, whis=1.5)
        for box in boxplot['boxes']:
            box.set(**style.equal_means_plot.boxplot.get('boxes', {}))
        
        # 2. CI for the mean #####################################
        ci_mean = ci.ci_mean(array, confidence=plot_obj.confidence)

        ax.plot([ci_mean[0], ci_mean[1]], [1.5,1.5], **style.equal_means_plot.ci_mean_plot)
        ax.axvline(x=ci_mean[0], ymin=9/14, ymax=11/14, **style.equal_means_plot.ci_mean_axvline)
        ax.axvline(x=ci_mean[1], ymin=9/14, ymax=11/14, **style.equal_means_plot.ci_mean_axvline)
        ax.scatter([array.mean()],[1.5], **style.equal_means_plot.ci_mean_scatter)

        ci_area = np.linspace(ci_mean[0], ci_mean[1], 100)
        ax.fill_between(x=(ci_mean[0], ci_mean[1]), y1=0, y2=2, label='CI sample mean', **style.equal_means_plot.ci_fill_between)
        ax.axvline(x=ci_mean[0], ymin=0, ymax=2, **style.equal_means_plot.ci_axvline)
        ax.axvline(x=ci_mean[1], ymin=0, ymax=2, **style.equal_means_plot.ci_axvline)
        
        
        # plot CI values
        ci_digits_str = bluebelt.helpers.text.format_digits([ci_mean[0], ci_mean[1]], digits=digits)
        ax.text(ci_mean[0], 1.5, f'{ci_mean[0]:{ci_digits_str}} ', **style.equal_means_plot.text_ci_min)
        ax.text(ci_mean[1], 1.5, f' {ci_mean[1]:{ci_digits_str}}', **style.equal_means_plot.text_ci_max)

        # plot popmean if it exists
        if hasattr(plot_obj, 'popmean'):
            ax.axvline(plot_obj.popmean, ymin=0, ymax=2, label='population mean', **style.equal_means_plot.popmean_axvline)
        

        # set xticks
        local_xticks = bluebelt.helpers.boxplot.get_boxplot_quantiles(array, whis=1.5)
        ax.set_xticks(local_xticks)
        ax.tick_params(axis="x", top=False, direction='in', pad=-18)
        

        ax.set_ylim([0.25, 2])
        ax.set_yticklabels([array.name])

        # add xticks to xtick list for formatting
        if row == 0:
            xticks = local_xticks
            xlims = ax.get_xlim()
            
            ax.set_title(plot_obj.h0 + f'   t={plot_obj.statistic:1.2f} p={plot_obj.p_value:1.2f} ('+r'$H_0$'+f' is {"accepted" if plot_obj.p_value > plot_obj.alpha else "rejected"})', **style.equal_means_plot.title)

            ax.legend(loc='best')

        else:
            xticks += local_xticks
            xlims = (min(ax.get_xlim()[0], xlims[0]), max(ax.get_xlim()[1], xlims[1]))
        
    # format things
    for row in range(0, plot_obj.ncols):

        if plot_obj.ncols == 1:
            ax = axes
        else:
            ax = axes[row]

        ax.xaxis.set_major_formatter(FormatStrFormatter(f'%{bluebelt.helpers.text.format_digits(xticks, digits=digits)}'))
        ax.set_xlim(xlims)

    # add suptitle
    fig.suptitle(t=f'{plot_obj.test} for {bluebelt.helpers.text.get_nice_list(plot_obj.frame.columns)}', x=fig.subplotpars.left, **style.equal_means_plot.suptitle)

    if path:
        plt.savefig(path)
        plt.close()
    else:
        plt.close()
        return fig

# equal variance
class FTest():
    """
    apply the F-test on a pandas dataframe with the columns as groups to compare
    the F-test is extremely sensitive to non-normality of the two arrays
    pvalue >= 0.05 : columns have equal variances
    pvalue < 0.05: columns don't have equal variances
    """
    
    def __init__(self, frame, columns=None, alpha=0.05, **kwargs):
        
        # check arguments
        ncols = 2
        check_kwargs(locals())
        
        self.frame = frame if columns is None else frame[columns]
        self.columms = columns
        self.array_a = self.frame.iloc[:,0]
        self.array_b = self.frame.iloc[:,1]
        self.nrows = self.frame.shape[0]
        self.ncols = self.frame.shape[1]
        self.alpha = alpha
        self.test = 'F-test'
        self.calculate(**kwargs)

    def calculate(self, **kwargs):
        
        self.statistic = self.array_a.var() / self.array_b.var()
        self.p_value = stats.f.cdf(self.statistic, self.array_a.size-1, self.array_b.size-1)
        self.passed = True if self.p_value >= self.alpha else False
    def plot(self, **kwargs):
        return _equal_variance_plot(self, **kwargs)

class Levene():
    """
    apply levene's test on a pandas dataframe with the columns as groups to compare
    Returns the scipy LeveneResult; statistic, pvalue
    pvalue >= 0.05 : columns have equal variances
    pvalue < 0.05: columns don't have equal variances
    """
    
    def __init__(self, frame, columns=None, alpha=0.05, **kwargs):
        
        # check arguments
        ncols = (2, None)
        check_kwargs(locals())
        

        self.frame = frame if columns is None else frame[columns]
        self.columms = columns
        self.nrows = self.frame.shape[0]
        self.ncols = self.frame.shape[1]
        self.alpha = alpha
        self.test = 'Levene'
        self.calculate(**kwargs)

    def calculate(self, **kwargs):
        
        self.statistic, self.p_value = stats.levene(*self.frame.dropna().T.values)
        self.passed = True if self.p_value >= self.alpha else False
    
    def plot(self, **kwargs):
        return _equal_variance_plot(self, **kwargs)

class Bartlett():
    """
    apply Bartlett's test on a pandas dataframe with the columns as groups to compare
    Returns the scipy BartlettResult; statistic, pvalue
    pvalue >= 0.05 : columns have equal variances
    pvalue < 0.05: columns don't have equal variances
    """
    
    def __init__(self, frame, columns=None, alpha=0.05, **kwargs):
        
        # check arguments
        ncols = (2,None)
        check_kwargs(locals())
        
        self.frame = frame if columns is None else frame[columns]
        self.columms = columns
        self.nrows = self.frame.shape[0]
        self.ncols = self.frame.shape[1]
        self.alpha = alpha
        self.test = 'Bartlett'
        self.calculate(**kwargs)

    def calculate(self, **kwargs):
        
        self.statistic, self.p_value = stats.bartlett(*self.frame.dropna().T.values)
        self.passed = True if self.p_value >= self.alpha else False
    
    def plot(self, **kwargs):
        return _equal_variance_plot(self, **kwargs)

class EqualVariance():
    """
    Test a dataframe for equal variance over the dataframe columns.

    frame: a pandas DataFrame
    columns: string or list of strings with the column names to be tested
    alpha: test threshold

    applies F-test, Bartlett's or Levene's

    pvalue >= alpha (default 0.05) : columns have equal variances
    pvalue < alpha: columns don't have equal variances
    
    """
    def __init__(self, frame, columns=None, alpha=0.05, **kwargs):
        
        # check arguments
        ncols = (2,None)
        check_kwargs(locals())
        
        self.frame = frame if columns is None else frame[columns]
        self.columms = columns
        self.nrows = self.frame.shape[0]
        self.ncols = self.frame.shape[1]
        self.alpha = alpha
        self.calculate(**kwargs)

    def calculate(self, **kwargs):

        # test for normal distribution; null hypothesis: values come from a normal distribution
        if NormalDistribution(frame=self.frame, alpha=self.alpha).passed:
            # all columns in the dataframe come from a normal distribution
            if self.ncols == 2:
                test = FTest(frame=self.frame, alpha=self.alpha)
            else:
                test = Bartlett(frame=self.frame, alpha=self.alpha)
        else:
            # not all columns in the dataframe come from a normal distribution
            test=Levene(frame=self.frame, alpha=self.alpha)
        
        self.test = test.test
        self.statistic = test.statistic
        self.p_value = test.p_value
    
    def plot(self, **kwargs):
        return _equal_variance_plot(self, **kwargs)

def _equal_variance_plot(plot_obj, **kwargs):

    style = kwargs.pop('style', bluebelt.styles.paper)
    path = kwargs.pop('path', None)
    digits = kwargs.pop('digits', 3)
    bins = kwargs.pop('bins', 17)

    # prepare figure
    fig, axes = plt.subplots(nrows=plot_obj.ncols, ncols=1, sharex='col', gridspec_kw={'wspace': 0, 'hspace': 0}, **kwargs)

    for row in range(0, plot_obj.ncols):

        if plot_obj.ncols == 1:
            ax = axes
        else:
            ax = axes[row]
        
        # set right zorder
        ax.set_zorder((plot_obj.ncols-row)*10)
        # get the data
        array = plot_obj.frame.iloc[:,row]

        # calculate bin width
        bin_width = (plot_obj.frame.values.max() - plot_obj.frame.values.min()) / bins

        # 1. histogram ############################################
        ax.hist(array, bins=np.arange(plot_obj.frame.values.min(), plot_obj.frame.values.max() + bin_width, bin_width), **style.equal_variance_plot.histogram)
        ax.text(0.05, 0.9, array.name, transform=ax.transAxes, **style.equal_variance_plot.text)
        ax.set_yticks([])

        # add xlims to xlims list for formatting
        if row == 0:
            xlims = ax.get_xlim()
            ax.set_title(f't={plot_obj.statistic:1.2f} p={plot_obj.p_value:1.2f} ('+r'$H_0$'+f' is {"accepted" if plot_obj.p_value > plot_obj.alpha else "rejected"})', **style.equal_variance_plot.title)
 
        else:
            xlims = (min(ax.get_xlim()[0], xlims[0]), max(ax.get_xlim()[1], xlims[1]))
        
    # format things
    for row in range(0, plot_obj.ncols):

        if plot_obj.ncols == 1:
            ax = axes
        else:
            ax = axes[row]

        ax.set_xlim(xlims)

    # add suptitle
    fig.suptitle(t=f'{plot_obj.test} for {bluebelt.helpers.text.get_nice_list(plot_obj.frame.columns)}', x=fig.subplotpars.left, **style.equal_variance_plot.suptitle)

    if path:
        plt.savefig(path)
        plt.close()
    else:
        plt.close()
        return fig

# distribution
class AndersonDarling():
    """
    Anderson-Darling test for distribution testing
    """
    
    def __init__(self, series, dist='norm', alpha=0.05, **kwargs):
        
        # check arguments
        check_kwargs(locals())

        self.series = series
        self.dist = dist
        self.nrows = self.series.size
        self.ncols = 1
        self.alpha = alpha
        self.calculate(**kwargs)

    def calculate(self, **kwargs):
        
        AD, critical_values, significance_level = stats.anderson(self.series, dist=self.dist)
        
        AD_adjusted = AD*(1 + (.75/50) + 2.25/(50**2))
        if AD_adjusted >= .6:
            pvalue = math.exp(1.2937 - 5.709*AD_adjusted - .0186*(AD_adjusted**2))
        elif AD_adjusted >=.34:
            pvalue = math.exp(.9177 - 4.279*AD_adjusted - 1.38*(AD_adjusted**2))
        elif AD_adjusted >.2:
            pvalue = 1 - math.exp(-8.318 + 42.796*AD_adjusted - 59.938*(AD_adjusted**2))
        else:
            pvalue = 1 - math.exp(-13.436 + 101.14*AD_adjusted - 223.73*(AD_adjusted**2))

        self.statistic = AD
        self.p_value = pvalue
        self.passed = True if self.p_value >= self.alpha else False

class DAgostinoPearson():
    """
    D’Agostino-Pearson test
    
    null hypothesis: x comes from a normal distribution
    
    test all pandas dataframe columns for normal distribution
    input pivoted dataframe
    Returns the scipy NormaltestResult; statistic, pvalue for a single column
    Returns the worst scipy NormaltestResult as a tuple; statistic, pvalue for multiple columns
    pvalue >= alpha : column values are (all) normally distributed
    pvalue < alpha: column values are not (all) normally distributed
    """
    
    def __init__(self, frame, columns=None, alpha=0.05, **kwargs):
        
        if isinstance(frame, pd.Series):
            frame = pd.DataFrame(frame)
        elif isinstance(columns, (list, str)):
            frame = frame[columns]
        
        # check arguments
        ncols = (1, None)
        check_kwargs(locals())

        self.frame = frame
        self.columns = columns
        self.nrows = self.frame.shape[0]
        self.ncols = self.frame.shape[1]
        self.alpha = alpha
        self.test = "D’Agostino-Pearson"
        self.calculate(**kwargs)

    def calculate(self, **kwargs):
        pvalue = 1
        statistic = 0
        
        for col in self.frame.columns:
            result = stats.normaltest(self.frame[col].dropna().values)
            if result.pvalue < pvalue:
                pvalue = result.pvalue
                statistic = result.statistic

        self.statistic = statistic
        self.p_value = pvalue
        self.passed = True if pvalue >= self.alpha else False

NormalDistribution = DAgostinoPearson


