プログラムで計算した数値が、手計算の結果と微妙に違って戸惑ったことはありませんか。
そうしたズレはバグとは限らず、多くはコンピュータの数の扱い方に由来します。
数値誤差は完全には避けられませんが、仕組みを理解してコントロールすれば実害を小さくできます。
本記事では、初心者の方にも分かるように原因と簡単な対処法を順序立てて解説します。
数値誤差とは?プログラミングで起きること
数値誤差の基本
コンピュータは有限の桁数しか持たない
数値誤差とは、数学的に正しい結果とコンピュータが計算した結果が一致しない現象の総称です。
原因の大半は、コンピュータが数値を有限の桁数で表すために、ある小数がぴったり表せず近い値に置き換えていることにあります。
これは紙に無限小数を書き切れないのと同じで、コンピュータでも限界があるということです。
誤差は累積する
一度生じたわずかな誤差は、繰り返しの計算や多くの値の合計で少しずつ蓄積し、目に見えるズレになることがあります。
最初の1回では気付かない差でも、100回、1000回と重なると結果が数%変わることもあります。
なぜ0.1+0.2が0.3にならない?
驚きの理由
0.1や0.2は2進数では有限桁で正確に表せないため、内部では近似値として保存されます。
その近似同士を足すと、理論上の0.3からわずかにズレた0.30000000000000004のような値になります。
これは多くの言語(JavaScriptやPythonなど)で観察できます。
例(実行結果)
同じ現象は複数の言語で再現できます。
確認のために次の一行を実行してみてください。
// JavaScript
console.log(0.1 + 0.2); // 0.30000000000000004
# Python
print(0.1 + 0.2) # 0.30000000000000004
このズレはバグではなく、浮動小数点の仕様通りの挙動です。
想定して対処するのが正解です。
ビット表現のイメージ
10進数の0.1は2進数では0.0001100110011…と無限に続く循環小数になります。
保存時にどこかで打ち切る必要があるため、必ず四捨五入などの丸めが発生し、そこから誤差が始まります。
どこで発生する?
誤差は小数の計算全般で起こり得ますが、とくに合計や平均、繰り返しの加算、近い値の差、比較演算で現れやすいです。
また、ファイルやAPIでの数値の入出力時に文字列へ変換する過程でも、桁数や書式の違いが影響します。
数値誤差の主な原因
浮動小数点の仕組み
科学技術計算向けの表記
浮動小数点数は、概念的にはm×b^eという科学的記数法で数を表現します。
ここでmは有効数字、bは基数(2または10など)、eは指数です。
一般的なプログラミング言語の小数は、IEEE 754という規格に従う2進の浮動小数点数です。
よく使う型の目安
型ごとの桁数と範囲を把握しておくと、扱い方の見当がつきます。
以下は代表的な型の目安です。
| 型の例 | ビット数 | 有効桁数(約) | 代表的な言語の型名 | 主な用途 |
|---|---|---|---|---|
| 単精度 | 32 | 7桁 | float(C, C#, Java) | 画像処理、軽量な計算 |
| 倍精度 | 64 | 15~16桁 | double(C, C#, Java), float(Python) | 一般的な計算 |
| 10進固定小数 | 128相当 | 28~34桁 | decimal(C#), BigDecimal(Java), Decimal(Python) | 通貨、桁管理が厳密な計算 |
倍精度(double)が標準的ですが、通貨や厳密な桁管理には10進のdecimal系を選ぶのが安全です。
丸め誤差
保存や計算の途中で発生する四捨五入によるズレが丸め誤差です。
たとえば0.1を内部で表現可能な最も近い数に丸める時点で、極小の差が生じます。
さらに、加算や乗算の各ステップで再び丸めが行われ、トータルの誤差が広がります。
桁落ち
ほぼ等しい2つの大きな数を引き算すると、多くの有効桁が打ち消し合い、有効数字が少ない結果だけが残る現象を桁落ちと呼びます。
たとえば(1000000.1 – 1000000.0)のような計算では、差が小さいため相対的な誤差が大きくなりやすいです。
避け方のヒント
式を並べ替えて引き算を減らす、または小さい値同士を先に合計するなどで安定化できます。
アルゴリズムの工夫が効果的です。
オーバーフロー・アンダーフロー
扱える範囲を超えるとオーバーフロー(大きすぎ)やアンダーフロー(小さすぎ)が起きます。
倍精度では非常に大きな数はInfinityになることがあり、極端に小さな数は0に丸められることがあります。
こうした扱いも結果を歪める原因になります。
型変換のミス
整数と小数、文字列の間での暗黙の変換や丸め落ちが、意図しない誤差や情報の欠落を生みます。
例として、小数を整数に変換する時の切り捨て、ロケールが違う小数点記号(,や.)の解釈違いなどがあります。
計算順序の違い
浮動小数点では結合法則が厳密には成り立たないため、(a+b)+cとa+(b+c)の結果が異なることがあります。
特に非常に大きな数と非常に小さな数を混ぜて足すと、順序で結果が変わります。
等値比較(==)の落とし穴
浮動小数点の等値比較は基本的に避け、許容誤差で比較するのが鉄則です。
0.1+0.2==0.3のような比較は失敗しがちで、バグの温床になります。
よくある例と再現方法
0.1+0.2の例
最も有名な再現例が0.1+0.2です。
以下の通り多くの言語で0.30000000000000004となります。
console.log(0.1 + 0.2); // 0.30000000000000004
print(0.1 + 0.2) # 0.30000000000000004
通貨計算の誤差
通貨をfloatやdoubleで扱うと、1円未満のズレが蓄積して合計が合わないことがあります。
たとえば0.1ドルを10回足しても、ちょうど1.0にならないことがあります。
請求書や会計では致命的なズレに繋がるため注意が必要です。
合計や平均の誤差
多くの値を足す処理では、小さな誤差が累積して合計や平均がわずかにずれることがあります。
特に大きな値と小さな値が混在すると、順序によって結果差が拡大します。
比較演算子での不一致
0.3との等値比較がfalseになるなど、条件分岐が期待通りに動かないケースが生じます。
この場合、絶対誤差や相対誤差の範囲で比較する工夫が必要です。
文字列と数値の変換ミス
文字列から数値に変換する際に、桁区切りや小数点記号の違いで誤解釈されることがあります。
また、表示時に丸めた値を再パースして再計算すると、桁落ちや丸め直しでズレが拡大します。
初心者向けの簡単な対処法
許容誤差で比較する
等値比較は避け、許容誤差(イプシロン)を設けて「十分近ければ等しい」と判定します。
これが最も簡単かつ効果的な対策です。
// 絶対誤差で比較
function nearlyEqual(a, b, eps = 1e-9) {
return Math.abs(a - b) < eps;
}
nearlyEqual(0.1 + 0.2, 0.3); // true
# Python 3.5+ は math.isclose が便利
import math
math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9, abs_tol=0.0) # True
表示用途は厳しめに、内部計算は緩めにするなど、目的に合わせてepsを調整します。
桁数を決めて丸める
表示や保存の段階で小数点以下の桁数を決めて丸めると、不要なノイズを抑えられます。
ただし、計算の途中で過度に丸めると逆に誤差が増えるため、基本は「計算は高精度、最後に丸める」が安全です。
const price = 0.1 + 0.2;
const shown = Number(price.toFixed(2)); // 0.30 を表示や保存に使う
小数を整数で扱う
通貨など桁が決まっている値は、小数を避けて整数に変換して扱うのが定石です。
たとえば円ならそのまま、ドルならセント単位で保管し、計算は整数で行い最後に小数点を付けます。
// $1.23 を 123 セントとして扱う
const a = 10; // 10 セント
const b = 20; // 20 セント
const totalCents = a + b; // 30
const dollars = (totalCents / 100).toFixed(2); // "0.30"
小数点以下の桁数(スケール)を明確に決めて、全体で統一しましょう。
decimal型や任意精度の利用
通貨や桁の厳密さが必要な処理では、10進のdecimal系や任意精度の数値型を使うと安全です。
言語ごとの例は次の通りです。
- Python: Decimal
- Java: BigDecimal
- C#: decimal
- JavaScript: ライブラリ(decimal.jsなど)を利用
decimal系は速度やメモリのコストが上がりやすいので、必要な部分に限定して使うのが実務的です。
計算順序を安定化する
合計は小さい値から大きい値へ足す、ペアワイズに足すなど、計算順序を工夫すると誤差が減ります。
データを昇順に並べてから合計するだけでも効果があります。
# 小さい順に足す
nums = [1e16, 1, 1, 1, 1]
for s in [sum(nums), sum(sorted(nums))]:
print(s) # 並べ替えで結果が安定しやすい
差分計算が多いときは式変形で引き算を避けることも有効です。
型を統一する
同じ計算の中で型を混在させないことが、地味ですが効きます。
整数と小数、文字列を明示的に統一し、暗黙の型変換を避けるだけで、意図しない丸めを防げます。
テストで誤差を検出する
ユニットテストでは「誤差を許容した比較」を使い、境界的なケースを含めて検証します。
代表的なアサーションは次の通りです。
| 言語/フレームワーク | 誤差付きアサート例 |
|---|---|
| Python unittest | assertAlmostEqual(a, b, places) または math.isclose |
| Java JUnit | assertEquals(expected, actual, delta) |
| JavaScript Jest | expect(a).toBeCloseTo(b, digits) |
| C# NUnit | Assert.That(a, Is.EqualTo(b).Within(eps)) |
表示用の丸め結果と内部計算結果の両方をテストしておくと安心です。
仕様と前提を明記する
小数点以下の桁数、丸め規則(四捨五入や銀行丸めなど)、比較時の許容誤差、通貨単位などをドキュメントに明記しましょう。
実装とレビュー、テストで共通認識が持てれば、チーム全体の品質が安定します。
まとめ
数値誤差はコンピュータの表現限界から生まれる必然であり、正しく理解して扱えば怖くありません。
本記事では、0.1+0.2が0.3にならない理由、どこで誤差が発生するか、主な原因(浮動小数点の仕組み、丸め誤差、桁落ち、オーバーフローや型変換、計算順序、等値比較の落とし穴)を解説し、初心者でもすぐ使える対処法(許容誤差比較、桁数の丸め、整数化、decimal型、順序の安定化、型の統一、テスト、仕様化)を紹介しました。
まずは等値比較を避けて許容誤差で比較する、通貨は整数やdecimalで扱う、最後に丸めて表示するの3点から始めてください。
日々の実装で小さな工夫を積み重ねれば、誤差による不具合は大幅に減らせます。
