Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions diffly/_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
from polars.datatypes import DataType, DataTypeClass

from diffly._utils import (
ABS_TOL_DEFAULT,
ABS_TOL_COMPARE_DEFAULT,
ABS_TOL_TEMPORAL_DEFAULT,
REL_TOL_DEFAULT,
REL_TOL_COMPARE_DEFAULT,
Side,
)

Expand Down Expand Up @@ -51,8 +51,8 @@ def condition_equal_columns(
dtype_left: pl.DataType,
dtype_right: pl.DataType,
max_list_length: int | None,
abs_tol: float = ABS_TOL_DEFAULT,
rel_tol: float = REL_TOL_DEFAULT,
abs_tol: float = ABS_TOL_COMPARE_DEFAULT,
rel_tol: float = REL_TOL_COMPARE_DEFAULT,
abs_tol_temporal: dt.timedelta = ABS_TOL_TEMPORAL_DEFAULT,
) -> pl.Expr:
"""Build an expression whether two columns are equal, depending on the columns' data
Expand Down
11 changes: 9 additions & 2 deletions diffly/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,15 @@ def capitalize_first(s: str) -> str:
return s[0].upper() + s[1:] if s else s


ABS_TOL_DEFAULT = 1e-08
REL_TOL_DEFAULT = 1e-05
# Comparison defaults match `polars.Expr.is_close` and Python's `math.isclose`; a diff
# tool should bias toward flagging differences.
ABS_TOL_COMPARE_DEFAULT = 0.0
REL_TOL_COMPARE_DEFAULT = 1e-09

# Testing defaults match `polars.testing.assert_frame_equal`.
ABS_TOL_TESTING_DEFAULT = 1e-08
REL_TOL_TESTING_DEFAULT = 1e-05
Comment thread
haydena7 marked this conversation as resolved.

ABS_TOL_TEMPORAL_DEFAULT = dt.timedelta(0)


Expand Down
14 changes: 9 additions & 5 deletions diffly/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
from diffly import compare_frames

from ._compat import typer
from ._utils import ABS_TOL_DEFAULT, ABS_TOL_TEMPORAL_DEFAULT, REL_TOL_DEFAULT
from ._utils import (
ABS_TOL_COMPARE_DEFAULT,
ABS_TOL_TEMPORAL_DEFAULT,
REL_TOL_COMPARE_DEFAULT,
)
from .metrics.change import DEFAULT_CHANGE_METRICS
from .metrics.data import DEFAULT_DATA_METRICS

Expand All @@ -34,15 +38,15 @@ def main(
abs_tol: Annotated[
float,
typer.Option(
help="Absolute tolerance for numerical comparisons. Default is 1e-08."
help="Absolute tolerance for numerical comparisons. Default is 0.0."
),
] = ABS_TOL_DEFAULT,
] = ABS_TOL_COMPARE_DEFAULT,
rel_tol: Annotated[
float,
typer.Option(
help="Relative tolerance for numerical comparisons. Default is 1e-05."
help="Relative tolerance for numerical comparisons. Default is 1e-09."
),
] = REL_TOL_DEFAULT,
] = REL_TOL_COMPARE_DEFAULT,
abs_tol_temporal: Annotated[
float,
typer.Option(
Expand Down
8 changes: 4 additions & 4 deletions diffly/comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
from ._conditions import condition_equal_columns, condition_equal_rows
from ._exceptions import PrimaryKeyError
from ._utils import (
ABS_TOL_DEFAULT,
ABS_TOL_COMPARE_DEFAULT,
ABS_TOL_TEMPORAL_DEFAULT,
REL_TOL_DEFAULT,
REL_TOL_COMPARE_DEFAULT,
Side,
get_select_columns,
is_primary_key,
Expand All @@ -40,8 +40,8 @@ def compare_frames(
/,
*,
primary_key: str | Sequence[str] | None = None,
abs_tol: float | Mapping[str, float] = ABS_TOL_DEFAULT,
rel_tol: float | Mapping[str, float] = REL_TOL_DEFAULT,
abs_tol: float | Mapping[str, float] = ABS_TOL_COMPARE_DEFAULT,
rel_tol: float | Mapping[str, float] = REL_TOL_COMPARE_DEFAULT,
abs_tol_temporal: dt.timedelta
| Mapping[str, dt.timedelta] = ABS_TOL_TEMPORAL_DEFAULT,
) -> DataFrameComparison:
Expand Down
12 changes: 6 additions & 6 deletions diffly/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
import polars as pl

from diffly._utils import (
ABS_TOL_DEFAULT,
ABS_TOL_TEMPORAL_DEFAULT,
REL_TOL_DEFAULT,
ABS_TOL_TESTING_DEFAULT,
REL_TOL_TESTING_DEFAULT,
Side,
)
from diffly.summary import WIDTH
Expand Down Expand Up @@ -90,8 +90,8 @@ def assert_collection_equal(
/,
*,
check_dtypes: bool = True,
abs_tol: float | Mapping[str, float] = ABS_TOL_DEFAULT,
rel_tol: float | Mapping[str, float] = REL_TOL_DEFAULT,
abs_tol: float | Mapping[str, float] = ABS_TOL_TESTING_DEFAULT,
rel_tol: float | Mapping[str, float] = REL_TOL_TESTING_DEFAULT,
abs_tol_temporal: dt.timedelta
| Mapping[str, dt.timedelta] = ABS_TOL_TEMPORAL_DEFAULT,
show_perfect_column_matches: bool = False,
Expand Down Expand Up @@ -237,8 +237,8 @@ def assert_frame_equal(
*,
primary_key: str | Sequence[str] | None = None,
check_dtypes: bool = True,
abs_tol: float | Mapping[str, float] = ABS_TOL_DEFAULT,
rel_tol: float | Mapping[str, float] = REL_TOL_DEFAULT,
abs_tol: float | Mapping[str, float] = ABS_TOL_TESTING_DEFAULT,
rel_tol: float | Mapping[str, float] = REL_TOL_TESTING_DEFAULT,
abs_tol_temporal: dt.timedelta
| Mapping[str, dt.timedelta] = ABS_TOL_TEMPORAL_DEFAULT,
show_perfect_column_matches: bool = False,
Expand Down
1 change: 1 addition & 0 deletions docs/guides/features/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Unlike `polars.testing.assert_frame_equal`, `diffly`'s version:
- Prints a comprehensive summary of all differences
- Supports tolerance-based comparisons for floating point and temporal values
- Allows mixing eager and lazy frames in the same comparison
- Uses the same default tolerances as `polars.testing.assert_frame_equal`, which are looser than those of `compare_frames` (see {doc}`tolerances`)

## Asserting equality of `dataframely` collections

Expand Down
12 changes: 9 additions & 3 deletions docs/guides/features/tolerances.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@
"source": [
"## Default behavior\n",
"\n",
"By default, `diffly` uses `abs_tol=1e-08` and `rel_tol=1e-05` for floating point comparisons, matching the defaults of `polars.testing.assert_frame_equal` (note these are looser than the defaults of `polars.Expr.is_close` and Python's `math.isclose`). Temporal types (dates, datetimes) are compared exactly (`abs_tol_temporal=0`). This means tiny floating point rounding is automatically ignored, but even a one-second timestamp difference will be flagged.\n",
"By default, `diffly` uses `abs_tol=0.0` and `rel_tol=1e-09` for floating point comparisons, matching `polars.Expr.is_close` and Python's `math.isclose`. This means tiny floating point rounding is ignored for nonzero values but flagged near zero. Temporal types (dates, datetimes) are compared exactly (`abs_tol_temporal=0`), so even one-second timestamp differences are flagged.\n",
"\n",
"```{note}\n",
"With `abs_tol=0.0`, two values that are both effectively zero but differ by cancellation noise (e.g., `0.0` and `1e-17`) are flagged as unequal. Following [PEP 485](https://peps.python.org/pep-0485/#absolute-tolerance-default), `diffly` does not guess a magnitude for you; set `abs_tol` to a value that is negligible at your data's scale.\n",
"```\n",
"\n",
"`diffly.testing` is an exception to these defaults: it uses the looser `abs_tol=1e-08` and `rel_tol=1e-05` to match `polars.testing.assert_frame_equal`. Consequently, `compare_frames(a, b).equal()` can be `False` where `assert_frame_equal(a, b)` passes.\n",
"\n",
"In our scenario, the `total` column has some values that differ only at the 10th decimal place due to how the totals were calculated in each system:"
]
Expand Down Expand Up @@ -327,7 +333,7 @@
},
{
"cell_type": "code",
"execution_count": 20,
"execution_count": null,
"id": "cell-13",
"metadata": {},
"outputs": [],
Expand All @@ -339,7 +345,7 @@
" df_current,\n",
" primary_key=\"transaction_id\",\n",
" abs_tol=defaultdict(lambda: 0, {\"total\": 0.01}),\n",
" rel_tol=defaultdict(lambda: 1e-05, {\"total\": 0}),\n",
" rel_tol=defaultdict(lambda: 1e-09, {\"total\": 0}),\n",
")"
]
},
Expand Down
16 changes: 16 additions & 0 deletions tests/test_assert_frame_equal.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@ def test_success_with_nan() -> None:
assert_frame_equal(df, df, primary_key="id")


@pytest.mark.parametrize(
("left_value", "right_value"),
[
(1.0, 1.0 + 1e-7), # within 1e-05 rel_tol, outside 1e-09
(0.0, 1e-17), # within 1e-08 abs_tol, outside 0.0
],
)
def test_compare_frames_flags_what_assert_frame_equal_tolerates(
left_value: float, right_value: float
) -> None:
left = pl.DataFrame({"id": [1], "value": [left_value]})
right = pl.DataFrame({"id": [1], "value": [right_value]})
assert not compare_frames(left, right, primary_key="id").equal()
assert_frame_equal(left, right, primary_key="id") # must not raise


def test_error_exposes_comparison() -> None:
# Arrange
left = pl.DataFrame({"id": [1, 2], "value": [10.0, 20.0]})
Expand Down
8 changes: 4 additions & 4 deletions tests/test_performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
from diffly import compare_frames
from diffly._conditions import condition_equal_columns
from diffly._utils import (
ABS_TOL_DEFAULT,
ABS_TOL_COMPARE_DEFAULT,
ABS_TOL_TEMPORAL_DEFAULT,
REL_TOL_DEFAULT,
REL_TOL_COMPARE_DEFAULT,
Side,
)

Expand Down Expand Up @@ -115,8 +115,8 @@ def test_eq_missing_not_slower_than_element_wise_for_list_columns() -> None:
dtype_left=df.schema[col_left],
dtype_right=df.schema[col_right],
max_list_length=list_len,
abs_tol=ABS_TOL_DEFAULT,
rel_tol=REL_TOL_DEFAULT,
abs_tol=ABS_TOL_COMPARE_DEFAULT,
rel_tol=REL_TOL_COMPARE_DEFAULT,
abs_tol_temporal=ABS_TOL_TEMPORAL_DEFAULT,
)
).to_series()
Expand Down