Rule Combination#
Functions#
combine_rules_full_search#
- iguanas.rule_combination.combine_rules_full_search(R: polars.DataFrame, n: int = 3, max_combinations_per_n: int = 200000, batch_size: int = 50000, operator: str = 'or') polars.DataFrame[source]#
Combine rules using logical operations to create new composite rules.
Generates all possible combinations of 2 to n rules and creates new columns where each combination is evaluated using the specified logical operation (OR/AND). The combined rule name reflects the operation between component rules.
Optimized for speed using batch processing and vectorized operations.
- Parameters:
R (pl.DataFrame) – DataFrame containing rule columns to be combined. Each column should represent a boolean or binary rule evaluation. All columns will be used as candidate rules.
n (int, default=3) – Maximum number of rules to combine. Generates all combinations from size 2 up to size n.
max_combinations_per_n (int, default=200_000) – Maximum number of combinations to generate per combination size. If exceeded, only the first max_combinations_per_n are used.
batch_size (int, default=50_000) – Number of combinations to process in each batch to manage memory.
operator (str, default='or') – Boolean operator to apply: ‘or’ for OR operations (any True), ‘and’ for AND operations (all True).
- Returns:
DataFrame containing the original rules plus all generated combined rules. Combined rule columns are named using the pattern:
”(rule1) | (rule2) | …” for OR operations
”(rule1) & (rule2) & …” for AND operations
- Return type:
pl.DataFrame
Examples
>>> import polars as pl >>> R = pl.DataFrame({"rule_A": [1, 0, 1], "rule_B": [0, 1, 1]}) >>> combine_rules_full_search(R, n=2, operator='or') # Returns DataFrame with original columns plus "(rule_A) | (rule_B)" >>> combine_rules_full_search(R, n=2, operator='and') # Returns DataFrame with original columns plus "(rule_A) & (rule_B)"
combine_rules_cumulative#
- iguanas.rule_combination.combine_rules_cumulative(R: polars.DataFrame, output_names: list[str] | None = None, operator: str = 'or') polars.DataFrame[source]#
Compute horizontal cumulative boolean operations across all columns.
- Parameters:
R (pl.DataFrame) – Input DataFrame. All columns will be used in the cumulative operation.
output_names (list[str] | None, default=None) – List of names for the output columns. If None, generates names based on operator. Must have the same length as R.columns.
operator (str, default='or') –
Boolean operator to apply:
’or’: cumulative OR (any True)
’and’: cumulative AND (all True)
- Returns:
DataFrame with boolean values:
If operator=’or’: True if at least one condition is True up to that position
If operator=’and’: True if all conditions are True up to that position
- Return type:
pl.DataFrame
- Raises:
ValueError – If operator is not ‘or’ or ‘and’, or if output_names length doesn’t match columns.
Examples
>>> import polars as pl >>> R = pl.DataFrame({ ... "rule_A": [True, False, True], ... "rule_B": [False, True, True], ... "rule_C": [True, True, False], ... }) >>> combine_rules_cumulative(R, operator="or") # Column 1: rule_A | ...; Column 2: rule_A | rule_B | ...; Column 3: all three >>> combine_rules_cumulative(R, operator="and", output_names=["step1", "step2", "step3"]) # Named columns, each True only if all rules up to that position are True
combine_rules_budgeted#
- iguanas.rule_combination.combine_rules_budgeted(R: polars.DataFrame, y: polars.Series, max_alert_rate: float, max_rules: int = 5, weights: polars.Series | None = None) polars.DataFrame[source]#
Build the OR-ruleset that catches the most positives within an alert budget.
Operational rule systems are constrained by review capacity, not by F1: only a fixed fraction of the population can be flagged. This selects a disjunction maximising recall subject to that constraint, which is the budgeted maximum coverage problem. The greedy rule below is its standard
1 - 1/eapproximation.This differs from
combine_rules_greedy()in what it optimises. Greedy metric maximisation ignores coverage, so under OR it tends to keep widening the ruleset until it flags far more than the budget permits; here a candidate is simply infeasible once the union breaches the budget.- Parameters:
R (pl.DataFrame) – DataFrame containing boolean rule columns. All columns are candidates.
y (pl.Series) – Boolean target series indicating true labels.
max_alert_rate (float) – Maximum fraction of the population the ruleset may flag, in (0, 1]. With weights, this is a fraction of total weight rather than of rows.
max_rules (int, default=5) – Maximum number of rules in the returned disjunction.
weights (pl.Series | None, default=None) – Optional sample weights. When given, both the budget and the positives caught are measured in weight rather than row counts.
- Returns:
Single boolean column named by the combined rule expression. Empty when no single rule fits within the budget.
- Return type:
pl.DataFrame
- Raises:
ValueError – If max_alert_rate is outside (0, 1], or R has no columns.
Examples
>>> import polars as pl >>> R = pl.DataFrame({"a": [True, False, False], "b": [False, True, False]}) >>> y = pl.Series([True, True, False]) >>> ruleset = combine_rules_budgeted(R, y, max_alert_rate=0.7)
Notes
Coverage is monotone under OR, so a rule that breaches the budget can never be rescued by adding more rules. Each step therefore takes the feasible rule with the greatest marginal gain in positives caught, breaking ties toward the cheaper rule, and stops as soon as nothing feasible adds a positive.
combine_rules_greedy#
- iguanas.rule_combination.combine_rules_greedy(R: polars.DataFrame, y: polars.Series, metric: str = 'f1', max_rules: int = 5, operator: str = 'or', weights: polars.Series | None = None, min_improvement: float = 0.0) polars.DataFrame[source]#
Greedily select rules that maximize a performance metric.
Starts with the best single rule, then iteratively adds rules that provide the largest metric improvement. Stops when no rule improves the metric by at least min_improvement or when max_rules is reached.
- Parameters:
R (pl.DataFrame) – DataFrame containing boolean rule columns. All columns will be used as candidate rules.
y (pl.Series) – Boolean target series indicating true labels.
metric (str, default="f1") – Performance metric to optimize. Must be a column name produced by compute_metrics (e.g., “f1”, “accuracy”, “precision”, “recall”).
max_rules (int, default=5) – Maximum number of rules to select.
operator (str, default="or") – Boolean operator for combining rules: ‘or’ or ‘and’.
weights (pl.Series | None, default=None) – Optional sample weights for weighted metric computation.
min_improvement (float, default=0.0) – Minimum metric improvement required to add a new rule.
- Returns:
DataFrame with single column containing the combined rule. Column name reflects the selected rules using the operator.
- Return type:
pl.DataFrame
Examples
>>> import polars as pl >>> R = pl.DataFrame({"rule_A": [True, False, True], ... "rule_B": [False, True, True], ... "rule_C": [True, True, False]}) >>> y = pl.Series([True, True, False]) >>> result_R = combine_rules_greedy( ... R, y, metric="f1", max_rules=2 ... ) >>> print(result_R.columns) # e.g., ['(rule_B) | (rule_A)']
- Raises:
ValueError – If operator is not ‘or’ or ‘and’, or if metric column not found.
combine_rules_beam_search#
- iguanas.rule_combination.combine_rules_beam_search(R: polars.DataFrame, y: polars.Series, metric: str = 'f1', beam_width: int = 4, max_rules: int = 5, operator: str = 'or', weights: polars.Series | None = None, min_improvement: float = 0.0, return_top_k: int = 10) polars.DataFrame[source]#
Find top rule combinations using beam search.
Maintains beam_width best partial combinations at each depth level, exploring a broader set of combinations than greedy search while evaluating far fewer than exhaustive enumeration. Like greedy search this is a heuristic: it offers no optimality guarantee. Use
combine_rules_a_star()when the top-k must be provably optimal.- Parameters:
R (pl.DataFrame) – DataFrame containing boolean rule columns. All columns will be used as candidate rules.
y (pl.Series) – Boolean target series indicating true labels.
metric (str, default="f1") – Performance metric to optimize. Must be a column name produced by compute_metrics (e.g., “accuracy”, “f1”, “precision”, “recall”).
beam_width (int, default=4) – Number of best candidates to keep at each depth level.
max_rules (int, default=5) – Maximum number of rules in a combination.
operator (str, default="or") – Boolean operator for combining rules: ‘or’ or ‘and’.
weights (pl.Series | None, default=None) – Optional sample weights for weighted metric computation.
min_improvement (float, default=0.0) – Minimum metric improvement required over parent combination to add a new rule. Acts as a pruning criterion to avoid expanding combinations that don’t provide sufficient benefit.
return_top_k (int, default=10) – Number of top combinations to return.
- Returns:
DataFrame containing columns for the top rule combinations found. Each column represents one combination, with the column name showing the combined rule expression.
- Return type:
pl.DataFrame
Examples
>>> import polars as pl >>> R = pl.DataFrame({"rule_A": [True, False, True], ... "rule_B": [False, True, True], ... "rule_C": [True, True, False]}) >>> y = pl.Series([True, True, False]) >>> result_R = combine_rules_beam_search( ... R, y, metric="f1", beam_width=3, max_rules=2 ... ) >>> print(result_R.columns) # Shows top rule combinations
- Raises:
ValueError – If operator is not ‘or’ or ‘and’, or if metric column not found.
combine_rules_a_star#
- iguanas.rule_combination.combine_rules_a_star(R: polars.DataFrame, y: polars.Series, metric: str = 'f1', max_rules: int = 5, operator: str = 'or', weights: polars.Series | None = None, min_improvement: float | None = None, return_top_k: int = 10, protected: polars.Series | None = None, reference_group: object | None = None, min_dir: float | None = None, max_alert_rate: float | None = None, return_diagnostics: bool = False) polars.DataFrame | tuple[polars.DataFrame, dict][source]#
Find the top rule combinations by best-first branch-and-bound search.
Searches the space of rule subsets containing between 1 and
max_rulesrules, and returns thereturn_top_kbest-scoring combinations. Unlike greedy or beam search, this returns a provably optimal top-k under the conditions in Notes.Search formulation#
Nodes are rule subsets; children extend a subset by one rule. Each node carries an upper bound on the metric attainable by any descendant. The frontier is ordered by that bound (best-first), and a node is discarded when its bound cannot beat the current k-th best incumbent. Search stops as soon as the best remaining bound falls below the k-th incumbent, at which point no unexplored subset can enter the top-k.
- param R:
DataFrame containing boolean rule columns. All columns are candidates.
- type R:
pl.DataFrame
- param y:
Boolean target series indicating true labels.
- type y:
pl.Series
- param metric:
Metric to maximise: “precision”, “recall”, “accuracy”, “mcc”, or an F-beta score (“f1”, “f0.5”, “f2”, …).
- type metric:
str, default=”f1”
- param max_rules:
Maximum number of rules in a combination.
- type max_rules:
int, default=5
- param operator:
Boolean operator for combining rules: ‘or’ or ‘and’.
- type operator:
str, default=”or”
- param weights:
Optional sample weights for weighted metric computation.
- type weights:
pl.Series | None, default=None
- param min_improvement:
If set, a child is discarded unless it improves on its parent by at least this much. This is a heuristic filter that forfeits the optimality guarantee (the best subset may only be reachable through a temporarily worse ancestor – under OR-composition, adding a rule typically lowers precision before later rules raise recall). Leave as None for exact search.
- type min_improvement:
float | None, default=None
- param return_top_k:
Number of top combinations to return. Set to 1 for the single best.
- type return_top_k:
int, default=10
- param protected:
Optional protected-attribute series used to enforce a fairness constraint on returned combinations.
- type protected:
pl.Series | None, default=None
- param reference_group:
Value of protected to treat as the reference group. Defaults to the most frequent value.
- type reference_group:
object | None, default=None
- param min_dir:
Minimum acceptable disparate impact ratio. When set (together with protected), a combination is only admitted to the results if its DIR is at least this value; 0.8 corresponds to the “four-fifths rule”. Search remains exact over the feasible set.
- type min_dir:
float | None, default=None
- param max_alert_rate:
If set, only combinations flagging at most this fraction of the population are admitted. This makes the search the exact counterpart of
combine_rules_budgeted(), whose greedy rule is a1 - 1/eapproximation, so the two together measure the approximation gap.- type max_alert_rate:
float | None, default=None
- param return_diagnostics:
If True, return
(results, diagnostics)where diagnostics reports node counts and whether the optimality guarantee held.- type return_diagnostics:
bool, default=False
- returns:
pl.DataFrame – One column per returned combination, named by the combined rule expression, ordered best-first.
dict – Only when return_diagnostics is True. Keys:
nodes_expanded,nodes_pruned,combinations_evaluated,bounds_computed,exact,bound_is_tight.
Examples
>>> import polars as pl >>> R = pl.DataFrame({"rule_A": [True, False, True], ... "rule_B": [False, True, True], ... "rule_C": [True, True, False]}) >>> y = pl.Series([True, True, False]) >>> best = combine_rules_a_star(R, y, metric="f1", return_top_k=1) >>> top_5, diag = combine_rules_a_star(R, y, return_top_k=5, ... return_diagnostics=True)
- raises ValueError:
If operator is not ‘or’ or ‘and’, or the metric is unsupported.
Notes
Optimality. The returned top-k is exact when
min_improvementis None and the metric admits a non-trivial bound (see below). Exactness holds over the feasible set when a fairness constraint is active.Why a bound rather than an A* heuristic. Metric value is a property of a state, not a sum of edge costs, so classical A*
f = g + hdoes not apply: there is no additive path cost to decompose. This routine is therefore a best-first branch-and-bound, which is the correct formulation for maximising a non-additive set function. The public name is retained for backwards compatibility.Admissibility. Under OR-composition, coverage is monotonically non-decreasing in the rule set: TP and FP can only grow and FN can only shrink. Writing
Pfor total positive mass andkfor the remaining slots, an attainable-TP upper bound follows from the union bound,TP_max = TP + (sum of the k largest per-rule new-TP masses), clipped atP, whileFPis bounded below by its current value. Every metric below is non-decreasing in TP and non-increasing in FP, so evaluating it at(TP_max, FP_min)maximises it over the reachable region and never understates what a descendant can achieve:recall = TP / Pprecision = TP / (TP + FP)accuracy = (TP + (N - P) - FP) / Nf_beta = (1 + b^2) TP / (TP + b^2 P + FP)
The F-beta identity uses
FN = P - TPto eliminate FN, which is what makes the bound monotone in only two quantities. Under AND-composition the monotonicity reverses – coverage shrinks, so TP is bounded above by its current value and FP is bounded below by subtracting the k largest per-rule FP removals – and the same four expressions apply.mcchas no non-trivial bound implemented and falls back to 1.0. That is admissible, so the result is still exact, but no pruning occurs and the search degenerates to exhaustive enumeration.Alert budget. Under OR the constraint is monotone: coverage only grows, so a node already over budget can never be rescued and its entire subtree is discarded – the constraint makes the search cheaper, not more expensive. It also tightens the bound, since any rows a descendant adds consume the remaining capacity, capping attainable TP at
budget - coverage. Under AND coverage shrinks, so an over-budget node may still have feasible descendants; there it is excluded from the results but still expanded.Prior behaviour. Before v1.4 this function ordered the frontier by
-mean(best remaining single-rule metrics), which is not admissible: for disjoint rules under OR the achievable recall gain is the sum of per-rule gains, so a mean understates it and the optimum could be ordered away. It also defaulted tomin_improvement=0.0, discarding every non-improving child, which reduced the search to hill-climbing. Both are fixed here;min_improvementnow defaults to None.