Skip to content

Configuration (ProfilingConfig)

ProfilingConfig is the only tuning surface for fit_profiler. Compose one immutable config from small, single-responsibility section dataclasses.

In short

  • Data goes to fit_profiler directly; every tuning knob goes into one ProfilingConfig.
  • clustering, archetypes, runtime and evaluation always exist with defaults.
  • rules, explanations, eda and report run only when you set them.
  • A bare ExplanationsConfig() runs PDP, permutation importance and a surrogate.
  • Every config is frozen: vary a knob with dataclasses.replace.
from parleml_mli import (
    ProfilingConfig, ClusteringConfig, RulesConfig, MinerConfig, AcceptanceGates,
    ExplanationsConfig, PdpConfig, ReportConfig, fit_profiler,
)

config = ProfilingConfig(
    clustering=ClusteringConfig(min_cluster_size=10, n_components=10),
    rules=RulesConfig(miner=MinerConfig(
        acceptance=AcceptanceGates(min_lift=1.3, min_tp_count=8))),
    explanations=ExplanationsConfig(pdp=PdpConfig(n_features=6)),
    report=ReportConfig(output_dir="out/"),
)

result = fit_profiler(
    model=model, X=X_train, y=y, target_mask=(y == 1),  # data stays flat
    config=config,                                       # every tuning knob
)

