Repository: scipy/scipy

Issue:
Unexpected Result in factorial function with NaN input
<!-- 
Thank you for taking the time to file a bug report. 
Please fill in the fields below, deleting the sections that 
don't apply to your issue. You can view the final output
by clicking the preview button above.
Note: This is a comment, and won't appear in the output.
-->

When the argument to the factorial function is NaN, the result is
zero. Dropping the NaN seems strange. Perhaps it should be propagated instead?

#### Reproducing code example:
<!-- 
If you place your code between the triple backticks below, 
it will be rendered as a code block. 
-->

```
Python 3.6.8 (default, Aug 20 2019, 17:12:48) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from scipy import special
>>> import numpy as np
>>> special.factorial(np.nan)
array(0.)
```


#### Scipy/Numpy/Python version information:
1.2.0 1.15.4 sys.version_info(major=3, minor=6, micro=8, releaselevel='final', serial=0)
<!-- You can simply run the following and paste the result in a code block
```
import sys, scipy, numpy; print(scipy.__version__, numpy.__version__, sys.version_info)
```
-->


Discussion:
@jacksonvanover Can you update your SciPy version to 1.3.1 and see if you still see this behavior? 

I reran your example on my dev branch and I see the `nan` propagate:
```
[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from scipy import special
>>> import numpy as np
>>> special.factorial(np.nan)
nan
>>> import scipy
>>> scipy.__version__
'1.4.0.dev0+f07eed9'
```
Note version 1.4.0 is ***not*** released yet. I think we are shooting for a mid December 2019 release.
@rlucas7, that behavior appears to be a consequence of https://github.com/scipy/scipy/pull/10954, which changed how scalars are handled.  If you pass an array-like to `factorial`, `nan`s in the input result in 0 in the output:

```
In [7]: import scipy                                                                                                                  

In [8]: scipy.__version__                                                                                                             
Out[8]: '1.4.0.dev0+b40d93e'

In [9]: from scipy.special import factorial                                                                                           

In [10]: factorial([np.nan])                                                                                                          
Out[10]: array([0.])
```
@WarrenWeckesser thanks for pointing this out. Looks like I'm ahead of that commit (`558ec14`) too so the issue was transmogrified from scalar to array-likes :). Should the behavior here be to propogate? I'm less familiar with the behavior of `scipy.special` handling of `nan`. I'm not sure if there is a `nan_policy` concept similar to `scipy.stats`, if default behavior would be propagate, throw error, etc. 
No NaN policies-inputs in the array that enter as NaN should remain NaN.
I agree with @person142, just propagate the `nan`s. 
Thanks for clarifying guys. I'm finishing up a patch to close:
https://github.com/scipy/scipy/issues/7058
after that is up I can take a look at a fix for this one. 
Probably won't be for another week or 2 though. 

Unless @jacksonvanover is interested in opening a PR to fix? 

Hi all! Thanks for the confirmation/discussion. If I find the time in the coming week, I will take a look.

Resolving patch:
diff --git a/scipy/special/_basic.py b/scipy/special/_basic.py
--- a/scipy/special/_basic.py
+++ b/scipy/special/_basic.py
@@ -2323,13 +2323,17 @@ def factorial(n, exact=False):
     """
     if exact:
         if np.ndim(n) == 0:
+            if np.isnan(n):
+                return n
             return 0 if n < 0 else math.factorial(n)
         else:
             n = asarray(n)
             un = np.unique(n).astype(object)
 
             # Convert to object array of long ints if np.int can't handle size
-            if un[-1] > 20:
+            if np.isnan(n).any():
+                dt = float
+            elif un[-1] > 20:
                 dt = object
             elif un[-1] > 12:
                 dt = np.int64
@@ -2339,9 +2343,11 @@ def factorial(n, exact=False):
             out = np.empty_like(n, dtype=dt)
 
             # Handle invalid/trivial values
-            un = un[un > 1]
-            out[n < 2] = 1
-            out[n < 0] = 0
+            # Ignore runtime warning when less operator used w/np.nan
+            with np.errstate(all='ignore'):
+                un = un[un > 1]
+                out[n < 2] = 1
+                out[n < 0] = 0
 
             # Calculate products of each range of numbers
             if un.size:
@@ -2352,14 +2358,14 @@ def factorial(n, exact=False):
                     current = un[i + 1]
                     val *= _range_prod(prev, current)
                     out[n == current] = val
+
+            if np.isnan(n).any():
+                out = out.astype(np.float64)
+                out[np.isnan(n)] = n[np.isnan(n)]
             return out
     else:
-        if np.ndim(n) == 0:
-            return 0 if n < 0 else gamma(n + 1)
-
-        n = asarray(n)
-        vals = gamma(n + 1)
-        return where(n >= 0, vals, 0)
+        out = ufuncs._factorial(n)
+        return out
 
 
 def factorial2(n, exact=False):
diff --git a/scipy/special/add_newdocs.py b/scipy/special/add_newdocs.py
--- a/scipy/special/add_newdocs.py
+++ b/scipy/special/add_newdocs.py
@@ -9463,3 +9463,8 @@ def add_newdoc(name, doc):
     .. [1] M. Patefield and D. Tandy, "Fast and accurate calculation of
            Owen's T Function", Statistical Software vol. 5, pp. 1-25, 2000.
     """)
+
+add_newdoc("_factorial",
+    """
+    Internal function, do not use.
+    """)


Test patch:
diff --git a/scipy/special/tests/test_basic.py b/scipy/special/tests/test_basic.py
--- a/scipy/special/tests/test_basic.py
+++ b/scipy/special/tests/test_basic.py
@@ -1820,6 +1820,23 @@ def test_factorialk(self):
         assert_equal(special.factorialk(5, 1, exact=True), 120)
         assert_equal(special.factorialk(5, 3, exact=True), 10)
 
+    @pytest.mark.parametrize('x, exact', [
+        (np.nan, True),
+        (np.nan, False),
+        (np.array([np.nan]), True),
+        (np.array([np.nan]), False),
+    ])
+    def test_nan_inputs(self, x, exact):
+        result = special.factorial(x, exact=exact)
+        assert_(np.isnan(result))
+
+    def test_mixed_nan_inputs(self):
+        x = np.array([np.nan, 1, 2, 3, np.nan])
+        result = special.factorial(x, exact=True)
+        assert_equal(np.array([np.nan, 1, 2, 6, np.nan]), result)
+        result = special.factorial(x, exact=False)
+        assert_equal(np.array([np.nan, 1, 2, 6, np.nan]), result)
+
 
 class TestFresnel(object):
     def test_fresnel(self):