Rule Explanation#

Functions#

verbalize_rule#

iguanas.rule_explanation.verbalize_rule(rule: str) str[source]#

Convert a rule expression to a plain-English sentence.

Parameters:

rule (str) – Rule expression using X["col"] notation with & / | operators.

Returns:

Plain-English description of the rule.

Return type:

str

Examples

>>> verbalize_rule('(X["age"] > 30) & (X["income"] < 50000)')
'age is greater than 30 AND income is less than 50000'
>>> verbalize_rule('(X["score"] >= 0.8) | (X["flag"] == 1)')
'score is at least 0.8 OR flag is equal to 1'

compute_coverage_overlap#

iguanas.rule_explanation.compute_coverage_overlap(R: polars.DataFrame) polars.DataFrame[source]#

Compute pairwise Jaccard overlap between rule predictions.

For each pair of rules, computes the Jaccard similarity: (samples flagged by both) / (samples flagged by either). A high score means the two rules flag nearly the same population.

Parameters:

R (pl.DataFrame) – Boolean DataFrame of rule predictions (columns = rules, rows = samples).

Returns:

Long-format DataFrame with columns: rule_a, rule_b, jaccard, flagged_by_both, flagged_by_either. Only the upper triangle of the pair matrix is returned. Sorted by jaccard descending (most overlapping first).

Return type:

pl.DataFrame

Examples

>>> import polars as pl
>>> R = pl.DataFrame({
...     "rule_A": [True, True, False, False],
...     "rule_B": [True, False, True, False],
... })
>>> compute_coverage_overlap(R)
shape: (1, 5)
┌────────┬────────┬─────────┬────────────────┬──────────────────┐
│ rule_a │ rule_b │ jaccard │ flagged_by_both │ flagged_by_either│
│ ---    │ ---    │ ---     │ ---            │ ---              │
│ str    │ str    │ f64     │ i64            │ i64              │
╞════════╪════════╪═════════╪════════════════╪══════════════════╡
│ rule_A │ rule_B │ 0.333…  │ 1              │ 3                │
└────────┴────────┴─────────┴────────────────┴──────────────────┘

compute_counterfactual#

iguanas.rule_explanation.compute_counterfactual(rule: str, sample: polars.DataFrame, epsilon: float = 1e-06) list[dict[str, Any]][source]#

Find minimal feature changes to un-flag a sample from a rule.

For each atomic condition in the rule that is currently satisfied by sample, computes the smallest perturbation of that feature that would violate the condition. Results are sorted by abs_change ascending so the caller can pick the least-effort option.

AND rules: breaking any single condition un-flags the sample. The first (cheapest) entry is sufficient.

OR rules: every currently-satisfied condition must be broken to un-flag the sample. All returned entries together form the counterfactual.

Parameters:
  • rule (str) – Rule expression to un-flag the sample from.

  • sample (pl.DataFrame) – Single-row DataFrame representing the sample to explain. Must contain all columns referenced in rule.

  • epsilon (float, default=1e-6) – Small perturbation used when breaking strict inequalities.

Returns:

Candidate counterfactuals sorted by abs_change ascending. Each dict has keys:

  • feature — feature name to change

  • condition — the atomic condition that would be broken

  • current_value — current feature value

  • suggested_value — value that breaks the condition

  • abs_change — magnitude of the required change

Returns an empty list if the sample is not flagged by the rule or if no atomic conditions can be broken numerically.

Return type:

list[dict]

Raises:

ValueError – If sample does not have exactly one row.

Examples

>>> sample = pl.DataFrame({"age": [45], "income": [80_000]})
>>> rule = '(X["age"] > 30) & (X["income"] >= 50_000)'
>>> compute_counterfactual(rule, sample)
[
    {'feature': 'age', 'condition': ..., 'current_value': 45.0,
     'suggested_value': 29.999999, 'abs_change': 15.000001},
    {'feature': 'income', ...},
]