2025, Dec 18 23:00

Vectorized NumPy method to compute per-index lengths of increasing runs using cumsum and maximum.accumulate resets

Learn a compact, fully vectorized NumPy approach to compute increasing run lengths per index using a boolean mask, cumsum, and maximum.accumulate resets.

When you have a 1D NumPy array that mostly decreases but occasionally rises, it is often useful to quantify each contiguous increasing segment by its length at every position. The catch is that a straightforward cumulative sum over a boolean mask does not reset when the trend flips back down. Below is a compact, vectorized way to compute the length of every increasing run while resetting the count as soon as the sequence stops increasing, with the result kept in the same shape as the input so it can be plotted or processed further.

Example that frames the task

The goal is an array of the same length that records, for each index, how many consecutive steps up we have seen in the current run so far, and zero elsewhere.

import numpy as np

def rising_run_lengths(x: np.ndarray) -> np.ndarray:
    ...

arr = np.array([9, 8, 7, 9, 6, 5, 6, 7, 8, 4, 3, 1, 2, 3, 0])
res = rising_run_lengths(arr)

print(arr)
print(res)

# >>> [9 8 7 9 6 5 6 7 8 4 3 1 2 3 0]
# >>> [0 0 0 1 0 0 1 2 3 0 0 0 1 2 0]

What is going on under the hood

Think of a boolean mask that marks each position where the array increases relative to the previous element. If you cumulatively sum that mask, you get a running count of increases seen so far. However, that naive count never resets; it just keeps growing across the whole array. The trick is to subtract, for each increasing block, the cumulative count right before that block starts. To do that in a vectorized way, take a cumulative maximum of the naive counts at positions that are not increasing. This yields the most recent “baseline” to subtract, effectively resetting the accumulation exactly when the sequence stops rising.

Solution and working code

The following implementation computes a boolean mask of increases, a naive cumulative sum over that mask, and then removes the stale prefix counts via a cumulative maximum taken over the non-increasing indices.

import numpy as np

def rising_run_lengths(x: np.ndarray) -> np.ndarray:
    up = np.diff(x, prepend=x[0]) > 0
    naive = np.cumsum(up)
    resets = np.maximum.accumulate(naive * ~up)
    return naive - resets

arr = np.array([9, 8, 7, 9, 6, 5, 6, 7, 8, 4, 3, 1, 2, 3, 0])
out = rising_run_lengths(arr)

print(arr)  # >>> [9 8 7 9 6 5 6 7 8 4 3 1 2 3 0]
print(out)  # >>> [0 0 0 1 0 0 1 2 3 0 0 0 1 2 0]

The idea is concise and fully vectorized. First, np.diff with prepend builds the “increase” mask aligned to the input. Next, np.cumsum counts every True as one step up. Finally, multiplying the running count by the inverse of the mask keeps only the counts at non-increasing positions, and np.maximum.accumulate propagates the latest such count forward; subtracting it resets each new rising streak to zero.

Readable variant with np.where

If you prefer to make the reset logic a bit more explicit, replace the product with a conditional selection. Functionally it’s the same pattern, just expressed differently.

import numpy as np

def rising_run_lengths_readable(x: np.ndarray) -> np.ndarray:
    up = np.diff(x, prepend=x[0]) > 0
    naive = np.cumsum(up)
    baseline = np.maximum.accumulate(np.where(up, 0, naive))
    return naive - baseline

Why this matters

This pattern gives you a run-length of increasing values at each index while preserving the input shape, which makes visualization straightforward and avoids looping. It also shows a reusable technique: build a cumulative counter and then use a cumulative maximum over a masked view to reset it exactly where needed.

You can also specify integer dtypes for the accumulation steps. Using dtype=np.uint32 for the cumulative operations has been reported to make the computation about 30% faster on one machine, and it is safe for arrays smaller than 4_000_000_000 items. For small arrays, i.e. smaller than 65535 items, using np.uint16 instead was reported to yield around a 60% speed-up.

import numpy as np

def rising_run_lengths_typed(x: np.ndarray) -> np.ndarray:
    up = np.diff(x, prepend=x[0]) > 0
    naive = np.cumsum(up, dtype=np.uint32)
    baseline = np.maximum.accumulate(np.where(up, 0, naive), dtype=np.uint32)
    return (naive - baseline).astype(np.uint32)

Takeaway

When you need the lengths of increasing contiguous sub-arrays in NumPy, compute a boolean “increase” mask, form its cumulative sum, and subtract the propagated baseline obtained via a cumulative maximum over non-increasing positions. This gives a compact, vectorized result aligned with the original array, convenient for plotting and downstream analysis. If performance matters, consider specifying an unsigned integer dtype for the cumulative steps; it can deliver a measurable speed-up for the sizes mentioned above.