【Python】NumPyの整数計算が桁あふれする原因

PythonのTopに戻る

NumPyの整数は、Pythonのintと違って保存できる範囲が決まっている。計算後に大きな型へ変換しても、既に桁あふれした値は元に戻らない。入力値だけでなく計算途中と結果の最大値を見積もり、演算する前にdtypeを選ぶことが対策になる。

最小例で確かめる

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

import numpy as np
x = np.array([120, 125], dtype=np.int8)
bad = x + np.int8(10)  # 意図的な桁あふれ
safe = x.astype(np.int64) + 10
late = bad.astype(np.int64)
print("int8 range:", np.iinfo(np.int8).min, np.iinfo(np.int8).max)
print("overflow:", bad.tolist(), str(bad.dtype))
print("cast first:", safe.tolist(), str(safe.dtype))
print("cast afterward:", late.tolist())
print("mixed type:", str(np.result_type(np.int64, np.uint64)))
large = np.array([2**53 + 1], dtype=np.uint64)
mixed = large + np.array([0], dtype=np.int64)
print("exact integer:", int(large[0]))
print("float converted back:", int(mixed[0]))
try:
    np.array([128], dtype=np.int8)  # 意図的な範囲外入力
except OverflowError:
    print("expected: OverflowError")
else:
    raise AssertionError("out-of-range input was accepted")
np.testing.assert_array_equal(bad, [-126, -121])
np.testing.assert_array_equal(safe, [130, 135])
assert mixed.dtype == np.float64
assert int(mixed[0]) != int(large[0])

実行結果

int8 range: -128 127
overflow: [-126, -121] int8
cast first: [130, 135] int64
cast afterward: [-126, -121]
mixed type: float64
exact integer: 9007199254740993
float converted back: 9007199254740992
expected: OverflowError

入力が入る型と結果が入る型は違う

符号付き8bit整数int8の範囲は-128から127である。120や125は保存できるが、10を足した130や135は範囲を超える。例では結果が負の数へ回り込んでいる。演算の例外が出なかったとしても、数学的な整数の答えを保存できたとは限らない。

np.iinfo(dtype)を使えば、その型の最小値と最大値を調べられる。加算なら加数の範囲、乗算なら積の範囲、積算なら項数も考える。元のセンサー値が16bitに収まっていても、多数回の和や二乗の和は同じ型に収まるとは限らない。ファイルの保存型をそのまま計算型に使う必要はない。

型変換は計算より先に行う

x.astype(np.int64) + 10は、先に64bitの作業用配列へ変換してから足す。bad.astype(np.int64)では、既に失われた値を64bitで保存し直すだけになる。この順序の違いは、二乗や差分にも共通である。計算結果が不自然なら、最後のdtypeだけでなく途中式のdtypeも調べよう。

sumなど一部の集約は小さい整数型をより広い整数型へ昇格させるが、すべての演算に同じ規則が適用されるわけではない。要素ごとの乗算が先に桁あふれすれば、後段でsumのdtypeを広げても救えない。必要な型を各演算の前で確保することが重要である。

符号付きと符号なしの混在も確認する

int64とuint64を混ぜると、両者の全範囲を表現できる通常の符号付き整数型がないため、例の組合せではfloat64へ昇格する。np.result_typeはこのような型の組合せを調べる手掛かりになる。ただし型が浮動小数点になれば、大きな整数をすべて正確に保持できるわけではない。

例の2**53 + 1はfloat64ではそのまま表せず、整数へ戻すと別の値になっている。IDやカウンターを一度浮動小数点にすると、見た目の桁数が多いだけでは精度損失を見落とす。差をfloat同士で比較する前に、元の整数表現を保持して正確な整数同士で確認すると分かりやすい。

警告だけを検査手段にしない

NumPy 2系ではPython整数との演算や範囲外整数の変換にも型の規則がある。例では128をint8として作ろうとするとOverflowErrorを受け取る。一方、配列の整数演算による桁あふれは警告されない場合がある。警告が無いので安全、と判断しないこと。

int64も無制限ではない。上限を超える厳密な整数計算が必要ならPythonのintなど別の表現を検討する。逆に最終的な保存容量を減らす目的で小さい型へ戻す場合は、変換前に最小値・最大値を確認する。安易なfloat化は範囲問題と引き換えに精度問題を導入するため、用途を分けて選びたい。

動作確認環境と参考資料

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に戻る