Source code for statsmodels.stats.oneway

"""
Created on Wed Mar 18 10:33:38 2020

Author: Josef Perktold
License: BSD-3

"""

from dataclasses import dataclass, fields
from typing import ClassVar, NamedTuple

import numpy as np
from scipy import stats
from scipy.special import ncfdtrinc

from statsmodels.stats.base import LimitedIterationMixin

# functions that use scipy.special instead of boost based function in stats
from statsmodels.stats.power import ncf_cdf, ncf_ppf
from statsmodels.stats.robust_compare import TrimmedMean, scale_transform
from statsmodels.tools.rng_qrng import check_random_state
from statsmodels.tools.validation import string_like


[docs] def effectsize_oneway(means, vars_, nobs, use_var="unequal", ddof_between=0): """ Effect size corresponding to Cohen's f = nc / nobs for oneway anova This contains adjustment for Welch and Brown-Forsythe Anova so that effect size can be used with FTestAnovaPower. Parameters ---------- means : array_like Mean of samples to be compared vars_ : float or array_like Residual (within) variance of each sample or pooled If ``vars_`` is scalar, then it is interpreted as pooled variance that is the same for all samples, ``use_var`` will be ignored. Otherwise, the variances are used depending on the ``use_var`` keyword. nobs : int or array_like Number of observations for the samples. If nobs is scalar, then it is assumed that all samples have the same number ``nobs`` of observation, i.e., a balanced sample case. Otherwise, statistics will be weighted corresponding to nobs. Only relative sizes are relevant, any proportional change to nobs does not change the effect size. use_var : {"unequal", "equal", "bf"}, optional If ``use_var`` is "unequal", then the variances can differ across samples and the effect size for Welch anova will be computed. ddof_between : int, optional Degrees of freedom correction for the weighted between sum of squares. The denominator is ``nobs_total - ddof_between`` This can be used to match differences across reference literature. Returns ------- f2 : float Effect size corresponding to squared Cohen's f, which is also equal to the noncentrality divided by total number of observations. Notes ----- This currently handles the following cases for oneway anova - balanced sample with homoscedastic variances - samples with different number of observations and with homoscedastic variances - samples with different number of observations and with heteroskedastic variances. This corresponds to Welch anova In the case of "unequal" and "bf" methods for unequal variances, the effect sizes do not directly correspond to the test statistic in Anova. Both have correction terms dropped or added, so the effect sizes match up with using FTestAnovaPower. If all variances are equal, then all three methods result in the same effect size. If variances are unequal, then the three methods produce small differences in effect size. Note, the effect size and power computation for BF Anova was not found in the literature. The correction terms were added so that FTestAnovaPower provides a good approximation to the power. Status: experimental We might add additional returns, if those are needed to support power and sample size applications. Examples -------- The following shows how to compute effect size and power for each of the three anova methods. The null hypothesis is that the means are equal which corresponds to a zero effect size. Under the alternative, means differ with two sample means at a distance delta from the mean. We assume the variance is the same under the null and alternative hypothesis. ``nobs`` for the samples defines the fraction of observations in the samples. ``nobs`` in the power method defines the total sample size. In simulations, the computed power for standard anova, i.e.``use_var="equal"`` overestimates the simulated power by a few percent. The equal variance assumption does not hold in this example. >>> from statsmodels.stats.oneway import effectsize_oneway >>> from statsmodels.stats.power import FTestAnovaPower >>> >>> nobs = np.array([10, 12, 13, 15]) >>> delta = 0.5 >>> means_alt = np.array([-1, 0, 0, 1]) * delta >>> vars_ = np.arange(1, len(means_alt) + 1) >>> >>> f2_alt = effectsize_oneway(means_alt, vars_, nobs, use_var="equal") >>> f2_alt 0.04581300813008131 >>> >>> kwds = {'effect_size': np.sqrt(f2_alt), 'nobs': 100, 'alpha': 0.05, ... 'k_groups': 4} >>> power = FTestAnovaPower().power(**kwds) >>> power 0.39165892158983273 >>> >>> f2_alt = effectsize_oneway(means_alt, vars_, nobs, use_var="unequal") >>> f2_alt 0.060640138408304504 >>> >>> kwds['effect_size'] = np.sqrt(f2_alt) >>> power = FTestAnovaPower().power(**kwds) >>> power 0.5047366512800622 >>> >>> f2_alt = effectsize_oneway(means_alt, vars_, nobs, use_var="bf") >>> f2_alt 0.04391324307956788 >>> >>> kwds['effect_size'] = np.sqrt(f2_alt) >>> power = FTestAnovaPower().power(**kwds) >>> power 0.3765792117047725 """ # the code here is largely a copy of onway_generic with adjustments use_var = string_like( use_var, "use_var", options=("unequal", "equal", "bf"), lower=True ) means = np.asarray(means) n_groups = means.shape[0] if np.size(nobs) == 1: nobs = np.ones(n_groups) * nobs nobs_t = nobs.sum() if use_var == "equal": if np.size(vars_) == 1: var_resid = vars_ else: vars_ = np.asarray(vars_) var_resid = ((nobs - 1) * vars_).sum() / (nobs_t - n_groups) vars_ = var_resid # scalar, if broadcasting works weights = nobs / vars_ w_total = weights.sum() w_rel = weights / w_total # meanw_t = (weights * means).sum() / w_total meanw_t = w_rel @ means f2 = np.dot(weights, (means - meanw_t) ** 2) / (nobs_t - ddof_between) if use_var.lower() == "bf": weights = nobs w_total = weights.sum() w_rel = weights / w_total meanw_t = w_rel @ means # TODO: reuse general case with weights tmp = ((1.0 - nobs / nobs_t) * vars_).sum() statistic = 1.0 * (nobs * (means - meanw_t) ** 2).sum() statistic /= tmp f2 = statistic * (1.0 - nobs / nobs_t).sum() / nobs_t # correction factor for df_num in BFM df_num2 = n_groups - 1 df_num = tmp**2 / ( (vars_**2).sum() + (nobs / nobs_t * vars_).sum() ** 2 - 2 * (nobs / nobs_t * vars_**2).sum() ) f2 *= df_num / df_num2 return f2
[docs] class EffectSizeFsquResult(NamedTuple): """ Result of :func:`convert_effectsize_fsqu`. Parameters ---------- f2 : float or ndarray Squared Cohen's f effect size, the signal to noise ratio ``var_explained / var_residual``. eta2 : float or ndarray Squared eta effect size, the proportion of explained variance ``var_explained / var_total``. """ f2: float eta2: float
[docs] def convert_effectsize_fsqu(f2=None, eta2=None): """ Convert squared effect sizes in f family f2 is signal to noise ratio, var_explained / var_residual eta2 is proportion of explained variance, var_explained / var_total uses the relationship: f2 = eta2 / (1 - eta2) Parameters ---------- f2 : None or array_like, optional Squared Cohen's F effect size. If f2 is not None, then eta2 will be computed. eta2 : None or array_like, optional Squared eta effect size. If f2 is None and eta2 is not None, then f2 is computed. Returns ------- EffectSizeFsquResult A namedtuple with f2 and eta2 as attributes. """ if f2 is not None: eta2 = 1 / (1 + 1 / f2) elif eta2 is not None: f2 = eta2 / (1 - eta2) res = EffectSizeFsquResult(f2=f2, eta2=eta2) return res
[docs] class FstatEffectSizeResult(NamedTuple): """ Result of :func:`_fstat2effectsize`. Parameters ---------- f2 : float or ndarray Squared Cohen's f effect size, ``f_stat * df1 / df2``. eta2 : float or ndarray Squared eta effect size, ``f2 / (f2 + 1)``. omega2 : float or ndarray Squared omega effect size, computed as ``(f2 - df1 / df2) / (f2 + 1 + 1 / df2)``. eps2 : float or ndarray Squared epsilon effect size, computed as ``(f2 - df1 / df2) / (f2 + 1)``. eps2_alt : float or ndarray Squared epsilon effect size, computed with the alternative expression ``(f_stat - 1) / (f_stat + df2 / df1)``. omega2_alt : float or ndarray Squared omega effect size, computed with the alternative expression ``(f_stat - 1) / (f_stat + (df2 + 1) / df1)``. """ f2: np.ndarray eta2: np.ndarray omega2: np.ndarray eps2: np.ndarray eps2_alt: np.ndarray omega2_alt: np.ndarray
[docs] def _fstat2effectsize(f_stat, df): """ Compute anova effect size from F-statistic This might be combined with convert_effectsize_fsqu Parameters ---------- f_stat : array_like Test statistic of an F-test df : tuple degrees of freedom ``df = (df1, df2)`` where - df1 : numerator degrees of freedom, number of constraints - df2 : denominator degrees of freedom, df_resid Returns ------- FstatEffectSizeResult This namedtuple contains effect size measures f2, eta2, omega2 and eps2 as attributes. Notes ----- This uses the following definitions: - f2 = f_stat * df1 / df2 - eta2 = f2 / (f2 + 1) - omega2 = (f2 - df1 / df2) / (f2 + 2) - eps2 = (f2 - df1 / df2) / (f2 + 1) This differs from effect size measures in other function which define ``f2 = f_stat * df1 / nobs`` or an equivalent expression for power computation. The noncentrality index for the hypothesis test is in those cases given by ``nc = f_stat * df1``. Currently omega2 and eps2 are computed in two different ways. Those values agree for regular cases but can show different behavior in corner cases (e.g., zero division). """ df1, df2 = df f2 = f_stat * df1 / df2 eta2 = f2 / (f2 + 1) omega2_alt = (f_stat - 1) / (f_stat + (df2 + 1) / df1) omega2 = (f2 - df1 / df2) / (f2 + 1 + 1 / df2) # rewrite eps2_alt = (f_stat - 1) / (f_stat + df2 / df1) eps2 = (f2 - df1 / df2) / (f2 + 1) # rewrite return FstatEffectSizeResult( f2=f2, eta2=eta2, omega2=omega2, eps2=eps2, eps2_alt=eps2_alt, omega2_alt=omega2_alt, )
# conversion functions for Wellek's equivalence effect size # these are mainly to compare with literature
[docs] def wellek_to_f2(eps, n_groups): """ Convert Wellek's effect size (sqrt) to Cohen's f-squared This computes the following effect size : f2 = 1 / n_groups * eps**2 Parameters ---------- eps : float or ndarray Wellek's effect size used in anova equivalence test n_groups : int Number of groups in oneway comparison Returns ------- f2 : float or ndarray Effect size Cohen's f-squared. """ f2 = 1 / n_groups * eps**2 return f2
[docs] def f2_to_wellek(f2, n_groups): """ Convert Cohen's f-squared to Wellek's effect size (sqrt) This computes the following effect size : eps = sqrt(n_groups * f2) Parameters ---------- f2 : float or ndarray Effect size Cohen's f-squared n_groups : int Number of groups in oneway comparison Returns ------- eps : float or ndarray Wellek's effect size used in anova equivalence test """ eps = np.sqrt(n_groups * f2) return eps
[docs] def fstat_to_wellek(f_stat, n_groups, nobs_mean): """ Convert F statistic to wellek's effect size eps squared This computes the following effect size : es = f_stat * (n_groups - 1) / nobs_mean Parameters ---------- f_stat : float or ndarray Test statistic of an F-test. n_groups : int Number of groups in oneway comparison nobs_mean : float or ndarray Average number of observations across groups. Returns ------- eps : float or ndarray Wellek's effect size used in anova equivalence test """ es = f_stat * (n_groups - 1) / nobs_mean return es
[docs] def confint_noncentrality(f_stat, df, alpha=0.05, alternative="two-sided"): """ Confidence interval for noncentrality parameter in F-test This does not yet handle non-negativity constraint on nc. Currently only two-sided alternative is supported. Parameters ---------- f_stat : float F-statistic for which the noncentrality parameter is inverted. df : tuple degrees of freedom ``df = (df1, df2)`` where - df1 : numerator degrees of freedom, number of constraints - df2 : denominator degrees of freedom, df_resid alpha : float, optional Significance level for the confidence interval. alternative : {"two-sided"}, optional Other alternatives have not been implemented. Returns ------- ndarray Lower and upper confidence limits for the noncentrality parameter. See Also -------- confint_effectsize_oneway Notes ----- The algorithm inverts the cdf of the noncentral F distribution with respect to the noncentrality parameters. See Steiger 2004 and references cited in it. References ---------- .. [1] Steiger, James H. 2004. “Beyond the F Test: Effect Size Confidence Intervals and Tests of Close Fit in the Analysis of Variance and Contrast Analysis.” Psychological Methods 9 (2): 164-82. https://doi.org/10.1037/1082-989X.9.2.164. """ df1, df2 = df alternative = string_like( alternative, "alternative", options=("two-sided",), lower=False, deprecated={"2s": "two-sided", "ts": "two-sided"}, removed_after="0.16", ) alpha1s = alpha / 2 ci = ncfdtrinc(df1, df2, [1 - alpha1s, alpha1s], f_stat) return ci
[docs] class ConfintEffectSizeResult(NamedTuple): """ Result of :func:`confint_effectsize_oneway`. Parameters ---------- f2 : ndarray Squared Cohen's f effect size at the confidence limits of the noncentrality parameter. eta2 : ndarray Squared eta effect size at the confidence limits of the noncentrality parameter. ci_omega2 : ndarray Confidence interval for the omega-squared effect size. ci_nc : ndarray Confidence interval for the noncentrality parameter. ci_f : ndarray Confidence interval for Cohen's f effect size, ``sqrt(f2)``. ci_eta : ndarray Confidence interval for the eta effect size, ``sqrt(eta2)``. ci_f_corrected : ndarray Confidence interval for Cohen's f effect size with the small-sample correction used by Steiger (2004). """ f2: float eta2: float ci_omega2: np.ndarray ci_nc: np.ndarray ci_f: np.ndarray ci_eta: np.ndarray ci_f_corrected: np.ndarray
[docs] def confint_effectsize_oneway(f_stat, df, alpha=0.05, nobs=None): """ Confidence interval for effect size in oneway anova for F distribution This does not yet handle non-negativity constraint on nc. Currently only two-sided alternative is supported. Parameters ---------- f_stat : float F-statistic for which the effect size confidence interval is computed. df : tuple degrees of freedom ``df = (df1, df2)`` where - df1 : numerator degrees of freedom, number of constraints - df2 : denominator degrees of freedom, df_resid alpha : float, optional Significance level for the confidence interval. nobs : int, optional Total number of observations. If None, then it is set to ``df1 + df2 + 1``. Returns ------- ConfintEffectSizeResult Namedtuple with effect size and confidence attributes. See Also -------- confint_noncentrality Notes ----- The confidence interval for the noncentrality parameter is obtained by inverting the cdf of the noncentral F distribution. Confidence intervals for other effect sizes are computed by endpoint transformation. R package ``effectsize`` does not compute the confidence intervals in the same way. Their confidence intervals can be replicated with >>> ci_nc = confint_noncentrality(f_stat, (df1, df2), alpha=0.1) >>> ci_es = smo._fstat2effectsize(ci_nc / df1, (df1, df2)) """ df1, df2 = df if nobs is None: nobs = df1 + df2 + 1 ci_nc = confint_noncentrality(f_stat, df, alpha=alpha) ci_f2 = ci_nc / nobs es = convert_effectsize_fsqu(f2=ci_f2) ci_res = ConfintEffectSizeResult( f2=es.f2, eta2=es.eta2, ci_omega2=(ci_f2 - df1 / df2) / (ci_f2 + 1 + 1 / df2), ci_nc=ci_nc, ci_f=np.sqrt(es.f2), ci_eta=np.sqrt(es.eta2), ci_f_corrected=np.sqrt(es.f2 * (df1 + 1) / df1), ) return ci_res
[docs] @dataclass(frozen=True, slots=True, kw_only=True) class AnovaResult(LimitedIterationMixin[float]): """ Result of :func:`anova_generic` and :func:`anova_oneway`. Parameters ---------- statistic : float Test statistic for k-sample mean comparison which is approximately F-distributed. pvalue : float If ``use_var="bf"``, then the p-value is based on corrected degrees of freedom following Mehrotra 1997. df : tuple Degrees of freedom ``(df_num, df_denom)`` for the F-distribution used for `pvalue`. df_num : float Numerator degrees of freedom. df_denom : float Denominator degrees of freedom used for `pvalue`. nobs_total : float Total number of observations across all samples. n_groups : int Number of samples being compared. means : ndarray Mean of each sample. nobs : ndarray Number of observations in each sample. vars_ : ndarray Residual (within) variance of each sample. use_var : {"unequal", "equal", "bf"} The `use_var` option that was used to compute the test. welch_correction : bool Whether the Welch correction was included in the test statistic. df2 : tuple or None Degrees of freedom ``(df_num2, df_denom)`` for the Brown-Forsythe 1974 p-value. Only set if ``use_var="bf"``, otherwise None. df_num2 : float or None Numerator degrees of freedom for the Brown-Forsythe 1974 p-value. Only set if ``use_var="bf"``, otherwise None. pvalue2 : float or None p-value based on degrees of freedom as in Brown-Forsythe 1974. Only set if ``use_var="bf"``, otherwise None. Notes ----- Unpacks as ``statistic, pvalue = result``. Other values are only accessible using attributes. """ _iter_fields: ClassVar[tuple[str, ...]] = ("statistic", "pvalue") statistic: float pvalue: float df: tuple df_num: float df_denom: float nobs_total: float n_groups: int means: np.ndarray nobs: np.ndarray vars_: np.ndarray use_var: str welch_correction: bool df2: tuple | None = None df_num2: float | None = None pvalue2: float | None = None
[docs] def anova_generic( means, variances, nobs, use_var="unequal", welch_correction=True, info=None ): """ Oneway Anova based on summary statistics Parameters ---------- means : ndarray Mean of samples to be compared variances : float or array_like Residual (within) variance of each sample or pooled. If ``variances`` is scalar, then it is interpreted as pooled variance that is the same for all samples, ``use_var`` will be ignored. Otherwise, the variances are used depending on the ``use_var`` keyword. nobs : ndarray Number of observations for each sample. Statistics are weighted corresponding to nobs. Only relative sizes are relevant, any proportional change to nobs does not change the effect size. use_var : {"unequal", "equal", "bf"}, optional If ``use_var`` is "unequal", then the variances can differ across samples and the effect size for Welch anova will be computed. welch_correction : bool, optional If this is false, then the Welch correction to the test statistic is not included. This allows the computation of an effect size measure that corresponds more closely to Cohen's f. info : optional Not used yet. Returns ------- AnovaResult This includes `statistic` and `pvalue`. """ use_var = string_like( use_var, "use_var", options=("unequal", "equal", "bf"), lower=True ) options = {"use_var": use_var, "welch_correction": welch_correction} if means.ndim != 1: raise ValueError("data (means, ...) has to be one-dimensional") nobs_t = nobs.sum() n_groups = len(means) # mean_t = (nobs * means).sum() / nobs_t if use_var == "unequal": weights = nobs / variances else: weights = nobs w_total = weights.sum() w_rel = weights / w_total # meanw_t = (weights * means).sum() / w_total meanw_t = w_rel @ means statistic = np.dot(weights, (means - meanw_t) ** 2) / (n_groups - 1.0) df_num = n_groups - 1.0 if use_var == "unequal": tmp = ((1 - w_rel) ** 2 / (nobs - 1)).sum() / (n_groups**2 - 1) if welch_correction: statistic /= 1 + 2 * (n_groups - 2) * tmp df_denom = 1.0 / (3.0 * tmp) elif use_var == "equal": # variance of group demeaned total sample, pooled var_resid tmp = ((nobs - 1) * variances).sum() / (nobs_t - n_groups) statistic /= tmp df_denom = nobs_t - n_groups elif use_var == "bf": tmp = ((1.0 - nobs / nobs_t) * variances).sum() statistic = 1.0 * (nobs * (means - meanw_t) ** 2).sum() statistic /= tmp df_num2 = n_groups - 1 df_denom = ( tmp**2 / ((1.0 - nobs / nobs_t) ** 2 * variances**2 / (nobs - 1)).sum() ) df_num = tmp**2 / ( (variances**2).sum() + (nobs / nobs_t * variances).sum() ** 2 - 2 * (nobs / nobs_t * variances**2).sum() ) pval2 = stats.f.sf(statistic, df_num2, df_denom) options["df2"] = (df_num2, df_denom) options["df_num2"] = df_num2 options["pvalue2"] = pval2 else: raise ValueError('use_var is to be one of "unequal", "equal" or "bf"') pval = stats.f.sf(statistic, df_num, df_denom) res = AnovaResult( statistic=statistic, pvalue=pval, df=(df_num, df_denom), df_num=df_num, df_denom=df_denom, nobs_total=nobs_t, n_groups=n_groups, means=means, nobs=nobs, vars_=variances, **options, ) return res
[docs] def anova_oneway( data, groups=None, use_var="unequal", welch_correction=True, trim_frac=0 ): """ Oneway Anova This implements standard anova, Welch and Brown-Forsythe, and trimmed (Yuen) variants of those. Parameters ---------- data : tuple of array_like or DataFrame or Series Data for k independent samples, with k >= 2. The data can be provided as a tuple or list of arrays or in long format with outcome observations in ``data`` and group membership in ``groups``. groups : ndarray or Series, optional If data is in long format, then groups is needed as indicator to which group or sample and observations belongs. use_var : {"unequal", "equal", "bf"}, optional `use_var` specified how to treat heteroscedasticity, unequal variance, across samples. Three approaches are available "unequal" : Variances are not assumed to be equal across samples. Heteroscedasticity is taken into account with Welch Anova and Satterthwaite-Welch degrees of freedom. This is the default. "equal" : Variances are assumed to be equal across samples. This is the standard Anova. "bf" : Variances are not assumed to be equal across samples. The method is Browne-Forsythe (1971) for testing equality of means with the corrected degrees of freedom by Merothra. The original BF degrees of freedom are available as additional attributes in the results instance, ``df_denom2`` and ``p_value2``. welch_correction : bool, optional If this is false, then the Welch correction to the test statistic is not included. This allows the computation of an effect size measure that corresponds more closely to Cohen's f. trim_frac : float in [0, 0.5), optional Optional trimming for Anova with trimmed mean and winsorized variances. With the default trim_frac equal to zero, the oneway Anova statistics are computed without trimming. If `trim_frac` is larger than zero, then the largest and smallest observations in each sample are trimmed. The number of trimmed observations is the fraction of number of observations in the sample truncated to the next lower integer. `trim_frac` has to be smaller than 0.5, however, if the fraction is so large that there are not enough observations left over, then `nan` will be returned. Returns ------- AnovaResult See :class:`AnovaResult` for a description of the attributes. If ``use_var="bf"``, then `pvalue` is based on corrected degrees of freedom following Mehrotra 1997, and `pvalue2`, `df2` and `df_num2` hold the Brown-Forsythe 1974 alternative; otherwise those three are None. See Also -------- anova_generic Notes ----- Welch's anova is correctly sized (not liberal or conservative) in smaller samples if the distribution of the samples is not very far away from the normal distribution. The test can become liberal if the data is strongly skewed. Welch's Anova can also be correctly sized for discrete distributions with finite support, like Likert scale data. The trimmed version is robust to many non-normal distributions, it stays correctly sized in many cases, and is more powerful in some cases with skewness or heavy tails. Trimming is currently based on the integer part of ``nobs * trim_frac``. The default might change to including fractional observations as in the original articles by Yuen. References ---------- Brown, Morton B., and Alan B. Forsythe. 1974. “The Small Sample Behavior of Some Statistics Which Test the Equality of Several Means.” Technometrics 16 (1) (February 1): 129-132. doi:10.2307/1267501. Mehrotra, Devan V. 1997. “Improving the Brown-Forsythe Solution to the Generalized Behrens-Fisher Problem.” Communications in Statistics - Simulation and Computation 26 (3): 1139-1145. doi:10.1080/03610919708813431. """ if groups is not None: uniques = np.unique(groups) data = [data[groups == uni] for uni in uniques] else: # uniques = None # not used yet, add to info? pass args = [np.asarray(x) for x in data] if any(x.ndim != 1 for x in args): raise ValueError("data arrays have to be one-dimensional") nobs = np.array([len(x) for x in args], float) # n_groups = len(args) # not used # means = np.array([np.mean(x, axis=0) for x in args], float) # vars_ = np.array([np.var(x, ddof=1, axis=0) for x in args], float) if trim_frac == 0: means = np.array([x.mean() for x in args]) vars_ = np.array([x.var(ddof=1) for x in args]) else: tms = [TrimmedMean(x, trim_frac) for x in args] means = np.array([tm.mean_trimmed for tm in tms]) # R doesn't use uncorrected var_winsorized # vars_ = np.array([tm.var_winsorized for tm in tms]) vars_ = np.array( [tm.var_winsorized * (tm.nobs - 1) / (tm.nobs_reduced - 1) for tm in tms] ) # nobs_original = nobs # store just in case nobs = np.array([tm.nobs_reduced for tm in tms]) res = anova_generic( means, vars_, nobs, use_var=use_var, welch_correction=welch_correction ) return res
[docs] @dataclass(frozen=True, slots=True) class EquivalenceOnewayResult(LimitedIterationMixin[float]): """ Result of :func:`equivalence_oneway_generic` and :func:`equivalence_oneway`. Parameters ---------- statistic : float F-statistic used for the equivalence test, same value as `f_stat`. pvalue : float p-value of the equivalence test. effectsize : float Estimated effect size, in the units given by `type_effectsize`. crit_f : float Critical value for the F-statistic at the boundary of the equivalence margin. crit_es : float Critical value for the effect size at the boundary of the equivalence margin, in the same units as `effectsize`. reject : bool True if the null hypothesis of non-equivalence is rejected, i.e. the data supports equivalence within `equiv_margin`. power_zero : float Power of the equivalence test if the true effect size were zero. df : tuple Degrees of freedom ``(df1, df2)`` of the F-distribution used for the test. f_stat : float F-statistic used for the equivalence test, same value as `statistic`. type_effectsize : str Description of the effect size type used for `effectsize` and `crit_es`, determined by `margin_type`. Notes ----- Unpacks as ``statistic, pvalue = result``. Other values are only accessible using attributes. """ _iter_fields: ClassVar[tuple[str, ...]] = ("statistic", "pvalue") statistic: float pvalue: float effectsize: float crit_f: float crit_es: float reject: bool power_zero: float df: tuple f_stat: float type_effectsize: str
[docs] def equivalence_oneway_generic( f_stat, n_groups, nobs, equiv_margin, df, alpha=0.05, margin_type="f2" ): """ Equivalence test for oneway anova (Wellek and extensions) This is an helper function when summary statistics are available. Use `equivalence_oneway` instead. The null hypothesis is that the means differ by more than `equiv_margin` in the anova distance measure. If the Null is rejected, then the data supports that means are equivalent, i.e., within a given distance. Parameters ---------- f_stat : float F-statistic n_groups : int Number of groups in oneway comparison. nobs : ndarray Array of number of observations in groups. equiv_margin : float Equivalence margin in terms of effect size. Effect size can be chosen with `margin_type`. default is squared Cohen's f. df : tuple degrees of freedom ``df = (df1, df2)`` where - df1 : numerator degrees of freedom, number of constraints - df2 : denominator degrees of freedom, df_resid alpha : float in (0, 1), optional Significance level for the hypothesis test. margin_type : {"f2", "wellek"}, optional Type of effect size used for equivalence margin. Returns ------- EquivalenceOnewayResult The two main attributes are test statistic `statistic` and p-value `pvalue`. See :class:`EquivalenceOnewayResult` for the full list. Notes ----- Equivalence in this function is defined in terms of a squared distance measure similar to Mahalanobis distance. Alternative definitions for the oneway case are based on maximum difference between pairs of means or similar pairwise distances. The equivalence margin is used for the noncentrality parameter in the noncentral F distribution for the test statistic. In samples with unequal variances estimated using Welch or Brown-Forsythe Anova, the f-statistic depends on the unequal variances and corrections to the test statistic. This means that the equivalence margins are not fully comparable across methods for treating unequal variances. References ---------- Wellek, Stefan. 2010. Testing Statistical Hypotheses of Equivalence and Noninferiority. 2nd ed. Boca Raton: CRC Press. Cribbie, Robert A., Chantal A. Arpin-Cribbie, and Jamie A. Gruman. 2009. “Tests of Equivalence for One-Way Independent Groups Designs.” The Journal of Experimental Education 78 (1): 1-13. https://doi.org/10.1080/00220970903224552. Jan, Show-Li, and Gwowen Shieh. 2019. “On the Extended Welch Test for Assessing Equivalence of Standardized Means.” Statistics in Biopharmaceutical Research 0 (0): 1-8. https://doi.org/10.1080/19466315.2019.1654915. """ nobs_t = nobs.sum() nobs_mean = nobs_t / n_groups if margin_type == "wellek": nc_null = nobs_mean * equiv_margin**2 es = f_stat * (n_groups - 1) / nobs_mean type_effectsize = "Wellek's psi_squared" elif margin_type in ["f2", "fsqu", "fsquared"]: nc_null = nobs_t * equiv_margin es = f_stat / nobs_t type_effectsize = "Cohen's f_squared" else: raise ValueError('`margin_type` should be "f2" or "wellek"') crit_f = ncf_ppf(alpha, df[0], df[1], nc_null) if margin_type == "wellek": # TODO: do we need a sqrt crit_es = crit_f * (n_groups - 1) / nobs_mean elif margin_type in ["f2", "fsqu", "fsquared"]: crit_es = crit_f / nobs_t reject = es < crit_es pv = ncf_cdf(f_stat, df[0], df[1], nc_null) pwr = ncf_cdf(crit_f, df[0], df[1], 1e-13) # scipy, cannot be 0 res = EquivalenceOnewayResult( statistic=f_stat, pvalue=pv, effectsize=es, # match es type to margin_type crit_f=crit_f, crit_es=crit_es, reject=reject, power_zero=pwr, df=df, f_stat=f_stat, type_effectsize=type_effectsize, ) return res
[docs] def equivalence_oneway( data, equiv_margin, groups=None, use_var="unequal", welch_correction=True, trim_frac=0, margin_type="f2", ): """ Equivalence test for oneway anova (Wellek's Anova) The null hypothesis is that the means differ by more than `equiv_margin` in the anova distance measure. If the Null is rejected, then the data supports that means are equivalent, i.e., within a given distance. Parameters ---------- data : tuple of array_like or DataFrame or Series Data for k independent samples, with k >= 2. The data can be provided as a tuple or list of arrays or in long format with outcome observations in ``data`` and group membership in ``groups``. equiv_margin : float Equivalence margin in terms of effect size. Effect size can be chosen with `margin_type`. default is squared Cohen's f. groups : ndarray or Series, optional If data is in long format, then groups is needed as indicator to which group or sample and observations belongs. use_var : {"unequal", "equal", "bf"}, optional `use_var` specified how to treat heteroscedasticity, unequal variance, across samples. Three approaches are available "unequal" : Variances are not assumed to be equal across samples. Heteroscedasticity is taken into account with Welch Anova and Satterthwaite-Welch degrees of freedom. This is the default. "equal" : Variances are assumed to be equal across samples. This is the standard Anova. "bf" : Variances are not assumed to be equal across samples. The method is Browne-Forsythe (1971) for testing equality of means with the corrected degrees of freedom by Merothra. The original BF degrees of freedom are available as additional attributes in the results instance, ``df_denom2`` and ``p_value2``. welch_correction : bool, optional If this is false, then the Welch correction to the test statistic is not included. This allows the computation of an effect size measure that corresponds more closely to Cohen's f. trim_frac : float in [0, 0.5), optional Optional trimming for Anova with trimmed mean and winsorized variances. With the default trim_frac equal to zero, the oneway Anova statistics are computed without trimming. If `trim_frac` is larger than zero, then the largest and smallest observations in each sample are trimmed. The number of trimmed observations is the fraction of number of observations in the sample truncated to the next lower integer. `trim_frac` has to be smaller than 0.5, however, if the fraction is so large that there are not enough observations left over, then `nan` will be returned. margin_type : {"f2", "wellek"}, optional Type of effect size used for equivalence margin, either squared Cohen's f or Wellek's psi. Default is "f2". Returns ------- EquivalenceOnewayResult The two main attributes are test statistic `statistic` and p-value `pvalue`. See :class:`EquivalenceOnewayResult` for the full list. See Also -------- anova_oneway equivalence_scale_oneway """ # use anova to compute summary statistics and f-statistic res0 = anova_oneway( data, groups=groups, use_var=use_var, welch_correction=welch_correction, trim_frac=trim_frac, ) f_stat = res0.statistic res = equivalence_oneway_generic( f_stat, res0.n_groups, res0.nobs_total, equiv_margin, res0.df, alpha=0.05, margin_type=margin_type, ) return res
[docs] def _power_equivalence_oneway_emp(f_stat, n_groups, nobs, eps, df, alpha=0.05): """ Empirical power of oneway equivalence test This only returns post-hoc, empirical power. Warning: eps is currently effect size margin as defined as in Wellek, and not the signal to noise ratio (Cohen's f family). Parameters ---------- f_stat : float F-statistic from oneway anova, used to compute empirical effect size n_groups : int Number of groups in oneway comparison. nobs : ndarray Array of number of observations in groups. eps : float Equivalence margin in terms of effect size given by Wellek's psi. df : tuple Degrees of freedom for F distribution. alpha : float in (0, 1), optional Significance level for the hypothesis test. Returns ------- pow : float Ex-post, post-hoc or empirical power at f-statistic of the equivalence test. """ res = equivalence_oneway_generic( f_stat, n_groups, nobs, eps, df, alpha=alpha, margin_type="wellek" ) nobs_mean = nobs.sum() / n_groups fn = f_stat # post-hoc power, empirical power at estimate esn = fn * (n_groups - 1) / nobs_mean # Wellek psi pow_ = ncf_cdf(res.crit_f, df[0], df[1], nobs_mean * esn) return pow_
[docs] def power_equivalence_oneway( f2_alt, equiv_margin, nobs_t, n_groups=None, df=None, alpha=0.05, margin_type="f2" ): """ Power of oneway equivalence test Parameters ---------- f2_alt : float Effect size, squared Cohen's f, under the alternative. equiv_margin : float Equivalence margin in terms of effect size. Effect size can be chosen with `margin_type`. default is squared Cohen's f. nobs_t : ndarray Total number of observations summed over all groups. n_groups : int, optional Number of groups in oneway comparison. If margin_type is "wellek", then either ``n_groups`` or ``df`` has to be given. df : tuple, optional Degrees of freedom for F distribution, ``df = (n_groups - 1, nobs_t - n_groups)`` alpha : float in (0, 1), optional Significance level for the hypothesis test. margin_type : {"f2", "wellek"}, optional Type of effect size used for equivalence margin, either squared Cohen's f or Wellek's psi. Default is "f2". Returns ------- pow_alt : float Power of the equivalence test at given equivalence effect size under the alternative. """ # one of n_groups or df has to be specified if df is None: if n_groups is None: raise ValueError("either df or n_groups has to be provided") df = (n_groups - 1, nobs_t - n_groups) # esn = fn * (n_groups - 1) / nobs_mean # Wellek psi # fix for scipy, ncf does not allow nc == 0, fixed in scipy master if f2_alt == 0: f2_alt = 1e-13 # effect size, critical value at margin # f2_null = equiv_margin if margin_type in ["f2", "fsqu", "fsquared"]: f2_null = equiv_margin elif margin_type == "wellek": if n_groups is None: raise ValueError( "If margin_type is wellek, then n_groups has to be provided" ) # f2_null = (n_groups - 1) * n_groups / nobs_t * equiv_margin**2 nobs_mean = nobs_t / n_groups f2_null = nobs_mean * equiv_margin**2 / nobs_t f2_alt = nobs_mean * f2_alt**2 / nobs_t else: raise ValueError('`margin_type` should be "f2" or "wellek"') crit_f_margin = ncf_ppf(alpha, df[0], df[1], nobs_t * f2_null) pwr_alt = ncf_cdf(crit_f_margin, df[0], df[1], nobs_t * f2_alt) return pwr_alt
[docs] class SimulatePowerEquivalenceResult(NamedTuple): """ Result of :func:`simulate_power_equivalence_oneway`. Parameters ---------- f_stat : ndarray Simulated F-statistics, or Wellek's effect size estimate depending on `margin_type`, with one row per Monte Carlo replication and one column per entry in `options_var`. other : ndarray Simulated `crit_f`, `crit_es` and `power_zero` from :class:`EquivalenceOnewayResult`, stacked column-wise for each Monte Carlo replication and entry in `options_var`. pvalue : ndarray Simulated p-values, with one row per Monte Carlo replication and one column per entry in `options_var`. reject : ndarray Simulated rejection decisions, with one row per Monte Carlo replication and one column per entry in `options_var`. """ f_stat: np.ndarray other: np.ndarray pvalue: np.ndarray reject: np.ndarray
[docs] def simulate_power_equivalence_oneway( means, nobs, equiv_margin, vars_=None, k_mc=1000, trim_frac=0, options_var=None, margin_type="f2", rng=None, ): # , anova_options=None): # TODO """ Simulate Power for oneway equivalence test (Wellek's Anova) This function is experimental and written to evaluate asymptotic power function. This function will change without backwards compatibility constraints. The only part that is stable is `pvalue` attribute in results. Effect size for equivalence margin. Parameters ---------- means : array_like Mean of samples to be compared. Currently only four groups are supported. nobs : ndarray Number of observations for the samples. equiv_margin : float Equivalence margin in terms of effect size. Effect size can be chosen with `margin_type`. default is squared Cohen's f. vars_ : array_like, optional Variances of the samples used to simulate the data. If None, then unit variance, i.e., standard deviation equal to 1, is used for all samples. k_mc : int, optional Number of Monte Carlo replications. trim_frac : float in [0, 0.5), optional Optional trimming for Anova with trimmed mean and winsorized variances, see `trim_frac` in `anova_oneway`. options_var : list of str, optional List of `use_var` options that are used in the loop over Monte Carlo replications. If None, then ``["unequal", "equal", "bf"]`` is used. margin_type : {"f2", "wellek"}, optional Type of effect size used for equivalence margin. rng : int, array_like of int, numpy.random.Generator, numpy.random.RandomState, optional If `rng` is None, a new ``Generator`` is created using fresh entropy from the operating system. If `rng` is an int or array of ints, a new ``Generator`` is created, seeded with `rng`. If `rng` is already a ``Generator`` or ``RandomState`` instance, that instance is used. Returns ------- SimulatePowerEquivalenceResult Namedtuple with Monte Carlo results in the attributes `f_stat`, `other`, `pvalue` and `reject`, each an ndarray with one row per Monte Carlo replication and one column per entry in `options_var`. """ if options_var is None: options_var = ["unequal", "equal", "bf"] if vars_ is not None: stds = np.sqrt(vars_) else: stds = np.ones(len(means)) nobs_mean = nobs.mean() n_groups = len(nobs) res_mc = [] f_mc = [] reject_mc = [] other_mc = [] rng = check_random_state(rng) for _ in range(k_mc): y0, y1, y2, y3 = ( m + std * rng.standard_normal(n) for (n, m, std) in zip(nobs, means, stds, strict=True) ) res_i = [] f_i = [] reject_i = [] other_i = [] for uv in options_var: # for welch in options_welch: # res1 = sma.anova_generic(means, vars_, nobs, use_var=uv, # welch_correction=welch) res0 = anova_oneway([y0, y1, y2, y3], use_var=uv, trim_frac=trim_frac) f_stat = res0.statistic res1 = equivalence_oneway_generic( f_stat, n_groups, nobs.sum(), equiv_margin, res0.df, alpha=0.05, margin_type=margin_type, ) res_i.append(res1.pvalue) es_wellek = f_stat * (n_groups - 1) / nobs_mean f_i.append(es_wellek) reject_i.append(res1.reject) other_i.extend([res1.crit_f, res1.crit_es, res1.power_zero]) res_mc.append(res_i) f_mc.append(f_i) reject_mc.append(reject_i) other_mc.append(other_i) f_mc = np.asarray(f_mc) other_mc = np.asarray(other_mc) res_mc = np.asarray(res_mc) reject_mc = np.asarray(reject_mc) res = SimulatePowerEquivalenceResult( f_stat=f_mc, other=other_mc, pvalue=res_mc, reject=reject_mc ) return res
[docs] @dataclass(frozen=True, slots=True, kw_only=True) class ScaleAnovaResult(AnovaResult): """ Result of :func:`test_scale_oneway`. Extends :class:`AnovaResult`, computed on the transformed data, with one additional field. Parameters ---------- data_transformed : list of ndarray The centered and transformed data used to compute the test. See Also -------- AnovaResult Notes ----- Unpacks as ``statistic, pvalue = result``. Other values are only accessible using attributes. """ data_transformed: list
[docs] def test_scale_oneway( data, method="bf", center="median", transform="abs", trim_frac_mean=0.1, trim_frac_anova=0.0, ): """ Oneway Anova test for equal scale, variance or dispersion This hypothesis test performs a oneway anova test on transformed data and includes Levene and Brown-Forsythe tests for equal variances as special cases. Parameters ---------- data : tuple of array_like or DataFrame or Series Data for k independent samples, with k >= 2. The data can be provided as a tuple or list of arrays or in long format with outcome observations in ``data`` and group membership in ``groups``. method : {"unequal", "equal", "bf"}, optional How to treat heteroscedasticity across samples. This is used as `use_var` option in `anova_oneway` and refers to the variance of the transformed data, i.e., assumption is on 4th moment if squares are used as transform. Three approaches are available: "unequal" : Variances are not assumed to be equal across samples. Heteroscedasticity is taken into account with Welch Anova and Satterthwaite-Welch degrees of freedom. "equal" : Variances are assumed to be equal across samples. This is the standard Anova. "bf" : Variances are not assumed to be equal across samples. The method is Browne-Forsythe (1971) for testing equality of means with the corrected degrees of freedom by Merothra. The original BF degrees of freedom are available as additional attributes in the results instance, ``df_denom2`` and ``p_value2``. This is the default. center : {"median", "mean", "trimmed"} or float, optional Statistic used for centering observations. If a float, then this value is used to center. Default is median. transform : {"abs", "square", "identity"} or callable, optional Transformation for the centered observations. If a callable, then this function is called on the centered data. Default is absolute value. trim_frac_mean : float in [0, 0.5), optional Trim fraction for the trimmed mean when `center` is "trimmed" trim_frac_anova : float in [0, 0.5), optional Optional trimming for Anova with trimmed mean and Winsorized variances. With the default trim_frac equal to zero, the oneway Anova statistics are computed without trimming. If `trim_frac` is larger than zero, then the largest and smallest observations in each sample are trimmed. see ``trim_frac`` option in `anova_oneway` Returns ------- ScaleAnovaResult See :class:`ScaleAnovaResult` for a description of the attributes. If ``method="bf"``, then `pvalue` is based on corrected degrees of freedom following Mehrotra 1997, and `pvalue2`, `df2` and `df_num2` hold the Brown-Forsythe 1974 alternative; otherwise those three are None. See Also -------- anova_oneway scale_transform """ xxd = [ scale_transform( np.asarray(x), center=center, transform=transform, trim_frac=trim_frac_mean ) for x in data ] res0 = anova_oneway( xxd, groups=None, use_var=method, welch_correction=True, trim_frac=trim_frac_anova, ) res = ScaleAnovaResult( data_transformed=xxd, **{f.name: getattr(res0, f.name) for f in fields(res0)}, ) return res
[docs] @dataclass(frozen=True, slots=True) class ScaleEquivalenceResult(LimitedIterationMixin[float]): """ Result of :func:`equivalence_scale_oneway`. Has the same attributes as :class:`EquivalenceOnewayResult`, computed on the transformed data, plus `x_transformed`. Parameters ---------- statistic : float F-statistic used for the equivalence test, same value as `f_stat`. pvalue : float p-value of the equivalence test. effectsize : float Estimated effect size, in the units given by `type_effectsize`. crit_f : float Critical value for the F-statistic at the boundary of the equivalence margin. crit_es : float Critical value for the effect size at the boundary of the equivalence margin, in the same units as `effectsize`. reject : bool True if the null hypothesis of non-equivalence is rejected, i.e. the data supports equivalence within `equiv_margin`. power_zero : float Power of the equivalence test if the true effect size were zero. df : tuple Degrees of freedom ``(df1, df2)`` of the F-distribution used for the test. f_stat : float F-statistic used for the equivalence test, same value as `statistic`. type_effectsize : str Description of the effect size type used for `effectsize` and `crit_es`, determined by `margin_type`. x_transformed : list of ndarray The centered and transformed data used to compute the test. Notes ----- Unpacks as ``statistic, pvalue = result``. Other values are only accessible using attributes. """ _iter_fields: ClassVar[tuple[str, ...]] = ("statistic", "pvalue") statistic: float pvalue: float effectsize: float crit_f: float crit_es: float reject: bool power_zero: float df: tuple f_stat: float type_effectsize: str x_transformed: list
[docs] def equivalence_scale_oneway( data, equiv_margin, method="bf", center="median", transform="abs", trim_frac_mean=0.0, trim_frac_anova=0.0, ): """ Oneway Anova test for equivalence of scale, variance or dispersion This hypothesis test performs a oneway equivalence anova test on transformed data. Note, the interpretation of the equivalence margin `equiv_margin` will depend on the transformation of the data. Transformations like absolute deviation are not scaled to correspond to the variance under normal distribution. Parameters ---------- data : tuple of array_like or DataFrame or Series Data for k independent samples, with k >= 2. The data can be provided as a tuple or list of arrays or in long format with outcome observations in ``data`` and group membership in ``groups``. equiv_margin : float Equivalence margin in terms of effect size. Effect size can be chosen with `margin_type`. default is squared Cohen's f. method : {"unequal", "equal", "bf"}, optional How to treat heteroscedasticity across samples. This is used as `use_var` option in `anova_oneway` and refers to the variance of the transformed data, i.e., assumption is on 4th moment if squares are used as transform. Three approaches are available: "unequal" : Variances are not assumed to be equal across samples. Heteroscedasticity is taken into account with Welch Anova and Satterthwaite-Welch degrees of freedom. "equal" : Variances are assumed to be equal across samples. This is the standard Anova. "bf" : Variances are not assumed to be equal across samples. The method is Browne-Forsythe (1971) for testing equality of means with the corrected degrees of freedom by Merothra. The original BF degrees of freedom are available as additional attributes in the results instance, ``df_denom2`` and ``p_value2``. This is the default. center : {"median", "mean", "trimmed"} or float, optional Statistic used for centering observations. If a float, then this value is used to center. Default is median. transform : {"abs", "square", "identity"} or callable, optional Transformation for the centered observations. If a callable, then this function is called on the centered data. Default is absolute value. trim_frac_mean : float in [0, 0.5), optional Trim fraction for the trimmed mean when `center` is "trimmed" trim_frac_anova : float in [0, 0.5), optional Optional trimming for Anova with trimmed mean and Winsorized variances. With the default trim_frac equal to zero, the oneway Anova statistics are computed without trimming. If `trim_frac` is larger than zero, then the largest and smallest observations in each sample are trimmed. see ``trim_frac`` option in `anova_oneway` Returns ------- ScaleEquivalenceResult The two main attributes are test statistic `statistic` and p-value `pvalue`. See :class:`ScaleEquivalenceResult` for the full list. See Also -------- anova_oneway scale_transform equivalence_oneway """ xxd = [ scale_transform( np.asarray(x), center=center, transform=transform, trim_frac=trim_frac_mean ) for x in data ] res0 = equivalence_oneway( xxd, equiv_margin, use_var=method, welch_correction=True, trim_frac=trim_frac_anova, ) res = ScaleEquivalenceResult( x_transformed=xxd, **{f.name: getattr(res0, f.name) for f in fields(res0)}, ) return res