Two rules make the surface predictable:

  • Presence enables a stage. An optional section left as None (rules / explanations / eda / report) means "skip it" — there are no separate boolean toggles. clustering / archetypes / runtime / evaluation are always present (they have defaults).
  • Data is not config. model / X / y / target_mask / holdout splits (X_valid / X_test for evaluation, X_holdout / y_holdout / holdout_basis for the rules' Test numbers) / encoding_registry / task_type are always direct arguments to fit_profiler — never part of ProfilingConfig.

Every config is frozen — vary a knob with dataclasses.replace. For the per-parameter tables of the standalone config records (EDAConfig, RunSettings, CardinalityCaps) see Configuration parameters.

ProfilingConfig

ProfilingConfig dataclass

ProfilingConfig(
    clustering: ClusteringConfig = ClusteringConfig(),
    archetypes: ArchetypeConfig = ArchetypeConfig(),
    runtime: RuntimeConfig = RuntimeConfig(),
    evaluation: EvaluationConfig = EvaluationConfig(),
    rules: RulesConfig | None = None,
    explanations: ExplanationsConfig | None = None,
    eda: EDAConfig | None = None,
    report: ReportConfig | None = None,
)

Everything :func:parleml_mli.fit_profiler needs that is not data.

Optional sections (rules / explanations / eda / report) are None by default — a profiling run discovers archetypes only, unless those stages are explicitly configured.

Clustering, archetypes & runtime

The always-present sections that shape archetype discovery.

ClusteringConfig dataclass

ClusteringConfig(
    space_type: SpaceType | None = None,
    min_cluster_size: int = 15,
    min_samples: int | None = None,
    cluster_selection_method: ClusterSelectionMethod = AUTO,
    shap_distance_metric: ShapDistanceMetric = EUCLIDEAN,
    top_n_features: int = 10,
    enable_umap: bool = True,
    umap_sample_size: int | None = 50000,
    n_components: int | None = 10,
    n_trials: int = 0,
    tree_weights: "np.ndarray | None" = None,
)

Archetype-clustering geometry (HDBSCAN over the chosen space).

tree_weights class-attribute instance-attribute

tree_weights: "np.ndarray | None" = field(
    default=None, compare=False
)

Optional per-feature weights for the leaf-space distance metric.

An array enables a callable weighted metric (and disables UMAP pre-compression + sklearn tree acceleration). Excluded from __eq__ / __hash__ (compare=False) because a NumPy array is unhashable and has no scalar truth value under the frozen dataclass's generated methods.

ArchetypeConfig dataclass

ArchetypeConfig(
    score_threshold: float | None = None,
    contamination_min_coverage: float | None = None,
    contamination_beta: float = 1.0,
    min_odds_ratio: float = 3.0,
    n_bootstrap: int = 0,
    min_stability: float = DEFAULT_MIN_STABILITY,
)

Post-clustering survival filters: contamination, odds, stability.

RuntimeConfig dataclass

RuntimeConfig(
    random_state: int = 42,
    n_jobs: int = -1,
    verbose: bool = False,
    use_native_shap: bool = True,
    shap_cache_path: str | None = None,
    shap_cache_overwrite: bool = False,
)

SHAP computation, parallelism, determinism and caching.

EvaluationConfig dataclass

EvaluationConfig(
    decision_threshold: float | str | None = None,
    n_bootstrap: int = 0,
    ci_alpha: float = 0.05,
    multiclass_threshold_metric: str = "f1",
)

Holdout-evaluation knobs (the single decision-threshold source).

ReportConfig dataclass

ReportConfig(
    output_dir: str | None = None,
    persist_inference_data: (
        bool | Literal["overwrite"]
    ) = False,
    key_columns: list[str] | None = None,
)

One-call report generation + inference-data persistence.

Present (non-None on :class:ProfilingConfig) ⇒ the populated result is written via result.generate_report(output_dir, ...) and, when requested, the per-case inference-data sidecar is persisted.

The report is deterministic: no fit or report step calls a model. AI-written narration is an annotation the host computes afterwards with :func:parleml_mli.reporting.narrate_result and an injected :class:parleml_mli.ask.Ask.

Rules

Set ProfilingConfig(rules=RulesConfig(...)) to mine interpretable rules. See the Rule extraction guide. Every rule gets Train numbers on the mining rows and, when fit_profiler receives X_holdout / y_holdout, Test numbers and a holds-up verdict — RulesConfig.holdout sets the verdict's covered-row floor, and MinerConfig.wilson is the run's confidence for every bound.

RulesConfig dataclass

RulesConfig(
    engine: (
        Literal["all"] | RuleEngine | tuple[RuleEngine, ...]
    ) = "all",
    metric: AllClassMetric = "f1",
    conf_threshold: float = 0.5,
    cov_threshold: float = 0.0,
    column_names: list[str] | None = None,
    max_trees: int | None = DEFAULT_RULE_MAX_TREES,
    miner: MinerConfig | None = None,
    all_class: AllClassConfig | None = None,
    holdout: RulesHoldoutConfig = RulesHoldoutConfig(),
    class_workers: int = DEFAULT_RULE_CLASS_WORKERS,
)

Rule extraction. Present ⇒ run it; replaces rules_kwargs.

engine names the engines to mine: "all" (the default) runs the positive miner and the sequential miner (:data:DEFAULT_RULE_ENGINES); one engine name runs that engine alone; a tuple of distinct names runs each of them, e.g. ("positive_miner", "sequential", "all_class") adds the all-class engine — a slow mixed-class decision list, binary models only — to the default pair. :attr:engines resolves the spec.

class_workers is how many one-vs-rest classes of a multiclass model are mined at once, each in its own process (capped at the class count; 1 mines them one after another in this process). The rules do not depend on it.

Raises:

Type Description
ValueError

engine is not "all", an engine name, or a non-empty tuple of distinct engine names; class_workers is below 1.

engines property

engines: tuple[RuleEngine, ...]

The engines to mine, in order: "all" → :data:DEFAULT_RULE_ENGINES, one name → that engine, a tuple → itself.

engine_spec property

engine_spec: str | list[str]

:attr:engine as run records store it: a tuple becomes a JSON list.

RulesHoldoutConfig dataclass

RulesHoldoutConfig(min_test_rows: int = 20)

How rules are tested on held-out rows.

A rule gets a verdict only when it covers at least min_test_rows held-out rows (counting covered rows, not positives); below that it has too few test rows. With enough, it holds up when the Wilson lower bound of its test precision — at the run's :class:WilsonConfig alpha / one_sided, whatever its use_lower_bound — exceeds the test base rate of its class. The Test numbers are measured whenever held-out rows are given.

MinerConfig dataclass

MinerConfig(
    max_rules: int | None = None,
    max_combination_iterations: int | None = None,
    max_candidates: (
        int | None
    ) = DEFAULT_RULE_MAX_CANDIDATES,
    time_budget_s: (
        float | None
    ) = DEFAULT_RULE_TIME_BUDGET_S,
    float_threshold: float = 1e-06,
    catboost_combination_handling: str = "collapse-merge",
    expansion: ExpansionGates = ExpansionGates(),
    wilson: WilsonConfig = WilsonConfig(),
    acceptance: AcceptanceGates = AcceptanceGates(),
    dedup: DedupConfig = DedupConfig(),
    business_value: BusinessValueConfig = BusinessValueConfig(),
)

Tuning surface for the positive / sequential rule miners.

Replaces the miner_kwargs dict. Composed from cohesive sections (:class:ExpansionGates, :class:WilsonConfig, :class:AcceptanceGates, :class:DedupConfig, :class:BusinessValueConfig) instead of one flat field pile. random_state / verbose / column_names are sourced from :class:RuntimeConfig and :class:RulesConfig, not duplicated here.

The top-level fields are the miner-wide knobs that do not belong to any single gate (combination control, output cap, CatBoost handling). The sequential miner ignores the whole :attr:dedup section. The positive class is not a miner knob: :func:parleml_mli.rules.extract_rules resolves it once (positive_class=) and every engine mines the {0, 1} indicator with 1 positive. :attr:wilson is the run's confidence for every engine (the all-class engine included).

max_candidates and time_budget_s bound one search (one engine, one target class): the candidate pool grows with every tree combined, so the search stops when the pool reaches max_candidates (deterministic) or after time_budget_s wall-clock seconds (machine-dependent backstop), keeps the best rules found so far and records why on :attr:~parleml_mli.rules.RuleSetSummary.budget_stop. None disables either.

MinerConfig is composed from cohesive sub-sections rather than a flat field pile — each groups one concern (the whole dedup section is ignored by the sequential engine):

ExpansionGates dataclass

ExpansionGates(
    min_coverage: float = 0.0005,
    min_tp_count: int = 5,
    min_lift: float = 1.5,
)

Loose gate applied during pairwise tree-combination.

A partial body must clear these thresholds to be worth expanding further; the strict :class:AcceptanceGates then decides which of the fully-grown candidates are actually emitted. Deliberately looser than acceptance so promising conjunctions are not pruned before they are complete: min_lift is capped at the resolved acceptance min_lift (floored at 1), since lift cannot exceed 1 / prevalence.

WilsonConfig dataclass

WilsonConfig(
    use_lower_bound: bool = True,
    alpha: float = 0.05,
    one_sided: bool = True,
)

Wilson score interval behind the precision bounds — the run's confidence.

precision_lower drives candidate scoring, the acceptance min_precision_lower gate, ranking and the dedup Wilson tiebreak, and the same interval bounds every reported precision, Train and Test alike, and decides whether a rule holds up on held-out rows (:class:RulesHoldoutConfig). one_sided=True makes each bound a one-sided (1 - alpha) limit; use_lower_bound=False puts the raw precision wherever a bound would appear in mining and in the reported numbers — but not in the holds-up verdict, which always uses the interval's lower bound at alpha / one_sided.

AcceptanceGates dataclass

AcceptanceGates(
    min_lift: float | None = None,
    min_tp_count: int | None = None,
    min_coverage: float | None = None,
    max_conditions: int = 6,
    min_precision: float | None = None,
    min_precision_lower: float | None = None,
)

Strict gate deciding which expanded candidates are emitted as rules.

The three data-aware gates — min_lift / min_tp_count / min_coverage — default to None ("derive from the target's class balance and dataset size"). Leaving them None lets :func:parleml_mli.fit_profiler fill them from :func:parleml_mli.rules.kwargs_suggest.suggest_rule_miner_kwargs (prevalence-scaled min_lift, size-scaled min_tp_count / min_coverage) via parleml_mli.pipeline.finalize._enrich_rules_config; the leaf projection parleml_mli.rules.extraction_api._miner_config_to_kwargs drops any still-None gate so the underlying miner falls back to its own default. Set a literal to pin the gate and skip derivation.

DedupConfig dataclass

DedupConfig(
    strategy: Literal[
        "structural_then_coverage", "body_jaccard", "none"
    ] = "structural_then_coverage",
    improvement_min_delta: float = 0.005,
    coverage_jaccard_max: float = 0.7,
    subset_coverage_jaccard_max: float = 0.5,
    containment_max: float = 0.85,
    min_subgroup_size_ratio: float = 0.4,
    max_siblings_per_rule: int = 5,
    diversity_jaccard_max: float = 0.9,
)

Near-duplicate suppression run after the acceptance gate.

Ignored wholesale by the sequential miner — grouping every dedup knob into one section makes that a structural fact (pass no :class:DedupConfig, or omit it, and there is nothing to ignore) rather than a docstring caveat on a flat field list. strategy selects the walk; the six structural_then_coverage knobs and the legacy diversity_jaccard_max (body_jaccard only) tune it.

BusinessValueConfig dataclass

BusinessValueConfig(
    value_tp: float = 10.0,
    cost_fp: float = 1.0,
    value_fn: Callable[[int, int], float] | None = None,
)

Net-value scoring of a candidate's confusion counts.

The default linear value_tp * tp - cost_fp * fp is used unless value_fn supplies a custom (tp, fp) -> float payoff (which then overrides value_tp / cost_fp).

AllClassConfig dataclass

AllClassConfig(
    c: float = 0.25,
    percent_training: float | None = None,
    early_stop: float = 0.0,
    rule_order: Literal["supp", "cov", "conf"] = "supp",
    sort_by_class: bool | list[int] | None = None,
    float_threshold: float = 1e-06,
    catboost_combination_handling: Literal[
        "collapse-merge", "skip-tree", "fail"
    ] = "collapse-merge",
)

Tuning surface for the all-class rule engine.

Replaces the all_class_kwargs dict (passthrough keys only — the first-class metric / conf_threshold / cov_threshold / column_names live on :class:RulesConfig, random_state on :class:RuntimeConfig).

Global explanations

Set ProfilingConfig(explanations=ExplanationsConfig(...)) and enable each artifact by giving its sub-section (leave it None to skip). See the Global explanations guide.

A bare ExplanationsConfig() is not a no-op

Unlike the other optional sections, an ExplanationsConfig with no per-artifact sub-section set resolves at consumption time to a sensible default trio — pdp + permutation_importance + surrogate — so "just explain the model" always does meaningful work. Use ExplanationsConfig.default(...) to materialise that trio explicitly, or set at least one sub-section to pin exactly what runs.

ExplanationsConfig dataclass

ExplanationsConfig(
    pdp: PdpConfig | None = None,
    ale: AleConfig | None = None,
    shap_interactions: ShapInteractionsConfig | None = None,
    surrogate: SurrogateConfig | None = None,
    surrogate_residual: (
        ResidualSurrogateConfig | None
    ) = None,
    permutation_importance: (
        PermutationImportanceConfig | None
    ) = None,
    fairness: FairnessConfig | None = None,
    psi: PsiConfig | None = None,
    compacity: CompacityConfig | None = None,
    error_tree: ErrorTreeConfig | None = None,
    ovr_classes: bool | list[Any] | None = None,
)

Global model-interpretability artifacts.

Present ⇒ run :func:parleml_mli.explanations.explain. A non-None per-artifact section enables that artifact (replacing the old compute_explanations toggle + 10 *_kwargs dicts).

A bare ExplanationsConfig() (no section set) is resolved to a sensible default trio — pdp + permutation_importance + surrogate — at consumption time by :func:resolve_explanations_config, so "just explain the model" is never a silent no-op (§2.4). The config object itself is a faithful, genuinely frozen record of what the caller asked for: it is never mutated at construction (the previous __post_init__ / object.__setattr__ self-mutation made "frozen" a half-truth, broke dataclasses.replace(cfg, pdp=None) round-trips, and left no way to express an intentionally-empty config — audit §1.3). Use :meth:default to materialise the trio explicitly.

has_any_artifact property

has_any_artifact: bool

True when at least one per-artifact section is set.

default classmethod

default(**overrides: Any) -> 'ExplanationsConfig'

The default artifact trio, materialised explicitly.

ExplanationsConfig.default() == ExplanationsConfig(pdp=PdpConfig(), permutation_importance=PermutationImportanceConfig(), surrogate=SurrogateConfig()). Keyword overrides are applied on top (e.g. default(ale=AleConfig()) adds ALE to the trio).

PdpConfig dataclass

PdpConfig(
    features: list[str] | None = None,
    n_features: int = 6,
    grid_resolution: int = 30,
    sample_size: int = 5000,
    response_method: str = "auto",
    target_class: Any | None = None,
    categorical_features: list[str] | None = None,
    random_state: int = 0,
)

Partial-dependence curves (:func:parleml_mli.explanations.pdp.compute_pdp).

features=None draws curves for the model's top n_features drivers — by the profile's own importance when there is one, else by the model's prediction sensitivity — never for identifier or datetime columns; an explicit features list is drawn as given. Every column without a curve is recorded with its reason on ExplanationsResult.pdp_selection.

AleConfig dataclass

AleConfig(
    features: list[str] | None = None,
    n_features: int = 6,
    n_bins: int = 20,
    sample_size: int = 5000,
    response_method: str = "auto",
    target_class: Any | None = None,
    random_state: int = 0,
)

Accumulated-local-effects curves (:func:parleml_mli.explanations.ale.compute_ale).

Chosen like :class:PdpConfig's, among the numeric columns only; the reasons for the columns without a curve land on ExplanationsResult.ale_selection.

ShapInteractionsConfig dataclass

ShapInteractionsConfig(
    top_features: int = 20,
    sample_size: int = 5000,
    target_class: Any | None = None,
    random_state: int = 0,
)

SurrogateConfig dataclass

SurrogateConfig(
    target_class: Any | None = None,
    engine: EngineKey | None = None,
    max_depth: int = 5,
    min_samples_leaf: int = 20,
    n_estimators: int = 1,
    sample_size: int = 50000,
    categorical_features: list[str] | None = None,
    optimize: bool | dict[str, Any] = False,
    random_state: int = 0,
    holdout_fraction: float = 0.2,
    target_kind: Literal[
        "prediction", "residual"
    ] = "prediction",
    residual_kind: Literal[
        "auto", "signed", "squared", "logloss"
    ] = "auto",
)

Bases: SurrogateTuning

Glass-box surrogate of the model's predictions (or residuals).

ResidualSurrogateConfig dataclass

ResidualSurrogateConfig(
    target_class: Any | None = None,
    engine: EngineKey | None = None,
    max_depth: int = 5,
    min_samples_leaf: int = 20,
    n_estimators: int = 1,
    sample_size: int = 50000,
    categorical_features: list[str] | None = None,
    optimize: bool | dict[str, Any] = False,
    random_state: int = 0,
    holdout_fraction: float = 0.2,
    residual_kind: Literal[
        "auto", "signed", "squared", "logloss"
    ] = "auto",
)

Bases: SurrogateTuning

Surrogate fitted to the model's residuals (target_kind forced).

PermutationImportanceConfig dataclass

PermutationImportanceConfig(
    n_repeats: int = 10,
    scoring: str | None = None,
    sample_size: int = 5000,
    target_class: Any | None = None,
    random_state: int = 0,
    n_jobs: int = 1,
)

FairnessConfig dataclass

FairnessConfig(
    sensitive_features: DataFrame | Series | None = None,
    reference_group: dict[str, str] | None = None,
    air_thresholds: tuple[float, float | None] = (
        0.8,
        None,
    ),
    me_thresholds: tuple[float, float] = (-0.05, 0.05),
    smd_thresholds: tuple[float, float] = (-0.5, 0.5),
    target_class: Any | None = None,
    sample_size: int = 50000,
    max_distinct_numeric: int = 20,
    min_group_size: int = 30,
    random_state: int = 0,
)

PsiConfig dataclass

PsiConfig(
    n_buckets: int = 10,
    eps: float = 0.0001,
    categorical_columns: list[str] | None = None,
    binning_strategy: BinningStrategy = "union",
    pooling_strategy: CategoricalPoolingStrategy = "union",
    max_categories: int = 200,
)

CompacityConfig dataclass

CompacityConfig(approx: float = 0.9, fixed_k: int = 5)

ErrorTreeConfig dataclass

ErrorTreeConfig(
    engine: EngineKey | None = None,
    max_depth: int = 4,
    min_samples_leaf: int = 30,
    n_estimators: int = 1,
    sample_size: int = 50000,
    residual_threshold: float | None = None,
    target_class: Any | None = None,
    categorical_features: list[str] | None = None,
    random_state: int = 0,
)

Next