Python浮動小数点数の誤差対応比較:math.isclose()の使い方を徹底解説

浮動小数点数の計算では、内部表現の制約により微小な誤差が生じます。この誤差により、理論上同じ値になるはずの計算結果が等価判定で異なる値として扱われる問題があります。

浮動小数点数比較の問題

# 基本的な誤差の例
result = 0.1 + 0.2
expected = 0.3

print(f"0.1 + 0.2 = {result}")
print(f"期待値: {expected}")
print(f"等価判定: {result == expected}")  # False
print(f"差: {result - expected}")

math.isclose()関数の基本使用法

Python 3.5で導入されたmath.isclose()は、浮動小数点数の誤差を考慮した比較を行う関数です。

import math

# 基本的な使用法
a = 0.1 + 0.2
b = 0.3

print(f"通常の比較: {a == b}")
print(f"isclose比較: {math.isclose(a, b)}")

# より複雑な例
result = (0.1 + 0.1 + 0.1) - 0.3
print(f"計算結果: {result}")
print(f"ゼロとの比較: {math.isclose(result, 0.0)}")

math.isclose()のパラメータ詳解

rel_tol(相対許容誤差)

import math

# 相対許容誤差の設定
a = 1000000.1
b = 1000000.2

# デフォルト(1e-09)
print(f"デフォルト: {math.isclose(a, b)}")

# 許容誤差を緩める
print(f"rel_tol=1e-6: {math.isclose(a, b, rel_tol=1e-6)}")
print(f"rel_tol=1e-7: {math.isclose(a, b, rel_tol=1e-7)}")

# 実際の相対誤差を計算
rel_error = abs(a - b) / max(abs(a), abs(b))
print(f"実際の相対誤差: {rel_error}")

abs_tol(絶対許容誤差)

import math

# 絶対許容誤差の設定
small_a = 1e-10
small_b = 2e-10

# 相対許容誤差のみ(デフォルト)
print(f"rel_tolのみ: {math.isclose(small_a, small_b)}")

# 絶対許容誤差を設定
print(f"abs_tol=1e-10: {math.isclose(small_a, small_b, abs_tol=1e-10)}")
print(f"abs_tol=1e-9: {math.isclose(small_a, small_b, abs_tol=1e-9)}")

# ゼロとの比較では絶対許容誤差が重要
zero_like = 1e-15
print(f"ゼロとの比較: {math.isclose(zero_like, 0.0, abs_tol=1e-14)}")

実用的な使用例

三角関数の計算

import math

# 三角関数の計算例
angle = math.pi / 6  # 30度
sin_result = math.sin(angle)
expected = 0.5

print(f"sin(π/6) = {sin_result}")
print(f"期待値: {expected}")
print(f"通常比較: {sin_result == expected}")
print(f"isclose比較: {math.isclose(sin_result, expected)}")

# より複雑な三角関数
complex_result = math.sin(math.pi)**2 + math.cos(math.pi)**2
print(f"sin²π + cos²π = {complex_result}")
print(f"1との比較: {math.isclose(complex_result, 1.0)}")

反復計算での誤差累積

import math

# 反復計算での誤差例
def calculate_series(n):
    result = 0.0
    for i in range(n):
        result += 0.1
    return result

# 異なる方法で同じ値を計算
method1 = calculate_series(10)
method2 = 1.0
method3 = 0.1 * 10

print(f"反復加算: {method1}")
print(f"直接代入: {method2}")
print(f"乗算: {method3}")

print(f"method1 == method2: {method1 == method2}")
print(f"isclose(method1, method2): {math.isclose(method1, method2)}")

カスタム比較関数の作成

用途別の比較関数

import math

def financial_close(a, b):
    """金融計算用(小数点以下2桁程度)"""
    return math.isclose(a, b, abs_tol=1e-2, rel_tol=1e-5)

def scientific_close(a, b):
    """科学計算用(高精度)"""
    return math.isclose(a, b, abs_tol=1e-15, rel_tol=1e-12)

def engineering_close(a, b):
    """工学計算用(中程度精度)"""
    return math.isclose(a, b, abs_tol=1e-6, rel_tol=1e-6)

