import cmath import math import numpy as np from numpy cimport import_array import_array() from pandas._libs.util cimport ( is_array, is_complex_object, is_real_number_object, ) from pandas.core.dtypes.common import is_dtype_equal from pandas.core.dtypes.missing import ( array_equivalent, isna, ) 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. .. versionadded:: 1.1.0 atol : float, default 1e-8 Absolute tolerance. .. versionadded:: 1.1.0 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. .. versionadded:: 1.1.0 """ 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, 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 not is_dtype_equal(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 isna(a) and isna(b): # TODO: Should require same-dtype NA? # nan / None comparison return True if isna(a) and not isna(b) or not isna(a) and isna(b): # boolean value of pd.NA is ambigous raise AssertionError(f"{a} != {b}") if a == b: # object comparison return True if is_real_number_object(a) and is_real_number_object(b): if array_equivalent(a, b, strict_nan=True): # inf comparison return True 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 array_equivalent(a, b, strict_nan=True): # inf comparison return True 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}")