Repository: statsmodels/statsmodels (Python)

Issue:
ENH: confint_poisson only has two-sided confidence intervals
https://www.statsmodels.org/dev/generated/statsmodels.stats.rates.confint_poisson.html

confint_poisson does not have an one-sided confidence interval option.
As mentioned in #9249, add a new option 'alternative' to confint_poisson.

Maybe same for multinomial_proportions_confint, later.


Hints:
for docstrings: 
I will come up with a short explanation for the meaning and direction of alternative option for confidence intervals. Then, we can copy it to all confint functions that have the alternative keyword.

one sided confint for multinomial_proportions_confint will, most likely, not be obvious and we might have to check with the original articles.
For docstrings, should we create a new issue and PR like 'DOCS: XXX'?

Patch:
diff --git a/statsmodels/stats/rates.py b/statsmodels/stats/rates.py
--- a/statsmodels/stats/rates.py
+++ b/statsmodels/stats/rates.py
@@ -186,15 +186,15 @@ def test_poisson(count, nobs, value, method=None, alternative="two-sided",
     return res
 
 
-def confint_poisson(count, exposure, method=None, alpha=0.05):
+def confint_poisson(count, exposure, method=None, alpha=0.05,
+                    alternative="two-sided"):
     """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.
+    smaller or equal to alpha / 2.
 
     Parameters
     ----------
@@ -209,10 +209,16 @@ def confint_poisson(count, exposure, method=None, alpha=0.05):
     alpha : float in (0, 1)
         Significance level, nominal coverage of the confidence interval is
         1 - alpha.
+    alternative : {"two-sider", "larger", "smaller")
+        default: "two-sided"
+        Specifies whether to calculate a two-sided or one-sided confidence
+        interval.
 
     Returns
     -------
     tuple (low, upp) : confidence limits.
+        When alternative is not "two-sided", lower or upper bound is set to
+        0 or inf respectively.
 
     Notes
     -----
@@ -270,7 +276,12 @@ def confint_poisson(count, exposure, method=None, alpha=0.05):
     """
     n = exposure  # short hand
     rate = count / exposure
-    alpha = alpha / 2  # two-sided
+
+    if alternative == 'two-sided':
+        alpha = alpha / 2
+    elif alternative not in ['larger', 'smaller']:
+        raise NotImplementedError(
+            f"alternative {alternative} is not available")
 
     if method is None:
         msg = "method needs to be specified, currently no default method"
@@ -365,6 +376,11 @@ def confint_poisson(count, exposure, method=None, alpha=0.05):
     else:
         raise ValueError("unknown method %s" % method)
 
+    if alternative == "larger":
+        ci = (0, ci[1])
+    elif alternative == "smaller":
+        ci = (ci[0], np.inf)
+
     ci = (np.maximum(ci[0], 0), ci[1])
     return ci
 


Test patch:
diff --git a/statsmodels/stats/tests/test_rates_poisson.py b/statsmodels/stats/tests/test_rates_poisson.py
--- a/statsmodels/stats/tests/test_rates_poisson.py
+++ b/statsmodels/stats/tests/test_rates_poisson.py
@@ -1146,3 +1146,24 @@ def test_power_negbin():
 
     assert_allclose(pow_p, pow1, atol=5e-2)
     assert_allclose(pow_p, pow_, rtol=1e-13)
+
+
+@pytest.mark.parametrize('count',    [0, 1, 10, 100])
+@pytest.mark.parametrize('exposure', [1, 10, 100, 1000])
+@pytest.mark.parametrize('alpha',    [0.01, 0.05, 0.1])
+@pytest.mark.parametrize('method',
+                         method_names_poisson_1samp['confint'])
+@pytest.mark.parametrize('alternative', ['larger', 'smaller'])
+def test_confint_poisson_alternative(count, exposure, method, alpha,
+                                     alternative):
+    # regression test
+    two_sided_ci = confint_poisson(count, exposure, method=method,
+                                   alpha=alpha * 2, alternative='two-sided')
+    one_sided_ci = confint_poisson(count, exposure, method=method,
+                                   alpha=alpha, alternative=alternative)
+    if alternative == 'larger':
+        two_sided_ci = (0, two_sided_ci[1])
+        assert_allclose(one_sided_ci, two_sided_ci, rtol=1e-12)
+    elif alternative == 'smaller':
+        two_sided_ci = (two_sided_ci[0], np.inf)
+        assert_allclose(one_sided_ci, two_sided_ci, rtol=1e-12)