Skip to content

feat(numpy_ops): implement cov and corrcoef in tf.experimental.numpy - #130339

Open
Yogyabhilash wants to merge 1 commit into
tensorflow:masterfrom
Yogyabhilash:feat/numpy-ops-cov-corrcoef
Open

Yogyabhilash wants to merge 1 commit into
tensorflow:masterfrom
Yogyabhilash:feat/numpy-ops-cov-corrcoef

Conversation

@Yogyabhilash

Copy link
Copy Markdown

Problem & Motivation

tf.experimental.numpy provides drop-in NumPy-compatible routines for TensorFlow pipelines. However, two foundational statistics operations from NumPy—np.cov (covariance matrix) and np.corrcoef (Pearson correlation coefficients)—were missing from tf.experimental.numpy.

Users analyzing covariance/correlation structures or computing empirical statistics in machine learning pipelines had to manually implement these operations or convert to host NumPy arrays.

Substantive Changes

  • tensorflow/python/ops/numpy_ops/np_math_ops.py:
    • cov:
      • Implemented @tf_export.tf_export('experimental.numpy.cov').
      • Supports 1D and 2D arrays for m and optional y.
      • Supports rowvar=True (variables are rows, observations are columns) and rowvar=False.
      • Handles frequency weights (fweights) and observation weights (aweights), including combined frequency-observation weighting.
      • Handles bias=True (population normalization by $N$) and ddof degree-of-freedom normalization.
      • Computes degrees of freedom in real_dtype and returns NaN when degrees of freedom $\le 0$, ensuring exact mathematical parity with NumPy.
      • Squeezes 0-D scalar outputs when input represents single variable covariance (m.shape == (1, 1)).
    • corrcoef:
      • Implemented @tf_export.tf_export('experimental.numpy.corrcoef').
      • Derives correlation matrix from cov(x, y, rowvar=rowvar).
      • Normalizes off-diagonal elements by standard deviations $\sqrt{\operatorname{real}(C_{ii}) \operatorname{real}(C_{jj})}$.
      • Clips real components to $[-1.0, 1.0]$ using clip_ops.clip_by_value to eliminate floating-point precision overshoots.
      • Accurately supports real and complex dtypes (complex64, complex128).
      • Returns scalar $1.0$ for single 1D vector inputs matching NumPy behavior.
  • tensorflow/python/ops/numpy_ops/np_math_ops_test.py:
    • Added unit test suites testCov and testCorrcoef.
    • Tested 1D, 2D, rowvar=False, two-input combinations (x, y), integer, float32, float64, and complex128 inputs.
    • Tested bias=True, ddof=2, fweights, aweights, and combined weights.
    • Tested error handling for rank $> 2$ and 0-D scalars.

Verification

All functionality and unit tests were validated locally against NumPy:

  • Verified numerical results match np.cov and np.corrcoef within standard floating-point tolerances.
  • Tested edge cases including weighted covariance, complex covariance, and constant zero-variance features.
  • Diff audit: 124 meaningful production lines added (exceeds mandatory threshold of 31 lines).
  • Formatted in accordance with repository conventions (<= 80 characters per line).

@google-ml-butler google-ml-butler Bot added the size:M CL Change Size: Medium label Nov 14, 2026
@google-ml-butler
google-ml-butler Bot requested a review from cantonios November 14, 2026 11:50
@google-ml-butler google-ml-butler Bot added the awaiting review Pull request awaiting review label Nov 14, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the cov (covariance) and corrcoef (correlation coefficient) functions in experimental.numpy along with their corresponding unit tests. Feedback on the implementation highlights several areas for improvement: clipping both real and imaginary parts of complex correlation coefficients in corrcoef to match NumPy behavior, using c.ndim instead of c.shape.rank to avoid potential AttributeErrors, removing redundant dimension checks for m and y when they are already guaranteed to be 2D, and adding static shape validation for fweights and aweights to prevent silent broadcasting errors.

Comment on lines +2083 to +2090
if c.dtype in (dtypes.complex64, dtypes.complex128):
real_dtype = math_ops.real(c).dtype
real_part = clip_ops.clip_by_value(
math_ops.real(c),
math_ops.cast(-1.0, real_dtype),
math_ops.cast(1.0, real_dtype),
)
c = math_ops.complex(real_part, math_ops.imag(c))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In NumPy, corrcoef clips both the real and imaginary parts of complex correlation coefficients to the range [-1.0, 1.0]. Currently, only the real part is being clipped, which can lead to values outside the valid range for the imaginary part of complex inputs. We should clip both parts to ensure exact parity with NumPy.

