Skip to content

Model Sources

ModelSource / ComparedModelSource — fitted-model adapters that feed the diagnostics.

Ferrum — a statistical visualization library with a Rust core.

ComparedModelSource

Multi-model wrapper exposing the same surface as ModelSource.

Every derived-data method is proxied through each underlying ModelSource and the per-model outputs are concatenated with a model: Utf8 column stamped on each frame, so downstream chart builders can route color="model" to render one curve per model.

The read-only BaseSource properties (X / y / feature_names / capabilities) and their _-prefixed aliases resolve to the first source's values — every wrapped source shares X / y by construction in ModelSource.compare, so any one will do. The proxied set is derived from BaseSource's property descriptors (see _collect_compared_proxied_attrs), so a new public property proxies automatically. Accessing model / _model raises since there is no single estimator. model_names reports the configured ordering. stack is the callable-form sibling of the auto-dispatch, for chart builders that need to compose several method calls (e.g. class selection before aggregation) before stacking per-model frames.

Parameters:

Name Type Description Default
sources dict[str, ModelSource]

Mapping from model name (used for the model column) to the underlying ModelSource. Must contain at least one entry — passing an empty dict raises ValueError.

required

Examples:

>>> import ferrum as fm
>>> cms = fm.ModelSource.compare({"ridge": ridge, "lasso": lasso}, X, y)
>>> fm.roc_chart(cms)                  # overlay both curves
>>> cms.model_names
['ridge', 'lasso']
>>> cms.roc_curve()                    # long-form frame with `model` column

model_names property

model_names: list[str]

Ordered list of model display names.

Returns the keys of the sources dict supplied at construction time, in insertion order. Each name corresponds to the value written into the model column on every derived-data DataFrame.

Returns:

Type Description
list[str]

Model names in the order they were registered.

items

items() -> list[tuple[str, 'ModelSource']]

Ordered (name, ModelSource) pairs for each wrapped model.

The public accessor over the wrapped sources, in registration order. Chart builders iterate this to compose one panel per model without reaching into the private _sources mapping.

Returns:

Type Description
list[tuple[str, ModelSource]]

(model_name, source) pairs in the order they were registered.

stack

stack(frame_fn: 'Callable[[ModelSource], pl.DataFrame]') -> pl.DataFrame

Build one frame per wrapped model via frame_fn and stack them.

The callable-form sibling of the auto-dispatch in _dispatch: _dispatch calls a fixed ModelSource method name per model, while stack accepts an arbitrary per-model callable, for chart builders that need to compose several method calls (e.g. class selection before aggregation) before stacking. Both share the same iteration-and-stamping idiom, in registration order, with a model: Utf8 column recording each source's registered name.

Parameters:

Name Type Description Default
frame_fn callable

frame_fn(model_source) -> polars.DataFrame — produces one model's rows. The model column is stamped by this method, not by frame_fn.

required

Returns:

Type Description
DataFrame

Vertical concatenation of every model's frame with a trailing model Utf8 column, rows in registration order.

ModelSource

Bases: PredictionsMixin, ClassificationCurvesMixin, FeatureImportanceMixin, ModelSelectionMixin, ClusteringMixin, RankingMixin, BaseSource

Wrap a fitted estimator + dataset and expose model-diagnostic derived data as polars DataFrames.

Constructing a ModelSource is sklearn-free — only attribute introspection runs at __init__ time. Derived-data methods that need sklearn / shap lazy-import on call, so import ferrum never pulls those packages into the user's process unless they actually compute a diagnostic that requires them. (UMAP embeddings run in Rust via _core.umap_embedding; there is no Python umap dependency.)

Each derived-data method returns a long-form polars DataFrame whose schema is documented in ferrum.diagnostics._internal.schemas — chart builders and Visualizers consume the same frames.

Parameters:

Name Type Description Default
model Any

A fitted estimator. Must expose at least predict; some methods require additional protocol attributes (predict_proba, coef_, feature_importances_, cluster_centers_, explained_variance_ratio_, …) and raise AttributeError with the missing attribute name when called against an incompatible model.

required
X DataFrame | DataFrame | Table | ndarray

Feature matrix. Coerced internally to a polars DataFrame; any narwhals-compatible input also works.

required
y array - like

Target. Required by methods that depend on ground truth (every method except probabilities and the unsupervised silhouette / pca_variance / embeddings / intercluster_distance / rank1d(algorithm != "covariance") / rank2d family).

None
feature_names sequence of str

Column labels. Defaults to X.columns when X is a DataFrame, or ["f0", "f1", ...] otherwise.

None
class_names sequence of str

Per-class display labels for classification diagnostics. Defaults to model.classes_ when available, else the unique values of y.

None
sample_weight array - like

Per-row weights forwarded to sklearn scorers that accept them.

None
random_state int

Seed propagated to every derived-data method whose underlying compute consumes randomness (importances permutation, SHAP background sampling, UMAP / t-SNE / MDS embeddings, cross-validation curves, partial-dependence sampling). Deterministic methods ignore the value.

None

Examples:

>>> import ferrum as fm
>>> source = fm.ModelSource(model, X, y, random_state=0)
>>> fm.roc_chart(source)              # use directly with a figure function
>>> source.predictions()              # access derived data as a DataFrame
>>> source.confusion_matrix(normalize="true")

X property

X: DataFrame

Feature matrix coerced to a polars DataFrame.

Returns the value supplied to __init__ (after coercion). Use this for read-only access from chart builders and external callers — source._X is an internal alias preserved for back-compat.

y property

y: 'pl.Series | None'

Target series, or None when no y was supplied.

Returns the polars Series the constructor coerced from the y argument. None means unsupervised — methods that need ground truth raise on call.

model property

model: Any

The wrapped fitted estimator.

Returns the model object supplied at construction time unchanged. Chart builders use it for occasional native introspection (e.g. model.classes_, model.n_clusters); prefer the public derived-data methods when one exists.

feature_names property

feature_names: list[str]

Column labels for the feature matrix.

Returns the names supplied at construction time, or the DataFrame column names when X was a DataFrame, or ["f0", "f1", ...] for unlabeled array inputs.

Returns:

Type Description
list[str]

Feature names in the same order as the columns of X.

capabilities property

capabilities: frozenset[str]

Protocol attributes present on the wrapped estimator.

A frozen subset of _PROTOCOL_ATTRS ("predict", "predict_proba", "coef_", "feature_importances_", …) detected at construction time via hasattr. Derived-data methods gate on this set to pick the appropriate code path and raise AttributeError with a clear message when a required attribute is absent.

Returns:

Type Description
frozenset[str]

Attribute names that are present on the wrapped model.

predictions

predictions() -> pl.DataFrame

Return y_true, y_pred, residual, studentized_residual, cooks_distance, leverage.

leverage is the diagonal of the hat matrix H = X (XᵀX)⁻¹ Xᵀ for linear estimators (those exposing coef_); NaN otherwise. Used by the residuals-vs-leverage panel of multi-panel residuals charts.

probabilities

probabilities() -> pl.DataFrame

Return y_true + one column per class with predicted probability.

rank1d

rank1d(*, algorithm: str = 'shapiro') -> pl.DataFrame

Univariate feature ranking.

The Shapiro-Wilk and variance algorithms operate on X alone; "covariance" ranks features by absolute sample covariance with y and therefore requires y to be present.

Output schema (SCHEMA_RANK1D): feature: Utf8, score: Float64, rank: Int64. Rows are pre-sorted by descending score so rank=1 is always the top feature.

Parameters:

Name Type Description Default
algorithm ('shapiro', 'variance', 'covariance')

Univariate ranking statistic. "covariance" requires y.

"shapiro"

rank2d

rank2d(*, algorithm: str = 'pearson') -> pl.DataFrame

Pairwise feature ranking — long-form correlation matrix.

