float 컬렉션에 대한 Python 단위 테스트의 assertAlmostEqual


81

assertAlmostEqual (X, Y) 에있어서 파이썬 유닛 테스트 워크 시험 여부 xy정도가 플로트 가정하에 동일하다.

문제 assertAlmostEqual()는 플로트에서만 작동한다는 것입니다. 나는 assertAlmostEqual()수레 목록, 수레 세트, 수레 사전, 수레 튜플, 수레 튜플 목록, 수레 목록 세트 등에서 작동 하는 방법을 찾고 있습니다 .

예를 들어 보자 x = 0.1234567890, y = 0.1234567891. x그리고 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()은 동일한 기능을 가져야합니다.

  • 두 목록이 정확히 같은 순서로 거의 동일한 숫자를 가지고 있으면 거의 동일한 것으로 간주됩니다. 공식적으로 for i in range(n): self.assertAlmostEqual(list1[i], list2[i]).

  • 마찬가지로, 두 세트가 거의 동일한 목록으로 변환 될 수 있다면 (각 세트에 순서를 할당하여) 거의 동일한 것으로 간주됩니다.

  • 유사하게, 각 사전의 키 세트가 다른 사전의 키 세트와 거의 같고 거의 동일한 키 쌍마다 해당하는 거의 동일한 값이있는 경우 두 사전은 거의 동일한 것으로 간주됩니다.

  • 일반적으로 : 서로 거의 동일한 일부 부동 소수점을 제외하면 두 컬렉션이 같으면 거의 같다고 생각합니다. 즉, 나는 정말로 객체를 비교하고 싶지만 도중에 부동 소수점을 비교할 때 낮은 (사용자 정의) 정밀도로 비교하고 싶습니다.


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

하지만 그것은 시작입니다. 또한 사전, 컬렉션 등을 비교할 수 있도록 수정할 수있는 소스 코드에 액세스 할 수 있습니다. np.testing.assert_equal예를 들어 딕셔너리를 인수로 인식합니다 ==.
Pierre GM

물론 @BrenBarn이 언급했듯이 세트를 비교할 때 여전히 문제가 발생합니다.
Pierre GM

의 현재 문서에서는 , 또는 대신 assert_array_almost_equal사용 을 권장 합니다. assert_allcloseassert_array_almost_equal_nulpassert_array_max_ulp
phunehehe

10

파이썬 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)함수를 구현 한 방법은 다음과 같습니다 .

먼저 비교해야하는 객체 ( firstsecond)를 복제하되 정확한 사본을 만들지는 마십시오. 객체 내부에서 만나는 부동 소수점의 중요하지 않은 소수 자릿수를 잘라냅니다.

이제 당신의 사본을 가지고 firstsecond하찮은 진수가 사라있는 위해를 바로 비교 firstsecond사용하여 ==연산자를.

cut_insignificant_digits_recursively(obj, places)복제 obj하지만 places원래의 각 float의 최상위 소수 자릿수 만 남겨 두는 함수 가 있다고 가정 해 봅시다 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에서 확인할 수 있습니다 . 개선 및 버그 수정을 환영합니다.


수레를 비교하는 대신 문자열을 비교하고 있습니까? 좋아요 ...하지만 공통 형식을 설정하는 것이 더 쉬울까요? 처럼 fmt="{{0:{0}f}}".format(decimals),이 fmt형식을 사용 하여 수레를 "문자열 화"하시겠습니까?
Pierre GM

1
이것은 멋져 보이지만 작은 요점 : places유효 숫자의 수가 아닌 소수 자릿수를 제공합니다. 예를 들어, 비교 1024.1231023.999중요한 3은 동일 반환해야하지만, 3 소수점에 그들은 아니에요.
Rodney Richardson

1
@pir, 라이센스는 실제로 정의되지 않았습니다. 라이선스를 선택 / 추가 할 시간이 없지만 사용 / 수정 권한을 부여하는 이 문제 에서 snalile의 답변을 참조하십시오 . 공유해 주셔서 감사합니다, BTW.
Jérôme

1
@RodneyRichardson는, 그래이처럼 소수점 이하 자릿수입니다 assertAlmostEqual : "이 방법이 아니라 유효 숫자 (예 : 라운드 () 함수와 같은) 소수 자릿수의 지정된 수에 값을 반올림하는 것으로."
Jérôme

2
@ Jérôme, 의견 주셔서 감사합니다. 방금 MIT 라이센스를 추가했습니다.
snakile

5

numpy패키지를 사용해도 괜찮다 numpy.testingassert_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.00001 거의 1.00002와 동일한 지 어떤지를 판정합니다. 기본적으로 거의 같지는 않지만 (기본 정밀도는 소수점 이하 7 자리이기 때문에) 거의 같을만큼 충분히 작은 값입니다 places.
snakile

1
@BrenBarn : IMO, floatdict에서 유형의 키를 사용 하는 것은 명백한 이유로 권장하지 않아야합니다 (허용되지 않을 수도 있음). dict의 대략적인 동등성은 값만을 기반으로해야합니다. 테스트 프레임 워크 float는 키 의 잘못된 사용에 대해 걱정할 필요가 없습니다 . 세트의 경우 비교 전에 정렬 할 수 있으며 정렬 된 목록을 비교할 수 있습니다.
최대

2

직접 구현해야 할 수도 있지만 목록과 집합은 동일한 방식으로 반복 될 수 있지만 사전은 다른 이야기이고 값이 아닌 키를 반복하며 세 번째 예는 저에게 약간 모호해 보입니다. 세트 내의 각 값 또는 각 세트의 각 값을 비교합니다.

여기에 간단한 코드 스 니펫이 있습니다.

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))

감사합니다.이 솔루션은 목록과 튜플에 대해서는 정확하지만 다른 유형의 컬렉션 (또는 중첩 된 컬렉션)에는 적합하지 않습니다. 질문에 추가 한 설명을 참조하십시오. 이제 제 의도가 분명해 졌으면합니다. 숫자가 매우 정확하게 측정되지 않는 세계에서 두 세트가 동일하다고 간주된다면 거의 동일합니다.
snakile

1

이 답변 중 어느 것도 나를 위해 작동하지 않습니다. 다음 코드는 파이썬 컬렉션, 클래스, 데이터 클래스 및 명명 된 튜플에 대해 작동합니다. 나는 뭔가를 잊었을 수도 있지만 지금까지 이것은 나를 위해 작동합니다.

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]

파이썬 문서에 따르면 (참조 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.