【Python】同じ添字への足し込みが抜ける問題を直す

PythonのTopに戻る

同じ添字に複数回足し込むとき、a[idx] += valueでは加算が抜けたような結果になることがある。高度なインデックスの一時的な読み出しを経由するためで、重複を累積したい場合はnp.add.atを使う。観測点を区画ごとに集計するときなど、添字の重複が正常に起こる処理で重要な違いである。

最小例で確かめる

以下は説明用に作った小さなデータである。実測データや実行速度の測定結果ではない。コード全体をexample.pyとして保存すれば、入力ファイルを別途用意せずに実行できる。assertは、この例で成り立つべき形や値を確認するために入れてある。

import numpy as np
idx = np.array([0, 0, 2])
value = np.array([1., 1., 3.])
bad = np.zeros(4)
bad[idx] += value  # 意図的な誤集計例
correct = np.zeros(4)
np.add.at(correct, idx, value)
weighted = np.bincount(idx, weights=value, minlength=4)
print("buffered +=:", bad.tolist())
print("add.at:", correct.tolist())
print("bincount:", weighted.tolist())
row = np.array([0, 0, 1])
col = np.array([1, 1, 0])
grid = np.zeros((2, 2))
np.add.at(grid, (row, col), value)
print("2D:", grid.tolist())
np.testing.assert_array_equal(bad, [1, 0, 3, 0])
np.testing.assert_array_equal(correct, [2, 0, 3, 0])
np.testing.assert_array_equal(weighted, correct)
np.testing.assert_array_equal(grid, [[0, 2], [3, 0]])
assert correct.sum() == value.sum()

実行結果

buffered +=: [1.0, 0.0, 3.0, 0.0]
add.at: [2.0, 0.0, 3.0, 0.0]
bincount: [2.0, 0.0, 3.0, 0.0]
2D: [[0.0, 2.0], [3.0, 0.0]]

+=が順番に累積するとは限らない

idxの先頭二つはどちらも0である。ループで順に足すなら0番目は1+1で2になるが、bad[idx] += valueの結果は1になっている。高度なインデックスは選択した値の一時的な配列を使うため、同じ元の0を参照した二つの更新が、必ず連続した加算として適用されるわけではない。

ここでは重複位置へ同じ値を加える単純な例を使い、期待する誤結果も確認している。異なる値を重複添字へ書き込む場合に、どの値が最後に残るかを利用した処理を書くべきではない。通常の代入の順序に頼らず、累積・最大・最後の値など本来必要な規則に合った処理を選ぶ。

np.add.atでその場に加算する

np.add.at(correct, idx, value)は、指定位置への加算をバッファリングせずに行う。したがって添字0への二つの寄与が両方反映され、結果は2になる。関数の戻り値を結果配列として使うのではなく、第1引数のcorrect自体が変更されることにも注意する。

多次元なら添字配列をタプルで渡せる。例の(row, col)は(0, 1)、(0, 1)、(1, 0)を表すので、格子の右上へ2、左下へ3が入る。行番号と列番号と値の配列は同じ観測順で対応していなければならない。どれか一つだけ並べ替えると、別の格子に値を足すことになる。

bincountが適している場合

非負整数の1次元区画番号ごとの加重和なら、np.bincountも簡潔である。weightsに寄与値、minlengthに欲しい区画数を指定している。ただしminlengthは最低長さであり、入力により大きな区画番号があれば結果はそれ以上に長くなる。区画数が固定なら、先に添字の範囲を確認しておく。

add.atは一般的な添字や既存配列への更新に使いやすく、bincountは非負の整数ラベルを集約する目的に特化している。どちらが適切かは問題の形による。add.atを使えば常に速いという話ではなく、まず期待する累積が行われることを確保し、必要なら実データの条件で性能を測る。

集約の前後で確認すること

NumPyの負の添字は後ろからの位置を指すため、未対応を-1で表した配列をそのままadd.atに渡すと、最後の区画へ誤って加算する。負値や上限超えを検査し、未対応データは別のマスクで除外する。空の添字配列や、観測のない区画の値をどう扱うかも決めておく。

集約先のdtypeは累積結果が入るものを選ぶ。整数型なら桁あふれ、浮動小数点なら足す順序による丸め差があり得る。全入力を一度ずつ足す設計なら、例のように結果の合計と入力寄与の合計を比較すると、足し込み漏れの検査に使える。ただしキャンセルする誤りもあるので、代表区画の値も併せて確認したい。

動作確認環境と参考資料

Linux・CPython 3.12.14、NumPy 2.3.5、pandas 2.2.3、SciPy 1.17.0、Matplotlib 3.10.8の環境で掲載コードを実行した。使用するライブラリはコード冒頭のimportを参照してほしい。公式資料の最新版と、この実行確認版は区別している。数値の末尾や表の表示幅は環境によって変わることがある。

関連するTips

PythonのTopに戻る