From 36514eafa4e12573ee72004376c3b560a5a450bc Mon Sep 17 00:00:00 2001 From: SkyeAv Date: Fri, 4 Sep 2026 15:14:40 -0700 Subject: [PATCH] feat(lib): lower the minimum number_of_cases threshold from 25 to 10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release-mode case-count gate for `applied_to_treat` edges now requires 10 cases instead of 25, so release builds retain small-cohort treatment evidence that earlier releases dropped. ### Change - **Threshold:** the `drop_low_number_of_cases` default in `src/tablassert/lib.py` moves from `25.0` to `10.0`; edges with `number_of_cases >= 10` (and nulls, as before) are kept, `< 10` dropped. - **Scope unchanged:** gating (`--release` *and* `applied_to_treat`), null handling, non-numeric tolerance, and `significance`-phase placement are untouched — only the boundary moves. - **Tests:** the boundary test now exercises 9/10/11, and `test_drop_low_number_of_cases_keeps_non_numeric_cells` uses `9` for its below-threshold row (`10` is now kept). - **Changelog:** adds an `Unreleased` / `Changed` entry to `CHANGELOG.md`. ### Testing - `uv run pytest tests/test_lib.py -k number_of_cases -q --no-cov` → `8 passed`. - `make test` → `1253 passed, 3 skipped`. - `make lint` and `uv run ruff format --check .` → all clean. --- CHANGELOG.md | 5 +++++ src/tablassert/lib.py | 2 +- tests/test_lib.py | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09a0112..3922f12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project are documented in this file. +## Unreleased + +### Changed +- **The release-mode minimum `number_of_cases` threshold for `applied_to_treat` edges is now 10 instead of 25.** `drop_low_number_of_cases` keeps edges whose case count is >= 10 (nulls kept, as before); release builds now ship `applied_to_treat` edges backed by 10-24 cases that earlier releases dropped. Gating (`--release` *and* `applied_to_treat`), null handling, non-numeric tolerance, and `significance`-phase placement are unchanged. + ## 16.6.2 - 2026-09-04 ### Fixed diff --git a/src/tablassert/lib.py b/src/tablassert/lib.py index 31f82d6..c0892c7 100644 --- a/src/tablassert/lib.py +++ b/src/tablassert/lib.py @@ -1058,7 +1058,7 @@ def drop_zero_effect_size(lf: pl.LazyFrame, col: str = "effect_size") -> pl.Lazy return lf.filter(pl.col(col).cast(pl.Float64, strict=False).ne_missing(0.0)) -def drop_low_number_of_cases(lf: pl.LazyFrame, col: str = "number_of_cases", threshold: float = 25.0) -> pl.LazyFrame: +def drop_low_number_of_cases(lf: pl.LazyFrame, col: str = "number_of_cases", threshold: float = 10.0) -> pl.LazyFrame: """Drop release-mode ``applied_to_treat`` edges whose number of cases is below the threshold. Args: diff --git a/tests/test_lib.py b/tests/test_lib.py index 95b93c9..081037b 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -1309,11 +1309,11 @@ def test_drop_zero_effect_size_noop_without_column() -> None: def test_drop_low_number_of_cases_removes_below_threshold_keeps_at_threshold_and_nulls() -> None: - """drop_low_number_of_cases drops case counts under 25 while keeping 25+ and nulls.""" - lf: pl.LazyFrame = pl.DataFrame({"subject": ["a", "b", "c", "d", "e"], "number_of_cases": [24, 25, 26, None, 0]}).lazy() + """drop_low_number_of_cases drops case counts under 10 while keeping 10+ and nulls.""" + lf: pl.LazyFrame = pl.DataFrame({"subject": ["a", "b", "c", "d", "e"], "number_of_cases": [9, 10, 11, None, 0]}).lazy() result: pl.DataFrame = drop_low_number_of_cases(lf).collect() assert list(result["subject"]) == ["b", "c", "d"] - assert list(result["number_of_cases"]) == [25, 26, None] + assert list(result["number_of_cases"]) == [10, 11, None] def test_drop_low_number_of_cases_noop_without_column() -> None: @@ -1328,7 +1328,7 @@ def test_drop_low_number_of_cases_keeps_non_numeric_cells() -> None: """drop_low_number_of_cases keeps non-numeric cells (a header row read as data) instead of crashing on a strict cast.""" # The csv op reads sources with has_header=False, so a TSV's header row flows # through as a data row; the strict cast raised InvalidOperationError on it. - lf: pl.LazyFrame = pl.DataFrame({"subject": ["hdr", "a", "b", "c"], "number_of_cases": ["number_of_cases", "30", "10", None]}).lazy() + lf: pl.LazyFrame = pl.DataFrame({"subject": ["hdr", "a", "b", "c"], "number_of_cases": ["number_of_cases", "30", "9", None]}).lazy() result: pl.DataFrame = drop_low_number_of_cases(lf).collect() assert list(result["subject"]) == ["hdr", "a", "c"]