Skip to content

Fix cumulative argmax/argmin returning window-local indices (GH#11336) - #11412

Closed
C1-BA-B1-F3 wants to merge 1 commit into
pydata:mainfrom
C1-BA-B1-F3:fix-cumulative-argmax-11336
Closed

C1-BA-B1-F3 wants to merge 1 commit into
pydata:mainfrom
C1-BA-B1-F3:fix-cumulative-argmax-11336

Conversation

@C1-BA-B1-F3

Copy link
Copy Markdown
Contributor

Problem

When calling argmax() or argmin() on a cumulative rolling window, the result returns window-local indices (positions within the NaN-padded window) instead of absolute indices in the original array.

For example, with data [1, 2, 1.5, 3.5, 4]:

  • cumulative('time').argmax() returns [4, 4, 3, 4, 4]
  • Expected: [0, 1, 1, 3, 4]

Root Cause

Cumulative rolling constructs a window with NaN padding on the left. argmax/argmin returns the position within this padded window, not the absolute index in the original array.

For example, at position 2:

  • Window: [nan, nan, 1.0, 2.0, 1.5]
  • argmax returns 3 (position of 2.0 in the window)
  • But the absolute index should be 1 (position of 2.0 in the original array)

Fix

The fix detects cumulative rolling (window size == dimension size, not centered) and adjusts the returned indices by subtracting the NaN padding offset:

absolute = local - (window_size - 1 - position)

Regression Tests

  • test_cumulative_argminmax: Tests both argmax and argmin with 1D data
  • test_cumulative_argmax_2d: Tests argmax with 2D data

Closes #11336

When calling argmax/argmin on a cumulative rolling window, the result
returned window-local indices (positions within the NaN-padded window)
instead of absolute indices in the original array.

The fix detects cumulative rolling (window size == dimension size, not
centered) and adjusts the returned indices by subtracting the NaN
padding offset: absolute = local - (window_size - 1 - position).

Regression tests included for 1D and 2D cases.

@headtr1ck headtr1ck left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for working on this! Unfortunately I think this patches the symptom rather than the cause, and it introduces a regression:

Dataset rolling argmax/argmin is broken. _reduce_method is shared by DataArrayRolling and DatasetRolling, but _adjust_argminmax_result only exists on DataArrayRolling:

ds = xr.Dataset({"a": ("t", [1.0, 3, 2, 5])})
ds.rolling(t=2).argmax()
# main: a = [nan, 1, 0, 1]
# this PR: AttributeError: 'DatasetRolling' object has no attribute '_adjust_argminmax_result'

This affects every Dataset.rolling(...).argmax(), not just cumulative ones.

_is_cumulative() is a heuristic. window == size and not center also matches da.rolling(time=da.sizes["time"], min_periods=1), which now returns absolute indices while rolling(time=size - 1) still returns window-local ones. The meaning of the result shouldn't depend on whether the window happens to equal the dimension length.

The root cause isn't specific to cumulative. Rolling argmax/argmin return the position in the padded window (the padding is filled with ∓inf). cumulative is just a rolling window of the full dimension length, so almost the entire window is padding. Regular rolling has the same problem at the edges, e.g. for [1, 2, 1.5, 3.5, 4]:

da.rolling(time=3, center=True, min_periods=1).argmax()
# [2, 1, 2, 2, 1] -> at i=0 the window is [pad, 1, 2], so 2 is the position of the value 2,
# counted from the start of the padded window instead of the data

A fix at the root would be to subtract the left padding from the result in _array_reduce (after all of the numpy/bottleneck/numbagg/dask paths), i.e. return the position within the valid part of the window. Interior windows are unchanged, edge windows no longer point into the padding, and cumulative naturally returns absolute indices since its window always starts at 0, without having to detect it. It also works for Dataset and center=True.

Multiple rolling dimensions already fail on main (da.cumulative(["x", "y"]).argmax() raises TypeError: 'tuple' object cannot be interpreted as an integer), so the loop over self.dim here is never exercised; test_cumulative_argmax_2d only rolls over a single dimension.

Finally, what #11336 actually asks for ("fetch the time for the largest value") is idxmax, which rolling objects don't have. Adding idxmin/idxmax to rolling (absolute index → coordinate label) would give users an unambiguous way to get labels without changing the meaning of argmax. That could be a follow-up though.

@headtr1ck

Copy link
Copy Markdown
Collaborator

superseeded by #11696

@headtr1ck headtr1ck closed this Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cumulate+argmax uses padded index instead of absolute index

2 participants