Repository: scipy/scipy

Issue:
Median-averaging of complex CSDs
<!-- 
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.
-->

`signal.csd` does not median-average complex CSDs correctly because of unexpected behavior of `np.median` for complex-valued arrays (see [the numpy issue](https://github.com/numpy/numpy/issues/12943)). [Line 589 of signal/spectral.py](https://github.com/scipy/scipy/blob/master/scipy/signal/spectral.py#L589) could be modified so that if the CSD is complex, its real and imaginary parts are separately median-averaged.

#### Reproducing code example:
<!-- 
If you place your code between the triple backticks below, 
it will be rendered as a code block. 
-->

```
import numpy as np
from scipy import signal

s = 0.3 * np.random.uniform(size=100000) # white noise
n1 = np.random.uniform(size=100000)
n2 = np.random.uniform(size=100000)
d1 = s + n1
d2 = s + n2
freq, Pss = signal.csd(s, s, nperseg=100) # mean average PSD of s
_, Pssmed = signal.csd(s, s, nperseg=100, average='median') # median average PSD of s
_, P12 = signal.csd(d1, d2, nperseg=100) # mean average CSD of d1 and d2
_, P12med = signal.csd(d1, d2, nperseg=100, average='median') # median average CSD of d1 and d2
```

The real and imaginary parts of `P12` should have similar variances, but in the median-averaged case the variance of the imaginary portion is much too high.

![sig_csd_mean](https://user-images.githubusercontent.com/34320279/89476232-6546a580-d747-11ea-90d0-9bad579d1b1c.png)
![sig_csd_median](https://user-images.githubusercontent.com/34320279/89476236-68419600-d747-11ea-81ea-7d5585fd0d63.png)


#### Scipy/Numpy/Python version information:
<!-- 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)
```
-->

```
import sys, scipy, numpy; print(scipy.__version__, numpy.__version__, sys.version_info)
1.4.1 1.16.2 sys.version_info(major=3, minor=7, micro=3, releaselevel='final', serial=0)
```


Discussion:
@hadrev agreed they should be processed separately (not 100% sure it's correct but seems reasonable to me), would you be up for making a PR for this?

Resolving patch:
diff --git a/scipy/signal/spectral.py b/scipy/signal/spectral.py
--- a/scipy/signal/spectral.py
+++ b/scipy/signal/spectral.py
@@ -505,7 +505,9 @@ def csd(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None, nfft=None,
         Axis along which the CSD is computed for both inputs; the
         default is over the last axis (i.e. ``axis=-1``).
     average : { 'mean', 'median' }, optional
-        Method to use when averaging periodograms. Defaults to 'mean'.
+        Method to use when averaging periodograms. If the spectrum is
+        complex, the average is computed separately for the real and
+        imaginary parts. Defaults to 'mean'.
 
         .. versionadded:: 1.2.0
 
@@ -585,7 +587,13 @@ def csd(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None, nfft=None,
     if len(Pxy.shape) >= 2 and Pxy.size > 0:
         if Pxy.shape[-1] > 1:
             if average == 'median':
-                Pxy = np.median(Pxy, axis=-1) / _median_bias(Pxy.shape[-1])
+                # np.median must be passed real arrays for the desired result
+                if np.iscomplexobj(Pxy):
+                    Pxy = (np.median(np.real(Pxy), axis=-1)
+                           + 1j * np.median(np.imag(Pxy), axis=-1))
+                    Pxy /= _median_bias(Pxy.shape[-1])
+                else:
+                    Pxy = np.median(Pxy, axis=-1) / _median_bias(Pxy.shape[-1])
             elif average == 'mean':
                 Pxy = Pxy.mean(axis=-1)
             else:


Test patch: