Profiling — fit_profiler & ProfilingResult¶
fit_profiler is the entry point. It takes a fitted model and a training frame
and returns a single ProfilingResult — the
hub every other capability reads from.
In short
- Pass a fitted
model, its training frameXand atarget_mask: the rows whose archetypes you want, usually the positives. - Every tuning knob lives in one
ProfilingConfig. An optional section (rules,explanations,eda,report) runs only when you set it. - Archetypes cluster in tree-leaf space or SHAP space. HistGradientBoosting, single trees and glass-box models always use SHAP space.
- Regression profiles a slice you choose; multiclass profiles every class one-vs-rest.
result.save(...)writes one joblib that reloads without refitting.
from parleml_mli import ProfilingConfig, fit_profiler
config = ProfilingConfig() # archetypes only; optional stages omitted
result = fit_profiler(
model=clf, X=X_train, y=y_train, target_mask=(y_train == 1), config=config,
)
See the full signature and every data argument on the
fit_profiler reference. All tuning knobs live
in ProfilingConfig.
The three core inputs¶
| Argument | What it is |
|---|---|
model |
A fitted estimator (CatBoost / LightGBM / XGBoost / sklearn ensemble / HGB / single tree / linear / EBM / RuleFit) or a raw booster. |
X |
The feature frame the model was trained on (one row per observation). |
target_mask |
A boolean mask selecting which rows to profile — the archetype cohort. |
target_mask is the single most important choice: archetype clustering,
signatures, and similar-case retrieval all describe the rows where the mask is
True. For a classifier that's usually the positive class
(y_train == 1); for regression it's a quantile or business cutoff (below).
The ProfilingResult hub¶
fit_profiler always returns a populated-on-demand ProfilingResult. Configure
the heavier artifacts in ProfilingConfig, or populate them lazily afterwards:
| Attribute | Config section | Lazy builder (a function, not a method) | Holds |
|---|---|---|---|
summary_df, labels_ |
core profiling | — | per-archetype summary and target-cohort cluster assignments |
stability_ |
archetypes=ArchetypeConfig(n_bootstrap=...) |
build_stability_result(result, ...) |
optional bootstrap stability; default n_bootstrap=0 skips it |
rules_ |
rules=RulesConfig(...) |
build_rules_result(result, ...) |
RulesResult (engine → RuleSetSummary) |
explanations_ |
explanations=ExplanationsConfig(...) |
build_explanations_result(result, ...) |
ExplanationsResult |
eda_ |
eda=EDAConfig(...) |
build_eda_result(result, ...) |
EDAResult |
model_evaluation_ |
pass labelled X_test / X_valid splits |
build_model_evaluation_result(result, ...) |
ModelEvaluationResult |
classes |
(multiclass auto) | — | per-class ClassProfile fan-out |
from parleml_mli import (
ProfilingConfig, RulesConfig, ExplanationsConfig, EDAConfig,
PdpConfig, fit_profiler,
)
config = ProfilingConfig(
rules=RulesConfig(),
explanations=ExplanationsConfig(pdp=PdpConfig()),
eda=EDAConfig(),
)
result = fit_profiler(
model=clf, X=X_train, y=y_train, target_mask=(y_train == 1),
X_test=X_test, y_test=y_test,
config=config,
)
result.plot_umap()
result.save("out/profiling_result.joblib")
Tuning with ProfilingConfig¶
All tuning knobs are passed as a single typed
ProfilingConfig composed from small, frozen
section dataclasses. Data arguments (model / X / y / target_mask /
holdout splits / encoding_registry / task_type) always stay flat — only the
tuning knobs move into config:
from parleml_mli import (
ProfilingConfig, ClusteringConfig, RulesConfig, MinerConfig, AcceptanceGates,
ExplanationsConfig, PdpConfig, ReportConfig, SpaceType, fit_profiler,
)
config = ProfilingConfig(
clustering=ClusteringConfig(space_type=SpaceType.SHAP, min_cluster_size=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=clf, X=X_train, y=y_train,
target_mask=(y_train == 1), config=config)
Two rules keep it predictable: presence enables a stage (an optional section
left None — rules / explanations / eda / report — is skipped; there are
no separate boolean toggles), and data stays flat; all tuning lives in one
typed ProfilingConfig. See the
configuration reference for every section and
field.
Details: reading back the fit inputs
The result is a self-contained bundle. Alongside the artifacts above, the fitted inputs they were derived from are exposed as read-only accessors, so a saved profile is usable on its own:
| Accessor | Holds |
|---|---|
model_ / model_type_ |
the fitted estimator and its detected ModelType |
feature_names_ |
encoded feature names (aligned with target_features_ / target_shap_values_) |
target_features_ |
encoded target-cohort matrix the profile was built from |
target_features_original_ |
original-space values (only when X_target_orig= was passed) |
target_predictions_ |
model scores for the target cohort (aligned with labels_) |
target_shap_values_ |
(n_rows, n_features) SHAP matrix for the target cohort |
target_mask_ |
boolean mask selecting target rows from the fit-time X |
id_columns_ |
original columns pinned into representative examples |
The full evaluation matrix X is not stored. Reload the source dataset and pass X= to
the build_*_result functions when you need to recompute an artifact.
Spaces — leaf vs SHAP¶
Archetype distances are computed in either leaf space (co-occurrence in tree
leaves) or SHAP space (contribution vectors). Set space_type on
ClusteringConfig:
from parleml_mli import ProfilingConfig, ClusteringConfig, SpaceType, fit_profiler
config = ProfilingConfig(clustering=ClusteringConfig(space_type=SpaceType.LEAF))
result = fit_profiler(model=clf, X=X_train, y=y_train, config=config)
config = ProfilingConfig(clustering=ClusteringConfig(space_type=SpaceType.SHAP))
result = fit_profiler(model=clf, X=X_train, y=y_train, config=config)
Families with no per-sample leaf structure (HGB, single DecisionTree, and all
glass-box models) auto-switch LEAF → SHAP.
Space changes the question
Leaf space groups cases by the model's tree paths; SHAP space groups them by feature contributions. Coverage depends on the data, geometry and survival filters. Compare the resulting coverage and signatures on your own run.
Regression¶
Pass task_type=TaskType.REGRESSION and choose the slice you want via
target_mask. Provide y= to unlock ground-truth-aware per-archetype
diagnostics.
import numpy as np
from parleml_mli import ProfilingConfig, ClusteringConfig, SpaceType, TaskType, fit_profiler
threshold = float(np.quantile(y_train, 0.85)) # top 15% by y
config = ProfilingConfig(clustering=ClusteringConfig(space_type=SpaceType.LEAF))
result = fit_profiler(
model=my_regressor, X=X_train, y=y_train,
target_mask=(y_train > threshold),
task_type=TaskType.REGRESSION,
config=config,
)
print(result.summary_df[[
"cluster_id", "n_target_cases",
"mean_y_true", "mean_prediction", "mean_residual", "mae", "rmse",
]])
Common target_mask recipes for regression:
| Slice | target_mask recipe |
Profiles |
|---|---|---|
Top-quantile y |
y_train > np.quantile(y_train, q) |
sub-types of high-value cases |
Top-quantile y_pred |
y_pred > np.quantile(y_pred, q) |
model-driven hot zones |
| Residual outliers | np.abs(y_train - y_pred) > q |
where the regressor is most uncertain |
| Systematic over-pred | (y_pred - y_train) > q |
sub-types of positive bias |
Rule mining is binary-only
Rule miners assume a {positive, negative} dichotomy (lift, odds ratio,
Wilson bound, F-beta). On a regression problem, binarize via target_mask
first and mine rules against a classifier of the same family. With
task_type=TaskType.REGRESSION, ProfilingConfig(rules=RulesConfig(...)) is skipped with a
UserWarning and result.rules_ stays None.
Multiclass¶
When len(model.classes_) > 2, profiling fans out one-vs-rest per class and
result.classes holds the per-class ClassProfile containers. When profiling
with explanations, restrict the fan-out with
ExplanationsConfig(ovr_classes=...) when only a subset of labels is
interesting.
Glass-box native structure¶
For inherently-interpretable families — GLM / logistic (SKLEARN_LINEAR), EBM
(EBM), RuleFit (RULEFIT) — result.native_explanation() returns the model's
exact readable structure (GLM coefficients + odds ratios, EBM additive shape
functions, or the fitted RuleFit rule table) as a JSON-clean dict. It returns
None for opaque models (trees / boosters), which have no single readable
structure beyond importance + SHAP.
Native categorical features¶
Native categoricals (LightGBM categorical_feature=, XGBoost
enable_categorical=True, sklearn HGB categorical_features=, CatBoost
cat_features=) flow through end-to-end. The rule extractor decodes
set-membership splits into Operator.IN / Operator.NOT_IN antecedents like
(CITY ∈ {Toronto, Montréal}) in the original labels — see the
Rules guide.
Persisting¶
Reload it later with ProfilingResult.load("out/profiling_result.joblib") to
read every populated surface without re-running the pipeline. Set
ReportConfig(output_dir="out", persist_inference_data=True) on
ProfilingConfig.report to also materialise the supplied train/valid/test frames
under out/model_training_data/. Persistence requires output_dir; calling
save() alone does not write those frames or create the destination directory.
See Consuming saved results for schema compatibility and
the separate narration annotation.
Next¶
- Rule extraction: mine IF-THEN rules and test them on held-out rows.
- Consuming saved results: read a saved result without refitting.