# テスト
test_pairs = [
    (1.001, 1.002),
    (1e-10, 2e-10),
    (1000.0001, 1000.0002)
]

for a, b in test_pairs:
    print(f"値: {a}, {b}")
    print(f"  金融用: {financial_close(a, b)}")
    print(f"  科学用: {scientific_close(a, b)}")
    print(f"  工学用: {engineering_close(a, b)}")

リストや配列での一括比較

numpy配列との比較

import math

def compare_lists(list1, list2, **kwargs):
    """リストの要素を一括比較"""
    if len(list1) != len(list2):
        return False
    
    for a, b in zip(list1, list2):
        if not math.isclose(a, b, **kwargs):
            return False
    return True

# テスト用データ
calc_results = [0.1 + 0.2, 0.2 + 0.3, 0.3 + 0.4]
expected = [0.3, 0.5, 0.7]

print(f"通常比較: {calc_results == expected}")
print(f"isclose一括比較: {compare_lists(calc_results, expected)}")

# 個別確認
for i, (calc, exp) in enumerate(zip(calc_results, expected)):
    print(f"要素{i}: {math.isclose(calc, exp)}")

注意すべき特殊ケース

無限大とNaNの扱い

import math

# 無限大の比較
inf1 = float('inf')
inf2 = float('inf')
print(f"inf同士: {math.isclose(inf1, inf2)}")

# NaNの比較
nan1 = float('nan')
nan2 = float('nan')
print(f"nan同士: {math.isclose(nan1, nan2)}")  # False

# infとnanの判定
def safe_isclose(a, b, **kwargs):
    """NaNやinfを考慮した安全な比較"""
    if math.isnan(a) or math.isnan(b):
        return False
    if math.isinf(a) and math.isinf(b):
        return a == b  # 同じ符号のinfかチェック
    return math.isclose(a, b, **kwargs)

# テスト
test_cases = [
    (float('inf'), float('inf')),
    (float('-inf'), float('-inf')),
    (float('nan'), float('nan')),
    (1.0, 1.0000000001)
]

for a, b in test_cases:
    print(f"{a}, {b}: {safe_isclose(a, b)}")

パフォーマンスの比較

import math
import time

def benchmark_comparison():
    """比較方法のパフォーマンステスト"""
    a = 0.1 + 0.2
    b = 0.3
    iterations = 1000000
    
    # 通常の等価比較
    start = time.time()
    for _ in range(iterations):
        result = a == b
    normal_time = time.time() - start
    
    # isclose比較
    start = time.time()
    for _ in range(iterations):
        result = math.isclose(a, b)
    isclose_time = time.time() - start
    
    print(f"通常比較: {normal_time:.6f}秒")
    print(f"isclose比較: {isclose_time:.6f}秒")
    print(f"倍率: {isclose_time / normal_time:.2f}倍")

benchmark_comparison()

まとめ

math.isclose()は浮動小数点数の誤差を考慮した比較において非常に有用な関数です。rel_tolとabs_tolパラメータを適切に設定することで、用途に応じた精度での比較が可能になります。特に科学計算、金融計算、工学計算などの分野では、通常の等価演算子(==)の代わりにmath.isclose()を使用することで、より堅牢なプログラムを作成できます。


0との比較ではabs_tolが必須

math.isclose()を使ううえで、最も見落とされやすい落とし穴です。

import math

x = 1e-10

print(math.isclose(x, 0.0))
print(math.isclose(x, 0.0, abs_tol=1e-9))

実行結果:

False
True

限りなく0に近い値でも、既定の設定では0と「近い」と判定されません。

どれだけ小さくしても結果は変わりません。

import math

for x in [1e-10, 1e-100, 1e-300]:
    print(f"{x:>8} → {math.isclose(x, 0.0)}")

実行結果:

   1e-10 → False
  1e-100 → False
  1e-300 → False

なぜこうなるのか

isclose()の判定式は、次の形になっています。

|a - b| <= max(rel_tol × max(|a|, |b|), abs_tol)

