Repository: scipy/scipy

Issue:
BUG: fix stats.rice.pdf
The implementation of stats.rice.pdf was completely wrong; it
did not correspond at all to the equation.  Now it does.  The
calculation has to be done in pieces in order to handle the
potential overflow of the modified Bessel function with large
arguments.



Discussion:
Uhm, why was is completely wrong? I thought `(x-b)**2 / 2 + xb == (x**2 + b**2)/2`. The PDF was written in this particular form specifically to improve numerical stability at large arguments, do you have a case where it fails with master?

https://github.com/scipy/scipy/pull/2845 --- much messier than it should be, ignore vonmises changes.

@ev-br Did you try plotting the pdf and comparing the result to http://en.wikipedia.org/wiki/Rice_distribution?  

There are two errors in the version prior to my changeset, and introduced by #2845: first, the switch to `(x-b)**2` is missing the addition of `2xb` that would make it correct; and second, the change from `i0` to `i0e` is missing a factor of `exp(x)`.   

@efiring  the -xb part is included in `i0e(xb)`, AFAICS

If I copy paste correctly, the master and this PR version give the same result for simple cases

```
>>> x = np.arange(5.); b=1; x * np.exp(-(x-b)*(x-b)/2.0) * special.i0e(x*b)
array([ 0.        ,  0.46575961,  0.37423951,  0.09865957,  0.00919833])
>>> x * np.exp(-(x**2 + b**2)/2.0) * special.i0(x*b)
array([ 0.        ,  0.46575961,  0.37423951,  0.09865957,  0.00919833])

>>> x = np.arange(5.); b=2; x * np.exp(-(x-b)*(x-b)/2.0) * special.i0e(x*b)
array([ 0.        ,  0.18711976,  0.41400384,  0.30324853,  0.07764552])
>>> x * np.exp(-(x**2 + b**2)/2.0) * special.i0(x*b)
array([ 0.        ,  0.18711976,  0.41400384,  0.30324853,  0.07764552])
```

@ev-br, @josef-pkt you are correct; I don't quite know how I managed to get so confused, but I did, and I apologize for the noise as well as the erroneous accusation that you had made the code incorrect.  (A comment in the pdf code would have been nice, though; it is not obvious that it corresponds to the analytic form.)  I do know that I went down this rabbit hole when I got strange results from trying to calculate the ppf; I will go back to that, but I suspect I was making an error in the arguments.  
The code does start with a FIXME warning about the ppf? is that still valid?  Is there a problem?

@efiring  No worries;  I've done some head-scratching myself over scipy's formula for the Rice PDF in the past, but eventually found it to be correct.  Even though it turned out to not be a bug, thanks for working on fixing it!

@efiring can you suggest some wording for the comment in PDF so that it'd make it clearer?
 Checking the ppf would be nice, I am only not quite sure how to check it conclusively.

How about

```
# For numerical stability, we use (x-b)**2 / 2 + xb == (x**2 + b**2)/2.
# The factor of exp(-xb) is then included in the i0e function in place of
# the modified Bessel function, i0. 
```

I found that my original difficulties were all triggered by confusion over the use of args and kwargs: b versus scale; order of args in ppf; etc.  Then as usual things got worse because I was not sufficiently systematic and careful when investigating.
Scipy has some of the same sorts of documentation problems as matplotlib: automation can leave behind inappropriate kwargs (e.g. `loc` in the case of rice, and things like `agg_filter` among the vast number in the case of matplotlib).  There is no easy solution to this.
Regarding the ppf: If I understand correctly, it is being calculated numerically from the cdf, which is in turn being calculated numerically from the pdf.  It's slow, but it's hard to see how it could go seriously wrong, so I think the ppf is OK, and the "FIXME" comment is misleading.

ppf is not defined for `rice` 

(worth looking into, browsing Wikipedia: 
rice cdf is defined in terms of Marcum-Q-function, which is also used for the non-central chisquare distribution, ncx2, for which we have the `special` functions.
also rice has support for positive numbers, so R**2 is a monotone transformation (R>=0),  so we can get cdf and ppf for `rice` from the ones of the `ncx2`
`If R \sim \text{Rice}\left(\nu,1\right) then R^2 has a noncentral chi-squared distribution with two degrees of freedom and noncentrality parameter \nu^2.`
)

@efiring  how about putting this into a PR? I'd be happy to press the green button on this (which I can do from a phone, unlike an actual PR). IIRC, i0e was used for stability, and then the exponential was rewritten to account for the extra exponential.

