You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
222 lines
6.3 KiB
222 lines
6.3 KiB
import cmath
|
|
import math
|
|
import warnings
|
|
|
|
import numpy as np
|
|
|
|
from numpy cimport import_array
|
|
|
|
import_array()
|
|
|
|
from pandas._libs.missing cimport (
|
|
checknull,
|
|
is_matching_na,
|
|
)
|
|
from pandas._libs.util cimport (
|
|
is_array,
|
|
is_complex_object,
|
|
is_real_number_object,
|
|
)
|
|
|
|
from pandas.util._exceptions import find_stack_level
|
|
|
|
from pandas.core.dtypes.missing import array_equivalent
|
|
|
|
|
|
cdef bint isiterable(obj):
|
|
return hasattr(obj, "__iter__")
|
|
|
|
|
|
cdef bint has_length(obj):
|
|
return hasattr(obj, "__len__")
|
|
|
|
|
|
cdef bint is_dictlike(obj):
|
|
return hasattr(obj, "keys") and hasattr(obj, "__getitem__")
|
|
|
|
|
|
cpdef assert_dict_equal(a, b, bint compare_keys=True):
|
|
assert is_dictlike(a) and is_dictlike(b), (
|
|
"Cannot compare dict objects, one or both is not dict-like"
|
|
)
|
|
|
|
a_keys = frozenset(a.keys())
|
|
b_keys = frozenset(b.keys())
|
|
|
|
if compare_keys:
|
|
assert a_keys == b_keys
|
|
|
|
for k in a_keys:
|
|
assert_almost_equal(a[k], b[k])
|
|
|
|
return True
|
|
|
|
|
|
cpdef assert_almost_equal(a, b,
|
|
rtol=1.e-5, atol=1.e-8,
|
|
bint check_dtype=True,
|
|
obj=None, lobj=None, robj=None, index_values=None):
|
|
"""
|
|
Check that left and right objects are almost equal.
|
|
|
|
Parameters
|
|
----------
|
|
a : object
|
|
b : object
|
|
rtol : float, default 1e-5
|
|
Relative tolerance.
|
|
atol : float, default 1e-8
|
|
Absolute tolerance.
|
|
check_dtype: bool, default True
|
|
check dtype if both a and b are np.ndarray.
|
|
obj : str, default None
|
|
Specify object name being compared, internally used to show
|
|
appropriate assertion message.
|
|
lobj : str, default None
|
|
Specify left object name being compared, internally used to show
|
|
appropriate assertion message.
|
|
robj : str, default None
|
|
Specify right object name being compared, internally used to show
|
|
appropriate assertion message.
|
|
index_values : ndarray, default None
|
|
Specify shared index values of objects being compared, internally used
|
|
to show appropriate assertion message.
|
|
|
|
"""
|
|
cdef:
|
|
double diff = 0.0
|
|
Py_ssize_t i, na, nb
|
|
double fa, fb
|
|
bint is_unequal = False, a_is_ndarray, b_is_ndarray
|
|
str first_diff = ""
|
|
|
|
if lobj is None:
|
|
lobj = a
|
|
if robj is None:
|
|
robj = b
|
|
|
|
if isinstance(a, set) or isinstance(b, set):
|
|
assert a == b, f"{a} != {b}"
|
|
return True
|
|
|
|
if isinstance(a, dict) or isinstance(b, dict):
|
|
return assert_dict_equal(a, b)
|
|
|
|
if isinstance(a, str) or isinstance(b, str):
|
|
assert a == b, f"{a} != {b}"
|
|
return True
|
|
|
|
a_is_ndarray = is_array(a)
|
|
b_is_ndarray = is_array(b)
|
|
|
|
if obj is None:
|
|
if a_is_ndarray or b_is_ndarray:
|
|
obj = "numpy array"
|
|
else:
|
|
obj = "Iterable"
|
|
|
|
if isiterable(a):
|
|
|
|
if not isiterable(b):
|
|
from pandas._testing import assert_class_equal
|
|
|
|
# classes can't be the same, to raise error
|
|
assert_class_equal(a, b, obj=obj)
|
|
|
|
assert has_length(a) and has_length(b), (
|
|
f"Can't compare objects without length, one or both is invalid: ({a}, {b})"
|
|
)
|
|
|
|
if a_is_ndarray and b_is_ndarray:
|
|
na, nb = a.size, b.size
|
|
if a.shape != b.shape:
|
|
from pandas._testing import raise_assert_detail
|
|
raise_assert_detail(
|
|
obj, f"{obj} shapes are different", a.shape, b.shape)
|
|
|
|
if check_dtype and a.dtype != b.dtype:
|
|
from pandas._testing import assert_attr_equal
|
|
assert_attr_equal("dtype", a, b, obj=obj)
|
|
|
|
if array_equivalent(a, b, strict_nan=True):
|
|
return True
|
|
|
|
else:
|
|
na, nb = len(a), len(b)
|
|
|
|
if na != nb:
|
|
from pandas._testing import raise_assert_detail
|
|
|
|
# if we have a small diff set, print it
|
|
if abs(na - nb) < 10:
|
|
r = list(set(a) ^ set(b))
|
|
else:
|
|
r = None
|
|
|
|
raise_assert_detail(obj, f"{obj} length are different", na, nb, r)
|
|
|
|
for i in range(len(a)):
|
|
try:
|
|
assert_almost_equal(a[i], b[i], rtol=rtol, atol=atol)
|
|
except AssertionError:
|
|
is_unequal = True
|
|
diff += 1
|
|
if not first_diff:
|
|
first_diff = (
|
|
f"At positional index {i}, first diff: {a[i]} != {b[i]}"
|
|
)
|
|
|
|
if is_unequal:
|
|
from pandas._testing import raise_assert_detail
|
|
msg = (f"{obj} values are different "
|
|
f"({np.round(diff * 100.0 / na, 5)} %)")
|
|
raise_assert_detail(
|
|
obj, msg, lobj, robj, first_diff=first_diff, index_values=index_values
|
|
)
|
|
|
|
return True
|
|
|
|
elif isiterable(b):
|
|
from pandas._testing import assert_class_equal
|
|
|
|
# classes can't be the same, to raise error
|
|
assert_class_equal(a, b, obj=obj)
|
|
|
|
if checknull(a):
|
|
# nan / None comparison
|
|
if is_matching_na(a, b, nan_matches_none=False):
|
|
return True
|
|
elif checknull(b):
|
|
# GH#18463
|
|
warnings.warn(
|
|
f"Mismatched null-like values {a} and {b} found. In a future "
|
|
"version, pandas equality-testing functions "
|
|
"(e.g. assert_frame_equal) will consider these not-matching "
|
|
"and raise.",
|
|
FutureWarning,
|
|
stacklevel=find_stack_level(),
|
|
)
|
|
return True
|
|
raise AssertionError(f"{a} != {b}")
|
|
elif checknull(b):
|
|
raise AssertionError(f"{a} != {b}")
|
|
|
|
if a == b:
|
|
# object comparison
|
|
return True
|
|
|
|
if is_real_number_object(a) and is_real_number_object(b):
|
|
fa, fb = a, b
|
|
|
|
if not math.isclose(fa, fb, rel_tol=rtol, abs_tol=atol):
|
|
assert False, (f"expected {fb:.5f} but got {fa:.5f}, "
|
|
f"with rtol={rtol}, atol={atol}")
|
|
return True
|
|
|
|
if is_complex_object(a) and is_complex_object(b):
|
|
if not cmath.isclose(a, b, rel_tol=rtol, abs_tol=atol):
|
|
assert False, (f"expected {b:.5f} but got {a:.5f}, "
|
|
f"with rtol={rtol}, atol={atol}")
|
|
return True
|
|
|
|
raise AssertionError(f"{a} != {b}")
|
|
|