diff --git a/faircode/report.py b/faircode/report.py index 7542ea5..f8081db 100644 --- a/faircode/report.py +++ b/faircode/report.py @@ -118,6 +118,12 @@ def _delta(n: int | None) -> str: return "not available" if n is None else f"{n:+d}" +def _strip_neg_zero(val: float, dp: int = 1) -> float: + """Return 0.0 when ``val`` rounds to zero at ``dp`` decimals, so a tiny + negative value does not render as a misleading ``-0.0``.""" + return 0.0 if round(val, dp) == 0 else val + + def _dataset_score_line(dataset: dict) -> str: if dataset["overall_score"] is None: return f"{dataset['n_rows']:,} rows · score not measured" @@ -156,7 +162,8 @@ def compare_to_terminal(cmp: dict) -> str: tag = {"appeared": " (appeared)", "disappeared": " (disappeared)", "shifted": ""}[g["status"]] add(f" {g['label'][:18]:<18} {g['share_a'] * 100:5.1f}% → " - f"{g['share_b'] * 100:5.1f}% ({g['share_delta'] * 100:+5.1f} pp){tag}") + f"{g['share_b'] * 100:5.1f}% " + f"({_strip_neg_zero(g['share_delta'] * 100):+5.1f} pp){tag}") if len(cd["groups"]) > DISPLAY_GROUPS: add(f" … and {len(cd['groups']) - DISPLAY_GROUPS} more groups") add("") @@ -345,6 +352,7 @@ def esc(s) -> str: return html.escape(str(s)) def signed(val: float | int, dp: int = 1) -> str: + val = _strip_neg_zero(val, dp) prefix = "+" if val > 0 else "" return f"{prefix}{val:.{dp}f}" diff --git a/tests/test_report.py b/tests/test_report.py index 51f4f8e..ea338b4 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -361,3 +361,31 @@ def test_compare_reports_unmeasured_score_without_formatting_none(): assert "Overall score change: not available" in terminal_out assert "score change not available" in html_out assert "None" not in html_out + + +def test_compare_does_not_render_negative_zero_share_delta(): + """A tiny negative share_delta that rounds to 0.0 pp must not print as + '-0.0 pp' in either the terminal or the HTML compare report.""" + result = { + "score_delta": 0, + "a": {"name": "A", "overall_score": 100, "n_rows": 10000, "grade": "A"}, + "b": {"name": "B", "overall_score": 100, "n_rows": 10000, "grade": "A"}, + "added_dimensions": [], "removed_dimensions": [], "flags": [], + "dimensions": [ + { + "name": "Gender", "kind": "Demographic", "drift_level": "none", + "psi": 0.0, "tvd": 0.0, + "dimension_score_a": 100, "dimension_score_b": 100, + "dimension_score_delta": 0, + "groups": [ + {"label": "Female", "status": "shifted", + "share_a": 0.5, "share_b": 0.4999, "share_delta": -0.0001}, + {"label": "Male", "status": "shifted", + "share_a": 0.5, "share_b": 0.5001, "share_delta": 0.0001}, + ], + } + ], + } + + assert "-0.0 pp" not in compare_to_terminal(result) + assert "-0.0 pp" not in compare_to_html(result)