ML Foundations

Count repeated NumPy indices without losing updates

Advanced-indexed += is not a scatter-sum. Use np.add.at when repeated indices must each contribute to an accumulator.

Evidence and testing

Review record: Tensor Almanac automated evidence gate (no independent human review). Example versions, recorded outputs and limitations appear below.

Repeated destinations are the important case

When a batch contains repeated category IDs, a count array needs one increment for every occurrence. The expression `counts[ids] += 1` looks like that operation, but advanced indexing uses buffered values. Repeated destinations can therefore receive one increment rather than the sum of all their contributions. NumPy explicitly documents this distinction for `ufunc.at`. [1, 2]

For an engineer building a small feature counter, the useful question is not whether the output has the expected dimensions. It is whether repeated inputs retain their multiplicity. A test containing only distinct IDs cannot distinguish an indexed update from the aggregation that the application actually needs.

References: [1], [2]

Use an operation that accumulates every occurrence

The example starts with separate zero arrays and sends four updates to indices zero, zero, one, and zero. Indexed addition produces counts of one, one, and zero, whereas `np.add.at` produces three, one, and zero. These are directly observed outputs for the recorded environment. NumPy documents `at` as applying the operation without buffering and accumulating repeated indices. [1]

The final assertion-style print checks an additional invariant: with unit contributions, the total accumulated count should equal the number of input indices. This is a deliberately small test of the counting contract, not a measurement of throughput or a demonstration of every possible indexing configuration.

Executed with Python 3.13.5 and NumPy 2.3.5; bounded synthetic example. Exact stdout and source-byte SHA-256 are preserved in private evidence.

import numpy as np

indices = np.array([0, 0, 1, 0])
buffered = np.zeros(3, dtype=np.int64)
accumulated = np.zeros(3, dtype=np.int64)
buffered[indices] += 1
np.add.at(accumulated, indices, 1)
print("indices:", indices.tolist())
print("indexed +=:", buffered.tolist())
print("add.at:", accumulated.tolist())
print("all updates counted:", int(accumulated.sum()) == len(indices))

Recorded output

indices: [0, 0, 1, 0]
indexed +=: [1, 1, 0]
add.at: [3, 1, 0]
all updates counted: True

References: [1], [2]

Test the aggregation contract, not just the shape

Use repeated destinations in the regression fixture, including repetition at the start and end of the input. For this integer-counting example, compare the entire output with an independently calculated expected count as well as checking the total. The recommendation follows from the demonstrated loss of multiplicity; it is not a claim that all indexed assignments are incorrect.

This guide covers one-dimensional, valid nonnegative integer indices and unit increments. It does not establish thread safety, an optimal high-volume aggregation method, or behavior beyond the accumulator dtype's capacity. NumPy also permits other operations and shapes with `at`; their broadcasting and indexing requirements still need to match the application. [1, 2]

References: [1], [2]

Sources

  1. NumPy ufunc.at semantics
  2. NumPy advanced indexing and indexed assignments