片方が0の場合、rel_tolを掛ける相手も小さくなるため、許容範囲がほぼ0になってしまいます。

import math

a, b = 1e-10, 0.0
rel_tol = 1e-09

print("差:", abs(a - b))
print("相対許容:", rel_tol * max(abs(a), abs(b)))
print("判定:", abs(a - b) <= rel_tol * max(abs(a), abs(b)))

実行結果:

差: 1e-10
相対許容: 1.0000000000000001e-19
判定: False

差(1e-10)のほうが許容範囲(1e-19)より大きいため、Falseになります。

abs_tolを指定すると、相対値に関係のない固定の許容範囲が加わり、正しく判定できます。

import math

def is_zero(x, tol=1e-9):
    return math.isclose(x, 0.0, abs_tol=tol)

print(is_zero(math.sin(math.pi)))
print(is_zero(0.1 + 0.2 - 0.3))
print(is_zero(0.5))

実行結果:

True
True
False

「0と比べるときは必ずabs_tol」と覚えておいてください。


rel_tolの既定値1e-09の意味

import math

print(math.isclose(1000000.0, 1000000.001))
print(math.isclose(1.0, 1.000000001))
print(math.isclose(1.0, 1.00000001))

実行結果:

True
True
False

既定のrel_tol=1e-09は、「上位9桁が一致していれば近いとみなす」という意味です。

値の大きさに応じて許容範囲が変わるため、スケールの違うデータでも同じ設定で扱えます。

import math

pairs = [(1.0, 1.0000000001), (1e6, 1000000.0001), (1e-6, 1.0000000001e-06)]

for a, b in pairs:
    print(f"{a:>10} vs {b:<16} → {math.isclose(a, b)}")

実行結果:

       1.0 vs 1.0000000001     → True
 1000000.0 vs 1000000.0001     → True
     1e-06 vs 1.0000000001e-06 → True

絶対誤差で判定しようとすると、この3つには別々の閾値が必要になります。相対誤差なら1つの設定で済みます。


実務での閾値の決め方

どの値を指定すべきか迷ったら、次の基準が目安になります。

状況 設定
計算誤差を吸収したいだけ 既定のまま(rel_tol=1e-09)
0と比較する abs_tol=1e-9などを指定
金額(1円未満を無視) abs_tol=0.5
測定値(誤差1%まで許容) rel_tol=0.01
座標(1ピクセル未満) abs_tol=1.0
import math

price_a = 1000.4
price_b = 1000.6

print(math.isclose(price_a, price_b, abs_tol=0.5))

measured = 98.5
expected = 100.0

print(math.isclose(measured, expected, rel_tol=0.01))
print(math.isclose(measured, expected, rel_tol=0.05))

実行結果:

True
False
True

両方を同時に指定することもできます。どちらかの条件を満たせば「近い」と判定されます。

import math

print(math.isclose(1e-12, 0.0, rel_tol=1e-9, abs_tol=1e-9))
print(math.isclose(1000.0, 1000.0000001, rel_tol=1e-9, abs_tol=1e-9))

実行結果:

True
True

「小さい値は絶対誤差で、大きい値は相対誤差で」判定したい場合に有効な書き方です。


テストでの使い方

単体テストでは、専用のメソッドが用意されています。

import unittest

class TestCalc(unittest.TestCase):
    def test_float(self):
        self.assertAlmostEqual(0.1 + 0.2, 0.3)
        self.assertAlmostEqual(3.14159, 3.14, places=2)
        self.assertAlmostEqual(100.0, 100.4, delta=0.5)
方法 判定の基準
math.isclose() 相対誤差が中心
assertAlmostEqual() 小数点以下の桁数(既定7桁)

assertAlmostEqual()は絶対誤差ベースなので、値が非常に大きいと厳しすぎる判定になります。

import math

a, b = 1e10, 1e10 + 0.001

print(math.isclose(a, b))
print(round(a - b, 7) == 0)

実行結果:

True
False

大きな数値を扱うテストでは、math.isclose()で判定するほうが実態に合います。


あわせて読みたい

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら