Skip to content

Rule extraction

Mine IF-THEN rules from a fitted tree ensemble. See the Rules guide.

In short

  • extract_rules mines one engine's RuleSetSummary; extract_rules_ovr mines a multiclass model one class at a time.
  • fit_profiler stores a RulesResult (or MulticlassRulesResult) on result.rules_.
  • score_rules measures both the Train and the Test numbers; holds_up gives the verdict.
  • Saved rule sets carry a schema version and are refused with IncompatibleSchemaError when it differs.

extract_rules returns a RuleSetSummary (one engine's ruleset). The higher-level RulesResult is what fit_profiler / build_rules_result store on result.rules_ — it wraps the default engine's summary, the per-engine fan-out (by_engine), and the per-archetype rule-firing fingerprints; a multiclass run stores a MulticlassRulesResult of one-vs-rest rule sets (extract_rules_ovr).

Every rule carries its Train numbers (measured on the mining rows) and, given held-out rows, its Test numbers and holds-up verdict — one scorer, score_rules, measures both; see Train and Test numbers.

A RuleSetSummary carries parleml_mli.rules.operators.SCHEMA_VERSION (5) in its to_dict() payload and in its pickle; RuleSetSummary.from_dict and unpickling refuse any other version with IncompatibleSchemaError — re-run rule extraction to rebuild it. A numeric Antecedent's missing_fires says whether it also fires on missing values (rendered ... or missing) and missing_value names a finite value the model reads as missing; see Missing values and Unseen categories.

extract_rules

extract_rules

extract_rules(
    *,
    model: Any,
    X: DataFrame,
    y: ndarray,
    config: "RulesConfig | None" = None,
    random_state: int = 42,
    verbose: bool = False,
    extraction_classes: ndarray | None = None,
    positive_class: Any = None,
    X_holdout: DataFrame | None = None,
    y_holdout: ndarray | Series | None = None,
    holdout_basis: HoldoutBasis | None = None
) -> RuleSetSummary

Mine rules from a fitted ensemble + binary y and return a :class:RuleSetSummary with their Train and Test numbers.

This is the single canonical entry point for rule extraction in the :mod:rules library. It owns model-compatibility checks, raw-Booster wrapping, dtype validation, X preprocessing, engine selection, and engine dispatch — downstream orchestrators (e.g. :func:parleml_mli.build_rules_result) consume it as a black box.

config is a typed :class:parleml_mli.config.RulesConfig selecting the engine and carrying its tuning surface. None defaults to the positive-miner engine with library defaults (RulesConfig(engine="positive_miner")). The composed :attr:RulesConfig.miner / :attr:RulesConfig.all_class sections are projected onto the estimators' flat constructors here.

Three engines, picked by :attr:RulesConfig.engine:

  • "positive_miner" (default) — :class:rules.PositiveRuleMiner. Mines independently fireable, lift-gated, business-knowledge rules with sibling alternatives. Cascade-mode fields (metric / conf_threshold / cov_threshold / all_class) are silently ignored on this branch.

  • "sequential" — :class:rules.PositiveSequentialRuleMiner. Same front-end as "positive_miner" but greedy covering of the positive-class residue. Use when you want a minimal operational portfolio rather than the full positive-class knowledge surface.

  • "all_class" — :class:rules.AllClassRuleClassifier lifted through :meth:RuleSetSummary.from_all_class: a first-rule-wins list of mixed-class rules ending in a default rule.

Every engine's rules are measured by :mod:parleml_mli.rules.scoring — on the mining rows (X, y) (the Train numbers) and, when X_holdout / y_holdout are given, on those held-out rows (the Test numbers and the :func:~parleml_mli.rules.scoring.holds_up verdict at :attr:RulesConfig.holdout's min_test_rows), at the confidence of the run's :class:~parleml_mli.config.WilsonConfig (RulesConfig.miner.wilson). X_holdout shares X's feature space; y_holdout shares y's labels, and a label outside them counts against every rule; a row without a label is dropped with a warning (:func:~parleml_mli.rules.scoring.drop_unlabelled_holdout). holdout_basis is stamped on the summary as is when there are held-out rows; without any, a given basis is stamped as a "none" basis (its split_kind kept), so no surface claims Test rows it has no numbers for.