Regarding the documentation: `loc`  is certainly a valid parameter for rice! There are several PRs for the docs in the pipeline ( eg, https://github.com/scipy/scipy/pull/4559) and it's been tweaked recently, so that it goes on top of http://docs.scipy.org/doc/scipy-dev/reference/stats.html
Input's appreciated!

I would be happy to help, but first I have to get my confusion cleared up.  Right now I am struggling to understand how to apply rice; it seems like it should be straightforward, but I haven't been able to make anything come out in a way that makes sense to me.  The context is the distribution of amplitude for a vector with components `(c, s)`, and with independent normal noise added with standard deviation `sigma` for each component.  Suppose I have a grid of x values starting from zero, and I want the pdf (or cdf) on that grid.  Shouldn't the `b` parameter be `np.hypot(c, s)`?  And the kwarg be  `scale=sigma`?  Then for a value of `b=4`, for example, and `scale=1` (default), I get the expected pdf or cdf curve, with the peak near 4.  If I then make `scale=0.1`, I expect to get a peak in the same place, but only 1/10th as wide, so 10x as high. Instead, I get the narrow peak, but scooted way over to the left.  So the scale kwarg is not acting as I expect, based on what I have read in the docs and have seen with other distributions.

If a distribution is fundamentally inconsistent with the generic handling of loc and scale, then it should take all the parameters it needs in some other way; it looks to me like rice needs two parameters, b and sigma, not just the one.

> If I then make `scale=0.1`, I expect to get a peak in the same place

For the normal distribution this would be true, but I don't think this is generally true for distributions on [0, infinity].  Also, I don't think this means the distribution is fundamentally inconsistent with the generic handling of loc and scale.

I think the trick here is that if scale is not 1, then the b parameter needs to be divided by the scale.  The user should not have to figure this out; ideally, it should happen automatically, or at the very least it should be prominent in the documentation and examples. 
Note that I am looking at this from a user's standpoint (and from the standpoint of one teaching to new users).  So the handling of loc and scale might be perfectly consistent from a programming standpoint--they are just a shift and scale applied to `x`--but they are not consistent from the standpoint of user trying to understand how to get a given calculation to work correctly.  If one looks up the Rice distribution in Wolfram or Wikipedia, one finds that x and b are scaled by sigma; it's a surprise to discover, via painful trial and error, that the `scale` kwarg applies to one but not the other.  

This sounds like a candidate for https://github.com/scipy/scipy/issues/4538.


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
@@ -1002,7 +1002,7 @@ class exponnorm_gen(rv_continuous):
     The probability density function for `exponnorm` is::
 
         exponnorm.pdf(x, K) = 1/(2*K) exp(1/(2 * K**2)) exp(-x / K) * erfc(- (x - 1/K) / sqrt(2))
-           
+
     where the shape parameter ``K > 0``.
 
     It can be thought of as the sum of a normally distributed random
@@ -1032,7 +1032,7 @@ def _logpdf(self, x, K):
         invK = 1.0 / K
         exparg = 0.5 * invK**2 - invK * x
         return exparg + log(0.5 * invK * erfc(-(x - invK) / sqrt(2)))
-    
+
     def _cdf(self, x, K):
         invK = 1.0 / K
         expval = invK * (0.5 * invK - x)
@@ -3702,10 +3702,10 @@ def _cdf(self, r):
 
     def _ppf(self, q):
         return sqrt(-2 * special.log1p(-q))
-        
+
     def _sf(self, r):
         return exp(-0.5 * r**2)
-        
+
     def _isf(self, q):
         return sqrt(-2 * log(q))
 
@@ -3762,8 +3762,7 @@ def _entropy(self, a, b):
         return 0.5*log(a*b)+log(log(b/a))
 reciprocal = reciprocal_gen(name="reciprocal")
 
-
-# FIXME: PPF does not work.
+# FIXME: add _cdf, _ppf methods?
 class rice_gen(rv_continuous):
     """A Rice continuous random variable.
 
@@ -3779,6 +3778,12 @@ class rice_gen(rv_continuous):
 
     `rice` takes ``b`` as a shape parameter.
 
+    The Rice distribution describes the length, ``r``, of a 2-D vector
+    with components ``(U+u, V+v)``, where ``U, V`` are constant, ``u, v``
+    are independent Gaussian random variables with standard deviation
+    ``s``.  Let ``R = (U**2 + V**2)**0.5``. Then the pdf of ``r`` is
+    ``rice.pdf(x, R/s, scale=s)``.
+
     %(example)s
 
     """
@@ -3792,6 +3797,10 @@ def _rvs(self, b):
         return np.sqrt((t*t).sum(axis=0))
 
     def _pdf(self, x, b):
+        # We use (x**2 + b**2)/2 = ((x-b)**2)/2 + xb.
+        # The factor of exp(-xb) is then included in the i0e function
+        # in place of the modified Bessel function, i0, improving
+        # numerical stability for large values of xb.
         return x * exp(-(x-b)*(x-b)/2.0) * special.i0e(x*b)
 
     def _munp(self, n, b):


Test patch: