Skip to content

Commit 3ad3d7e

Browse files
author
Joao Veiga
committed
ENH: Allow relative and/or absolute precision in assert_almost_equal
This commit makes `assert_almost_equal` accept both relative and absolute precision when comparing numbers, through two new keyword arguments: `rtol`, and `atol`, respectively. Under the hood, `_libs.testing.assert_almost_equal` is now calling `math.isclose`, instead of an adaptaion of [numpy.testing.assert_almost_equal](https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.testing.assert_almost_equal.html).
1 parent 8806ed7 commit 3ad3d7e

File tree

4 files changed

+237
-67
lines changed

4 files changed

+237
-67
lines changed

doc/source/whatsnew/v1.0.0.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,9 @@ Other enhancements
226226
- Added new writer for exporting Stata dta files in version 118, ``StataWriter118``. This format supports exporting strings containing Unicode characters (:issue:`23573`)
227227
- :meth:`Series.map` now accepts ``collections.abc.Mapping`` subclasses as a mapper (:issue:`29733`)
228228
- The ``pandas.datetime`` class is now deprecated. Import from ``datetime`` instead (:issue:`30296`)
229-
230-
229+
- :meth:`util.testing.assert_almost_equal` has new keyword argument ``check_low_values``
230+
for comparing numeric values by decimal places when ``check_less_precise=True``
231+
(:issue:`13357`).
231232

232233
Build Changes
233234
^^^^^^^^^^^^^

pandas/_libs/testing.pyx

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import math
2+
13
import numpy as np
24

35
from pandas.core.dtypes.missing import isna, array_equivalent
@@ -38,13 +40,6 @@ cdef bint is_dictlike(obj):
3840
return hasattr(obj, 'keys') and hasattr(obj, '__getitem__')
3941

4042

41-
cdef bint decimal_almost_equal(double desired, double actual, int decimal):
42-
# Code from
43-
# http://docs.scipy.org/doc/numpy/reference/generated
44-
# /numpy.testing.assert_almost_equal.html
45-
return abs(desired - actual) < (0.5 * 10.0 ** -decimal)
46-
47-
4843
cpdef assert_dict_equal(a, b, bint compare_keys=True):
4944
assert is_dictlike(a) and is_dictlike(b), (
5045
"Cannot compare dict objects, one or both is not dict-like"
@@ -63,7 +58,7 @@ cpdef assert_dict_equal(a, b, bint compare_keys=True):
6358

6459

6560
cpdef assert_almost_equal(a, b,
66-
check_less_precise=False,
61+
rtol=1e-9, atol=0.0,
6762
bint check_dtype=True,
6863
obj=None, lobj=None, robj=None):
6964
"""
@@ -73,25 +68,23 @@ cpdef assert_almost_equal(a, b,
7368
----------
7469
a : object
7570
b : object
76-
check_less_precise : bool or int, default False
77-
Specify comparison precision.
78-
5 digits (False) or 3 digits (True) after decimal points are
79-
compared. If an integer, then this will be the number of decimal
80-
points to compare
71+
rtol : float, default 1e-9
72+
Relative tolerance.
73+
atol : float, default 0.0
74+
Absolute tolerance.
8175
check_dtype: bool, default True
82-
check dtype if both a and b are np.ndarray
76+
check dtype if both a and b are np.ndarray.
8377
obj : str, default None
8478
Specify object name being compared, internally used to show
85-
appropriate assertion message
79+
appropriate assertion message.
8680
lobj : str, default None
8781
Specify left object name being compared, internally used to show
88-
appropriate assertion message
82+
appropriate assertion message.
8983
robj : str, default None
9084
Specify right object name being compared, internally used to show
91-
appropriate assertion message
85+
appropriate assertion message.
9286
"""
9387
cdef:
94-
int decimal
9588
double diff = 0.0
9689
Py_ssize_t i, na, nb
9790
double fa, fb
@@ -102,8 +95,6 @@ cpdef assert_almost_equal(a, b,
10295
if robj is None:
10396
robj = b
10497

105-
assert isinstance(check_less_precise, (int, bool))
106-
10798
if isinstance(a, dict) or isinstance(b, dict):
10899
return assert_dict_equal(a, b)
109100

@@ -161,8 +152,7 @@ cpdef assert_almost_equal(a, b,
161152

162153
for i in range(len(a)):
163154
try:
164-
assert_almost_equal(a[i], b[i],
165-
check_less_precise=check_less_precise)
155+
assert_almost_equal(a[i], b[i], rtol=rtol, atol=atol)
166156
except AssertionError:
167157
is_unequal = True
168158
diff += 1
@@ -194,24 +184,11 @@ cpdef assert_almost_equal(a, b,
194184
# inf comparison
195185
return True
196186

197-
if check_less_precise is True:
198-
decimal = 3
199-
elif check_less_precise is False:
200-
decimal = 5
201-
else:
202-
decimal = check_less_precise
203-
204187
fa, fb = a, b
205188

206-
# case for zero
207-
if abs(fa) < 1e-5:
208-
if not decimal_almost_equal(fa, fb, decimal):
209-
assert False, (f'(very low values) expected {fb:.5f} '
210-
f'but got {fa:.5f}, with decimal {decimal}')
211-
else:
212-
if not decimal_almost_equal(1, fb / fa, decimal):
213-
assert False, (f'expected {fb:.5f} but got {fa:.5f}, '
214-
f'with decimal {decimal}')
189+
if not math.isclose(fa, fb, rel_tol=rtol, abs_tol=atol):
190+
assert False, (f"expected {fb:.5f} but got {fa:.5f}, "
191+
f"with rtol={rtol}, atol={atol}")
215192
return True
216193

217194
raise AssertionError(f"{a} != {b}")

pandas/tests/util/test_assert_almost_equal.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,62 @@ def test_assert_almost_equal_numbers(a, b):
8080
_assert_almost_equal_both(a, b)
8181

8282

83-
@pytest.mark.parametrize("a,b", [(1.1, 1), (1.1, True), (1, 2), (1.0001, np.int16(1))])
83+
@pytest.mark.parametrize(
84+
"a,b",
85+
[
86+
(1.1, 1),
87+
(1.1, True),
88+
(1, 2),
89+
(1.0001, np.int16(1)),
90+
# The following two examples are not "almost equal" due to tol.
91+
(0.1, 0.1001),
92+
(0.0011, 0.0012),
93+
],
94+
)
8495
def test_assert_not_almost_equal_numbers(a, b):
8596
_assert_not_almost_equal_both(a, b)
8697

8798

99+
@pytest.mark.parametrize(
100+
"a,b",
101+
[
102+
(1.1, 1.1),
103+
(1.1, 1.100001),
104+
(1.1, 1.1001),
105+
(0.000001, 0.000005),
106+
(1000.0, 1000.0005),
107+
# Testing this example, as per #13357
108+
(0.000011, 0.000012),
109+
],
110+
)
111+
def test_assert_almost_equal_numbers_atol(a, b):
112+
# Equivalent to the deprecated check_less_precise=True
113+
_assert_almost_equal_both(a, b, atol=1e-3)
114+
115+
116+
@pytest.mark.parametrize("a,b", [(1.1, 1.11), (0.1, 0.101), (0.000011, 0.001012)])
117+
def test_assert_not_almost_equal_numbers_atol(a, b):
118+
_assert_not_almost_equal_both(a, b, atol=1e-3)
119+
120+
121+
@pytest.mark.parametrize(
122+
"a,b",
123+
[
124+
(1.1, 1.1),
125+
(1.1, 1.100001),
126+
(1.1, 1.1001),
127+
(0.000001, 0.000005),
128+
(1000.0, 1000.0005),
129+
(0.000011, 0.000012),
130+
# These 2 examples pass because we're using relative tolerance
131+
(1.1, 1.11),
132+
(0.1, 0.101),
133+
],
134+
)
135+
def test_assert_almost_equal_numbers_rtol(a, b):
136+
_assert_almost_equal_both(a, b, rtol=.05)
137+
138+
88139
@pytest.mark.parametrize("a,b", [(0, 0), (0, 0.0), (0, np.float64(0)), (0.000001, 0)])
89140
def test_assert_almost_equal_numbers_with_zeros(a, b):
90141
_assert_almost_equal_both(a, b)
@@ -237,7 +288,7 @@ def test_assert_almost_equal_object():
237288

238289

239290
def test_assert_almost_equal_value_mismatch():
240-
msg = "expected 2\\.00000 but got 1\\.00000, with decimal 5"
291+
msg = "expected 2\\.00000 but got 1\\.00000, with rtol=1e-09, atol=1e-05"
241292

242293
with pytest.raises(AssertionError, match=msg):
243294
tm.assert_almost_equal(1, 2)

0 commit comments

Comments
 (0)