PythonユニットテストのassertAlmostEqual-floatのコレクションのテスト


81

PythonのユニットテストフレームワークassertAlmostEqual(x、y)メソッドは、とが浮動小数点数であると仮定してほぼ等しいかどうかをテストします。xy

の問題assertAlmostEqual()は、フロートでのみ機能することです。assertAlmostEqual()フロートのリスト、フロートのセット、フロートの辞書、フロートのタプル、フロートのタプルのリスト、フロートのリストのセットなどで機能するような方法を探しています。

たとえば、みましょうx = 0.1234567890y = 0.1234567891xそしてy、彼らは最後のものを除き、それぞれ、すべての桁に同意するので、ほぼ等しくなっています。したがってself.assertAlmostEqual(x, y)あるTrueので、assertAlmostEqual()フロートのための作品。

assertAlmostEquals()次の呼び出しも評価する、より一般的なものを探していますTrue

  • self.assertAlmostEqual_generic([x, x, x], [y, y, y])
  • self.assertAlmostEqual_generic({1: x, 2: x, 3: x}, {1: y, 2: y, 3: y})
  • self.assertAlmostEqual_generic([(x,x)], [(y,y)])

そのような方法はありますか、それとも自分で実装する必要がありますか?

明確化:

  • assertAlmostEquals()名前付きのオプションのパラメータがplacesあり、小数点以下の桁数に丸められた差を計算することによって数値が比較されますplaces。デフォルトではplaces=7、したがって、self.assertAlmostEqual(0.5, 0.4)はFalseですが、self.assertAlmostEqual(0.12345678, 0.12345679)はTrueです。私の投機assertAlmostEqual_generic()は同じ機能を持っているはずです。

  • 2つのリストがまったく同じ順序でほぼ等しい数である場合、それらはほぼ等しいと見なされます。正式には、for i in range(n): self.assertAlmostEqual(list1[i], list2[i])

  • 同様に、2つのセットは、(各セットに順序を割り当てることによって)ほぼ等しいリストに変換できる場合、ほぼ等しいと見なされます。

  • 同様に、各辞書のキーセットが他の辞書のキーセットとほぼ等しい場合、2つの辞書はほぼ等しいと見なされ、そのようなほぼ等しいキーペアごとに、対応するほぼ等しい値があります。

  • 一般的に:2つのコレクションは、互いにほぼ等しい対応するフロートを除いて、等しい場合はほぼ等しいと見なします。言い換えれば、私は実際にオブジェクトを比較したいのですが、途中でフロートを比較するときは精度が低く(カスタマイズされています)。


float辞書でキーを使用する意味は何ですか?正確に同じフロートを取得することは確実ではないため、ルックアップを使用してアイテムを見つけることはできません。また、ルックアップを使用していない場合は、辞書の代わりにタプルのリストを使用してみませんか?同じ議論がセットにも当てはまります。
最大

ただ、ソースへのリンクについてassertAlmostEqual
djvg

回答:


71

NumPy(Python(x、y)に付属)を使用してもかまわない場合np.testingは、特にassert_almost_equal関数を定義するモジュールを確認することをお勧めします。

署名は np.testing.assert_almost_equal(actual, desired, decimal=7, err_msg='', verbose=True)

>>> x = 1.000001
>>> y = 1.000002
>>> np.testing.assert_almost_equal(x, y)
AssertionError: 
Arrays are not almost equal to 7 decimals
ACTUAL: 1.000001
DESIRED: 1.000002
>>> np.testing.assert_almost_equal(x, y, 5)
>>> np.testing.assert_almost_equal([x, x, x], [y, y, y], 5)
>>> np.testing.assert_almost_equal((x, x, x), (y, y, y), 5)

4
これは近いですが、numpy.testingほぼ等しいメソッドは、数値、配列、タプル、およびリストでのみ機能します。それらは、辞書、セット、およびコレクションのコレクションでは機能しません。
snakile 2012

確かに、しかしそれは始まりです。さらに、辞書やコレクションなどを比較できるように変更できるソースコードにアクセスできます。np.testing.assert_equalたとえば、辞書を引数として認識します(比較が、機能しないによって行われた==場合でも)。
ピエールGM

もちろん、@ BrenBarnが述べたように、セットを比較するときにまだ問題が発生します。
ピエールGM

の現在のドキュメントでassert_array_almost_equalassert_allcloseassert_array_almost_equal_nulpまたはのassert_array_max_ulp代わりにを使用することを推奨していることに注意してください。
phunehehe

10

Python 3.5以降、を使用して比較できます

math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)

pep-0485で説明されているように。実装は同等である必要があります

abs(a-b) <= max( rel_tol * max(abs(a), abs(b)), abs_tol )

7
これは、質問が尋ねていたフロートとコンテナを比較するのにどのように役立ちますか?
最大

9

これが私がジェネリックis_almost_equal(first, second)関数を実装した方法です

まず、比較する必要のあるオブジェクト(firstおよびsecond)を複製しますが、正確なコピーは作成しないでください。オブジェクト内で遭遇するフロートの重要でない10進数を切り取ります。

今、あなたはのコピーがあることfirstsecond些細な小数点以下の桁がなくなっているために、単に比較firstしてsecond使用して==演算子を。

cut_insignificant_digits_recursively(obj, places)複製するobjplaces、元の各フロートの最上位の10進数のみを残す関数があると仮定しobjます。これが実際の実装ですis_almost_equals(first, second, places)

from insignificant_digit_cutter import cut_insignificant_digits_recursively

def is_almost_equal(first, second, places):
    '''returns True if first and second equal. 
    returns true if first and second aren't equal but have exactly the same
    structure and values except for a bunch of floats which are just almost
    equal (floats are almost equal if they're equal when we consider only the
    [places] most significant digits of each).'''
    if first == second: return True
    cut_first = cut_insignificant_digits_recursively(first, places)
    cut_second = cut_insignificant_digits_recursively(second, places)
    return cut_first == cut_second

そして、これが実際の実装ですcut_insignificant_digits_recursively(obj, places)

def cut_insignificant_digits(number, places):
    '''cut the least significant decimal digits of a number, 
    leave only [places] decimal digits'''
    if  type(number) != float: return number
    number_as_str = str(number)
    end_of_number = number_as_str.find('.')+places+1
    if end_of_number > len(number_as_str): return number
    return float(number_as_str[:end_of_number])

def cut_insignificant_digits_lazy(iterable, places):
    for obj in iterable:
        yield cut_insignificant_digits_recursively(obj, places)

def cut_insignificant_digits_recursively(obj, places):
    '''return a copy of obj except that every float loses its least significant 
    decimal digits remaining only [places] decimal digits'''
    t = type(obj)
    if t == float: return cut_insignificant_digits(obj, places)
    if t in (list, tuple, set):
        return t(cut_insignificant_digits_lazy(obj, places))
    if t == dict:
        return {cut_insignificant_digits_recursively(key, places):
                cut_insignificant_digits_recursively(val, places)
                for key,val in obj.items()}
    return obj

コードとその単体テストは、https//github.com/snakile/approximate_comparatorから入手できます。改善とバグ修正を歓迎します。


フロートを比較する代わりに、文字列を比較していますか?OK ...でも、共通のフォーマットを設定する方が簡単ではないでしょうか。のようにfmt="{{0:{0}f}}".format(decimals)、そしてこのfmtフォーマットを使用してフロートを「文字列化」しますか?
ピエールGM

1
これは見栄えがしますが、小さな点ですplaces。有効数字の数ではなく、小数点以下の桁数を示します。たとえば、1024.1231023.999を3つの重要なものと比較すると、等しいが返されますが、小数点以下3桁まではそうではありません。
ロドニーリチャードソン

1
@pir、ライセンスは確かに未定義です。この問題のsnalileの回答を参照してください。彼は、ライセンスを選択/追加する時間がないが、使用/変更のアクセス許可を付与していると述べています。これを共有してくれてありがとう、ところで。
ジェローム・

