np.add.at() in NumPy: Adding at Repeated Indices

np.add.at adds values into an array at the indices you give it, and it applies every index even when one repeats.

np.add.at(array, indices, values)      # in place, returns None

That repetition is the entire point. Plain array[indices] += values quietly keeps only the last write for any index that appears more than once.

Below I show that difference, the 2D form, and how it really compares with np.bincount once you measure it. Run on NumPy 2.5.3 on Python 3.12.5.

What np.add.at solves that += cannot

Fancy indexing with += is unbuffered. NumPy reads the whole slice, adds, then writes it back, so duplicate indices overwrite each other instead of accumulating:

import numpy as np

counts = np.zeros(5, dtype=int)
indices = [0, 1, 1, 3, 3, 3]        # index 1 appears twice, index 3 three times

# the obvious way silently loses the duplicates
plain = counts.copy()
plain[indices] += 1
print("plain += :", plain)

# np.add.at applies every index, one after another
buffered = counts.copy()
np.add.at(buffered, indices, 1)
print("np.add.at:", buffered)

print("expected :", [1, 2, 0, 3, 0])

Output:

plain += : [1 1 0 1 0]
np.add.at: [1 2 0 3 0]
expected : [1, 2, 0, 3, 0]
Command Prompt comparing plain fancy indexing with np.add.at, showing the plain version counting duplicate indices only once
Index 1 appears twice and index 3 three times. Only np.add.at counts them all.

That’s the bug that sends people looking for this function, usually after a histogram or a counter comes out too low.

If you ever see counts that are correct except where the same key appears twice, this is why.

np.add.at works in place and returns None

np.add.at changes the array in place. It hands back nothing, so assigning its result gives you None:

import numpy as np

totals = np.array([10, 20, 30])

result = np.add.at(totals, [0, 2], 5)

print("returns   :", result)          # None: it changes the array in place
print("totals now:", totals)

# so this is a mistake that is easy to make
broken = np.add.at(np.array([1, 2, 3]), [0], 10)
print("broken    :", broken)

Output:

returns   : None
totals now: [15 20 35]
broken    : None

Call it as a statement, then use the array you passed in. It trips people up more often than the indexing does.

np.add.at with one value or one per index

The third argument broadcasts. Pass a scalar and every index gets it; pass a sequence the same length as the indices and they’re matched up in order:

import numpy as np

stock = np.zeros(4, dtype=int)

# one value applied to every index
np.add.at(stock, [0, 0, 2], 3)
print("same value :", stock)

# or one value per index, matched up position by position
sales = np.zeros(4, dtype=int)
np.add.at(sales, [0, 1, 1, 3], [5, 2, 7, 1])
print("per index  :", sales)

# negative indices count from the end, as everywhere else in NumPy
np.add.at(sales, [-1], 100)
print("negative   :", sales)

Output:

same value : [6 0 3 0]
per index  : [5 9 0 1]
negative   : [  5   9   0 101]

Negative indices behave as they do everywhere else in NumPy, counting back from the end, so you don’t need to special-case them.

np.add.at on a 2D NumPy array

For a 2D array, pass a tuple of index arrays, one per dimension. The first array gives the rows, the second the columns:

import numpy as np

grid = np.zeros((3, 4), dtype=int)

rows = [0, 0, 2]
cols = [1, 1, 3]
np.add.at(grid, (rows, cols), 1)      # a tuple of index arrays, one per dimension

print(grid)
print("cell (0,1) got both hits:", grid[0, 1])

# a whole row at a time
np.add.at(grid, 1, 5)
print(grid)

Output:

[[0 2 0 0]
 [0 0 0 0]
 [0 0 0 1]]
cell (0,1) got both hits: 2
[[0 2 0 0]
 [5 5 5 5]
 [0 0 0 1]]
Command Prompt showing np.add.at applied to a 2D NumPy array with row and column index arrays, and then to a whole row
The pair (0, 1) appears twice, so that cell gets both hits.

Get the shapes wrong here and NumPy will tell you, which is the usual reason to keep 2D array indexing fresh in mind.

Is np.add.at slow?

You’ll find plenty of advice saying it is, and that np.bincount leaves it far behind. That advice is out of date.

NumPy has optimised this path considerably, so I measured it at three sizes rather than repeating the folklore:

import numpy as np
import timeit

rng = np.random.default_rng(0)
print("NumPy", np.__version__)

