Fix cardinality judged against sample size instead of real event volume

classify_field_shape() received a deduplicated set of observed values with
no real event-volume context, so cardinality was always measured against
itself — trivially 100%, making any field with more than ~16 distinct
values look "high cardinality" no matter how large the real traffic volume
was. This misclassified genuinely low-cardinality fields (e.g. username: 82
distinct values across 1,601 events) as free text, which the new shape-veto
work then surfaced as false-positive "remove this field" warnings in
production. Fixed by passing real event coverage as an explicit
total_events parameter; omitting it keeps prior behavior for other callers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-23 22:02:44 +02:00
parent 83ffef3570
commit caa37de98e
5 changed files with 61 additions and 6 deletions

View File

@@ -63,6 +63,25 @@ class FieldShapesTests(unittest.TestCase):
with_name = classify_field_shape(values, field_name="srcip")
self.assertLessEqual(with_name.confidence - without_name.confidence, 15)
def test_deduplicated_sample_alone_misjudges_cardinality_without_total_events(self):
# sample_values is commonly a deduplicated set (callers cap distinct values
# remembered per field), so its own length can't stand in for real event
# volume: 82 distinct plain usernames look "high cardinality" against
# themselves even though they're a tiny fraction of a much larger stream.
values = [f"user{n}" for n in range(82)]
without_total = classify_field_shape(values, field_name="username")
self.assertEqual(without_total.primary_shape, "free_text")
def test_total_events_corrects_cardinality_judgement_for_low_cardinality_field(self):
values = [f"user{n}" for n in range(82)]
result = classify_field_shape(values, field_name="username", total_events=880)
self.assertEqual(result.primary_shape, "categorical_low_cardinality")
def test_total_events_does_not_rescue_genuinely_high_cardinality_field(self):
values = [f"user{n}" for n in range(82)]
result = classify_field_shape(values, field_name="username", total_events=95)
self.assertEqual(result.primary_shape, "free_text")
def test_empty_values_return_zero_confidence(self):
result = classify_field_shape([], field_name="whatever")
self.assertEqual(result.confidence, 0)