1
@RodneyRichardson、はい、これはassertAlmostEqualのように小数点以下の桁数です:「これらのメソッドは、有効桁数ではなく、指定された小数点以下の桁数に値を丸めることに注意してください(つまり、round()関数のように)。」
ジェローム・

2
@Jérôme、コメントありがとうございます。MITライセンスを追加しました。
snakile 2017

5

numpyパッケージを使用してもかまわない場合numpy.testingは、assert_array_almost_equalメソッドがあります。

これはarray_likeオブジェクトに対して機能するため、floatの配列、リスト、タプルには問題ありませんが、セットや辞書には機能しません。

ドキュメントはこちらです。


4

そのような方法はありません、あなたはそれを自分でしなければならないでしょう。

リストとタプルの場合、定義は明白ですが、あなたが言及する他のケースは明白ではないことに注意してください。したがって、そのような関数が提供されないのも不思議ではありません。たとえば、は?に{1.00001: 1.00002}ほぼ等しい {1.00002: 1.00001}このような場合を処理するには、近さがキーまたは値、あるいはその両方に依存するかどうかを選択する必要があります。セットの場合、セットは順序付けられていないため、意味のある定義が見つからない可能性があります。したがって、「対応する」要素の概念はありません。


BrenBarn:質問に説明を追加しました。あなたの質問への答えはということである{1.00001: 1.00002}、ほぼ等しく{1.00002: 1.00001}、ほぼ1.00002と等しい場合にのみ1.00001場合。デフォルトでは、それらはほぼ等しくありません(デフォルトの精度は小数点以下7桁であるため)が、十分に小さい値の場合、placesほぼ等しくなります。
snakile 2012

1
@BrenBarn:IMO、floatdictでのタイプのキーの使用は、明らかな理由で推奨されない(場合によっては禁止される)必要があります。dictのおおよその同等性は、値のみに基づく必要があります。テストフレームワークは、floatforキーの誤った使用法について心配する必要はありません。セットの場合、比較する前に並べ替えることができ、並べ替えられたリストを比較できます。
最大

2

リストとセットを同じように繰り返すことができるのは事実ですが、辞書は別の話であり、値ではなくキーを繰り返す必要があります。3番目の例は、私には少しあいまいに見えます。セット内の各値、または各セットの各値を比較します。

ここに簡単なコードスニペットがあります。

def almost_equal(value_1, value_2, accuracy = 10**-8):
    return abs(value_1 - value_2) < accuracy

x = [1,2,3,4]
y = [1,2,4,5]
assert all(almost_equal(*values) for values in zip(x, y))

おかげで、ソリューションはリストとタプルに対しては正しいですが、他のタイプのコレクション(またはネストされたコレクション)に対しては正しくありません。質問に追加した説明を参照してください。私の意図が今はっきりしていることを願っています。数値があまり正確に測定されていない世界で等しいと見なされた場合、2つのセットはほぼ等しくなります。
snakile 2012

1

これらの答えはどれも私にはうまくいきません。次のコードは、Pythonコレクション、クラス、データクラス、および名前付きタプルで機能するはずです。私は何かを忘れたかもしれませんが、これまでのところこれは私にとってはうまくいきます。

import unittest
from collections import namedtuple, OrderedDict
from dataclasses import dataclass
from typing import Any