All algorithms run in Rust (Kendall uses Knight's O(n log n)).

Output schema (SCHEMA_RANK2D): feature_x: Utf8, feature_y: Utf8, correlation: Float64 — one row per ordered pair of features, p × p rows total.

Parameters:

Name Type Description Default
algorithm ('pearson', 'spearman', 'kendall', 'covariance')

Correlation / association statistic computed for each feature pair.

"pearson"

silhouette

silhouette(*, k: int | None = None) -> pl.DataFrame

Per-sample silhouette values, sorted within cluster descending.

Returns one row per sample with columns sample_id (original X index), y_position (sequential 0..n-1 stack order — used by mark_silhouette to render bars in a tightly-packed Rousseeuw layout), cluster, and silhouette_value.

Parameters:

Name Type Description Default
k int

Informational cluster count. When provided, the result is filtered to clusters in range(k).

None

pca_variance

pca_variance(*, n_components: int | None = None) -> pl.DataFrame

Explained-variance ratio per principal component plus the cumulative running sum.

If the wrapped model exposes explained_variance_ratio_ (e.g. sklearn.decomposition.PCA), reads it directly (backward compat). Otherwise computes from raw X via Rust SVD.

Parameters:

Name Type Description Default
n_components int

Truncate the result to the first n_components components. None (default) keeps all components.

None

embeddings

embeddings(*, method: str = 'umap', n_components: int = 2, **method_kwargs: Any) -> pl.DataFrame

Low-dimensional embedding of X via UMAP / t-SNE / PCA.

Returns dim_0dim_{n_components-1} plus a label column (y when provided, else zeros — used to color the scatter). random_state is taken from the source's random_state.

Parameters:

Name Type Description Default
method ('umap', 'tsne', 'pca')

Dimensionality-reduction algorithm.

"umap"
n_components int

Number of embedding dimensions to emit (dim_0dim_{n_components-1}).

2
**method_kwargs Any

Algorithm-specific options forwarded to the Rust kernel. "umap" accepts n_neighbors, min_dist, n_epochs; "tsne" accepts perplexity, learning_rate, n_iter; "pca" takes none.

{}

intercluster_distance

intercluster_distance(k: int, *, method: str = 'mds') -> pl.DataFrame

2D embedding of cluster centers + cluster size.

Returns one row per cluster with cluster (Utf8 — a stringified 0..k-1 index, matching SCHEMA_INTERCLUSTER_DISTANCE and the emitted column), x / y (Float64, the 2D embedded coordinate), and size (Int64, sample count). Requires the wrapped model to expose cluster_centers_.

Parameters:

Name Type Description Default
k int

Number of clusters to embed. Clamped to the number of available cluster_centers_.

required
method ('mds', 'tsne')

Embedding algorithm for projecting cluster centers to 2D. "tsne" requires at least 4 clusters; use "mds" for small k.

"mds"

learning_curve

learning_curve(*, cv: int = 5, scoring: Any = None, train_sizes: Any = None) -> pl.DataFrame

Learning curve: score per (train_size, fold, split).

Returns long-form rows — one per (train_size, fold, split). Each row carries the per-fold score plus the per-(train_size, split) aggregates mean_score, std_score, lower, upper (95% CI on the mean). Chart builders dedupe by (train_size, split) to render a ribbon + line; the per-fold rows enable per-fold strip overlays if a future caller wants them.

Parameters:

Name Type Description Default
cv int

Number of cross-validation folds.

5
scoring str or callable

Scorer passed to sklearn's learning_curve. None uses the estimator's default score.

None
train_sizes array - like

Training-set sizes (absolute or fractional) to evaluate. Defaults to numpy.linspace(0.1, 1.0, 5).

None

validation_curve

validation_curve(param: str, values: Any, *, cv: int = 5, scoring: Any = None) -> pl.DataFrame

Return validation-curve scores per (param_value, fold, split).

Same shape as learning_curve but parameterized by an estimator hyperparameter sweep.

Parameters:

Name Type Description Default
param str

Estimator hyperparameter to sweep — the kwarg name on the wrapped estimator (e.g. "alpha" for Ridge).

required
values array - like

Hyperparameter values to evaluate.

required
cv int

Number of cross-validation folds.

5
scoring str or callable

Scorer passed to sklearn's validation_curve. None uses the estimator's default score.

None

cv_scores

cv_scores(*, cv: int = 5, scoring: Any = None) -> pl.DataFrame

Per-fold cross-validation scores.

Returns one row per (fold, split) — train and test scores for each cross-validation fold. Chart builders use this for boxplot / bar / strip distributions across folds.

Parameters:

Name Type Description Default
cv int

Number of cross-validation folds.

5
scoring str or callable

Scorer passed to sklearn's cross_validate. None uses the estimator's default score.

None

alpha_selection

alpha_selection(alphas: Any, *, cv: int = 5, scoring: Any = None) -> pl.DataFrame

Regularization-strength sweep for linear models.

Returns one row per (alpha, fold) — the per-fold test score on the held-out split — plus per-alpha mean_score / std_score aggregates. Chart builders dedupe by alpha to render a single line, and use argmax(mean_score) to mark the best alpha.

Parameters:

Name Type Description Default
alphas array - like

Regularization strengths to evaluate.

required
cv int

Number of cross-validation folds.

5
scoring str or callable

Scorer passed to the underlying sweep. None uses the estimator's default score.

None

importances

importances(*, method: str = 'builtin', n_repeats: int = 30, scoring: Any = None, random_state: int | None = None) -> pl.DataFrame

Feature importance per feature, sorted by descending |importance|.

Parameters:

Name Type Description Default
method ('builtin', 'permutation')

"builtin" reads the wrapped model's feature_importances_ (tree-based estimators) or coef_ (linear estimators, averaged absolute value across classes for multi-output linears); std is zero in this path since the built-in attribute exposes no per-feature variance. "permutation" calls sklearn's permutation_importance and populates std with the per-feature standard deviation across repeats.

"builtin"
n_repeats int

Number of permutation repeats (method="permutation" only).

30
scoring str or callable

Scorer passed to permutation_importance (method="permutation" only). None uses the estimator's default score.

None
random_state int

Seed for the permutation shuffles. Falls back to the source's random_state when omitted.

None

shap_values

shap_values(*, background: Any = None, max_evals: int = 500) -> pl.DataFrame

Long-form SHAP values per (sample, feature, class).

Returns a DataFrame with sample_id, feature, shap_value, feature_value, feature_value_normalized, class_label.

  • Regression: class_label is the constant "target" on every row.
  • Binary classifiers: class_label is the positive-class name on every row; SHAP values are for the positive class.
  • Multi-class classifiers: one row per (sample, feature, class); class_label carries the class name. The result has n_samples * n_features * n_classes rows total.

Explainer is auto-picked by model capability:

  • coef_: shap.LinearExplainer (deterministic, fast).
  • feature_importances_: shap.TreeExplainer (deterministic for tree ensembles).
  • otherwise: shap.KernelExplainer (model-agnostic).

Parameters:

Name Type Description Default
background array - like

Background dataset for the model-agnostic shap.KernelExplainer path. When None, the first min(50, N) rows of X are used. Ignored by the deterministic LinearExplainer / TreeExplainer paths.

None
max_evals int

Reserved for future use (no-op today). Participates only in the result cache key; not yet forwarded to the SHAP explainer.

500

partial_dependence

partial_dependence(features: list[str | int], *, grid_resolution: int = 100, kind: str = 'average') -> pl.DataFrame

Partial dependence per feature.

Parameters:

Name Type Description Default
features list of str or int

Feature names or column indices to compute partial dependence for. One set of rows is emitted per feature.

required
grid_resolution int

Number of grid points sampled across each feature's range.

100
kind ('average', 'individual', 'both')

"average" returns the marginal PD curve per feature with sample_id = -1 (one row per grid point per feature). "individual" returns per-sample ICE curves: one row per (feature, sample_id, grid_point) triple with sample_id in [0, n_samples). "both" returns the union (ICE rows plus average rows), so a downstream chart can overlay both layers on the same DataFrame. Chart builders pair the ICE rows with the detail encoding channel on sample_id to render one polyline per sample.

"average"

roc_curve

roc_curve(*, average: str | None = None, drop_intermediate: bool = True) -> pl.DataFrame

ROC curve(s). One row per (class, threshold). auc repeats per class.

For binary classifiers, returns a single curve on the positive (second) class. For multiclass, returns one-vs-rest curves per class.

Parameters:

Name Type Description Default
average (None, 'micro', 'macro', 'weighted')

Multiclass averaging. None returns per-class one-vs-rest curves (or the single positive-class curve for binary classifiers); "micro" / "macro" / "weighted" additionally include a summary curve under class="<average>".

None
drop_intermediate bool

Drop collinear ROC points that do not change the curve's shape (sklearn's drop_intermediate), yielding a lighter frame.

