2025, Sep 24 17:00

Per-bin minima in NumPy: a vectorized segmented reduction with np.minimum.reduceat (no loops, no padding)

Learn how to compute per-bin minima in NumPy without Python loops or padding. Use np.minimum.reduceat for fast, vectorized segmented reduction on uneven bins.

Per-bin minima in NumPy without loops or padding

Working with a 1‑D array split into m bins of unequal length is a common pattern. Each element is associated with a bin index increasing from 0 to m−1, and the task is to compute the minimum of every bin. Straightforward strategies tend to either loop in Python or create padded intermediates. Both introduce overhead that scales poorly.

Problem setup and a typical attempt

Suppose you already know the contiguous slice for every bin. A direct approach iterates and reduces per slice:

import numpy as np

# data: 1-D vector
data_vec = np.array([...])  # length n

# group_count = m, and group_slices is a list of (lo, hi) pairs
# such that data for each bin is data_vec[lo:hi]

group_count = m
group_slices = [(lo0, hi0), (lo1, hi1), ...]  # non-uniform lengths

min_per_group = np.zeros(group_count)
for pos, (lo, hi) in enumerate(group_slices):
    min_per_group[pos] = np.min(data_vec[lo:hi])

It works, but the loop lives in Python space and pays the cost per bin. An alternative is to pad to a rectangle, reshape, and reduce along an axis, but that requires inventing dummy data and shuffling memory you don’t actually need.

import numpy as np

# data_vec as above
# valid_mask marks real elements among padded slots
# scratch_1d is prefilled with a large sentinel value; same total size as the padded rectangle

scratch_1d[valid_mask] = data_vec
scratch_2d = scratch_1d.reshape((group_count, slot_size))
min_per_group = np.min(scratch_2d, axis=1)

Both approaches either loop or move around artificial values, which results in extra traversals that don’t contribute to the result.

Why this happens

The bins are contiguous segments of the original 1‑D array, but they are not uniform in length. Looping aligns naturally with variable‑length segments, while vectorized reductions favor regular shapes. Padding tries to force irregular data into a uniform grid, but the cost is additional memory and compute. The goal is a vectorized reduction that respects existing segment boundaries without materializing a padded structure.

A vectorized solution with np.minimum.reduceat

NumPy can apply a segmented reduction directly using np.minimum.reduceat. The only requirement is an array of starting offsets for each bin, of length m, where the first start is 0. With those offsets, the reduction walks the original array once, computing the per‑segment minimums without loops or padding.

import numpy as np

# data_vec: the original 1-D array (length n)
# start_idx: 1-D array of length m with the begin index of each bin; start_idx[0] must be 0

min_per_group = np.minimum.reduceat(data_vec, start_idx)

This leverages the fact that the associated bin indices increase from 0 to m−1, so elements for each bin form contiguous slices. The reduction starts at every position listed in start_idx and computes minima per segment in a vectorized manner.

Why it’s worth knowing

This approach removes Python loops and avoids fabricating dummy elements. You keep memory traffic to what’s necessary, reduce complexity, and stay in NumPy’s optimized execution path. For large n and many uneven bins, that difference is often the line between a quick aggregation and a sluggish one.

Takeaways

If your 1‑D data is divided into contiguous bins with indices increasing from 0 to m−1, maintain the array of bin starts and use np.minimum.reduceat to compute per‑bin minima. It aligns with the data layout you already have, keeps the operation vectorized, and skips padding or extra traversals.

The article is based on a question from StackOverflow by emengd and an answer by emengd.