def are_almost_equal(o1: Any, o2: Any, max_abs_ratio_diff: float, max_abs_diff: float) -> bool:
    """
    Compares two objects by recursively walking them trough. Equality is as usual except for floats.
    Floats are compared according to the two measures defined below.

    :param o1: The first object.
    :param o2: The second object.
    :param max_abs_ratio_diff: The maximum allowed absolute value of the difference.
    `abs(1 - (o1 / o2)` and vice-versa if o2 == 0.0. Ignored if < 0.
    :param max_abs_diff: The maximum allowed absolute difference `abs(o1 - o2)`. Ignored if < 0.
    :return: Whether the two objects are almost equal.
    """
    if type(o1) != type(o2):
        return False

    composite_type_passed = False

    if hasattr(o1, '__slots__'):
        if len(o1.__slots__) != len(o2.__slots__):
            return False
        if any(not are_almost_equal(getattr(o1, s1), getattr(o2, s2),
                                    max_abs_ratio_diff, max_abs_diff)
            for s1, s2 in zip(sorted(o1.__slots__), sorted(o2.__slots__))):
            return False
        else:
            composite_type_passed = True

    if hasattr(o1, '__dict__'):
        if len(o1.__dict__) != len(o2.__dict__):
            return False
        if any(not are_almost_equal(k1, k2, max_abs_ratio_diff, max_abs_diff)
            or not are_almost_equal(v1, v2, max_abs_ratio_diff, max_abs_diff)
            for ((k1, v1), (k2, v2))
            in zip(sorted(o1.__dict__.items()), sorted(o2.__dict__.items()))
            if not k1.startswith('__')):  # avoid infinite loops
            return False
        else:
            composite_type_passed = True

    if isinstance(o1, dict):
        if len(o1) != len(o2):
            return False
        if any(not are_almost_equal(k1, k2, max_abs_ratio_diff, max_abs_diff)
            or not are_almost_equal(v1, v2, max_abs_ratio_diff, max_abs_diff)
            for ((k1, v1), (k2, v2)) in zip(sorted(o1.items()), sorted(o2.items()))):
            return False

    elif any(issubclass(o1.__class__, c) for c in (list, tuple, set)):
        if len(o1) != len(o2):
            return False
        if any(not are_almost_equal(v1, v2, max_abs_ratio_diff, max_abs_diff)
            for v1, v2 in zip(o1, o2)):
            return False

    elif isinstance(o1, float):
        if o1 == o2:
            return True
        else:
            if max_abs_ratio_diff > 0:  # if max_abs_ratio_diff < 0, max_abs_ratio_diff is ignored
                if o2 != 0:
                    if abs(1.0 - (o1 / o2)) > max_abs_ratio_diff:
                        return False
                else:  # if both == 0, we already returned True
                    if abs(1.0 - (o2 / o1)) > max_abs_ratio_diff:
                        return False
            if 0 < max_abs_diff < abs(o1 - o2):  # if max_abs_diff < 0, max_abs_diff is ignored
                return False
            return True

    else:
        if not composite_type_passed:
            return o1 == o2

    return True


class EqualityTest(unittest.TestCase):

    def test_floats(self) -> None:
        o1 = ('hi', 3, 3.4)
        o2 = ('hi', 3, 3.400001)
        self.assertTrue(are_almost_equal(o1, o2, 0.0001, 0.0001))
        self.assertFalse(are_almost_equal(o1, o2, 0.00000001, 0.00000001))

    def test_ratio_only(self):
        o1 = ['hey', 10000, 123.12]
        o2 = ['hey', 10000, 123.80]
        self.assertTrue(are_almost_equal(o1, o2, 0.01, -1))
        self.assertFalse(are_almost_equal(o1, o2, 0.001, -1))

    def test_diff_only(self):
        o1 = ['hey', 10000, 1234567890.12]
        o2 = ['hey', 10000, 1234567890.80]
        self.assertTrue(are_almost_equal(o1, o2, -1, 1))
        self.assertFalse(are_almost_equal(o1, o2, -1, 0.1))

    def test_both_ignored(self):
        o1 = ['hey', 10000, 1234567890.12]
        o2 = ['hey', 10000, 0.80]
        o3 = ['hi', 10000, 0.80]
        self.assertTrue(are_almost_equal(o1, o2, -1, -1))
        self.assertFalse(are_almost_equal(o1, o3, -1, -1))

    def test_different_lengths(self):
        o1 = ['hey', 1234567890.12, 10000]
        o2 = ['hey', 1234567890.80]
        self.assertFalse(are_almost_equal(o1, o2, 1, 1))

    def test_classes(self):
        class A:
            d = 12.3

            def __init__(self, a, b, c):
                self.a = a
                self.b = b
                self.c = c

        o1 = A(2.34, 'str', {1: 'hey', 345.23: [123, 'hi', 890.12]})
        o2 = A(2.34, 'str', {1: 'hey', 345.231: [123, 'hi', 890.121]})
        self.assertTrue(are_almost_equal(o1, o2, 0.1, 0.1))
        self.assertFalse(are_almost_equal(o1, o2, 0.0001, 0.0001))

        o2.hello = 'hello'
        self.assertFalse(are_almost_equal(o1, o2, -1, -1))

    def test_namedtuples(self):
        B = namedtuple('B', ['x', 'y'])
        o1 = B(3.3, 4.4)
        o2 = B(3.4, 4.5)
        self.assertTrue(are_almost_equal(o1, o2, 0.2, 0.2))
        self.assertFalse(are_almost_equal(o1, o2, 0.001, 0.001))

    def test_classes_with_slots(self):
        class C(object):
            __slots__ = ['a', 'b']

            def __init__(self, a, b):
                self.a = a
                self.b = b

        o1 = C(3.3, 4.4)
        o2 = C(3.4, 4.5)
        self.assertTrue(are_almost_equal(o1, o2, 0.3, 0.3))
        self.assertFalse(are_almost_equal(o1, o2, -1, 0.01))

    def test_dataclasses(self):
        @dataclass
        class D:
            s: str
            i: int
            f: float

        @dataclass
        class E:
            f2: float
            f4: str
            d: D

        o1 = E(12.3, 'hi', D('hello', 34, 20.01))
        o2 = E(12.1, 'hi', D('hello', 34, 20.0))
        self.assertTrue(are_almost_equal(o1, o2, -1, 0.4))
        self.assertFalse(are_almost_equal(o1, o2, -1, 0.001))

        o3 = E(12.1, 'hi', D('ciao', 34, 20.0))
        self.assertFalse(are_almost_equal(o2, o3, -1, -1))

    def test_ordereddict(self):
        o1 = OrderedDict({1: 'hey', 345.23: [123, 'hi', 890.12]})
        o2 = OrderedDict({1: 'hey', 345.23: [123, 'hi', 890.0]})
        self.assertTrue(are_almost_equal(o1, o2, 0.01, -1))
        self.assertFalse(are_almost_equal(o1, o2, 0.0001, -1))

0

self.assertEqual()たわごとがファンを襲ったときにそれが最も有益なままであるため、私はまだ使用します。あなたは丸めることによってそれをすることができます、例えば。

self.assertEqual(round_tuple((13.949999999999999, 1.121212), 2), (13.95, 1.12))

どこにround_tupleありますか

def round_tuple(t: tuple, ndigits: int) -> tuple:
    return tuple(round(e, ndigits=ndigits) for e in t)

def round_list(l: list, ndigits: int) -> list:
    return [round(e, ndigits=ndigits) for e in l]

Pythonのドキュメントによると、(参照https://stackoverflow.com/a/41407651/1031191をので、あなたは、13.94999999のような問題を丸めて逃げることができます) 13.94999999 == 13.95ですTrue


-1

別のアプローチは、たとえば各フロートを固定精度の文字列に変換することにより、データを同等の形式に変換することです。

def comparable(data):
    """Converts `data` to a comparable structure by converting any floats to a string with fixed precision."""
    if isinstance(data, (int, str)):
        return data
    if isinstance(data, float):
        return '{:.4f}'.format(data)
    if isinstance(data, list):
        return [comparable(el) for el in data]
    if isinstance(data, tuple):
        return tuple([comparable(el) for el in data])
    if isinstance(data, dict):
        return {k: comparable(v) for k, v in data.items()}

次に、次のことができます。

self.assertEquals(comparable(value1), comparable(value2))
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.