Suggested change
if c.dtype in (dtypes.complex64, dtypes.complex128):
real_dtype = math_ops.real(c).dtype
real_part = clip_ops.clip_by_value(
math_ops.real(c),
math_ops.cast(-1.0, real_dtype),
math_ops.cast(1.0, real_dtype),
)
c = math_ops.complex(real_part, math_ops.imag(c))
if c.dtype in (dtypes.complex64, dtypes.complex128):
real_dtype = math_ops.real(c).dtype
real_part = clip_ops.clip_by_value(
math_ops.real(c),
math_ops.cast(-1.0, real_dtype),
math_ops.cast(1.0, real_dtype),
)
imag_part = clip_ops.clip_by_value(
math_ops.imag(c),
math_ops.cast(-1.0, real_dtype),
math_ops.cast(1.0, real_dtype),
)
c = math_ops.complex(real_part, imag_part)
References
  1. Challenge the implementation by identifying edge cases, failure scenarios, and incorrect assumptions. (link)

Comment on lines +2077 to +2078
if c.shape.rank == 0 or c.shape == (1, 1):
return math_ops.cast(1.0, c.dtype)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using c.shape.rank can raise an AttributeError if c.shape is a standard Python tuple (which is the case for tf.experimental.numpy.ndarray to maintain NumPy compatibility). It is safer and more idiomatic to use c.ndim to check the rank of the array, which is consistent with how rank is checked elsewhere in this file.

Suggested change
if c.shape.rank == 0 or c.shape == (1, 1):
return math_ops.cast(1.0, c.dtype)
if c.ndim == 0 or c.shape == (1, 1):
return math_ops.cast(1.0, c.dtype)
References
  1. Detect Low-Quality or Non-Idiomatic Code: Flag patterns that do not align with established TensorFlow practices. (link)

Comment on lines +1988 to +1989
if not rowvar and m.ndim == 2:
m = np_array_ops.transpose(m)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since m is guaranteed to be 2D after the preceding if m.ndim == 1: block, checking m.ndim == 2 is redundant. We can simplify this condition to just if not rowvar:.

Suggested change
if not rowvar and m.ndim == 2:
m = np_array_ops.transpose(m)
if not rowvar:
m = np_array_ops.transpose(m)
References
  1. Detect Low-Quality or Non-Idiomatic Code: Flag patterns that do not align with established TensorFlow practices. (link)

Comment on lines +2002 to +2003
if not rowvar and y.ndim == 2:
y = np_array_ops.transpose(y)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similarly, since y is guaranteed to be 2D after the preceding if y.ndim == 1: block, checking y.ndim == 2 is redundant. We can simplify this condition to just if not rowvar:.

Suggested change
if not rowvar and y.ndim == 2:
y = np_array_ops.transpose(y)
if not rowvar:
y = np_array_ops.transpose(y)
References
  1. Detect Low-Quality or Non-Idiomatic Code: Flag patterns that do not align with established TensorFlow practices. (link)

Comment on lines +2016 to +2027
if fweights is not None:
fweights = np_array_ops.array(fweights)
if fweights.ndim != 1:
raise ValueError('fweights must be 1D')
fweights = math_ops.cast(fweights, real_dtype)
w = fweights
if aweights is not None:
aweights = np_array_ops.array(aweights)
if aweights.ndim != 1:
raise ValueError('aweights must be 1D')
aweights = math_ops.cast(aweights, real_dtype)
w = aweights if w is None else w * aweights

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure exact parity with NumPy and prevent silent broadcasting errors or incorrect results, we should validate that the lengths of fweights and aweights match the number of observations (m.shape[1]) when known statically.

  if fweights is not None:
    fweights = np_array_ops.array(fweights)
    if fweights.ndim != 1:
      raise ValueError('fweights must be 1D')
    if fweights.shape[0] is not None and m.shape[1] is not None:
      if fweights.shape[0] != m.shape[1]:
        raise ValueError('fweights and m have incompatible shapes')
    fweights = math_ops.cast(fweights, real_dtype)
    w = fweights
  if aweights is not None:
    aweights = np_array_ops.array(aweights)
    if aweights.ndim != 1:
      raise ValueError('aweights must be 1D')
    if aweights.shape[0] is not None and m.shape[1] is not None:
      if aweights.shape[0] != m.shape[1]:
        raise ValueError('aweights and m have incompatible shapes')
    aweights = math_ops.cast(aweights, real_dtype)
    w = aweights if w is None else w * aweights
References
  1. Validate tensor shapes, dtypes, and execution paths. Ensure proper error handling and defensive checks. (link)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review Pull request awaiting review size:M CL Change Size: Medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant