Numpy
Python
Cumsum
array manipulation
custom functions

Numpy custom Cumsum function with upper/lower limits?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

numpy.cumsum is great when you need running totals, but many real pipelines need guard rails. Risk models, signal processing, and game scoring often require a running sum that never goes below a floor or above a ceiling. This article shows a practical way to build a bounded cumulative sum that is fast, predictable, and easy to test.

Designing a Bounded Cumulative Sum

A normal cumulative sum on a one dimensional array is straightforward. The bounded version adds one policy decision: when the running value crosses a limit, clamp it. A clean implementation keeps three things explicit.

  • Inputs should be array-like and converted to a NumPy array early.
  • Limits can be optional, but at least one should be set for bounded behavior.
  • Output dtype should be chosen intentionally to avoid integer overflow.
python
1import numpy as np
2
3
4def bounded_cumsum_1d(values, lower=None, upper=None, dtype=np.float64):
5    arr = np.asarray(values, dtype=dtype)
6    run = np.cumsum(arr)
7
8    if lower is not None or upper is not None:
9        low = -np.inf if lower is None else lower
10        high = np.inf if upper is None else upper
11        run = np.clip(run, low, high)
12
13    return run
14
15
16x = np.array([4, -2, 5, -10, 3])
17print(bounded_cumsum_1d(x))
18print(bounded_cumsum_1d(x, lower=0, upper=6))

Expected output:

text
[ 4.  2.  7. -3.  0.]
[4. 2. 6. 0. 0.]

This implementation applies clamping after each cumulative step because each position in run is the running total at that index. That behavior is usually what people mean by bounded cumulative sum.

A Vectorized Axis-Aware Version

Many arrays are two dimensional or higher. In that case, you often need row-wise or column-wise running totals. The function below supports axis while keeping the same limit logic.

python
1import numpy as np
2
3
4def bounded_cumsum(values, axis=None, lower=None, upper=None, dtype=np.float64):
5    arr = np.asarray(values, dtype=dtype)
6    run = np.cumsum(arr, axis=axis)
7
8    if lower is None and upper is None:
9        return run
10
11    low = -np.inf if lower is None else lower
12    high = np.inf if upper is None else upper
13    return np.clip(run, low, high)
14
15
16m = np.array([
17    [2, 3, -8, 4],
18    [1, -1, 2, 2],
19], dtype=np.float64)
20
21print('Row-wise')
22print(bounded_cumsum(m, axis=1, lower=0, upper=5))
23
24print('Column-wise')
25print(bounded_cumsum(m, axis=0, lower=-2, upper=4))

Why vectorized code matters:

  • It avoids Python loops, which keeps performance strong on large arrays.
  • It behaves consistently across dimensions.
  • It is easy to compose with other NumPy operations in data pipelines.

When You Need Stateful Bounds Instead of Simple Clipping

There is an important distinction between clipping cumulative output and updating a stateful accumulator with bounds at each step. For many use cases these match, but not always. If your business logic says each next step starts from the already clamped value, write it explicitly.

python
1import numpy as np
2
3
4def bounded_accumulator(values, lower=0.0, upper=10.0):
5    arr = np.asarray(values, dtype=np.float64)
6    out = np.empty_like(arr)
7    acc = 0.0
8
9    for i, v in enumerate(arr):
10        acc = np.clip(acc + v, lower, upper)
11        out[i] = acc
12
13    return out
14
15
16x = np.array([8, 8, -20, 5, 5])
17print(bounded_accumulator(x, lower=0, upper=10))

This stateful approach is common in inventory caps, stamina meters, and battery simulation. It is loop based, but still clear and usually fast enough for medium-size inputs.

Common Pitfalls

The first pitfall is silently using integer dtypes for large sums. int32 can overflow before clipping runs, so use float64 or int64 when values can grow. The second pitfall is forgetting axis semantics on multidimensional input, which can make output look correct while encoding the wrong business meaning. The third pitfall is mixing None, nan, and numeric limits without a policy. Choose one convention and validate inputs up front. Another frequent issue is assuming clip based logic and stateful bound logic are identical. They are close but not always equivalent for domain rules that depend on previous clamped state.

Summary

  • Use np.cumsum plus np.clip for a fast bounded running total.
  • Pick dtype intentionally to avoid overflow and surprising truncation.
  • For multidimensional arrays, define axis based on domain meaning.
  • Use a loop based accumulator when each step must start from an already bounded state.
  • Add unit tests that cover negative values, missing bounds, and boundary equality cases.

Course illustration
Course illustration

All Rights Reserved.