feat(detector): compare distinct ratio for historically unique columns (#94) - #97
Conversation
|
Thanks for picking this up. The approach is right, the ratio branch reads well, and it leaves the adaptive-history path alone, which is what I hoped for. Two things before this can go in. 1. It needs testsEvery PR here carries unit tests, and #94 asked for the regression case specifically. I wrote them against your branch so you don't have to start from scratch — please drop this into class TestCardinalityNearUniqueColumns:
"""Distinct ratio, not distinct count, for columns that were near-unique.
Regression for #94: deleting rows also drops the distinct count of a
unique `id` or timestamp column, so one volume drop used to fire a
cardinality warning per unique column on top of `volume_drop`.
"""
# A 1,000-row table whose `id` column is unique.
STORED_DIST = {"distinct_count": 1000}
STORED_VOL = {"row_count": 1000}
def test_volume_drop_on_unique_column_is_silent(self):
"""60% of rows deleted: the id column is still unique, so nothing fires."""
anomalies = detect_cardinality_anomalies(
"orders",
"id",
{"distinct_count": 400},
self.STORED_DIST,
{"row_count": 400},
self.STORED_VOL,
)
assert anomalies == []
def test_duplicates_on_unique_column_still_fire(self):
"""Same row count, half the distinct values: uniqueness broke, so it fires."""
anomalies = detect_cardinality_anomalies(
"orders",
"id",
{"distinct_count": 500},
self.STORED_DIST,
{"row_count": 1000},
self.STORED_VOL,
)
assert len(anomalies) == 1
assert anomalies[0]["severity"] == Severity.WARNING
assert "decreased" in anomalies[0]["message"]
assert "distinct ratio" in anomalies[0]["message"]
def test_low_cardinality_column_keeps_absolute_comparison(self):
"""A status column exploding 5 -> 605 must still be CRITICAL after a volume drop."""
anomalies = detect_cardinality_anomalies(
"users",
"plan",
{"distinct_count": 605},
{"distinct_count": 5},
{"row_count": 400},
{"row_count": 1000},
)
assert len(anomalies) == 1
assert anomalies[0]["severity"] == Severity.CRITICAL
assert "distinct values" in anomalies[0]["message"]
def test_falls_back_to_absolute_without_volume_profiles(self):
"""Callers that pass no volume profiles keep the previous behaviour."""
anomalies = detect_cardinality_anomalies(
"orders", "id", {"distinct_count": 400}, self.STORED_DIST
)
assert len(anomalies) == 1
assert anomalies[0]["severity"] == Severity.WARNING
def test_falls_back_to_absolute_on_zero_row_counts(self):
"""An empty stored profile can't yield a ratio; don't divide by zero."""
anomalies = detect_cardinality_anomalies(
"orders",
"id",
{"distinct_count": 400},
self.STORED_DIST,
{"row_count": 0},
{"row_count": 0},
)
assert len(anomalies) == 1
assert anomalies[0]["severity"] == Severity.WARNING
def test_column_just_below_the_unique_threshold_uses_absolute(self):
"""90% distinct is not near-unique, so the absolute path still applies."""
anomalies = detect_cardinality_anomalies(
"orders",
"customer_id",
{"distinct_count": 360},
{"distinct_count": 900},
{"row_count": 400},
{"row_count": 1000},
)
assert len(anomalies) == 1
assert "distinct values" in anomalies[0]["message"]All six pass on your branch. On 2. The CRITICAL branch on the ratio path can never runThe ratio branch only executes when
The thresholds were written for absolute counts, where 200% means "3x"; they don't carry over to a bounded ratio. Two ways out, your call:
3. Minor
Happy to review again as soon as you push. Good catch on keeping the fallback for missing and zero row counts. |
|
Thanks for the detailed review. I pushed commit
Validation on the branch:
One boundary detail: the supplied |
|
Both of your points land. Two small things: Boundary — you followed my test, and my test was the wrong one. On the warning-band case: NULLs in the denominator —
|
|
Thanks for catching both! I agree with using non-NULL rows as the denominator. I’ll update the threshold comparisons to >=, handle the floating-point boundary, and add regression tests for NULLs and uniqueness loss. I’ll also rerun the full suite and demo before pushing the follow-up. |
…oat boundaries (rbmuller#94) Use non-null rows (rows - null_count) as the distinct-ratio denominator so nulling values in a unique column does not read as a uniqueness loss. Make the ratio critical band inclusive (>= 50) to match the absolute path, and add a 1e-9 tolerance so exact 10%/50% boundaries do not fall just under on binary floats. Update the 50% regression case to CRITICAL and add NULL, boundary and partial-loss tests.
|
Verified on your branch: 445 tests green, Merging. Thanks for the careful back-and-forth on this one. |
Ships the near-unique cardinality fix from #97. The 1.0.3 release left the dbt install snippet in README.md and website/dbt-package.md at revision v1.0.2, so dbt users were told to install a stale tag. It is the second partial bump after v1.0.0 never reaching PyPI, and in both cases the checklist in CONTRIBUTING was the only guard. tests/test_release_versions.py now fails if pyproject, dbt_project.yml, both server.json fields or the dbt install snippets disagree with __version__; the bundle keeps its own guards in tests/test_mcpb.py. Against main it fails on exactly those two pins.
Summary
Closes #94. When the stored profile shows a column was near-unique (
distinct_count / row_count >= 0.95) compare the distinct ratio (distinct / rows) instead of the absolute distinct count. A 60% row deletion on a uniqueidcolumn then stays silent for cardinality (volume_drop still fires), while duplicates on a historically unique column still trigger warning or critical using the existing thresholds.Changes
src/scherlok/detector/cardinality.py: addUNIQUE_RATIO = 0.95with comment, extenddetect_cardinality_anomaliesto accept optionalcurrent_volandstored_vol, implement ratio branch with safe fallback for missing or zero row counts, message mentionsdistinct ratiowhen that path is used. Preserves adaptive history behaviour unchanged.src/scherlok/service.py: wirecurrent_volandstored_volintodetect_cardinality_anomaliescall.Tests
tests/test_detector.py43 passed unchanged.id(1000 rows 1000 distinct -> 400 rows 400 distinct) producesvolume_droponly, no cardinality.distinct ratioand 50% change.status(5 distinct) keeps absolute thresholds: stable 5->5 gives no anomaly, 5->500 gives CRITICAL withdistinct valuesmessage.Noneor0row counts give WARNING for 100->160 (60% change).Notes
CARDINALITY_WARNING_PCT=50andCRITICAL=200for ordinary columns and for ratio comparison.