From 42ee6d931f7056559a1a56837c16070b8148e9af Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Sat, 25 Jul 2026 08:13:09 -0700 Subject: [PATCH] fix: keep small p-values from rounding to zero in correlate correlate() rounded p to the same number of decimals as r, so a real p-value like 0.0001 was reported as 0.0 at the default decimals=2, which reads as exactly zero rather than very small. p-values now round via _round_p(), which falls back to two significant digits when plain rounding would collapse a non-zero value to 0. r, NaN handling, and decimals=None are unchanged. Adds a regression test covering both the Series-pair and DataFrame matrix return paths. Closes #33 --- percentify/stats.py | 20 ++++++++++++++++++-- tests/test_inferential.py | 12 ++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/percentify/stats.py b/percentify/stats.py index 8741c31..f8b1eb7 100644 --- a/percentify/stats.py +++ b/percentify/stats.py @@ -20,6 +20,22 @@ def _round(value: float, decimals: Optional[int]) -> float: return round(value, decimals) +def _round_p(value: float, decimals: Optional[int]) -> float: + """Round a p-value without collapsing a small but non-zero result to 0. + + Plain rounding turns p=0.0001 into 0.0 at the default 2 decimals, which + reads as "exactly zero" instead of "very small". When that happens, keep + enough decimals to show the first two significant digits instead. + """ + if decimals is None or not np.isfinite(value): + return _round(value, decimals) + rounded = round(value, decimals) + if rounded != 0 or value == 0: + return rounded + magnitude = int(np.floor(np.log10(abs(value)))) + return round(value, -magnitude + 1) + + def _is_polars(obj) -> bool: """True if obj is a polars DataFrame/Series, without importing polars.""" return type(obj).__module__.split(".", 1)[0] == "polars" @@ -586,7 +602,7 @@ def correlate(a, b=None, method: str = "pearson", decimals: Optional[int] = 2): _warn("correlate needs at least 3 complete numeric pairs. Returning NaN.") return (float("nan"), float("nan")) r, p = corr_fn(pair["a"].to_numpy(), pair["b"].to_numpy()) - return (_round(float(r), decimals), _round(float(p), decimals)) + return (_round(float(r), decimals), _round_p(float(p), decimals)) if not isinstance(a, pd.DataFrame): raise TypeError(f"correlate expects a Series or DataFrame, got {type(a).__name__}.") @@ -606,7 +622,7 @@ def correlate(a, b=None, method: str = "pearson", decimals: Optional[int] = 2): if len(pair) < 3 or np.std(x) == 0 or np.std(y) == 0: continue r, p = corr_fn(x, y) - rows.append((c1, c2, _round(float(r), decimals), _round(float(p), decimals))) + rows.append((c1, c2, _round(float(r), decimals), _round_p(float(p), decimals))) result = pd.DataFrame(rows, columns=["feature_1", "feature_2", "r", "p"]) if result.empty: diff --git a/tests/test_inferential.py b/tests/test_inferential.py index e689255..8ca859c 100644 --- a/tests/test_inferential.py +++ b/tests/test_inferential.py @@ -46,6 +46,18 @@ def test_correlate_single_numeric_column_warns(): assert result.empty +def test_correlate_small_p_value_not_rounded_to_zero(): + np.random.seed(0) + x = pd.Series(np.random.randn(200)) + y = pd.Series(x * 0.6 + np.random.randn(200) * 0.5) + + r, p = correlate(x, y) + assert p > 0 + + result = correlate(pd.DataFrame({"a": x, "b": y})) + assert (result["p"] > 0).all() + + # ===== skew_report ===== def test_skew_report_columns():