for size, bins in ((200_000, 1_000), (2_000_000, 1_000), (2_000_000, 500_000)):
    indices = rng.integers(0, bins, size=size)

    add_at = timeit.timeit(lambda: np.add.at(np.zeros(bins, dtype=int), indices, 1), number=5) / 5 * 1000
    bincount = timeit.timeit(lambda: np.bincount(indices, minlength=bins), number=5) / 5 * 1000

    print(f"{size:>9,} indices into {bins:>7,} bins -> "
          f"add.at {add_at:7.2f} ms | bincount {bincount:6.2f} ms | {add_at / bincount:.2f}x")

Output:

NumPy 2.5.3
  200,000 indices into   1,000 bins -> add.at    0.24 ms | bincount   0.19 ms | 1.31x
2,000,000 indices into   1,000 bins -> add.at    2.29 ms | bincount   2.60 ms | 0.88x
2,000,000 indices into 500,000 bins -> add.at    7.42 ms | bincount   7.20 ms | 1.03x
Command Prompt comparing np.add.at with np.bincount at three different array sizes, showing add.at only slightly slower in each case
Three workloads, and the gap never reaches 1.4×.

bincount still wins, but by a fraction rather than an order of magnitude. On older NumPy the difference was dramatic, which is where the reputation came from.

So pick on fit, not fear. bincount is the natural choice for counting non-negative integers into bins, and it takes a weights argument for weighted sums.

np.add.at is the one to reach for when the target is 2D, the indices are negative, or you need a ufunc other than addition — and it costs very little to use.

Other NumPy ufunc .at methods

.at isn’t special to addition. Every NumPy ufunc has it, and each one accumulates in its own way:

import numpy as np

values = np.array([10, 10, 10, 10])
np.subtract.at(values, [0, 0, 1], 3)
print("subtract.at:", values)

products = np.array([1, 1, 1])
np.multiply.at(products, [0, 0, 2], 4)
print("multiply.at:", products)

# maximum.at keeps the largest value seen at each index
peaks = np.zeros(4)
np.maximum.at(peaks, [0, 0, 1, 1], [5.0, 2.0, 9.0, 11.0])
print("maximum.at :", peaks)

Output:

subtract.at: [ 4  7 10 10]
multiply.at: [16  1  4]
maximum.at : [ 5. 11.  0.  0.]
CallWhat it does at repeated indices
np.add.atAdds every value
np.subtract.atSubtracts every value
np.multiply.atMultiplies by every value
np.maximum.atKeeps the largest seen
np.minimum.atKeeps the smallest seen

np.add.at with an out-of-bounds index

There’s no silent clipping here. An index past the end raises, which is the behaviour you want:

import numpy as np

counts = np.zeros(3, dtype=int)
np.add.at(counts, [0, 5], 1)          # index 5 does not exist in a length-3 array

What NumPy raises:

Traceback (most recent call last):
  File "C:\pyguides\add_at_index_error.py", line 4, in <module>
    np.add.at(counts, [0, 5], 1)          # index 5 does not exist in a length-3 array
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
IndexError: index 5 is out of bounds for axis 0 with size 3
Command Prompt showing the NumPy IndexError raised when np.add.at is given an index beyond the end of the array
Length 3, index 5, and an immediate error.

Size the target array from your data rather than guessing, and you’ll never meet it.

More NumPy on this site:

Frequently asked questions

What does np.add.at do?

It adds values into an array at given indices, in place, applying every index even when one repeats. The behaviour is described in the ufunc.at reference.

Why not just use array[indices] += values?

Because that is unbuffered. NumPy reads, adds and writes back once, so a repeated index keeps only the last value instead of accumulating.

Why does np.add.at return None?

It modifies the array in place by design. Call it as a statement and then read the array you passed in.

How do I use np.add.at on a 2D array?

Pass a tuple of index arrays, one per dimension: np.add.at(grid, (rows, cols), 1).

Is np.add.at slow?

Not really, on current NumPy. Measured against np.bincount at three sizes it was only 1.1 to 1.3 times slower. The old advice that it is dramatically slow predates NumPy’s optimisations.

Can I use it with subtraction or multiplication?

Yes. Every ufunc has .at, so np.subtract.at, np.multiply.at, np.maximum.at and the rest all work the same way.

What happens with an out-of-range index?

It raises an IndexError straight away. There is no clipping or wrapping, though negative indices count from the end as usual.