Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions rulechef/ranking.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,19 @@ def prune_harmful_rules(
rules: list[Rule],
report: RankingReport,
min_marginal_f1: float = 0.0,
min_support: int = 1,
) -> tuple[list[Rule], list[Rule]]:
"""Split rules into (kept, dropped) based on marginal contribution.
"""Split rules into (kept, dropped) based on marginal contribution and support.

A rule is dropped when its marginal F1 is known and below
min_marginal_f1 — i.e. the ensemble measurably does better without it.
Rules without ablation data are always kept.

Dead rules are dropped too: a rule whose validated_support is below
min_support never fired on the eval split, so its marginal F1 is exactly
0.0 and the threshold above can never catch it (issue #4). Pass
min_support=0 to keep zero-support rules.

Returns:
Tuple of (kept_rules, dropped_rules).
"""
Expand All @@ -200,8 +206,10 @@ def prune_harmful_rules(
for r in report.rankings
if r.marginal_f1 is not None and r.marginal_f1 < min_marginal_f1
}
kept = [r for r in rules if r.id not in harmful_ids]
dropped = [r for r in rules if r.id in harmful_ids]
dead_ids = {r.id for r in rules if r.validated_support < min_support}
drop = harmful_ids | dead_ids
kept = [r for r in rules if r.id not in drop]
dropped = [r for r in rules if r.id in drop]
return kept, dropped


Expand Down
17 changes: 17 additions & 0 deletions tests/test_ranking.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,20 @@ def test_keeps_rules_without_ablation_data(self):
kept, dropped = prune_harmful_rules(rules, report)
assert len(kept) == 1
assert not dropped

def test_prunes_dead_zero_support_rule(self):
rules = [
_rule("exchange", "exchange_rate", r"exchange rate"),
_rule("dead", "card_arrival", r"zebra unicorn"), # never fires on _dataset()
]
report = rank_rules(rules, _dataset(), _apply_rules_fn(), compute_marginal=True)
kept, dropped = prune_harmful_rules(rules, report)
assert [r.id for r in dropped] == ["dead"]
assert {r.id for r in kept} == {"exchange"}

def test_min_support_zero_keeps_dead_rule(self):
rules = [_rule("dead", "card_arrival", r"zebra unicorn")]
report = rank_rules(rules, _dataset(), _apply_rules_fn(), compute_marginal=False)
kept, dropped = prune_harmful_rules(rules, report, min_support=0)
assert len(kept) == 1
assert not dropped
Loading