Rule extraction¶
Mine IF-THEN rules from a fitted tree ensemble. See the Rules guide.
In short
extract_rulesmines one engine'sRuleSetSummary;extract_rules_ovrmines a multiclass model one class at a time.fit_profilerstores aRulesResult(orMulticlassRulesResult) onresult.rules_.score_rulesmeasures both the Train and the Test numbers;holds_upgives the verdict.- Saved rule sets carry a schema version and are refused with
IncompatibleSchemaErrorwhen 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.AllClassRuleClassifierlifted 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: |
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 |
required |
X
|
DataFrame
|
Mining features + full label vector. |
required |
y
|
DataFrame
|
Mining features + full label vector. |
required |
config
|
RulesConfig | None
|
Typed :class: |
None
|
classes
|
Sequence[Any] | None
|
Optional subset of |
None
|
per_class_miner
|
dict[Any, MinerConfig] | None
|
Optional |
None
|
random_state
|
int
|
Runtime knobs forwarded to every per-class :func: |
42
|
verbose
|
int
|
Runtime knobs forwarded to every per-class :func: |
42
|
X_holdout
|
DataFrame | None
|
Held-out rows in |
None
|
y_holdout
|
DataFrame | None
|
Held-out rows in |
None
|
holdout_basis
|
DataFrame | None
|
Held-out rows in |
None
|
Returns:
| Type | Description |
|---|---|
OvRRules
|
|
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.
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: |
config |
dict[str, Any]
|
Kwargs used at extraction time, persisted for reproducibility.
Includes |
fingerprints_ |
Any | None
|
:class: |
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 ¶
Per-rule table for the default-engine ruleset, with its Train and Test numbers
(see :meth:parleml_mli.rules.summary.RuleSetSummary.to_dataframe).
predict ¶
First-rule-wins prediction over the default-engine ruleset.
predict_row ¶
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 ¶
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 |
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]]
|
|
config |
dict[str, Any]
|
Kwargs used at extraction time, persisted for reproducibility
(engine spec, |
failures |
list[RuleMiningFailure]
|
Each class × engine that failed to extract, with a non-empty reason
(:class: |
engines_skipped
property
¶
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
¶
Per class × engine: how many rules were mined, whether the engine failed (and why), and whether a budget stopped its search early.
for_class ¶
for_class(class_key: str) -> dict[str, RuleSetSummary]
Return the engine -> RuleSetSummary map for one class.
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) andcumulative(the curve in the train pick order), with each rule's Train numbers (see :class:~parleml_mli.rules.antecedent.Rule); - Test —
metrics_holdoutandcumulative_holdout(the same curve, in the train pick order) on held-out rows, filled by :meth:measure_holdoutwith each rule's Test numbers;Nonewithout held-out rows.holdout_basissays 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 ¶
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 |
to_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 ¶
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 ¶
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_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 ( |
required |
y
|
ndarray
|
One label per row of |
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: |
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
|
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
No rows, |
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.
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):valuecarries the float threshold;value_setisNone. A missing value — NaN, ormissing_valuewhen the model reads another value as missing — satisfies no order comparison (</<=/>/>=);missing_firesmakes the antecedent fire on it (the model's missing-value branch). - Set ops (
IN/NOT_IN):value_setis a frozenset of original-category labels (_NAN_SENTINELstanding for a missing value);valueis set to_NAN_SENTINELas an unused placeholder.INfires on the members only;NOT_INfires 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 ¶
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 |
DEFAULT_VALUE_DIGITS
|
caps
|
CardinalityCaps | None
|
Cardinality caps for set rendering. Sets with more than
|
None
|
to_dict ¶
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
¶
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 |
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¶
- Rule extraction guide
- Configuration:
RulesConfigandMinerConfig