The meta value engine="all" (the default of a bare :class:~parleml_mli.config.RulesConfig) is a fan-out handled by :func:parleml_mli.build_rules_result. Since :func:extract_rules mines exactly one engine, it resolves "all" to the primary single engine (positive_miner) — the same engine build_rules_result records as the default_engine_summary of an engine="all" run — so a bare RulesConfig() works here too. Call :func:parleml_mli.build_rules_result when you actually want every engine's ruleset.

positive_class is the label of y treated as the positive event — the only positive-class knob of rule extraction. It is resolved once by :func:parleml_mli.core.target.resolve_target (None: True for a boolean y, 1 for {0, 1} / {-1, 1} labels, else the last sorted label with a warning) and every engine then mines the {0, 1} positive-class indicator, so rule heads, lift and precision all refer to the same class.

extraction_classes is the OvR (multiclass) hook forwarded to the positive miners so booster trees are not collapsed onto the binary [0, 1] set. random_state / verbose are the runtime knobs sourced from :class:parleml_mli.config.RuntimeConfig by the profiler.

:attr:RulesConfig.max_trees caps how many boosting iterations (base estimators for forests, AdaBoost and gradient boosting) the miners read from a fitted ensemble, counting only the iterations the model predicts with. The candidate pool grows multiplicatively across trees, so a large booster (500+ trees) makes extraction effectively unbounded; mining the first max_trees iterations keeps runtime tractable while preserving the dominant signal. Defaults to :data:parleml_mli.config.DEFAULT_RULE_MAX_TREES (100); None mines the full ensemble. Best-effort — a model that can't be sliced is mined whole, with a warning.

Raises:

Type Description
TypeError

Model type unsupported by the chosen engine.

ValueError

Model not fitted, CatBoost + string categoricals, an unrecognised engine, or any Literal/range constraint on :attr:RulesConfig.all_class violated.

extract_rules_ovr

extract_rules_ovr

extract_rules_ovr(
    *,
    model: Any,
    X: DataFrame,
    y: ndarray,
    config: RulesConfig | None = None,
    classes: Sequence[Any] | None = None,
    per_class_miner: dict[Any, MinerConfig] | None = None,
    random_state: int = 42,
    verbose: bool = False,
    X_holdout: DataFrame | None = None,
    y_holdout: ndarray | Series | None = None,
    holdout_basis: HoldoutBasis | None = None
) -> OvRRules

Mine and measure rules over every class via One-vs-Rest.

Parameters:

Name Type Description Default
model Any

Fitted classifier (binary or multiclass). For multiclass, must expose classes_.

required
X DataFrame

Mining features + full label vector.

required
y DataFrame

Mining features + full label vector.

required
config RulesConfig | None

Typed :class:parleml_mli.config.RulesConfig shared by every per-class / per-engine :func:extract_rules call. engine="all" (default) runs the positive and the sequential miners; a concrete engine mines only that one, a tuple of engines each of them ("all_class": binary models only).

None
classes Sequence[Any] | None

Optional subset of model.classes_. None iterates all model classes; for a binary model it defaults to the positive class :func:parleml_mli.core.target.resolve_target resolves from the class order (classes_[1]). To mine a non-conventional positive class (e.g. the 0 / "no" class of a binary model), pass it explicitly as classes=[label].

None
per_class_miner dict[Any, MinerConfig] | None

Optional {class_label: MinerConfig} map for rare-class tuning (e.g. a higher min_lift for the 5%-prevalence class). A class's entry replaces :attr:RulesConfig.miner wholesale for that class.

None
random_state int

Runtime knobs forwarded to every per-class :func:extract_rules call (sourced from :class:parleml_mli.config.RuntimeConfig upstream).

42
verbose int

Runtime knobs forwarded to every per-class :func:extract_rules call (sourced from :class:parleml_mli.config.RuntimeConfig upstream).

42
X_holdout DataFrame | None

Held-out rows in X's space with y's labels: every class's rules get their Test numbers there (a label outside the classes counts against every class; a row without a label is dropped with a warning). holdout_basis is stamped on each summary.

None
y_holdout DataFrame | None

Held-out rows in X's space with y's labels: every class's rules get their Test numbers there (a label outside the classes counts against every class; a row without a label is dropped with a warning). holdout_basis is stamped on each summary.

None
holdout_basis DataFrame | None

Held-out rows in X's space with y's labels: every class's rules get their Test numbers there (a label outside the classes counts against every class; a row without a label is dropped with a warning). holdout_basis is stamped on each summary.