True

pr_curve

pr_curve(*, average: str | None = None) -> pl.DataFrame

Precision-recall curve(s). One row per (class, threshold).

For binary classifiers, returns a single curve on the positive (second) class. For multiclass, average=None returns one-vs-rest curves per class, while average in {"micro", "macro", "weighted"} returns a single summary curve with class="<average>" and no per-class rows. Macro / weighted variants interpolate per-class precision over a shared recall grid (100 points); micro ravels the binarized labels into one curve.

threshold is NaN at the final (recall=0) point of every per-class curve per sklearn's convention. For macro / weighted summaries it is NaN on every row (recall-grid interpolation discards thresholds); micro follows sklearn's padding convention.

Parameters:

Name Type Description Default
average (None, 'micro', 'macro', 'weighted')

Multiclass averaging strategy. Accepted but inert for binary classifiers, which have only one curve to draw.

None

calibration_curve

calibration_curve(*, n_bins: int = 10, strategy: str = 'uniform') -> pl.DataFrame

Calibration (reliability) curve for binary classifiers.

Returns one row per non-empty bin with mean_predicted, fraction_positive, and count. Delegates to the calibration_kernel Rust kernel.

Parameters:

Name Type Description Default
n_bins int

Number of bins used to group predicted probabilities.

10
strategy ('uniform', 'quantile')

