Repository: cupy/cupy

Issue:
Support `dtype` argument in `cupy.corrcoef` and `cupy.cov`
### Description

https://numpy.org/doc/stable/reference/generated/numpy.corrcoef.html

https://docs.cupy.dev/en/stable/reference/generated/cupy.corrcoef.html

Seems args are different

### Additional Information

`dtype` argument added in NumPy version 1.20.


Discussion:
Hi,

I have been looking at this issue a bit. Corrcoef is just a call to the cupy.cov method, so this would require the added argument of dtype to the cupy.cov method, is that correct? If so, I would like to work on this.
@BasLaa yes, please go ahead!
Will do!
If you are going to add `dtype` to cov, can you look into adding the other missing kwargs `fweights` and `aweights`?
https://docs.cupy.dev/en/stable/reference/generated/cupy.cov.html
https://numpy.org/doc/stable/reference/generated/numpy.cov.html

Thanks!
Yep, noticed that as well. Am gunna work on it over the weekend. I'll keep you updated

Resolving patch:
diff --git a/cupy/_statistics/correlation.py b/cupy/_statistics/correlation.py
--- a/cupy/_statistics/correlation.py
+++ b/cupy/_statistics/correlation.py
@@ -7,7 +7,7 @@
 from cupy import _core
 
 
-def corrcoef(a, y=None, rowvar=True, bias=None, ddof=None):
+def corrcoef(a, y=None, rowvar=True, bias=None, ddof=None, *, dtype=None):
     """Returns the Pearson product-moment correlation coefficients of an array.
 
     Args:
@@ -19,6 +19,8 @@ def corrcoef(a, y=None, rowvar=True, bias=None, ddof=None):
             transposed.
         bias (None): Has no effect, do not use.
         ddof (None): Has no effect, do not use.
+        dtype: Data type specifier. By default, the return data-type will have
+            at least `numpy.float64` precision.
 
     Returns:
         cupy.ndarray: The Pearson product-moment correlation coefficients of
@@ -31,7 +33,7 @@ def corrcoef(a, y=None, rowvar=True, bias=None, ddof=None):
         warnings.warn('bias and ddof have no effect and are deprecated',
                       DeprecationWarning)
 
-    out = cov(a, y, rowvar)
+    out = cov(a, y, rowvar, dtype=dtype)
     try:
         d = cupy.diag(out)
     except ValueError:
@@ -78,7 +80,8 @@ def correlate(a, v, mode='valid'):
     return out
 
 
-def cov(a, y=None, rowvar=True, bias=False, ddof=None):
+def cov(a, y=None, rowvar=True, bias=False, ddof=None,
+        fweights=None, aweights=None, *, dtype=None):
     """Returns the covariance matrix of an array.
 
     This function currently does not support ``fweights`` and ``aweights``
@@ -97,6 +100,20 @@ def cov(a, y=None, rowvar=True, bias=False, ddof=None):
             overridden. Note that ``ddof=1`` will return the unbiased estimate
             and ``ddof=0`` will return the simple average.
 
+        fweights (cupy.ndarray, int): 1-D array of integer frequency weights.
+            the number of times each observation vector should be repeated.
+            It is required that fweights >= 0. However, the function will not
+            error when fweights < 0 for performance reasons.
+        aweights (cupy.ndarray): 1-D array of observation vector weights.
+            These relative weights are typically large for observations
+            considered "important" and smaller for observations considered
+            less "important". If ``ddof=0`` the array of weights can be used
+            to assign probabilities to observation vectors.
+            It is required that aweights >= 0. However, the function will not
+            error when aweights < 0 for performance reasons.
+        dtype: Data type specifier. By default, the return data-type will have
+            at least `numpy.float64` precision.
+
     Returns:
         cupy.ndarray: The covariance matrix of the input array.
 
@@ -109,13 +126,16 @@ def cov(a, y=None, rowvar=True, bias=False, ddof=None):
     if a.ndim > 2:
         raise ValueError('Input must be <= 2-d')
 
-    if y is None:
-        dtype = numpy.promote_types(a.dtype, numpy.float64)
-    else:
-        if y.ndim > 2:
-            raise ValueError('y must be <= 2-d')
-        dtype = functools.reduce(numpy.promote_types,
-                                 (a.dtype, y.dtype, numpy.float64))
+    if dtype is None:
+        if y is None:
+            dtype = numpy.promote_types(a.dtype, numpy.float64)
+        else:
+            if y.ndim > 2:
+                raise ValueError('y must be <= 2-d')
+            dtype = functools.reduce(
+                numpy.promote_types,
+                (a.dtype, y.dtype, numpy.float64)
+            )
 
     X = cupy.array(a, ndmin=2, dtype=dtype)
     if not rowvar and X.shape[0] != 1:
@@ -131,13 +151,62 @@ def cov(a, y=None, rowvar=True, bias=False, ddof=None):
     if ddof is None:
         ddof = 0 if bias else 1
 
