Repository: scipy/scipy

Issue:
BUG: gamma distribution's inverse survival function overflows for very small input
`gamma.isf` overflows when `q` is small

#### Reproducing code example:
```ipynb
In [41]: gamma.isf(1e-17, 0.864)
Out[41]: inf

In [42]: gamma.isf(1e-17, 1)
Out[42]: inf

In [43]: gamma.isf(1e-17, 5)
Out[43]: inf

In [44]: gamma.isf(1e-17, 100)
Out[44]: inf
```

By comparison, this is the correct output produced by R:
```R
> qgamma(1e-17, 0.864, lower.tail=FALSE)
[1] 38.54902
> qgamma(1e-17, 1, lower.tail=FALSE)
[1] 39.14395
> qgamma(1e-17, 5, lower.tail=FALSE)
[1] 51.83694
> qgamma(1e-17, 100, lower.tail=FALSE)
[1] 210.006

```

I discovered this because it became a blocker in a PR i'm working on for improving the `invgauss` distribution.

#### Scipy/Numpy/Python version information:
```
1.7.0.dev0+f551178 1.20.1 sys.version_info(major=3, minor=8, micro=6, releaselevel='final', serial=0)

```


Discussion:
@zoj613 does it look like there is an easy fix?
If not, maybe Boost does better, in which case we could use it after gh-13328 merges.
@mdhaber I am not sure there is, I haven't had a thorough look at the implementation. I do know that there exists a paper that presents an algorithm for computing inverses for the normalized incomplete gamma functions that claims to be very accurate. Not too sure if it is already incorporated in scipy. See section 3 here: https://arxiv.org/pdf/1306.1754.pdf

I've implemented the other methods outlined in the paper some time ago, so maybe I could take a stab at this if there isn't a better or faster solution.
It looks like there is no direct implementation for `gamma.isf` method in the gamma_gen class : https://github.com/scipy/scipy/blob/9ebe12790b6a683669916a55fbf8e35deee93e0b/scipy/stats/_continuous_distns.py#L2627-L2745
The gamma distribution contains calls to scipy.special where there is overlap between probability functions (cdf,pdf,etc) and special functions. 

Most of the special functions used in stats come from cephes fork at the moment. 

I’m not clear what isf refers to, could you elaborate? 

-L

> On Mar 9, 2021, at 11:02 AM, zoj613 ***@***.***> wrote:
> 
> ﻿
> It looks like there is no direct implementation for gamma.isf method in the gamma_gen class : https://github.com/scipy/scipy/blob/9ebe12790b6a683669916a55fbf8e35deee93e0b/scipy/stats/_continuous_distns.py#L2627-L2745
> 
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub, or unsubscribe.


@rlucas7 `isf` is the inverse of the survival function. The right tail quantile function of the gamma distribution.
> @rlucas7 `isf` is the inverse of the survival function. The right tail quantile function of the gamma distribution.

Ah right, so I don't think that one is in cephes so I'm guessing we fall back to this:

https://github.com/scipy/scipy/blob/9ebe12790b6a683669916a55fbf8e35deee93e0b/scipy/stats/_distn_infrastructure.py#L1019

that is in `rv_generic` and it does not look like there is anything in `rv_continuous`:

https://github.com/scipy/scipy/blob/master/scipy/stats/_distn_infrastructure.py#L1492




Yes, there doesn't appear to be a function implementing it specifically for the gamma distribution, and as far as im aware there is no corresponding special function for it in the cephes library. I linked a paper in this thread that has an algorithm for the `isf` function. I believe scipy already has the required functions in place for making its implementation painless should someone be willing to contribute.
does this help

```
p = [0.5, 1e-10, 1e-15, 1e-20, 1e-50] 
stats.gamma.isf(p, 2), special.gammainccinv(2, p)
(array([ 1.67834699, 26.33398152, 38.20846876,         inf,         inf]),
 array([  1.67834699,  26.33398161,  38.20764823,  49.98319799,
        119.92442038]))
```
> does this help
> 
> ```
> p = [0.5, 1e-10, 1e-15, 1e-20, 1e-50] 
> stats.gamma.isf(p, 2), special.gammainccinv(2, p)
> (array([ 1.67834699, 26.33398152, 38.20846876,         inf,         inf]),
>  array([  1.67834699,  26.33398161,  38.20764823,  49.98319799,
>         119.92442038]))
> ```

It does! I had no idea no idea those inverse functions existed in scipy. Thank you. I wonder why they aren't used to compute the `isf` and `ppf` of the distribution.

Resolving patch:
diff --git a/scipy/stats/_continuous_distns.py b/scipy/stats/_continuous_distns.py
--- a/scipy/stats/_continuous_distns.py
+++ b/scipy/stats/_continuous_distns.py
@@ -2644,6 +2644,9 @@ def _sf(self, x, a):
     def _ppf(self, q, a):
         return sc.gammaincinv(a, q)
 
+    def _isf(self, q, a):
+        return sc.gammainccinv(a, q)
+
     def _stats(self, a):
         return a, a, 2.0/np.sqrt(a), 6.0/a
 


Test patch:
diff --git a/scipy/stats/tests/test_distributions.py b/scipy/stats/tests/test_distributions.py
--- a/scipy/stats/tests/test_distributions.py
+++ b/scipy/stats/tests/test_distributions.py
@@ -2649,6 +2649,28 @@ def test_fit_bad_keyword_args(self):
         x = [0.1, 0.5, 0.6]
         assert_raises(TypeError, stats.gamma.fit, x, floc=0, plate="shrimp")
 
+    def test_isf(self):
+        # Test cases for when the probability is very small. See gh-13664.
+        # The expected values can be checked with mpmath.  With mpmath,
+        # the survival function sf(x, k) can be computed as
+        #
+        #     mpmath.gammainc(k, x, mpmath.inf, regularized=True)
+        #
+        # Here we have:
+        #
+        # >>> mpmath.mp.dps = 60
+        # >>> float(mpmath.gammainc(1, 39.14394658089878, mpmath.inf,
+        # ...                       regularized=True))
+        # 9.99999999999999e-18
+        # >>> float(mpmath.gammainc(100, 330.6557590436547, mpmath.inf,
+        #                           regularized=True))
+        # 1.000000000000028e-50
+        #
+        assert np.isclose(stats.gamma.isf(1e-17, 1),
+                          39.14394658089878, atol=1e-14)
+        assert np.isclose(stats.gamma.isf(1e-50, 100),
+                          330.6557590436547, atol=1e-13)
+
 
 class TestChi2(object):
     # regression tests after precision improvements, ticket:1041, not verified