Bin-edge strategy (matches sklearn.calibration). "uniform" uses equal-width bins; "quantile" uses bins holding an equal number of samples.

"uniform"

cumulative_gain

cumulative_gain() -> pl.DataFrame

Cumulative-gain curve per class. Appends a 2-row class='baseline' diagonal for plotting reference.

lift_curve

lift_curve() -> pl.DataFrame

Lift curve per class. Appends a 2-row class='baseline' line at lift=1.0.

discrimination_threshold

discrimination_threshold(*, n_thresholds: int = 50, cv: Any = None) -> pl.DataFrame

Discrimination threshold sweep — binary classifiers only.

Sweeps n_thresholds evenly-spaced thresholds in [0, 1] and reports precision, recall, F1, and queue_rate at each. queue_rate is the hand-computed fraction (y_score >= t).mean().

Parameters:

Name Type Description Default
n_thresholds int

Number of evenly-spaced thresholds swept across [0, 1].

50
cv int or cross-validation splitter

When an int, runs the same sweep on each fold's held-out scores from a freshly-cloned + re-fit estimator and averages per-threshold metrics across folds. Pass a splitter object with a .split() method to override the fold generator. None (default) sweeps the in-sample scores directly.

None

confusion_matrix

confusion_matrix(*, normalize: str | None = None) -> pl.DataFrame

Confusion matrix in long form: one row per (actual, predicted) cell.

value is the (possibly normalized) count; value_fmt is a stringified label suitable for mark_text overlay (integer counts when unnormalized, two-decimal fractions when normalized).

Parameters:

Name Type Description Default
normalize (None, 'true', 'pred', 'all')

Normalization mode. None reports raw counts; "true" / "pred" / "all" apply sklearn-style normalization over rows / columns / the whole matrix.

None

compare classmethod

compare(models: dict[str, Any], X: Any, y: Any = None, **kwargs: Any) -> 'ComparedModelSource'

Build a ComparedModelSource over one ModelSource per model.

Each value in models is wrapped in its own ModelSource with the shared X and y. The returned ComparedModelSource proxies every derived-data method through all wrapped sources and stamps the model name as a model column on the concatenated output, so downstream chart builders can route color="model".

Parameters:

Name Type Description Default
models dict[str, Any]

Mapping from display name to fitted estimator. Each estimator is wrapped in its own ModelSource constructed with the shared X, y, and any additional kwargs (e.g. random_state, feature_names, class_names).

required
X array - like

Feature matrix shared by all models. Accepted types match ModelSource.__init__.

required
y array - like

Target shared by all models. Required by most derived-data methods (same constraints as ModelSource).

None
**kwargs Any

Keyword arguments forwarded verbatim to each ModelSource constructor (e.g. random_state, feature_names, class_names, sample_weight).

{}

Returns:

Type Description
ComparedModelSource

Multi-model wrapper whose derived-data methods return long-form DataFrames with an extra model: Utf8 column.

Examples:

>>> import ferrum as fm
>>> from sklearn.linear_model import Ridge, Lasso
>>> cms = fm.ModelSource.compare(
...     {"ridge": Ridge().fit(X, y), "lasso": Lasso().fit(X, y)},
...     X, y, random_state=0,
... )
>>> fm.roc_chart(cms)          # overlay both ROC curves
>>> cms.model_names
['ridge', 'lasso']