''' Test for ratio of Poisson intensities in two independent samples Author: Josef Perktold License: BSD-3 ''' import numpy as np import warnings from scipy import stats, optimize from statsmodels.stats.base import HolderTuple from statsmodels.stats.weightstats import _zstat_generic2 from statsmodels.stats._inference_tools import _mover_confint # shorthand norm = stats.norm method_names_poisson_1samp = { "test": [ "wald", "score", "exact-c", "midp-c", "waldccv", "sqrt-a", "sqrt-v", "sqrt", ], "confint": [ "wald", "score", "exact-c", "midp-c", "jeff", "waldccv", "sqrt-a", "sqrt-v", "sqrt", "sqrt-cent", "sqrt-centcc", ] } def test_poisson(count, nobs, value, method=None, alternative="two-sided", dispersion=1): """Test for one sample poisson mean or rate Parameters ---------- count : array_like Observed count, number of events. nobs : arrat_like Currently this is total exposure time of the count variable. This will likely change. value : float, array_like This is the value of poisson rate under the null hypothesis. method : str Method to use for confidence interval. This is required, there is currently no default method. See Notes for available methods. alternative : {'two-sided', 'smaller', 'larger'} alternative hypothesis, which can be two-sided or either one of the one-sided tests. dispersion : float Dispersion scale coefficient for Poisson QMLE. Default is that the data follows a Poisson distribution. Dispersion different from 1 correspond to excess-dispersion in Poisson quasi-likelihood (GLM). Dispersion coeffficient different from one is currently only used in wald and score method. Returns ------- HolderTuple instance with test statistic, pvalue and other attributes. Notes ----- The implementatio of the hypothesis test is mainly based on the references for the confidence interval, see confint_poisson. Available methods are: - "score" : based on score test, uses variance under null value - "wald" : based on wald test, uses variance base on estimated rate. - "waldccv" : based on wald test with 0.5 count added to variance computation. This does not use continuity correction for the center of the confidence interval. - "exact-c" central confidence interval based on gamma distribution - "midp-c" : based on midp correction of central exact confidence interval. this uses numerical inversion of the test function. not vectorized. - "sqrt" : based on square root transformed counts - "sqrt-a" based on Anscombe square root transformation of counts + 3/8. See Also -------- confint_poisson """ n = nobs # short hand rate = count / n if method is None: msg = "method needs to be specified, currently no default method" raise ValueError(msg) if dispersion != 1: if method not in ["wald", "waldcc", "score"]: msg = "excess dispersion only supported in wald and score methods" raise ValueError(msg) dist = "normal" if method == "wald": std = np.sqrt(dispersion * rate / n) statistic = (rate - value) / std elif method == "waldccv": # WCC in Barker 2002 # add 0.5 event, not 0.5 event rate as in waldcc # std = np.sqrt((rate + 0.5 / n) / n) # statistic = (rate + 0.5 / n - value) / std std = np.sqrt(dispersion * (rate + 0.5 / n) / n) statistic = (rate - value) / std elif method == "score": std = np.sqrt(dispersion * value / n) statistic = (rate - value) / std pvalue = stats.norm.sf(statistic) elif method.startswith("exact-c") or method.startswith("midp-c"): pv1 = stats.poisson.cdf(count, n * value) pv2 = stats.poisson.sf(count - 1, n * value) if method.startswith("midp-c"): pv1 = pv1 - 0.5 * stats.poisson.pmf(count, n * value) pv2 = pv2 - 0.5 * stats.poisson.pmf(count, n * value) if alternative == "two-sided": pvalue = 2 * np.minimum(pv1, pv2) elif alternative == "larger": pvalue = pv2 elif alternative == "smaller": pvalue = pv1 else: msg = 'alternative should be "two-sided", "larger" or "smaller"' raise ValueError(msg) statistic = np.nan dist = "Poisson" elif method == "sqrt": std = 0.5 statistic = (np.sqrt(count) - np.sqrt(n * value)) / std elif method == "sqrt-a": # anscombe, based on Swift 2009 (with transformation to rate) std = 0.5 statistic = (np.sqrt(count + 3 / 8) - np.sqrt(n * value + 3 / 8)) / std elif method == "sqrt-v": # vandenbroucke, based on Swift 2009 (with transformation to rate) std = 0.5 crit = stats.norm.isf(0.025) statistic = (np.sqrt(count + (crit**2 + 2) / 12) - # np.sqrt(n * value + (crit**2 + 2) / 12)) / std np.sqrt(n * value)) / std else: raise ValueError("unknown method %s" % method) if dist == 'normal': statistic, pvalue = _zstat_generic2(statistic, 1, alternative) res = HolderTuple( statistic=statistic, pvalue=np.clip(pvalue, 0, 1), distribution=dist, method=method, alternative=alternative, rate=rate, nobs=n ) return res def confint_poisson(count, exposure, method=None, alpha=0.05): """Confidence interval for a Poisson mean or rate The function is vectorized for all methods except "midp-c", which uses an iterative method to invert the hypothesis test function. All current methods are central, that is the probability of each tail is smaller or equal to alpha / 2. The one-sided interval limits can be obtained by doubling alpha. Parameters ---------- count : array_like Observed count, number of events. exposure : arrat_like Currently this is total exposure time of the count variable. This will likely change. method : str Method to use for confidence interval This is required, there is currently no default method alpha : float in (0, 1) Significance level, nominal coverage of the confidence interval is 1 - alpha. Returns ------- tuple (low, upp) : confidence limits. Notes ----- Methods are mainly based on Barker (2002) [1]_ and Swift (2009) [3]_. Available methods are: - "exact-c" central confidence interval based on gamma distribution - "score" : based on score test, uses variance under null value - "wald" : based on wald test, uses variance base on estimated rate. - "waldccv" : based on wald test with 0.5 count added to variance computation. This does not use continuity correction for the center of the confidence interval. - "midp-c" : based on midp correction of central exact confidence interval. this uses numerical inversion of the test function. not vectorized. - "jeffreys" : based on Jeffreys' prior. computed using gamma distribution - "sqrt" : based on square root transformed counts - "sqrt-a" based on Anscombe square root transformation of counts + 3/8. - "sqrt-centcc" will likely be dropped. anscombe with continuity corrected center. (Similar to R survival cipoisson, but without the 3/8 right shift of the confidence interval). sqrt-cent is the same as sqrt-a, using a different computation, will be deleted. sqrt-v is a corrected square root method attributed to vandenbrouke, which might also be deleted. Todo: - missing dispersion, - maybe split nobs and exposure (? needed in NB). Exposure could be used to standardize rate. - modified wald, switch method if count=0. See Also -------- test_poisson References ---------- .. [1] Barker, Lawrence. 2002. “A Comparison of Nine Confidence Intervals for a Poisson Parameter When the Expected Number of Events Is ≤ 5.” The American Statistician 56 (2): 85–89. https://doi.org/10.1198/000313002317572736. .. [2] Patil, VV, and HV Kulkarni. 2012. “Comparison of Confidence Intervals for the Poisson Mean: Some New Aspects.” REVSTAT–Statistical Journal 10(2): 211–27. .. [3] Swift, Michael Bruce. 2009. “Comparison of Confidence Intervals for a Poisson Mean – Further Considerations.” Communications in Statistics - Theory and Methods 38 (5): 748–59. https://doi.org/10.1080/03610920802255856. """ n = exposure # short hand rate = count / exposure alpha = alpha / 2 # two-sided if method is None: msg = "method needs to be specified, currently no default method" raise ValueError(msg) if method == "wald": whalf = stats.norm.isf(alpha) * np.sqrt(rate / n) ci = (rate - whalf, rate + whalf) elif method == "waldccv": # based on WCC in Barker 2002 # add 0.5 event, not 0.5 event rate as in BARKER waldcc whalf = stats.norm.isf(alpha) * np.sqrt((rate + 0.5 / n) / n) ci = (rate - whalf, rate + whalf) elif method == "score": crit = stats.norm.isf(alpha) center = count + crit**2 / 2 whalf = crit * np.sqrt(count + crit**2 / 4) ci = ((center - whalf) / n, (center + whalf) / n) elif method == "midp-c": # note local alpha above is for one tail ci = _invert_test_confint(count, n, alpha=2 * alpha, method="midp-c", method_start="exact-c") elif method == "sqrt": # drop, wrong n crit = stats.norm.isf(alpha) center = rate + crit**2 / (4 * n) whalf = crit * np.sqrt(rate / n) ci = (center - whalf, center + whalf) elif method == "sqrt-cent": crit = stats.norm.isf(alpha) center = count + crit**2 / 4 whalf = crit * np.sqrt(count + 3 / 8) ci = ((center - whalf) / n, (center + whalf) / n) elif method == "sqrt-centcc": # drop with cc, does not match cipoisson in R survival crit = stats.norm.isf(alpha) # avoid sqrt of negative value if count=0 center_low = np.sqrt(np.maximum(count + 3 / 8 - 0.5, 0)) center_upp = np.sqrt(count + 3 / 8 + 0.5) whalf = crit / 2 # above is for ci of count ci = (((np.maximum(center_low - whalf, 0))**2 - 3 / 8) / n, ((center_upp + whalf)**2 - 3 / 8) / n) # crit = stats.norm.isf(alpha) # center = count # whalf = crit * np.sqrt((count + 3 / 8 + 0.5)) # ci = ((center - whalf - 0.5) / n, (center + whalf + 0.5) / n) elif method == "sqrt-a": # anscombe, based on Swift 2009 (with transformation to rate) crit = stats.norm.isf(alpha) center = np.sqrt(count + 3 / 8) whalf = crit / 2 # above is for ci of count ci = (((np.maximum(center - whalf, 0))**2 - 3 / 8) / n, ((center + whalf)**2 - 3 / 8) / n) elif method == "sqrt-v": # vandenbroucke, based on Swift 2009 (with transformation to rate) crit = stats.norm.isf(alpha) center = np.sqrt(count + (crit**2 + 2) / 12) whalf = crit / 2 # above is for ci of count ci = (np.maximum(center - whalf, 0))**2 / n, (center + whalf)**2 / n elif method in ["gamma", "exact-c"]: # garwood exact, gamma low = stats.gamma.ppf(alpha, count) / exposure upp = stats.gamma.isf(alpha, count+1) / exposure if np.isnan(low).any(): # case with count = 0 if np.size(low) == 1: low = 0.0 else: low[np.isnan(low)] = 0.0 ci = (low, upp) elif method.startswith("jeff"): # jeffreys, gamma countc = count + 0.5 ci = (stats.gamma.ppf(alpha, countc) / exposure, stats.gamma.isf(alpha, countc) / exposure) else: raise ValueError("unknown method %s" % method) ci = (np.maximum(ci[0], 0), ci[1]) return ci def tolerance_int_poisson(count, exposure, prob=0.95, exposure_new=1., method=None, alpha=0.05, alternative="two-sided"): """tolerance interval for a poisson observation Parameters ---------- count : array_like Observed count, number of events. exposure : arrat_like Currently this is total exposure time of the count variable. prob : float in (0, 1) Probability of poisson interval, often called "content". With known parameters, each tail would have at most probability ``1 - prob / 2`` in the two-sided interval. exposure_new : float Exposure of the new or predicted observation. method : str Method to used for confidence interval of the estimate of the poisson rate, used in `confint_poisson`. This is required, there is currently no default method. alpha : float in (0, 1) Significance level for the confidence interval of the estimate of the Poisson rate. Nominal coverage of the confidence interval is 1 - alpha. alternative : {"two-sider", "larger", "smaller") The tolerance interval can be two-sided or one-sided. Alternative "larger" provides the upper bound of the confidence interval, larger counts are outside the interval. Returns ------- tuple (low, upp) of limits of tolerance interval. The tolerance interval is a closed interval, that is both ``low`` and ``upp`` are in the interval. Notes ----- verified against R package tolerance `poistol.int` See Also -------- confint_poisson confint_quantile_poisson References ---------- .. [1] Hahn, Gerald J., and William Q. Meeker. 1991. Statistical Intervals: A Guide for Practitioners. 1st ed. Wiley Series in Probability and Statistics. Wiley. https://doi.org/10.1002/9780470316771. .. [2] Hahn, Gerald J., and Ramesh Chandra. 1981. “Tolerance Intervals for Poisson and Binomial Variables.” Journal of Quality Technology 13 (2): 100–110. https://doi.org/10.1080/00224065.1981.11980998. """ prob_tail = 1 - prob alpha_ = alpha if alternative != "two-sided": # confint_poisson does not have one-sided alternatives alpha_ = alpha * 2 low, upp = confint_poisson(count, exposure, method=method, alpha=alpha_) if exposure_new != 1: low *= exposure_new upp *= exposure_new if alternative == "two-sided": low_pred = stats.poisson.ppf(prob_tail / 2, low) upp_pred = stats.poisson.ppf(1 - prob_tail / 2, upp) elif alternative == "larger": low_pred = 0 upp_pred = stats.poisson.ppf(1 - prob_tail, upp) elif alternative == "smaller": low_pred = stats.poisson.ppf(prob_tail, low) upp_pred = np.inf # clip -1 of ppf(0) low_pred = np.maximum(low_pred, 0) return low_pred, upp_pred def confint_quantile_poisson(count, exposure, prob, exposure_new=1., method=None, alpha=0.05, alternative="two-sided"): """confidence interval for quantile of poisson random variable Parameters ---------- count : array_like Observed count, number of events. exposure : arrat_like Currently this is total exposure time of the count variable. prob : float in (0, 1) Probability for the quantile, e.g. 0.95 to get the upper 95% quantile. With known mean mu, the quantile would be poisson.ppf(prob, mu). exposure_new : float Exposure of the new or predicted observation. method : str Method to used for confidence interval of the estimate of the poisson rate, used in `confint_poisson`. This is required, there is currently no default method. alpha : float in (0, 1) Significance level for the confidence interval of the estimate of the Poisson rate. Nominal coverage of the confidence interval is 1 - alpha. alternative : {"two-sider", "larger", "smaller") The tolerance interval can be two-sided or one-sided. Alternative "larger" provides the upper bound of the confidence interval, larger counts are outside the interval. Returns ------- tuple (low, upp) of limits of tolerance interval. The confidence interval is a closed interval, that is both ``low`` and ``upp`` are in the interval. See Also -------- confint_poisson tolerance_int_poisson References ---------- Hahn, Gerald J, and William Q Meeker. 2010. Statistical Intervals: A Guide for Practitioners. """ alpha_ = alpha if alternative != "two-sided": # confint_poisson does not have one-sided alternatives alpha_ = alpha * 2 low, upp = confint_poisson(count, exposure, method=method, alpha=alpha_) if exposure_new != 1: low *= exposure_new upp *= exposure_new if alternative == "two-sided": low_pred = stats.poisson.ppf(prob, low) upp_pred = stats.poisson.ppf(prob, upp) elif alternative == "larger": low_pred = 0 upp_pred = stats.poisson.ppf(prob, upp) elif alternative == "smaller": low_pred = stats.poisson.ppf(prob, low) upp_pred = np.inf # clip -1 of ppf(0) low_pred = np.maximum(low_pred, 0) return low_pred, upp_pred def _invert_test_confint(count, nobs, alpha=0.05, method="midp-c", method_start="exact-c"): """invert hypothesis test to get confidence interval """ def func(r): v = (test_poisson(count, nobs, value=r, method=method)[1] - alpha)**2 return v ci = confint_poisson(count, nobs, method=method_start) low = optimize.fmin(func, ci[0], xtol=1e-8, disp=False) upp = optimize.fmin(func, ci[1], xtol=1e-8, disp=False) assert np.size(low) == 1 return low[0], upp[0] def _invert_test_confint_2indep( count1, exposure1, count2, exposure2, alpha=0.05, method="score", compare="diff", method_start="wald" ): """invert hypothesis test to get confidence interval for 2indep """ def func(r): v = (test_poisson_2indep( count1, exposure1, count2, exposure2, value=r, method=method, compare=compare )[1] - alpha)**2 return v ci = confint_poisson_2indep(count1, exposure1, count2, exposure2, method=method_start, compare=compare) low = optimize.fmin(func, ci[0], xtol=1e-8, disp=False) upp = optimize.fmin(func, ci[1], xtol=1e-8, disp=False) assert np.size(low) == 1 return low[0], upp[0] method_names_poisson_2indep = { "test": { "ratio": [ "wald", "score", "score-log", "wald-log", "exact-cond", "cond-midp", "sqrt", "etest-score", "etest-wald" ], "diff": [ "wald", "score", "waldccv", "etest-score", "etest-wald" ] }, "confint": { "ratio": [ "waldcc", "score", "score-log", "wald-log", "sqrtcc", "mover", ], "diff": [ "wald", "score", "waldccv", "mover" ] } } def test_poisson_2indep(count1, exposure1, count2, exposure2, value=None, ratio_null=None, method=None, compare='ratio', alternative='two-sided', etest_kwds=None): '''Test for comparing two sample Poisson intensity rates. Rates are defined as expected count divided by exposure. The Null and alternative hypothesis for the rates, rate1 and rate2, of two independent Poisson samples are for compare = 'diff' - H0: rate1 - rate2 - value = 0 - H1: rate1 - rate2 - value != 0 if alternative = 'two-sided' - H1: rate1 - rate2 - value > 0 if alternative = 'larger' - H1: rate1 - rate2 - value < 0 if alternative = 'smaller' for compare = 'ratio' - H0: rate1 / rate2 - value = 0 - H1: rate1 / rate2 - value != 0 if alternative = 'two-sided' - H1: rate1 / rate2 - value > 0 if alternative = 'larger' - H1: rate1 / rate2 - value < 0 if alternative = 'smaller' Parameters ---------- count1 : int Number of events in first sample, treatment group. exposure1 : float Total exposure (time * subjects) in first sample. count2 : int Number of events in second sample, control group. exposure2 : float Total exposure (time * subjects) in second sample. ratio_null: float Ratio of the two Poisson rates under the Null hypothesis. Default is 1. Deprecated, use ``value`` instead. .. deprecated:: 0.14.0 Use ``value`` instead. value : float Value of the ratio or difference of 2 independent rates under the null hypothesis. Default is equal rates, i.e. 1 for ratio and 0 for diff. .. versionadded:: 0.14.0 Replacement for ``ratio_null``. method : string Method for the test statistic and the p-value. Defaults to `'score'`. see Notes. ratio: - 'wald': method W1A, wald test, variance based on observed rates - 'score': method W2A, score test, variance based on estimate under the Null hypothesis - 'wald-log': W3A, uses log-ratio, variance based on observed rates - 'score-log' W4A, uses log-ratio, variance based on estimate under the Null hypothesis - 'sqrt': W5A, based on variance stabilizing square root transformation - 'exact-cond': exact conditional test based on binomial distribution This uses ``binom_test`` which is minlike in the two-sided case. - 'cond-midp': midpoint-pvalue of exact conditional test - 'etest' or 'etest-score: etest with score test statistic - 'etest-wald': etest with wald test statistic diff: - 'wald', - 'waldccv' - 'score' - 'etest-score' or 'etest: etest with score test statistic - 'etest-wald': etest with wald test statistic compare : {'diff', 'ratio'} Default is "ratio". If compare is `ratio`, then the hypothesis test is for the rate ratio defined by ratio = rate1 / rate2. If compare is `diff`, then the hypothesis test is for diff = rate1 - rate2. alternative : {"two-sided" (default), "larger", smaller} The alternative hypothesis, H1, has to be one of the following - 'two-sided': H1: ratio, or diff, of rates is not equal to value - 'larger' : H1: ratio, or diff, of rates is larger than value - 'smaller' : H1: ratio, or diff, of rates is smaller than value etest_kwds: dictionary Additional optional parameters to be passed to the etest_poisson_2indep function, namely y_grid. Returns ------- results : instance of HolderTuple class The two main attributes are test statistic `statistic` and p-value `pvalue`. See Also -------- tost_poisson_2indep etest_poisson_2indep Notes ----- The hypothesis tests for compare="ratio" are based on Gu et al 2018. The e-tests are also based on ... - 'wald': method W1A, wald test, variance based on separate estimates - 'score': method W2A, score test, variance based on estimate under Null - 'wald-log': W3A, wald test for log transformed ratio - 'score-log' W4A, score test for log transformed ratio - 'sqrt': W5A, based on variance stabilizing square root transformation - 'exact-cond': exact conditional test based on binomial distribution - 'cond-midp': midpoint-pvalue of exact conditional test - 'etest': etest with score test statistic - 'etest-wald': etest with wald test statistic The hypothesis test for compare="diff" are mainly based on Ng et al 2007 and ... - wald - score - etest-score - etest-wald Note the etests use the constraint maximum likelihood estimate (cmle) as parameters for the underlying Poisson probabilities. The constraint cmle parameters are the same as in the score test. The E-test in Krishnamoorty and Thomson uses a moment estimator instead of the score estimator. References ---------- .. [1] Gu, Ng, Tang, Schucany 2008: Testing the Ratio of Two Poisson Rates, Biometrical Journal 50 (2008) 2, 2008 .. [2] Ng, H. K. T., K. Gu, and M. L. Tang. 2007. “A Comparative Study of Tests for the Difference of Two Poisson Means.” Computational Statistics & Data Analysis 51 (6): 3085–99. https://doi.org/10.1016/j.csda.2006.02.004. ''' # shortcut names y1, n1, y2, n2 = map(np.asarray, [count1, exposure1, count2, exposure2]) d = n2 / n1 rate1, rate2 = y1 / n1, y2 / n2 rates_cmle = None if compare == 'ratio': if method is None: # default method method = 'score' if ratio_null is not None: warnings.warn("'ratio_null' is deprecated, use 'value' keyword", FutureWarning) value = ratio_null if ratio_null is None and value is None: # default value value = ratio_null = 1 else: # for results holder instance, it still contains ratio_null ratio_null = value r = value r_d = r / d # r1 * n1 / (r2 * n2) if method in ['score']: stat = (y1 - y2 * r_d) / np.sqrt((y1 + y2) * r_d) dist = 'normal' elif method in ['wald']: stat = (y1 - y2 * r_d) / np.sqrt(y1 + y2 * r_d**2) dist = 'normal' elif method in ['score-log']: stat = (np.log(y1 / y2) - np.log(r_d)) stat /= np.sqrt((2 + 1 / r_d + r_d) / (y1 + y2)) dist = 'normal' elif method in ['wald-log']: stat = (np.log(y1 / y2) - np.log(r_d)) / np.sqrt(1 / y1 + 1 / y2) dist = 'normal' elif method in ['sqrt']: stat = 2 * (np.sqrt(y1 + 3 / 8.) - np.sqrt((y2 + 3 / 8.) * r_d)) stat /= np.sqrt(1 + r_d) dist = 'normal' elif method in ['exact-cond', 'cond-midp']: from statsmodels.stats import proportion bp = r_d / (1 + r_d) y_total = y1 + y2 stat = np.nan # TODO: why y2 in here and not y1, check definition of H1 "larger" pvalue = proportion.binom_test(y1, y_total, prop=bp, alternative=alternative) if method in ['cond-midp']: # not inplace in case we still want binom pvalue pvalue = pvalue - 0.5 * stats.binom.pmf(y1, y_total, bp) dist = 'binomial' elif method.startswith('etest'): if method.endswith('wald'): method_etest = 'wald' else: method_etest = 'score' if etest_kwds is None: etest_kwds = {} stat, pvalue = etest_poisson_2indep( count1, exposure1, count2, exposure2, value=value, method=method_etest, alternative=alternative, **etest_kwds) dist = 'poisson' else: raise ValueError(f'method "{method}" not recognized') elif compare == "diff": if value is None: value = 0 if method in ['wald']: stat = (rate1 - rate2 - value) / np.sqrt(rate1 / n1 + rate2 / n2) dist = 'normal' "waldccv" elif method in ['waldccv']: stat = (rate1 - rate2 - value) stat /= np.sqrt((count1 + 0.5) / n1**2 + (count2 + 0.5) / n2**2) dist = 'normal' elif method in ['score']: # estimate rates with constraint MLE count_pooled = y1 + y2 rate_pooled = count_pooled / (n1 + n2) dt = rate_pooled - value r2_cmle = 0.5 * (dt + np.sqrt(dt**2 + 4 * value * y2 / (n1 + n2))) r1_cmle = r2_cmle + value stat = ((rate1 - rate2 - value) / np.sqrt(r1_cmle / n1 + r2_cmle / n2)) rates_cmle = (r1_cmle, r2_cmle) dist = 'normal' elif method.startswith('etest'): if method.endswith('wald'): method_etest = 'wald' else: method_etest = 'score' if method == "etest": method = method + "-score" if etest_kwds is None: etest_kwds = {} stat, pvalue = etest_poisson_2indep( count1, exposure1, count2, exposure2, value=value, method=method_etest, compare="diff", alternative=alternative, **etest_kwds) dist = 'poisson' else: raise ValueError(f'method "{method}" not recognized') else: raise NotImplementedError('"compare" needs to be ratio or diff') if dist == 'normal': stat, pvalue = _zstat_generic2(stat, 1, alternative) rates = (rate1, rate2) ratio = rate1 / rate2 diff = rate1 - rate2 res = HolderTuple(statistic=stat, pvalue=pvalue, distribution=dist, compare=compare, method=method, alternative=alternative, rates=rates, ratio=ratio, diff=diff, value=value, rates_cmle=rates_cmle, ratio_null=ratio_null, ) return res def _score_diff(y1, n1, y2, n2, value=0, return_cmle=False): """score test and cmle for difference of 2 independent poisson rates """ count_pooled = y1 + y2 rate1, rate2 = y1 / n1, y2 / n2 rate_pooled = count_pooled / (n1 + n2) dt = rate_pooled - value r2_cmle = 0.5 * (dt + np.sqrt(dt**2 + 4 * value * y2 / (n1 + n2))) r1_cmle = r2_cmle + value eps = 1e-20 # avoid zero division in stat_func v = r1_cmle / n1 + r2_cmle / n2 stat = (rate1 - rate2 - value) / np.sqrt(v + eps) if return_cmle: return stat, r1_cmle, r2_cmle else: return stat def etest_poisson_2indep(count1, exposure1, count2, exposure2, ratio_null=None, value=None, method='score', compare="ratio", alternative='two-sided', ygrid=None, y_grid=None): """ E-test for ratio of two sample Poisson rates. Rates are defined as expected count divided by exposure. The Null and alternative hypothesis for the rates, rate1 and rate2, of two independent Poisson samples are: for compare = 'diff' - H0: rate1 - rate2 - value = 0 - H1: rate1 - rate2 - value != 0 if alternative = 'two-sided' - H1: rate1 - rate2 - value > 0 if alternative = 'larger' - H1: rate1 - rate2 - value < 0 if alternative = 'smaller' for compare = 'ratio' - H0: rate1 / rate2 - value = 0 - H1: rate1 / rate2 - value != 0 if alternative = 'two-sided' - H1: rate1 / rate2 - value > 0 if alternative = 'larger' - H1: rate1 / rate2 - value < 0 if alternative = 'smaller' Parameters ---------- count1 : int Number of events in first sample exposure1 : float Total exposure (time * subjects) in first sample count2 : int Number of events in first sample exposure2 : float Total exposure (time * subjects) in first sample ratio_null: float Ratio of the two Poisson rates under the Null hypothesis. Default is 1. Deprecated, use ``value`` instead. .. deprecated:: 0.14.0 Use ``value`` instead. value : float Value of the ratio or diff of 2 independent rates under the null hypothesis. Default is equal rates, i.e. 1 for ratio and 0 for diff. .. versionadded:: 0.14.0 Replacement for ``ratio_null``. method : {"score", "wald"} Method for the test statistic that defines the rejection region. alternative : string The alternative hypothesis, H1, has to be one of the following - 'two-sided': H1: ratio of rates is not equal to ratio_null (default) - 'larger' : H1: ratio of rates is larger than ratio_null - 'smaller' : H1: ratio of rates is smaller than ratio_null y_grid : None or 1-D ndarray Grid values for counts of the Poisson distribution used for computing the pvalue. By default truncation is based on an upper tail Poisson quantiles. ygrid : None or 1-D ndarray Same as y_grid. Deprecated. If both y_grid and ygrid are provided, ygrid will be ignored. .. deprecated:: 0.14.0 Use ``y_grid`` instead. Returns ------- stat_sample : float test statistic for the sample pvalue : float References ---------- Gu, Ng, Tang, Schucany 2008: Testing the Ratio of Two Poisson Rates, Biometrical Journal 50 (2008) 2, 2008 Ng, H. K. T., K. Gu, and M. L. Tang. 2007. “A Comparative Study of Tests for the Difference of Two Poisson Means.” Computational Statistics & Data Analysis 51 (6): 3085–99. https://doi.org/10.1016/j.csda.2006.02.004. """ y1, n1, y2, n2 = map(np.asarray, [count1, exposure1, count2, exposure2]) d = n2 / n1 eps = 1e-20 # avoid zero division in stat_func if compare == "ratio": if ratio_null is None and value is None: # default value value = 1 elif ratio_null is not None: warnings.warn("'ratio_null' is deprecated, use 'value' keyword", FutureWarning) value = ratio_null r = value # rate1 / rate2 r_d = r / d rate2_cmle = (y1 + y2) / n2 / (1 + r_d) rate1_cmle = rate2_cmle * r if method in ['score']: def stat_func(x1, x2): return (x1 - x2 * r_d) / np.sqrt((x1 + x2) * r_d + eps) # TODO: do I need these? return_results ? # rate2_cmle = (y1 + y2) / n2 / (1 + r_d) # rate1_cmle = rate2_cmle * r # rate1 = rate1_cmle # rate2 = rate2_cmle elif method in ['wald']: def stat_func(x1, x2): return (x1 - x2 * r_d) / np.sqrt(x1 + x2 * r_d**2 + eps) # rate2_mle = y2 / n2 # rate1_mle = y1 / n1 # rate1 = rate1_mle # rate2 = rate2_mle else: raise ValueError('method not recognized') elif compare == "diff": if value is None: value = 0 tmp = _score_diff(y1, n1, y2, n2, value=value, return_cmle=True) _, rate1_cmle, rate2_cmle = tmp if method in ['score']: def stat_func(x1, x2): return _score_diff(x1, n1, x2, n2, value=value) elif method in ['wald']: def stat_func(x1, x2): rate1, rate2 = x1 / n1, x2 / n2 stat = (rate1 - rate2 - value) stat /= np.sqrt(rate1 / n1 + rate2 / n2 + eps) return stat else: raise ValueError('method not recognized') # The sampling distribution needs to be based on the null hypotheis # use constrained MLE from 'score' calculation rate1 = rate1_cmle rate2 = rate2_cmle mean1 = n1 * rate1 mean2 = n2 * rate2 stat_sample = stat_func(y1, y2) if ygrid is not None: warnings.warn("ygrid is deprecated, use y_grid", FutureWarning) y_grid = y_grid if y_grid is not None else ygrid # The following uses a fixed truncation for evaluating the probabilities # It will currently only work for small counts, so that sf at truncation # point is small # We can make it depend on the amount of truncated sf. # Some numerical optimization or checks for large means need to be added. if y_grid is None: threshold = stats.poisson.isf(1e-13, max(mean1, mean2)) threshold = max(threshold, 100) # keep at least 100 y_grid = np.arange(threshold + 1) else: y_grid = np.asarray(y_grid) if y_grid.ndim != 1: raise ValueError("y_grid needs to be None or 1-dimensional array") pdf1 = stats.poisson.pmf(y_grid, mean1) pdf2 = stats.poisson.pmf(y_grid, mean2) stat_space = stat_func(y_grid[:, None], y_grid[None, :]) # broadcasting eps = 1e-15 # correction for strict inequality check if alternative in ['two-sided', '2-sided', '2s']: mask = np.abs(stat_space) >= (np.abs(stat_sample) - eps) elif alternative in ['larger', 'l']: mask = stat_space >= (stat_sample - eps) elif alternative in ['smaller', 's']: mask = stat_space <= (stat_sample + eps) else: raise ValueError('invalid alternative') pvalue = ((pdf1[:, None] * pdf2[None, :])[mask]).sum() return stat_sample, pvalue def tost_poisson_2indep(count1, exposure1, count2, exposure2, low, upp, method='score', compare='ratio'): '''Equivalence test based on two one-sided `test_proportions_2indep` This assumes that we have two independent poisson samples. The Null and alternative hypothesis for equivalence testing are for compare = 'ratio' - H0: rate1 / rate2 <= low or upp <= rate1 / rate2 - H1: low < rate1 / rate2 < upp for compare = 'diff' - H0: rate1 - rate2 <= low or upp <= rate1 - rate2 - H1: low < rate - rate < upp Parameters ---------- count1 : int Number of events in first sample exposure1 : float Total exposure (time * subjects) in first sample count2 : int Number of events in second sample exposure2 : float Total exposure (time * subjects) in second sample low, upp : equivalence margin for the ratio or difference of Poisson rates method: string TOST uses ``test_poisson_2indep`` and has the same methods. ratio: - 'wald': method W1A, wald test, variance based on observed rates - 'score': method W2A, score test, variance based on estimate under the Null hypothesis - 'wald-log': W3A, uses log-ratio, variance based on observed rates - 'score-log' W4A, uses log-ratio, variance based on estimate under the Null hypothesis - 'sqrt': W5A, based on variance stabilizing square root transformation - 'exact-cond': exact conditional test based on binomial distribution This uses ``binom_test`` which is minlike in the two-sided case. - 'cond-midp': midpoint-pvalue of exact conditional test - 'etest' or 'etest-score: etest with score test statistic - 'etest-wald': etest with wald test statistic diff: - 'wald', - 'waldccv' - 'score' - 'etest-score' or 'etest: etest with score test statistic - 'etest-wald': etest with wald test statistic Returns ------- results : instance of HolderTuple class The two main attributes are test statistic `statistic` and p-value `pvalue`. References ---------- Gu, Ng, Tang, Schucany 2008: Testing the Ratio of Two Poisson Rates, Biometrical Journal 50 (2008) 2, 2008 See Also -------- test_poisson_2indep confint_poisson_2indep ''' tt1 = test_poisson_2indep(count1, exposure1, count2, exposure2, value=low, method=method, compare=compare, alternative='larger') tt2 = test_poisson_2indep(count1, exposure1, count2, exposure2, value=upp, method=method, compare=compare, alternative='smaller') # idx_max = 1 if t1.pvalue < t2.pvalue else 0 idx_max = np.asarray(tt1.pvalue < tt2.pvalue, int) statistic = np.choose(idx_max, [tt1.statistic, tt2.statistic]) pvalue = np.choose(idx_max, [tt1.pvalue, tt2.pvalue]) res = HolderTuple(statistic=statistic, pvalue=pvalue, method=method, compare=compare, equiv_limits=(low, upp), results_larger=tt1, results_smaller=tt2, title="Equivalence test for 2 independent Poisson rates" ) return res def nonequivalence_poisson_2indep(count1, exposure1, count2, exposure2, low, upp, method='score', compare="ratio"): """Test for non-equivalence, minimum effect for poisson. This reverses null and alternative hypothesis compared to equivalence testing. The null hypothesis is that the effect, ratio (or diff), is in an interval that specifies a range of irrelevant or unimportant differences between the two samples. The Null and alternative hypothesis comparing the ratio of rates are for compare = 'ratio': - H0: low < rate1 / rate2 < upp - H1: rate1 / rate2 <= low or upp <= rate1 / rate2 for compare = 'diff': - H0: rate1 - rate2 <= low or upp <= rate1 - rate2 - H1: low < rate - rate < upp Notes ----- This is implemented as two one-sided tests at the minimum effect boundaries (low, upp) with (nominal) size alpha / 2 each. The size of the test is the sum of the two one-tailed tests, which corresponds to an equal-tailed two-sided test. If low and upp are equal, then the result is the same as the standard two-sided test. The p-value is computed as `2 * min(pvalue_low, pvalue_upp)` in analogy to two-sided equal-tail tests. In large samples the nominal size of the test will be below alpha. References ---------- .. [1] Hodges, J. L., Jr., and E. L. Lehmann. 1954. Testing the Approximate Validity of Statistical Hypotheses. Journal of the Royal Statistical Society, Series B (Methodological) 16: 261–68. .. [2] Kim, Jae H., and Andrew P. Robinson. 2019. “Interval-Based Hypothesis Testing and Its Applications to Economics and Finance.” Econometrics 7 (2): 21. https://doi.org/10.3390/econometrics7020021. """ tt1 = test_poisson_2indep(count1, exposure1, count2, exposure2, value=low, method=method, compare=compare, alternative='smaller') tt2 = test_poisson_2indep(count1, exposure1, count2, exposure2, value=upp, method=method, compare=compare, alternative='larger') # idx_min = 0 if tt1.pvalue < tt2.pvalue else 1 idx_min = np.asarray(tt1.pvalue < tt2.pvalue, int) pvalue = 2 * np.minimum(tt1.pvalue, tt2.pvalue) statistic = np.choose(idx_min, [tt1.statistic, tt2.statistic]) res = HolderTuple(statistic=statistic, pvalue=pvalue, method=method, results_larger=tt1, results_smaller=tt2, title="Equivalence test for 2 independent Poisson rates" ) return res def confint_poisson_2indep(count1, exposure1, count2, exposure2, method='score', compare='ratio', alpha=0.05, method_mover="score", ): """Confidence interval for ratio or difference of 2 indep poisson rates. Parameters ---------- count1 : int Number of events in first sample. exposure1 : float Total exposure (time * subjects) in first sample. count2 : int Number of events in second sample. exposure2 : float Total exposure (time * subjects) in second sample. method : string Method for the test statistic and the p-value. Defaults to `'score'`. see Notes. ratio: - 'wald': NOT YET, method W1A, wald test, variance based on observed rates - 'waldcc' : - 'score': method W2A, score test, variance based on estimate under the Null hypothesis - 'wald-log': W3A, uses log-ratio, variance based on observed rates - 'score-log' W4A, uses log-ratio, variance based on estimate under the Null hypothesis - 'sqrt': W5A, based on variance stabilizing square root transformation - 'sqrtcc' : - 'exact-cond': NOT YET, exact conditional test based on binomial distribution This uses ``binom_test`` which is minlike in the two-sided case. - 'cond-midp': NOT YET, midpoint-pvalue of exact conditional test - 'mover' : diff: - 'wald', - 'waldccv' - 'score' - 'mover' compare : {'diff', 'ratio'} Default is "ratio". If compare is `diff`, then the hypothesis test is for diff = rate1 - rate2. If compare is `ratio`, then the hypothesis test is for the rate ratio defined by ratio = rate1 / rate2. alternative : string The alternative hypothesis, H1, has to be one of the following - 'two-sided': H1: ratio of rates is not equal to ratio_null (default) - 'larger' : H1: ratio of rates is larger than ratio_null - 'smaller' : H1: ratio of rates is smaller than ratio_null alpha : float in (0, 1) Significance level, nominal coverage of the confidence interval is 1 - alpha. Returns ------- tuple (low, upp) : confidence limits. """ # shortcut names y1, n1, y2, n2 = map(np.asarray, [count1, exposure1, count2, exposure2]) rate1, rate2 = y1 / n1, y2 / n2 alpha = alpha / 2 # two-sided only if compare == "ratio": if method == "score": low, upp = _invert_test_confint_2indep( count1, exposure1, count2, exposure2, alpha=alpha * 2, # check how alpha is defined method="score", compare="ratio", method_start="waldcc" ) ci = (low, upp) elif method == "wald-log": crit = stats.norm.isf(alpha) c = 0 center = (count1 + c) / (count2 + c) * n2 / n1 std = np.sqrt(1 / (count1 + c) + 1 / (count2 + c)) ci = (center * np.exp(- crit * std), center * np.exp(crit * std)) elif method == "score-log": low, upp = _invert_test_confint_2indep( count1, exposure1, count2, exposure2, alpha=alpha * 2, # check how alpha is defined method="score-log", compare="ratio", method_start="waldcc" ) ci = (low, upp) elif method == "waldcc": crit = stats.norm.isf(alpha) center = (count1 + 0.5) / (count2 + 0.5) * n2 / n1 std = np.sqrt(1 / (count1 + 0.5) + 1 / (count2 + 0.5)) ci = (center * np.exp(- crit * std), center * np.exp(crit * std)) elif method == "sqrtcc": # coded based on Price, Bonett 2000 equ (2.4) crit = stats.norm.isf(alpha) center = np.sqrt((count1 + 0.5) * (count2 + 0.5)) std = 0.5 * np.sqrt(count1 + 0.5 + count2 + 0.5 - 0.25 * crit) denom = (count2 + 0.5 - 0.25 * crit**2) low_sqrt = (center - crit * std) / denom upp_sqrt = (center + crit * std) / denom ci = (low_sqrt**2, upp_sqrt**2) elif method == "mover": method_p = method_mover ci1 = confint_poisson(y1, n1, method=method_p, alpha=2*alpha) ci2 = confint_poisson(y2, n2, method=method_p, alpha=2*alpha) ci = _mover_confint(rate1, rate2, ci1, ci2, contrast="ratio") else: raise ValueError(f'method "{method}" not recognized') ci = (np.maximum(ci[0], 0), ci[1]) elif compare == "diff": if method in ['wald']: crit = stats.norm.isf(alpha) center = rate1 - rate2 half = crit * np.sqrt(rate1 / n1 + rate2 / n2) ci = center - half, center + half elif method in ['waldccv']: crit = stats.norm.isf(alpha) center = rate1 - rate2 std = np.sqrt((count1 + 0.5) / n1**2 + (count2 + 0.5) / n2**2) half = crit * std ci = center - half, center + half elif method == "score": low, upp = _invert_test_confint_2indep( count1, exposure1, count2, exposure2, alpha=alpha * 2, # check how alpha is defined method="score", compare="diff", method_start="waldccv" ) ci = (low, upp) elif method == "mover": method_p = method_mover ci1 = confint_poisson(y1, n1, method=method_p, alpha=2*alpha) ci2 = confint_poisson(y2, n2, method=method_p, alpha=2*alpha) ci = _mover_confint(rate1, rate2, ci1, ci2, contrast="diff") else: raise ValueError(f'method "{method}" not recognized') else: raise NotImplementedError('"compare" needs to be ratio or diff') return ci def power_poisson_ratio_2indep( rate1, rate2, nobs1, nobs_ratio=1, exposure=1, value=0, alpha=0.05, dispersion=1, alternative="smaller", method_var="alt", return_results=True, ): """Power of test of ratio of 2 independent poisson rates. This is based on Zhu and Zhu and Lakkis. It does not directly correspond to `test_poisson_2indep`. Parameters ---------- rate1 : float Poisson rate for the first sample, treatment group, under the alternative hypothesis. rate2 : float Poisson rate for the second sample, reference group, under the alternative hypothesis. nobs1 : float or int Number of observations in sample 1. nobs_ratio : float Sample size ratio, nobs2 = nobs_ratio * nobs1. exposure : float Exposure for each observation. Total exposure is nobs1 * exposure and nobs2 * exposure. alpha : float in interval (0,1) Significance level, e.g. 0.05, is the probability of a type I error, that is wrong rejections if the Null Hypothesis is true. value : float Rate ratio, rate1 / rate2, under the null hypothesis. dispersion : float Dispersion coefficient for quasi-Poisson. Dispersion different from one can capture over or under dispersion relative to Poisson distribution. method_var : {"score", "alt"} The variance of the test statistic for the null hypothesis given the rates under the alternative can be either equal to the rates under the alternative ``method_var="alt"``, or estimated under the constrained of the null hypothesis, ``method_var="score"``. alternative : string, 'two-sided' (default), 'larger', 'smaller' Alternative hypothesis whether the power is calculated for a two-sided (default) or one sided test. The one-sided test can be either 'larger', 'smaller'. return_results : bool If true, then a results instance with extra information is returned, otherwise only the computed power is returned. Returns ------- results : results instance or float If return_results is False, then only the power is returned. If return_results is True, then a results instance with the information in attributes is returned. power : float Power of the test, e.g. 0.8, is one minus the probability of a type II error. Power is the probability that the test correctly rejects the Null Hypothesis if the Alternative Hypothesis is true. Other attributes in results instance include : std_null standard error of difference under the null hypothesis (without sqrt(nobs1)) std_alt standard error of difference under the alternative hypothesis (without sqrt(nobs1)) References ---------- .. [1] Zhu, Haiyuan. 2017. “Sample Size Calculation for Comparing Two Poisson or Negative Binomial Rates in Noninferiority or Equivalence Trials.” Statistics in Biopharmaceutical Research, March. https://doi.org/10.1080/19466315.2016.1225594 .. [2] Zhu, Haiyuan, and Hassan Lakkis. 2014. “Sample Size Calculation for Comparing Two Negative Binomial Rates.” Statistics in Medicine 33 (3): 376–87. https://doi.org/10.1002/sim.5947. .. [3] PASS documentation """ # TODO: avoid possible circular import, check if needed from statsmodels.stats.power import normal_power_het rate1, rate2, nobs1 = map(np.asarray, [rate1, rate2, nobs1]) nobs2 = nobs_ratio * nobs1 v1 = dispersion / exposure * (1 / rate1 + 1 / (nobs_ratio * rate2)) if method_var == "alt": v0 = v1 elif method_var == "score": # nobs_ratio = 1 / nobs_ratio v0 = dispersion / exposure * (1 + value / nobs_ratio)**2 v0 /= value / nobs_ratio * (rate1 + (nobs_ratio * rate2)) else: raise NotImplementedError(f"method_var {method_var} not recognized") std_null = np.sqrt(v0) std_alt = np.sqrt(v1) es = np.log(rate1 / rate2) - np.log(value) pow_ = normal_power_het(es, nobs1, alpha, std_null=std_null, std_alternative=std_alt, alternative=alternative) p_pooled = None # TODO: replace or remove if return_results: res = HolderTuple( power=pow_, p_pooled=p_pooled, std_null=std_null, std_alt=std_alt, nobs1=nobs1, nobs2=nobs2, nobs_ratio=nobs_ratio, alpha=alpha, tuple_=("power",), # override default ) return res return pow_ def power_equivalence_poisson_2indep(rate1, rate2, nobs1, low, upp, nobs_ratio=1, exposure=1, alpha=0.05, dispersion=1, method_var="alt", return_results=False): """Power of equivalence test of ratio of 2 independent poisson rates. Parameters ---------- rate1 : float Poisson rate for the first sample, treatment group, under the alternative hypothesis. rate2 : float Poisson rate for the second sample, reference group, under the alternative hypothesis. nobs1 : float or int Number of observations in sample 1. low : float Lower equivalence margin for the rate ratio, rate1 / rate2. upp : float Upper equivalence margin for the rate ratio, rate1 / rate2. nobs_ratio : float Sample size ratio, nobs2 = nobs_ratio * nobs1. exposure : float Exposure for each observation. Total exposure is nobs1 * exposure and nobs2 * exposure. alpha : float in interval (0,1) Significance level, e.g. 0.05, is the probability of a type I error, that is wrong rejections if the Null Hypothesis is true. value : float Difference between rates 1 and 2 under the null hypothesis. method_var : {"score", "alt"} The variance of the test statistic for the null hypothesis given the rates uder the alternative, can be either equal to the rates under the alternative ``method_var="alt"``, or estimated under the constrained of the null hypothesis, ``method_var="score"``. alternative : string, 'two-sided' (default), 'larger', 'smaller' Alternative hypothesis whether the power is calculated for a two-sided (default) or one sided test. The one-sided test can be either 'larger', 'smaller'. return_results : bool If true, then a results instance with extra information is returned, otherwise only the computed power is returned. Returns ------- results : results instance or float If return_results is False, then only the power is returned. If return_results is True, then a results instance with the information in attributes is returned. power : float Power of the test, e.g. 0.8, is one minus the probability of a type II error. Power is the probability that the test correctly rejects the Null Hypothesis if the Alternative Hypothesis is true. Other attributes in results instance include : std_null standard error of difference under the null hypothesis (without sqrt(nobs1)) std_alt standard error of difference under the alternative hypothesis (without sqrt(nobs1)) References ---------- .. [1] Zhu, Haiyuan. 2017. “Sample Size Calculation for Comparing Two Poisson or Negative Binomial Rates in Noninferiority or Equivalence Trials.” Statistics in Biopharmaceutical Research, March. https://doi.org/10.1080/19466315.2016.1225594 .. [2] Zhu, Haiyuan, and Hassan Lakkis. 2014. “Sample Size Calculation for Comparing Two Negative Binomial Rates.” Statistics in Medicine 33 (3): 376–87. https://doi.org/10.1002/sim.5947. .. [3] PASS documentation """ rate1, rate2, nobs1 = map(np.asarray, [rate1, rate2, nobs1]) nobs2 = nobs_ratio * nobs1 v1 = dispersion / exposure * (1 / rate1 + 1 / (nobs_ratio * rate2)) if method_var == "alt": v0_low = v0_upp = v1 elif method_var == "score": v0_low = dispersion / exposure * (1 + low * nobs_ratio)**2 v0_low /= low * nobs_ratio * (rate1 + (nobs_ratio * rate2)) v0_upp = dispersion / exposure * (1 + upp * nobs_ratio)**2 v0_upp /= upp * nobs_ratio * (rate1 + (nobs_ratio * rate2)) else: raise NotImplementedError(f"method_var {method_var} not recognized") es_low = np.log(rate1 / rate2) - np.log(low) es_upp = np.log(rate1 / rate2) - np.log(upp) std_null_low = np.sqrt(v0_low) std_null_upp = np.sqrt(v0_upp) std_alternative = np.sqrt(v1) pow_ = _power_equivalence_het(es_low, es_upp, nobs2, alpha=alpha, std_null_low=std_null_low, std_null_upp=std_null_upp, std_alternative=std_alternative) if return_results: res = HolderTuple( power=pow_[0], power_margins=pow[1:], std_null_low=std_null_low, std_null_upp=std_null_upp, std_alt=std_alternative, nobs1=nobs1, nobs2=nobs2, nobs_ratio=nobs_ratio, alpha=alpha, tuple_=("power",), # override default ) return res else: return pow_[0] def _power_equivalence_het_v0(es_low, es_upp, nobs, alpha=0.05, std_null_low=None, std_null_upp=None, std_alternative=None): """power for equivalence test """ s0_low = std_null_low s0_upp = std_null_upp s1 = std_alternative crit = norm.isf(alpha) pow_ = ( norm.cdf((np.sqrt(nobs) * es_low - crit * s0_low) / s1) + norm.cdf((np.sqrt(nobs) * es_upp - crit * s0_upp) / s1) - 1 ) return pow_ def _power_equivalence_het(es_low, es_upp, nobs, alpha=0.05, std_null_low=None, std_null_upp=None, std_alternative=None): """power for equivalence test """ s0_low = std_null_low s0_upp = std_null_upp s1 = std_alternative crit = norm.isf(alpha) # Note: rejection region is an interval [low, upp] # Here we compute the complement of the two tail probabilities p1 = norm.sf((np.sqrt(nobs) * es_low - crit * s0_low) / s1) p2 = norm.cdf((np.sqrt(nobs) * es_upp + crit * s0_upp) / s1) pow_ = 1 - (p1 + p2) return pow_, p1, p2 def _std_2poisson_power( rate1, rate2, nobs_ratio=1, alpha=0.05, exposure=1, dispersion=1, value=0, method_var="score", ): rates_pooled = (rate1 + rate2 * nobs_ratio) / (1 + nobs_ratio) # v1 = dispersion / exposure * (1 / rate2 + 1 / (nobs_ratio * rate1)) if method_var == "alt": v0 = v1 = rate1 + rate2 / nobs_ratio else: # uaw n1 = 1 as normalization _, r1_cmle, r2_cmle = _score_diff( rate1, 1, rate2 * nobs_ratio, nobs_ratio, value=value, return_cmle=True) v1 = rate1 + rate2 / nobs_ratio v0 = r1_cmle + r2_cmle / nobs_ratio return rates_pooled, np.sqrt(v0), np.sqrt(v1) def power_poisson_diff_2indep(rate1, rate2, nobs1, nobs_ratio=1, alpha=0.05, value=0, method_var="score", alternative='two-sided', return_results=True): """Power of ztest for the difference between two independent poisson rates. Parameters ---------- rate1 : float Poisson rate for the first sample, treatment group, under the alternative hypothesis. rate2 : float Poisson rate for the second sample, reference group, under the alternative hypothesis. nobs1 : float or int Number of observations in sample 1. nobs_ratio : float Sample size ratio, nobs2 = nobs_ratio * nobs1. alpha : float in interval (0,1) Significance level, e.g. 0.05, is the probability of a type I error, that is wrong rejections if the Null Hypothesis is true. value : float Difference between rates 1 and 2 under the null hypothesis. method_var : {"score", "alt"} The variance of the test statistic for the null hypothesis given the rates uder the alternative, can be either equal to the rates under the alternative ``method_var="alt"``, or estimated under the constrained of the null hypothesis, ``method_var="score"``. alternative : string, 'two-sided' (default), 'larger', 'smaller' Alternative hypothesis whether the power is calculated for a two-sided (default) or one sided test. The one-sided test can be either 'larger', 'smaller'. return_results : bool If true, then a results instance with extra information is returned, otherwise only the computed power is returned. Returns ------- results : results instance or float If return_results is False, then only the power is returned. If return_results is True, then a results instance with the information in attributes is returned. power : float Power of the test, e.g. 0.8, is one minus the probability of a type II error. Power is the probability that the test correctly rejects the Null Hypothesis if the Alternative Hypothesis is true. Other attributes in results instance include : std_null standard error of difference under the null hypothesis (without sqrt(nobs1)) std_alt standard error of difference under the alternative hypothesis (without sqrt(nobs1)) References ---------- .. [1] Stucke, Kathrin, and Meinhard Kieser. 2013. “Sample Size Calculations for Noninferiority Trials with Poisson Distributed Count Data.” Biometrical Journal 55 (2): 203–16. https://doi.org/10.1002/bimj.201200142. .. [2] PASS manual chapter 436 """ # TODO: avoid possible circular import, check if needed from statsmodels.stats.power import normal_power_het rate1, rate2, nobs1 = map(np.asarray, [rate1, rate2, nobs1]) diff = rate1 - rate2 _, std_null, std_alt = _std_2poisson_power( rate1, rate2, nobs_ratio=nobs_ratio, alpha=alpha, value=value, method_var=method_var, ) pow_ = normal_power_het(diff - value, nobs1, alpha, std_null=std_null, std_alternative=std_alt, alternative=alternative) if return_results: res = HolderTuple( power=pow_, rates_alt=(rate2 + diff, rate2), std_null=std_null, std_alt=std_alt, nobs1=nobs1, nobs2=nobs_ratio * nobs1, nobs_ratio=nobs_ratio, alpha=alpha, tuple_=("power",), # override default ) return res else: return pow_ def _var_cmle_negbin(rate1, rate2, nobs_ratio, exposure=1, value=1, dispersion=0): """ variance based on constrained cmle, for score test version for ratio comparison of two negative binomial samples value = rate1 / rate2 under the null """ # definitions in Zhu # nobs_ratio = n1 / n0 # value = ratio = r1 / r0 rate0 = rate2 # control nobs_ratio = 1 / nobs_ratio a = - dispersion * exposure * value * (1 + nobs_ratio) b = (dispersion * exposure * (rate0 * value + nobs_ratio * rate1) - (1 + nobs_ratio * value)) c = rate0 + nobs_ratio * rate1 if dispersion == 0: r0 = -c / b else: r0 = (-b - np.sqrt(b**2 - 4 * a * c)) / (2 * a) r1 = r0 * value v = (1 / exposure / r0 * (1 + 1 / value / nobs_ratio) + (1 + nobs_ratio) / nobs_ratio * dispersion) r2 = r0 return v * nobs_ratio, r1, r2 def power_negbin_ratio_2indep( rate1, rate2, nobs1, nobs_ratio=1, exposure=1, value=1, alpha=0.05, dispersion=0.01, alternative="two-sided", method_var="alt", return_results=True): """ Power of test of ratio of 2 independent negative binomial rates. Parameters ---------- rate1 : float Poisson rate for the first sample, treatment group, under the alternative hypothesis. rate2 : float Poisson rate for the second sample, reference group, under the alternative hypothesis. nobs1 : float or int Number of observations in sample 1. low : float Lower equivalence margin for the rate ratio, rate1 / rate2. upp : float Upper equivalence margin for the rate ratio, rate1 / rate2. nobs_ratio : float Sample size ratio, nobs2 = nobs_ratio * nobs1. exposure : float Exposure for each observation. Total exposure is nobs1 * exposure and nobs2 * exposure. value : float Rate ratio, rate1 / rate2, under the null hypothesis. alpha : float in interval (0,1) Significance level, e.g. 0.05, is the probability of a type I error, that is wrong rejections if the Null Hypothesis is true. dispersion : float >= 0. Dispersion parameter for Negative Binomial distribution. The Poisson limiting case corresponds to ``dispersion=0``. method_var : {"score", "alt"} The variance of the test statistic for the null hypothesis given the rates under the alternative, can be either equal to the rates under the alternative ``method_var="alt"``, or estimated under the constrained of the null hypothesis, ``method_var="score"``, or based on a moment constrained estimate, ``method_var="ftotal"``. see references. alternative : string, 'two-sided' (default), 'larger', 'smaller' Alternative hypothesis whether the power is calculated for a two-sided (default) or one sided test. The one-sided test can be either 'larger', 'smaller'. return_results : bool If true, then a results instance with extra information is returned, otherwise only the computed power is returned. Returns ------- results : results instance or float If return_results is False, then only the power is returned. If return_results is True, then a results instance with the information in attributes is returned. power : float Power of the test, e.g. 0.8, is one minus the probability of a type II error. Power is the probability that the test correctly rejects the Null Hypothesis if the Alternative Hypothesis is true. Other attributes in results instance include : std_null standard error of difference under the null hypothesis (without sqrt(nobs1)) std_alt standard error of difference under the alternative hypothesis (without sqrt(nobs1)) References ---------- .. [1] Zhu, Haiyuan. 2017. “Sample Size Calculation for Comparing Two Poisson or Negative Binomial Rates in Noninferiority or Equivalence Trials.” Statistics in Biopharmaceutical Research, March. https://doi.org/10.1080/19466315.2016.1225594 .. [2] Zhu, Haiyuan, and Hassan Lakkis. 2014. “Sample Size Calculation for Comparing Two Negative Binomial Rates.” Statistics in Medicine 33 (3): 376–87. https://doi.org/10.1002/sim.5947. .. [3] PASS documentation """ # TODO: avoid possible circular import, check if needed from statsmodels.stats.power import normal_power_het rate1, rate2, nobs1 = map(np.asarray, [rate1, rate2, nobs1]) nobs2 = nobs_ratio * nobs1 v1 = ((1 / rate1 + 1 / (nobs_ratio * rate2)) / exposure + (1 + nobs_ratio) / nobs_ratio * dispersion) if method_var == "alt": v0 = v1 elif method_var == "ftotal": v0 = (1 + value * nobs_ratio)**2 / ( exposure * nobs_ratio * value * (rate1 + nobs_ratio * rate2)) v0 += (1 + nobs_ratio) / nobs_ratio * dispersion elif method_var == "score": v0 = _var_cmle_negbin(rate1, rate2, nobs_ratio, exposure=exposure, value=value, dispersion=dispersion)[0] else: raise NotImplementedError(f"method_var {method_var} not recognized") std_null = np.sqrt(v0) std_alt = np.sqrt(v1) es = np.log(rate1 / rate2) - np.log(value) pow_ = normal_power_het(es, nobs1, alpha, std_null=std_null, std_alternative=std_alt, alternative=alternative) if return_results: res = HolderTuple( power=pow_, std_null=std_null, std_alt=std_alt, nobs1=nobs1, nobs2=nobs2, nobs_ratio=nobs_ratio, alpha=alpha, tuple_=("power",), # override default ) return res return pow_ def power_equivalence_neginb_2indep(rate1, rate2, nobs1, low, upp, nobs_ratio=1, exposure=1, alpha=0.05, dispersion=0, method_var="alt", return_results=False): """ Power of equivalence test of ratio of 2 indep. negative binomial rates. Parameters ---------- rate1 : float Poisson rate for the first sample, treatment group, under the alternative hypothesis. rate2 : float Poisson rate for the second sample, reference group, under the alternative hypothesis. nobs1 : float or int Number of observations in sample 1. low : float Lower equivalence margin for the rate ratio, rate1 / rate2. upp : float Upper equivalence margin for the rate ratio, rate1 / rate2. nobs_ratio : float Sample size ratio, nobs2 = nobs_ratio * nobs1. alpha : float in interval (0,1) Significance level, e.g. 0.05, is the probability of a type I error, that is wrong rejections if the Null Hypothesis is true. dispersion : float >= 0. Dispersion parameter for Negative Binomial distribution. The Poisson limiting case corresponds to ``dispersion=0``. method_var : {"score", "alt"} The variance of the test statistic for the null hypothesis given the rates under the alternative, can be either equal to the rates under the alternative ``method_var="alt"``, or estimated under the constrained of the null hypothesis, ``method_var="score"``, or based on a moment constrained estimate, ``method_var="ftotal"``. see references. alternative : string, 'two-sided' (default), 'larger', 'smaller' Alternative hypothesis whether the power is calculated for a two-sided (default) or one sided test. The one-sided test can be either 'larger', 'smaller'. return_results : bool If true, then a results instance with extra information is returned, otherwise only the computed power is returned. Returns ------- results : results instance or float If return_results is False, then only the power is returned. If return_results is True, then a results instance with the information in attributes is returned. power : float Power of the test, e.g. 0.8, is one minus the probability of a type II error. Power is the probability that the test correctly rejects the Null Hypothesis if the Alternative Hypothesis is true. Other attributes in results instance include : std_null standard error of difference under the null hypothesis (without sqrt(nobs1)) std_alt standard error of difference under the alternative hypothesis (without sqrt(nobs1)) References ---------- .. [1] Zhu, Haiyuan. 2017. “Sample Size Calculation for Comparing Two Poisson or Negative Binomial Rates in Noninferiority or Equivalence Trials.” Statistics in Biopharmaceutical Research, March. https://doi.org/10.1080/19466315.2016.1225594 .. [2] Zhu, Haiyuan, and Hassan Lakkis. 2014. “Sample Size Calculation for Comparing Two Negative Binomial Rates.” Statistics in Medicine 33 (3): 376–87. https://doi.org/10.1002/sim.5947. .. [3] PASS documentation """ rate1, rate2, nobs1 = map(np.asarray, [rate1, rate2, nobs1]) nobs2 = nobs_ratio * nobs1 v1 = ((1 / rate2 + 1 / (nobs_ratio * rate1)) / exposure + (1 + nobs_ratio) / nobs_ratio * dispersion) if method_var == "alt": v0_low = v0_upp = v1 elif method_var == "ftotal": v0_low = (1 + low * nobs_ratio)**2 / ( exposure * nobs_ratio * low * (rate1 + nobs_ratio * rate2)) v0_low += (1 + nobs_ratio) / nobs_ratio * dispersion v0_upp = (1 + upp * nobs_ratio)**2 / ( exposure * nobs_ratio * upp * (rate1 + nobs_ratio * rate2)) v0_upp += (1 + nobs_ratio) / nobs_ratio * dispersion elif method_var == "score": v0_low = _var_cmle_negbin(rate1, rate2, nobs_ratio, exposure=exposure, value=low, dispersion=dispersion)[0] v0_upp = _var_cmle_negbin(rate1, rate2, nobs_ratio, exposure=exposure, value=upp, dispersion=dispersion)[0] else: raise NotImplementedError(f"method_var {method_var} not recognized") es_low = np.log(rate1 / rate2) - np.log(low) es_upp = np.log(rate1 / rate2) - np.log(upp) std_null_low = np.sqrt(v0_low) std_null_upp = np.sqrt(v0_upp) std_alternative = np.sqrt(v1) pow_ = _power_equivalence_het(es_low, es_upp, nobs1, alpha=alpha, std_null_low=std_null_low, std_null_upp=std_null_upp, std_alternative=std_alternative) if return_results: res = HolderTuple( power=pow_[0], power_margins=pow[1:], std_null_low=std_null_low, std_null_upp=std_null_upp, std_alt=std_alternative, nobs1=nobs1, nobs2=nobs2, nobs_ratio=nobs_ratio, alpha=alpha, tuple_=("power",), # override default ) return res else: return pow_[0]