-    fact = X.shape[1] - ddof
+    w = None
+    if fweights is not None:
+        if not isinstance(fweights, cupy.ndarray):
+            raise TypeError(
+                "fweights must be a cupy.ndarray")
+        if fweights.dtype.char not in 'bBhHiIlLqQ':
+            raise TypeError(
+                "fweights must be integer")
+        fweights = fweights.astype(dtype=float)
+        if fweights.ndim > 1:
+            raise RuntimeError(
+                "cannot handle multidimensional fweights")
+        if fweights.shape[0] != X.shape[1]:
+            raise RuntimeError(
+                "incompatible numbers of samples and fweights")
+        w = fweights
+
+    if aweights is not None:
+        if not isinstance(fweights, cupy.ndarray):
+            raise TypeError(
+                "aweights must be a cupy.ndarray")
+        aweights = aweights.astype(dtype=float)
+        if aweights.ndim > 1:
+            raise RuntimeError(
+                "cannot handle multidimensional aweights")
+        if aweights.shape[0] != X.shape[1]:
+            raise RuntimeError(
+                "incompatible numbers of samples and aweights")
+        if w is None:
+            w = aweights
+        else:
+            w *= aweights
+
+    avg, w_sum = cupy.average(X, axis=1, weights=w, returned=True)
+    w_sum = w_sum[0]
+
+    # Determine the normalization
+    if w is None:
+        fact = X.shape[1] - ddof
+    elif ddof == 0:
+        fact = w_sum
+    elif aweights is None:
+        fact = w_sum - ddof
+    else:
+        fact = w_sum - ddof * sum(w*aweights) / w_sum
+
     if fact <= 0:
         warnings.warn('Degrees of freedom <= 0 for slice',
                       RuntimeWarning, stacklevel=2)
         fact = 0.0
 
     X -= X.mean(axis=1)[:, None]
-    out = X.dot(X.T.conj()) * (1 / cupy.float64(fact))
+    if w is None:
+        X_T = X.T
+    else:
+        X_T = (X * w).T
+    out = X.dot(X_T.conj()) * (1 / cupy.float64(fact))
 
     return out.squeeze()


Test patch:
diff --git a/tests/cupy_tests/statistics_tests/test_correlation.py b/tests/cupy_tests/statistics_tests/test_correlation.py
--- a/tests/cupy_tests/statistics_tests/test_correlation.py
+++ b/tests/cupy_tests/statistics_tests/test_correlation.py
@@ -36,6 +36,14 @@ def test_corrcoef_rowvar(self, xp, dtype):
         y = testing.shaped_arange((2, 3), xp, dtype)
         return xp.corrcoef(a, y=y, rowvar=False)
 
+    @testing.with_requires('numpy>=1.20')
+    @testing.for_all_dtypes()
+    @testing.numpy_cupy_allclose(accept_error=True)
+    def test_corrcoef_dtype(self, xp, dtype):
+        a = testing.shaped_arange((2, 3), xp, dtype)
+        y = testing.shaped_arange((2, 3), xp, dtype)
+        return xp.corrcoef(a, y=y, dtype=dtype)
+
 
 @testing.gpu
 class TestCov(unittest.TestCase):
@@ -48,27 +56,38 @@ def generate_input(self, a_shape, y_shape, xp, dtype):
         return a, y
 
     @testing.for_all_dtypes()
-    @testing.numpy_cupy_allclose()
+    @testing.numpy_cupy_allclose(accept_error=True)
     def check(self, a_shape, y_shape=None, rowvar=True, bias=False,
-              ddof=None, xp=None, dtype=None):
+              ddof=None, xp=None, dtype=None,
+              fweights=None, aweights=None, name=None):
         a, y = self.generate_input(a_shape, y_shape, xp, dtype)
-        return xp.cov(a, y, rowvar, bias, ddof)
+        if fweights is not None:
+            fweights = name.asarray(fweights)
+        if aweights is not None:
+            aweights = name.asarray(aweights)
+        # print(type(fweights))
+        return xp.cov(a, y, rowvar, bias, ddof,
+                      fweights, aweights, dtype=dtype)
 
     @testing.for_all_dtypes()
-    @testing.numpy_cupy_allclose()
+    @testing.numpy_cupy_allclose(accept_error=True)
     def check_warns(self, a_shape, y_shape=None, rowvar=True, bias=False,
-                    ddof=None, xp=None, dtype=None):
+                    ddof=None, xp=None, dtype=None,
+                    fweights=None, aweights=None):
         with testing.assert_warns(RuntimeWarning):
             a, y = self.generate_input(a_shape, y_shape, xp, dtype)
-            return xp.cov(a, y, rowvar, bias, ddof)
+            return xp.cov(a, y, rowvar, bias, ddof,
+                          fweights, aweights, dtype=dtype)
 
     @testing.for_all_dtypes()
-    def check_raises(self, a_shape, y_shape=None, rowvar=True, bias=False,
-                     ddof=None, dtype=None):
+    def check_raises(self, a_shape, y_shape=None,
+                     rowvar=True, bias=False, ddof=None,
+                     dtype=None, fweights=None, aweights=None):
         for xp in (numpy, cupy):
             a, y = self.generate_input(a_shape, y_shape, xp, dtype)
             with pytest.raises(ValueError):
-                xp.cov(a, y, rowvar, bias, ddof)
+                xp.cov(a, y, rowvar, bias, ddof,
+                       fweights, aweights, dtype=dtype)
 
     def test_cov(self):
         self.check((2, 3))
@@ -77,6 +96,12 @@ def test_cov(self):
         self.check((2, 3), (2, 3), rowvar=False)
         self.check((2, 3), bias=True)
         self.check((2, 3), ddof=2)
+        self.check((2, 3))
+        self.check((1, 3), fweights=(1, 4, 1))
+        self.check((1, 3), aweights=(1.0, 4.0, 1.0))
+        self.check((1, 3), bias=True, aweights=(1.0, 4.0, 1.0))
+        self.check((1, 3), fweights=(1, 4, 1),
+                   aweights=(1.0, 4.0, 1.0))
 
     def test_cov_warns(self):
         self.check_warns((2, 3), ddof=3)