None

Returns:

Type Description
OvRRules

by_class — {class_key: {engine: RuleSetSummary}}, each summary relabelled into the class space (see :func:_remap_summary: the class's code is its label when every model label is an integer, else its position in model.classes_; the rest is -1) — and failures (each class × engine that raised, with a non-empty reason). Each class is mined in one of up to :attr:RulesConfig.class_workers processes; every search is bounded by the miner's max_candidates / time_budget_s and records an early stop on its summary's budget_stop.

OvRRules

OvRRules dataclass

OvRRules(
    by_class: dict[str, dict[RuleEngine, RuleSetSummary]],
    failures: list[RuleMiningFailure],
)

The rule sets of a One-vs-Rest fan-out.

by_class maps each class key to its {engine: RuleSetSummary} (every mined class, an empty map when every engine failed for it); failures lists each class × engine that failed, with why.

engines_skipped property

engines_skipped: list[str]

The failures as audit strings (see :func:skipped_messages).

class_status property

class_status: list[ClassRuleStatus]

Per class × engine: rules mined, failure, budget stop (:func:class_rule_status).

RulesResult

RulesResult dataclass

RulesResult(
    default_engine_summary: RuleSetSummary | None = None,
    config: dict[str, Any] = dict(),
    fingerprints_: Any | None = None,
    by_engine: dict[str, RuleSetSummary] = dict(),
    by_engine_fingerprints_: dict[str, Any] = dict(),
    engines_skipped: list[str] = list(),
)

Container for rule-extraction outputs.

Carries the global ruleset plus the per-archetype rule-firing fingerprint, plus the config used at extraction time for reproducibility.

Attributes:

Name Type Description
default_engine_summary RuleSetSummary | None

The :class:RuleSetSummary for the default engine — the single, unambiguous ruleset that scalar read paths (predict, plotting, to_dataframe) operate on. For single-engine runs it is that engine's summary; for engine="all" it is the positive_miner summary (the most informative for downstream predict / narration), which is not a canonical "global" ruleset — the engine that produced it is recorded in config["engine"] and every engine's summary is available in :attr:by_engine. None only if extraction failed mid-pipeline.

config dict[str, Any]

Kwargs used at extraction time, persisted for reproducibility. Includes engine — one of "positive_miner" / "sequential" / "all_class", or "all" for the fan-out run (the engine that produced :attr:default_engine_summary in that case is recorded here so the viewer can label it). Never includes X / y.

fingerprints_ Any | None

:class:FingerprintResult carrying per-archetype rule-firing matrices (fire-rate / specificity). Populated automatically when the result has cluster labels. Any so importing rules_result does not pull in the (cheap) numpy-heavy fingerprints module eagerly.

to_dict

to_dict() -> dict[str, Any]

Schema-versioned dict suitable for report.json.

engine_summaries

engine_summaries() -> dict[str, RuleSetSummary]

Every ruleset, keyed by the engine that mined it.

:attr:by_engine for an engine="all" run; for a single-engine run its one ruleset (:attr:default_engine_summary) under config["engine"]; empty when extraction produced no ruleset.

to_dataframe

to_dataframe() -> DataFrame

Per-rule table for the default-engine ruleset, with its Train and Test numbers (see :meth:parleml_mli.rules.summary.RuleSetSummary.to_dataframe).

predict

predict(X: DataFrame) -> ndarray

First-rule-wins prediction over the default-engine ruleset.

predict_row

predict_row(x_encoded: DataFrame) -> list[Any]

Every :class:parleml_mli._dsd_types.FiredRule for a single row — :meth:predict_rows of a 1-row frame. Raises ValueError for any other row count.

predict_rows

predict_rows(x_encoded: DataFrame) -> list[list[Any]]

Every :class:parleml_mli._dsd_types.FiredRule per row: out[i] lists the rules that fire on row i of x_encoded.

Unlike :meth:predict (which is first-rule-wins on the default-engine cascade), this walks all engines in :attr:by_engine (falling back to default_engine_summary for single-engine runs) and yields every non-default rule whose antecedents fire on the row, in engine-then-rule order. Each rule's antecedents are evaluated once over the whole frame, so a batch costs one vectorised pass per rule rather than one per row. Used by :meth:parleml_mli._justifier.Justifier.justify and :meth:~parleml_mli._justifier.Justifier.justify_batch to drive the fired-rules section of a case explanation.

Parameters:

Name Type Description Default
x_encoded DataFrame

DataFrame in engineered-feature order. Extra columns are tolerated; missing columns raise via the RuleSetSummary._as_aligned_array guard.

required
Notes

Default rules (is_default=True) are skipped — they would fire on every row by construction. The engine name in each FiredRule is "global" for single-engine runs and the by_engine key otherwise. Each FiredRule carries the rule's Train numbers and, when the rules were measured on held-out rows, its test precision and holds-up verdict. Components for the chip widget are derived from each :class:parleml_mli.rules.output.Antecedent's to_dict() payload, augmented with engineered / origin / kind keys when an :class:parleml_mli.core.encoding.EncodingRegistry is supplied via self.config[ENCODING_REGISTRY_CONFIG_KEY]. That registry is copied off the parent ProfilingResult into the config by :func:parleml_mli.result_builders.build_rules_result and by :meth:ProfilingResult.view_for_class (multiclass per-class views); it is a live Python object, so :meth:to_dict strips it before serialisation. When it is absent (e.g. a run with an identity registry) each chip degrades to origin=engineered-name, kind="RAW" — see PIPE-N4.

A rule that fires on several rows is described by one FiredRule shared by those rows' lists; like every per-case payload it is read-only.

MulticlassRulesResult

MulticlassRulesResult dataclass

MulticlassRulesResult(
    by_class: dict[str, dict[str, RuleSetSummary]] = dict(),
    config: dict[str, Any] = dict(),
    failures: list[RuleMiningFailure] = list(),
)

Typed container for multiclass one-vs-rest rule extraction.

The multiclass analogue of :class:RulesResult. Where the binary container carries a single default-engine ruleset (plus optional per-engine variants), a multiclass run mines one ruleset per class (each class vs. the rest), so the payload is nested: by_class[class_key][engine] is the :class:RuleSetSummary for that class / engine.

Populated by the multiclass fan-out in :func:parleml_mli.pipeline.multiclass.run_multiclass_fanout (mirroring each :attr:parleml_mli.class_profile.ClassProfile.rules) and stored on ProfilingResult.rules_ so that field carries a semantically-correct multiclass payload instead of None.

Global first-rule-wins prediction / plotting are undefined across a one-vs-rest ensemble — those operations live per class. Reach a single class's rules via result.view_for_class(class_key) or result.profile_for(class_key).rules.

Attributes:

Name Type Description
by_class dict[str, dict[str, RuleSetSummary]]

class_key -> {engine -> RuleSetSummary}. Insertion order is the class order used by the fan-out.

config dict[str, Any]

Kwargs used at extraction time, persisted for reproducibility (engine spec, random_state, tree budget). Never includes X / y.

failures list[RuleMiningFailure]

Each class × engine that failed to extract, with a non-empty reason (:class:~parleml_mli.rules.ovr.RuleMiningFailure). Read as audit strings via :attr:engines_skipped, and per class via :attr:class_status.

engines_skipped property

engines_skipped: list[str]

Engines that failed, with the reason — "<engine>: <ExcType>: <message>" when an engine failed alike for every class (e.g. a model whose trees cannot be read as rules), else "<engine> (class <class_key>): <ExcType>: <message>" per failing class.

class_status property

class_status: list[ClassRuleStatus]

Per class × engine: how many rules were mined, whether the engine failed (and why), and whether a budget stopped its search early.

classes property

classes: list[str]

Class keys that mined at least one ruleset (insertion order).

engines property

engines: list[str]

Union of engine names across all classes (first-seen order).

for_class

for_class(class_key: str) -> dict[str, RuleSetSummary]

Return the engine -> RuleSetSummary map for one class.

to_dict

to_dict() -> dict[str, Any]

Schema-versioned nested dict suitable for report.json.

RuleSetSummary

RuleSetSummary dataclass

RuleSetSummary(
    rules: list[Rule],
    feature_names: list[str],
    n_rules: int,
    n_unique_antecedents: int,
    n_total_antecedents: int,
    metrics: RuleSetMetrics,
    text: str,
    positive_class: int = 1,
    classes: tuple[int, ...] = (0, 1),
    ordered: bool = False,
    cumulative: Cumulative | None = None,
    metrics_holdout: RuleSetMetrics | None = None,
    cumulative_holdout: Cumulative | None = None,
    holdout_basis: HoldoutBasis | None = None,
    budget_stop: str | None = None,
    schema_version: int = SCHEMA_VERSION,
)

The rules one engine mined, with their Train and Test numbers.

rules are in the engine's order. ordered marks a list read first rule wins (sequential, all-class): its rules also carry cascade numbers, and its curve follows the list. Every number comes from :func:parleml_mli.rules.scoring.score_rules:

  • Train — metrics (the list's prediction on the mining rows) and cumulative (the curve in the train pick order), with each rule's Train numbers (see :class:~parleml_mli.rules.antecedent.Rule);
  • Test — metrics_holdout and cumulative_holdout (the same curve, in the train pick order) on held-out rows, filled by :meth:measure_holdout with each rule's Test numbers; None without held-out rows. holdout_basis says where those rows come from.

predict accepts a pandas.DataFrame (aligned by feature_names) or an np.ndarray in feature_names order. The to_dict payload and the pickle carry :data:~parleml_mli.rules.operators.SCHEMA_VERSION; another version (or none) is refused with :class:IncompatibleSchemaError.

predict

predict(X: Any) -> ndarray

First-rule-wins (cascade) prediction.

Walks self.rules in order; the first rule whose antecedents fire decides the predicted class for that row. Rows that no rule covers fall through to the negative class.

score

score(
    X: Any,
    y: Any,
    *,
    wilson: WilsonConfig,
    curve_order: Sequence[int] | None = None
) -> RuleSetScores

Measure the rules on (X, y) (see :func:parleml_mli.rules.scoring.score_rules).

measure_holdout

measure_holdout(
    X: Any,
    y: Any,
    *,
    wilson: WilsonConfig,
    min_test_rows: int,
    basis: HoldoutBasis | None = None
) -> None

Fill the Test numbers from held-out rows (X, y), in place.

Every rule (its siblings included) gets metrics_holdout and the :func:~parleml_mli.rules.scoring.holds_up verdict at min_test_rows (None for a default rule); an ordered list's rules get cascade_holdout. The set gets metrics_holdout and cumulative_holdout, curved in the train pick order, and holdout_basis. y holds the labels the rules predict; a label no rule predicts counts against every rule.

from_dict classmethod

from_dict(d: dict[str, Any]) -> RuleSetSummary

Inverse of :meth:to_dict — rebuild a summary from its wire form.

The Train and Test numbers are trusted from the dict, not re-measured (the dict carries no rows).

Raises:

Type Description
IncompatibleSchemaError

The payload's schema_version is missing or is not :data:~parleml_mli.rules.operators.SCHEMA_VERSION.

to_dict

to_dict() -> dict[str, Any]

Schema-versioned JSON-friendly dict.

to_dataframe

to_dataframe() -> DataFrame

One row per rule, suitable for tabular UIs.

Columns: rule_id, text, predicted_class, n_conditions, the Train numbers (coverage, confidence, recall, support, n_samples_per_class, lift, tp, fp, precision_lower, net_value), n_siblings, holds_up, and the Test numbers as <key>_holdout columns when measured.

from_rules classmethod

from_rules(
    rules: Sequence[Rule],
    feature_names: Sequence[str],
    X: Any,
    y: ndarray,
    *,
    positive_class: int = 1,
    classes: Sequence[int] = (0, 1),
    ordered: bool,
    wilson: WilsonConfig
) -> RuleSetSummary

Assemble a summary from rules carrying their Train numbers.

Measures the list on the mining rows (X, y): each rule's cascade numbers when ordered, the set's metrics and its cumulative curve (descending train recall for an unordered list, the list's order for an ordered one).

from_all_class classmethod

from_all_class(
    all_class_ruleset: RuleSet,
    X: Any,
    y: Any,
    feature_names: Sequence[str],
    *,
    positive_class: int = 1,
    wilson: WilsonConfig
) -> RuleSetSummary

Lift the all-class engine's :class:~parleml_mli.rules.rules.RuleSet into a summary.

Each :class:~parleml_mli.rules.rules.MinedRule becomes a :class:Rule measured on (X, y) by :meth:Rule.from_data; the list is ordered (first rule wins, its default rule last).

Rule

Rule dataclass

Rule(
    rule_id: int,
    antecedents: tuple[Antecedent, ...],
    predicted_class: int,
    is_default: bool,
    text: str,
    coverage: float,
    confidence: float,
    recall: float,
    support: float,
    n_samples_per_class: list[int],
    lift: float | None = None,
    tp: int | None = None,
    fp: int | None = None,
    precision_lower: float | None = None,
    net_value: float | None = None,
    cascade: CascadeMetrics | None = None,
    metrics_holdout: RuleMetrics | None = None,
    holds_up: HoldsUp | None = None,
    cascade_holdout: CascadeMetrics | None = None,
    siblings: list[Rule] = list(),
    target_products: list[str] | None = None,
    target_products_excluded: list[str] | None = None,
)

A rule body + head with its Train and Test numbers.

The Train numbers — coverage, confidence (precision), recall, support (tp / n), n_samples_per_class, lift, tp, fp, precision_lower — are measured on the rows the rule was mined on, standalone: every row counts, not only the rows other rules leave. They describe those rows and are optimistic by construction (the search kept the rules that looked best there). A rule covering no row reads 0.0 confidence and recall.

The Test numbers are measured the same way (:mod:parleml_mli.rules.scoring) on held-out rows neither the model nor the rule search saw: metrics_holdout (None without held-out rows) and holds_up, the verdict on them (None without held-out rows and for a default rule, which is no pattern). The rules of an ordered list (sequential, all-class) also carry cascade / cascade_holdout: their numbers on the rows the rules above them leave.

net_value is the positive miners' business value of the train counts.

evaluate

evaluate(X: ndarray) -> ndarray

Return a boolean mask: True where this rule fires on X.

Default rules (no antecedents) fire on every row. X must be a 2-D ndarray in column order matching the parent summary's feature_names (the summary's own predict / score handle DataFrame alignment before calling here).

to_dict

to_dict(*, include_siblings: bool = True) -> dict[str, Any]

Public JSON-friendly serialisation.

Includes per-antecedent payloads (see :meth:Antecedent.to_dict), standalone metrics, optional mining metrics, and (when include_siblings) flattened sibling rules. The cached text field is the full "(a) AND (b) -> [c]" rendering.

Siblings are flattened: a sibling carries no nested .siblings of its own, matching the two-level wire shape.

from_dict classmethod

from_dict(d: dict[str, Any]) -> Rule

Inverse of :meth:to_dict.

from_data classmethod

from_data(
    *,
    rule_id: int,
    antecedents: tuple[Antecedent, ...],
    predicted_class: int,
    X: ndarray,
    y: ndarray,
    classes: Sequence[int],
    wilson: WilsonConfig,
    net_value: float | None = None,
    siblings: Sequence[Rule] | None = None
) -> Rule

Build a rule and measure its Train numbers on (X, y).

X is aligned to the rule set's feature_names; classes is the class order n_samples_per_class counts in; wilson is the run's confidence for precision_lower. The numbers come from :func:parleml_mli.rules.scoring.standalone_metrics, the scorer of the Test column too.

score_rules

score_rules

score_rules(
    rules: Sequence[Rule],
    X: ndarray,
    y: ndarray,
    *,
    positive_class: int,
    classes: Sequence[int],
    ordered: bool,
    wilson: WilsonConfig,
    curve_order: Sequence[int] | None = None
) -> RuleSetScores

Measure a rule list on the rows (X, y).

Parameters:

Name Type Description Default
rules Sequence[Rule]

The list, in the engine's order.

required
X ndarray

2-D array in the rules' feature order (RuleSetSummary.feature_names).

required
y ndarray

One label per row of X, compared with each rule's predicted_class.

required
positive_class int

The list's positive class and class order.

required
classes int

The list's positive class and class order.

required
ordered bool

Whether the list is read first rule wins (sequential, all-class): then each rule gets :class:CascadeMetrics and the curve keeps the list's order.

required
wilson WilsonConfig

The run's confidence for the precision intervals.

required
curve_order Sequence[int] | None

Rule ids in the order to curve them — the train curve's rule_ids, so a held-out curve follows the train pick order. None: the list's order when ordered, else descending recall on these rows.

None

Raises:

Type Description
ValueError

No rows, X / y of different lengths, or a curve_order id that names no rule.

holds_up

holds_up

holds_up(
    metrics: RuleMetrics,
    base_rate: float,
    *,
    min_test_rows: int,
    wilson: WilsonConfig
) -> HoldsUp

The verdict on a rule's held-out numbers.

"too_few_rows" below min_test_rows covered rows (counting covered rows, not positives, so a rule covering many rows and no positive does not hide there); otherwise "holds_up" when the Wilson lower bound of the test precision — at wilson's alpha / one_sided — exceeds base_rate, the test share of the rule's class, and "does_not_hold_up" when it does not.

The bound is computed here from the rule's tp / fp, never read from metrics["precision_lower"]: wilson.use_lower_bound is a mining knob, and with it off the reported bound is the raw precision, which would turn the verdict into a bare "precision beats the base rate".

HoldoutBasis

HoldoutBasis dataclass

HoldoutBasis(
    kind: HoldoutBasisKind,
    n_rows: int,
    n_positive: int | dict[str, int] | None,
    split_kind: SplitKind | None,
)

What a rule set's held-out rows are.

n_positive counts the positive class of a binary target, and every class of a multiclass one keyed by :func:parleml_mli.core.ovr.class_key. split_kind is how the rows were split from the training rows (a "none" basis keeps it too); None when unknown.

to_dict

to_dict() -> dict[str, Any]

JSON-friendly form.

from_dict classmethod

from_dict(d: dict[str, Any]) -> HoldoutBasis

Inverse of :meth:to_dict.

Metric records

The dict shapes behind the Train and Test numbers: a rule's standalone numbers (Rule.metrics_holdout), its cascade numbers in an ordered list (Rule.cascade / Rule.cascade_holdout), a list's prediction (RuleSetSummary.metrics / metrics_holdout) and its curve (RuleSetSummary.cumulative / cumulative_holdout).

RuleMetrics

Bases: TypedDict

A rule's standalone numbers on one set of rows.

precision / recall / lift are None when undefined: no covered row, no row of the rule's class, or a zero base rate. precision_lower / precision_upper bound the precision by the Wilson score interval at the run's confidence; (0, 1) for no covered row.

CascadeMetrics

Bases: TypedDict

A rule's numbers on the rows the rules above it in an ordered list leave.

n_covered counts the rows the rule claims; recall is over every row of the rule's class. None when undefined.

RuleSetMetrics

Bases: TypedDict

Classification numbers of a rule list's prediction, for its positive class.

roc_auc scores the share of the positive rules that fire on a row, so it exists only for a positive-only list (None otherwise, or when the rows hold one class). coverage is the share of rows a (non-default) rule claims.

Cumulative

Bases: TypedDict

The list's numbers after each of its (non-default) rules.

Entry k of each cum_* list is measured on the prediction of the first k + 1 rules of rule_ids (rows no rule claims take default_class); an undefined precision or recall reads 0.0.

Antecedent

Antecedent dataclass

Antecedent(
    feature: str,
    op: Operator,
    value: float,
    feature_index: int,
    value_set: frozenset[Any] | None = None,
    approximation: str | None = None,
    missing_fires: bool = False,
    missing_value: float | None = None,
)

A single feature <op> value (or feature <op> value_set) clause.

Frozen + hashable so antecedents can live in sets and tuples and so rule bodies have a stable hash for de-duplication.

Two operator regimes share this dataclass:

  • Numeric ops (LT/LE/GT/GE/EQ/NE): value carries the float threshold; value_set is None. A missing value — NaN, or missing_value when the model reads another value as missing — satisfies no order comparison (< / <= / > / >=); missing_fires makes the antecedent fire on it (the model's missing-value branch).
  • Set ops (IN/NOT_IN): value_set is a frozenset of original-category labels (_NAN_SENTINEL standing for a missing value); value is set to _NAN_SENTINEL as an unused placeholder. IN fires on the members only; NOT_IN fires on everything else, a category the model never saw included, so the extractors pick the form that sends such a category down the model's own branch.

approximation records how faithfully the model's split was read, None (numeric) or "exact" (set) when it was read exactly: "ctr-counter" / "ctr-feature-freq" / "ctr-borders" (a CatBoost CTR split); "raw-cat-indices" / "raw-cat-int-hash" (the labels were not recoverable, the set holds codes / hashes); "unseen-as-missing" (an XGBoost categorical split: the model refuses a category outside its table, the rule routes it like a missing value); "combination-merged" (a CatBoost tree whose split on a combination of categorical columns was merged away). Several markers join with +.

The regime is enforced by __post_init__: mixing a numeric op with a non-None value_set (or a set op without one, or with an empty one, or with a missing_value) raises ValueError.

Equality / hash are regime-aware (eq=False on the dataclass bypasses the auto-generated __eq__ which would incorrectly return False for two set antecedents whose value fields are both NaN — since nan == nan is False under IEEE 754). Numeric antecedents key on (feature, op, feature_index, value, missing_fires, missing_value); set antecedents key on (feature, op, feature_index, value_set). approximation is deliberately excluded — it is epistemic metadata, not part of the predicate identity (two identical predicates with different approximation budgets are still the same predicate for dedup purposes).

evaluate

evaluate(column: ndarray) -> ndarray

Return a boolean mask: True where this antecedent fires.

column is the feature column (1-D ndarray) for feature. Numeric ops dispatch to :func:_evaluate_numeric_op; a row holding missing_value is taken out of the comparison, and a missing row (NaN or missing_value) fires exactly when missing_fires is set. Set ops dispatch to :func:_evaluate_set_membership, which reads NaN as the _NAN_SENTINEL member.

to_text

to_text(
    digits: int = DEFAULT_VALUE_DIGITS,
    caps: CardinalityCaps | None = None,
) -> str

ASCII rendering, e.g. "(age >= 65)" or "(city in {a, b, c})".

A bound within 1e-30 of zero reads at 0 with the operator that keeps its meaning: LightGBM splits zero against non-zero at ±1e-35, so (flag <= 1e-35) reads (flag <= 0) and (x <= -1e-35) reads (x < 0). Only the text changes; value keeps the model's split.

Parameters:

Name Type Description Default
digits int

Number of significant digits for the numeric value. Ignored for set ops.

DEFAULT_VALUE_DIGITS
caps CardinalityCaps | None

Cardinality caps for set rendering. Sets with more than caps.inline_limit members collapse to a capped form like "{a, b, c, ... (+N more)}". Defaults to CardinalityCaps(). The underlying value_set is not truncated — caps affect rendering only.

None

to_dict

to_dict() -> dict[str, Any]

Public JSON-friendly serialisation.

The canonical wire format produced by :meth:RuleSetSummary.to_dict for every antecedent. Numeric antecedents carry {feature, op, value, missing_fires, missing_value, feature_index, text} (missing_value is null when only NaN is missing); set antecedents carry {feature, op, value_set, feature_index, text}. Both add an approximation key when one is set. _NAN_SENTINEL members inside value_set serialise as JSON null.

The text field is the antecedent rendered via :meth:to_text (default digits, default caps) so downstream consumers (audit reports, viewers, LLM narrators) don't need to inspect the structured payload to display the rule.

from_dict classmethod

from_dict(d: dict[str, Any]) -> Antecedent

Inverse of :meth:to_dict. Tolerant to key order; the derived text field, if present, is ignored on read.

Operator

Operator

Bases: str, Enum

ASCII comparison and set-membership operators.

Stored as str so the value round-trips through JSON unchanged.

Numeric operators (LT/LE/GT/GE/EQ/NE) compare a column against Antecedent.value (a float). Set operators (IN/NOT_IN) test membership in Antecedent.value_set (a frozenset of original-category labels — see :func:parleml_mli.rules.antecedent._evaluate_set_membership).

CardinalityCaps

CardinalityCaps dataclass

CardinalityCaps(inline_limit: int = 15, top_k: int = 5)

Display-time caps applied by :meth:Antecedent.to_text for set antecedents.

These caps affect rendering only — the underlying Antecedent.value_set is never truncated, so semantic evaluation (Antecedent.evaluate) and the wire-format value_set array remain unbounded.

Attributes:

Name Type Description
inline_limit int

Maximum number of category labels rendered inline. Sets larger than this collapse to "{a, b, c, ... (+N more)}".

top_k int

Number of category labels shown in the renderer's "top-K" sample fallback when the full inline form is too large.

IncompatibleSchemaError

IncompatibleSchemaError

Bases: StaleArtifactError

A persisted rule summary carries a schema_version this mli does not read.

A :class:~parleml_mli.core.artifacts.StaleArtifactError (so a stale profile's rules are reported like any other stale artifact); the message says to re-run rule extraction, and nothing is migrated.

RulesUnavailableError

RulesUnavailableError

Bases: ValueError

The model's trees cannot be read as rules; the message says why, for the user.

Next