Skip to content

Summary

All global effect methods have a similar interface and workflow:

  1. create an instance of the global effect method you want to use
  2. (optional) .fit() to customize the method
  3. .plot() to plot the global effect of a feature
  4. .eval() to evaluate the global effect of a feature at a specific grid of points

Usage

# set up the input
X = ... # input data
predict = ... # model to be explained
jacobian = ... # jacobian of the model
  1. Create an instance of the global effect method you want to use:

    g_method = effector.PDP(data=X, model=predict)
    
    g_method = effector.RHALE(data=X, model=predict, model_jac=jacobian)
    
    g_method = effector.ShapDP(data=X, model=predict)
    
    g_method = effector.ALE(data=X, model=predict)
    
    g_method = effector.DerPDP(data=X, model=predict, model_jac=jacobian)
    
  2. Customize the global effect method (optional):

    .fit(features, **method_specific_args)

    This is the place for customization

    The .fit() step can be omitted if you are ok with the default settings; you can directly call the .plot(), or .eval() methods. However, if you want more control over the fitting process, you can pass additional arguments to the .fit() method. Check the Usage section below and the method-specific documentation for more information.

    Usage
    # customize the axis-partitioning (binning) method
    binning_method = effector.axis_partitioning.Agglomerative(
        init_nof_bins = 50, # start from 50 bins (default: 20)
        min_points_per_bin = 10, # minimum number of points per bin (default: 2)
    )
    g_method.fit(
        features=[0, 1], # list of features to be analyzed
        binning_method=binning_method, # custom binning method
    )
    
  3. Plot the global effect of a feature:

    .plot(feature)

    Usage
    feature = ...
    g_method.plot(feature, **plot_specific_args)
    
    Output

    Alt text

    Alt text

    Alt text

    Alt text

    Alt text

  4. Evaluate the global effect of a feature at a specific grid of points:

    .eval(feature, xs)

    Usage
    # Example input
    feature = ... # feature to be analyzed
    xs = ... # grid of points to evaluate the global effect, e.g., np.linspace(0, 1, 100)
    
    y = g_method.eval(feature, xs)
    het = g_method.eval_heter(feature, xs)
    

API

effector.global_effect_ale.ALE(data, model, *, nof_instances=10000, axis_limits=None, schema=None, random_state=21)

Bases: ALEBase

Accumulated Local Effects: the effect built bin-by-bin from local model differences — the safe choice for correlated features.

ale = effector.ALE(X, model)
ale.plot("hr")

The axis is split into \(K\) fixed bins with limits \(z_0 < \dots < z_K\). Each instance in bin \(k\) contributes the secant of the model across the bin; the per-bin means \(\mu_k\) are accumulated:

\[ \hat{f}^{ALE}(x) = \sum_{k=1}^{k_x - 1} (z_k - z_{k-1})\, \mu_k + (x - z_{k_x - 1})\, \mu_{k_x}, \qquad \mu_k = \frac{1}{|S_k|} \sum_{i \in S_k} \frac{f(x^i_{s=z_k}) - f(x^i_{s=z_{k-1}})}{z_k - z_{k-1}} \]

Instances only move within their own bin, so ALE stays close to the data manifold where PDP would extrapolate. The heterogeneity at \(x\) is the variance of the local effects within its bin.

Differentiable model? Use RHALE

effector.RHALE reads the local effects off the model Jacobian: no dependence on bin width, and automatic bin sizing.

Build an ALE explainer. No model calls happen here.

Heterogeneity

eval_heter returns a step function: the variance of the local effects within the bin containing \(x\),

\[ h(x) = \sigma^2_{k_x}, \qquad \sigma^2_k = \frac{1}{|S_k|} \sum_{i \in S_k} (\mathtt{effect}_i - \mu_k)^2 \]

The bin plot draws \(\sqrt{\sigma^2_k}\) as error bars.

Parameters:

Name Type Description Default
data ndarray

the design matrix, shape (N, D) — numpy only.

required
model callable

the black-box model — a Callable mapping (N, D) arrays to (N,) predictions.

required
nof_instances Union[int, str]

max instances kept (default 10_000) — an int subsamples randomly, "all" keeps everything.

10000
axis_limits Optional[ndarray]

per-feature plot limits, shape (2, D); None (default) infers them from data.

None
schema Optional[Union[Schema, dict]]

input metadata — an effector.Schema or a plain dict with any of feature_names, feature_types, cat_limit, target_name, scale_x_list, scale_y; omitted fields are inferred from data, explicit ones win. Coming from a DataFrame? Use effector.from_dataframe.

None
random_state Optional[int]

seed for every internal random step (default 21, reproducible); None for non-deterministic behavior.

21

Methods:

Name Description
fit

Declare per-feature defaults and warm the caches.

eval

The mean effect of a feature at positions xs.

eval_heter

The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

grid

The evaluation grid on which this feature's effect is model-free.

heter_score

One number for a feature's heterogeneity — the scalar find_regions minimizes.

payload

The raw fitted object behind eval/eval_heter — pure numpy, yours to inspect.

importance

How much a feature's mean effect moves the prediction (R13).

importances

The whole importance vector — rank your features in one call.

find_regions

Search for subregions that resolve a feature's heterogeneity.

select_regions

Greedily select which partitions earn their complexity — the CALM chain.

explain

The one-liner on this engine — effector.explain without leaving the session.

plot

Plot the (RH)ALE effect of feature.

Source code in effector/global_effect_ale.py
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
def __init__(
    self,
    data: np.ndarray,
    model: callable,
    *,
    nof_instances: Union[int, str] = 10_000,
    axis_limits: Optional[np.ndarray] = None,
    schema: Optional[Union[ingestion.Schema, dict]] = None,
    random_state: Optional[int] = 21,
):
    r"""Build an ALE explainer. No model calls happen here.

    ??? note "Heterogeneity"
        `eval_heter` returns a step function: the variance of the local
        effects within the bin containing $x$,

        $$
        h(x) = \sigma^2_{k_x},
        \qquad
        \sigma^2_k = \frac{1}{|S_k|} \sum_{i \in S_k}
        (\mathtt{effect}_i - \mu_k)^2
        $$

        The bin plot draws $\sqrt{\sigma^2_k}$ as error bars.

    Args:
        data: the design matrix, shape `(N, D)` — numpy only.
        model: the black-box model — a `Callable` mapping `(N, D)`
            arrays to `(N,)` predictions.
        nof_instances: max instances kept (default `10_000`) — an `int`
            subsamples randomly, `"all"` keeps everything.
        axis_limits: per-feature plot limits, shape `(2, D)`; `None`
            (default) infers them from `data`.
        schema: input metadata — an `effector.Schema` or a plain `dict`
            with any of `feature_names`, `feature_types`, `cat_limit`,
            `target_name`, `scale_x_list`, `scale_y`; omitted fields are
            inferred from `data`, explicit ones win. Coming from a
            DataFrame? Use `effector.from_dataframe`.
        random_state: seed for every internal random step (default `21`,
            reproducible); `None` for non-deterministic behavior.
    """
    super(ALE, self).__init__(
        data,
        model,
        nof_instances=nof_instances,
        axis_limits=axis_limits,
        schema=schema,
        random_state=random_state,
        method_name="ALE",
    )

fit(features='all', *, centering=True, binning_method='fixed', order=None)

Declare per-feature defaults and warm the caches.

ale.fit("hr", binning_method=Fixed(nof_bins=30))

fit is optional

eval, plot, heter_score compute what they need lazily with these defaults; fit declares the config once and pays the model cost upfront.

Parameters:

Name Type Description Default
features Union[int, str, list]

feature(s) to fit — index, name, list, or "all".

'all'
centering Union[bool, str]

default centering for this feature's queries — False (none), True/"zero_integral" (center around the y axis), or "zero_start" (start at y=0).

True
binning_method Union[str, Fixed]

"fixed" (default: 20 equal-width bins) or an effector.axis_partitioning.Fixed instance for custom parameters, e.g. Fixed(nof_bins=30, min_points_per_bin=0). ALE accepts only fixed binning — for adaptive bins use effector.RHALE.

'fixed'
order Union[None, str, list]

level order for a categorical feature of interest:

  • None (default): ascending encoded order — exact for ordinal features; for nominal ones it is arbitrary-but- deterministic, and only the adjacent-level differences are meaningful (see docs/method_semantics.md)
  • "similarity": induce the order from the other features (KS-distance seriation, Molnar/iml)
  • a list of the levels: declare it explicitly (applies to exactly one categorical feature)

Changing order invalidates the cached local effects: the next query recomputes them; re-fitting the same order is a cache hit.

None

Raises:

Type Description
ValueError

if binning_method is not fixed, or an explicit order list targets more than one categorical feature.

Source code in effector/global_effect_ale.py
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
def fit(
    self,
    features: typing.Union[int, str, list] = "all",
    *,
    centering: typing.Union[bool, str] = True,
    binning_method: typing.Union[str, ap.Fixed] = "fixed",
    order: typing.Union[None, str, list] = None,
) -> None:
    """Declare per-feature defaults and warm the caches.

    ```python
    ale.fit("hr", binning_method=Fixed(nof_bins=30))
    ```

    !!! note "fit is optional"
        `eval`, `plot`, `heter_score` compute what they need lazily with
        these defaults; `fit` declares the config once and pays the model
        cost upfront.

    Args:
        features: feature(s) to fit — index, name, list, or `"all"`.
        centering: default centering for this feature's queries —
            `False` (none), `True`/`"zero_integral"` (center around the
            y axis), or `"zero_start"` (start at `y=0`).
        binning_method: `"fixed"` (default: 20 equal-width bins) or an
            `effector.axis_partitioning.Fixed` instance for custom
            parameters, e.g. `Fixed(nof_bins=30, min_points_per_bin=0)`.
            ALE accepts only fixed binning — for adaptive bins use
            `effector.RHALE`.
        order: level order for a *categorical* feature of interest:

            - `None` (default): ascending encoded order — exact for
              ordinal features; for nominal ones it is arbitrary-but-
              deterministic, and only the adjacent-level differences are
              meaningful (see docs/method_semantics.md)
            - `"similarity"`: induce the order from the other features
              (KS-distance seriation, Molnar/iml)
            - a list of the levels: declare it explicitly (applies to
              exactly one categorical feature)

            Changing `order` invalidates the cached local effects: the
            next query recomputes them; re-fitting the same `order` is a
            cache hit.

    Raises:
        ValueError: if `binning_method` is not fixed, or an explicit
            `order` list targets more than one categorical feature.
    """
    self._check_binning_is_fixed(binning_method)
    self._validate_order_arg(features, order)

    self._fit_loop(
        features,
        centering,
        binning_method=binning_method,
        order=order,
    )

eval(feature, xs, centering=None, mask=None, rule=None)

The mean effect of a feature at positions xs.

xs = np.linspace(0, 24, 100)
y = pdp.eval("hr", xs)                            # (100,) mean effect
y_wd = pdp.eval("hr", xs, rule="workingday == 0") # same, on a subregion

One array, one type (R1)

eval always returns the mean effect only. The spread around it has its own ladder: eval_heter (curve), heter_score (scalar), payload (the raw fitted object).

Discrete features

Ordinal/nominal features are evaluated only at observed levels — any other xs value raises ValueError.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
xs ndarray

where to evaluate, (T,).

required
centering Union[None, bool, str]

None (class default), False, True/"zero_integral", or "zero_start".

None
mask Optional[ndarray]

optional boolean (N,) selecting a subregion — the effect within it, re-summarized from cached local effects with zero model calls. Nothing is stored.

None
rule Union[None, str, Rule]

sugar over mask — an effector.Rule or a string like "temp < 3 and season == 0". Mutually exclusive with mask.

None

Returns:

Type Description
ndarray

the mean effect at xs, shape (T,).

Source code in effector/global_effect.py
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
def eval(
    self,
    feature: Union[int, str],
    xs: np.ndarray,
    centering: Union[None, bool, str] = None,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The mean effect of a feature at positions `xs`.

    ```python
    xs = np.linspace(0, 24, 100)
    y = pdp.eval("hr", xs)                            # (100,) mean effect
    y_wd = pdp.eval("hr", xs, rule="workingday == 0") # same, on a subregion
    ```

    !!! note "One array, one type (R1)"
        `eval` always returns the mean effect only. The spread around it
        has its own ladder: `eval_heter` (curve), `heter_score` (scalar),
        `payload` (the raw fitted object).

    !!! warning "Discrete features"
        Ordinal/nominal features are evaluated **only at observed
        levels** — any other `xs` value raises `ValueError`.

    Args:
        feature: index or name of the feature of interest.
        xs: where to evaluate, `(T,)`.
        centering: `None` (class default), `False`,
            `True`/`"zero_integral"`, or `"zero_start"`.
        mask: optional boolean `(N,)` selecting a subregion — the effect
            *within* it, re-summarized from cached local effects with zero
            model calls. Nothing is stored.
        rule: sugar over `mask` — an `effector.Rule` or a string like
            `"temp < 3 and season == 0"`. Mutually exclusive with `mask`.

    Returns:
        the mean effect at `xs`, shape `(T,)`.
    """
    feature = self._resolve_feature(feature)
    centering = self.DEFAULT_CENTERING if centering is None else centering
    centering = helpers.prep_centering(centering)
    mask = self._resolve_mask(mask, rule)

    if not self._is_cat(feature):
        if mask is not None:
            self._effective_limits(feature, mask)  # degeneracy guard
        elif not self.axis_limits[0, feature] < self.axis_limits[1, feature]:
            raise ValueError(
                f"Feature {feature} has a degenerate axis interval "
                f"[{self.axis_limits[0, feature]}, {self.axis_limits[1, feature]}]"
            )

    params = self._summary(feature, mask)
    y = self._eval_mean(feature, xs, params, mask)
    if centering is not False:
        y = y - self._mean_norm_const(
            self._centering_const(feature, mask, centering)
        )
    return y

eval_heter(feature, xs, mask=None, rule=None)

The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

h = pdp.eval_heter("hr", xs)          # (T,) variance around the mean
band = np.sqrt(h)                     # std-like band, plot-ready

It's a variance, and it's method-specific (R2)

PDP: variance of centered ICE; DerPDP: of d-ICE slopes; ALE/RHALE: per-bin slope variance as a step function; ShapDP: interpolated per-bin φ variance. Take the square root for a band.

No centering argument — by design

Heterogeneity is invariant to centering; the signature enforces it.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
xs ndarray

where to evaluate, (T,).

required
mask Optional[ndarray]

optional boolean (N,) subregion — re-summarized from cached local effects, zero model calls.

None
rule Union[None, str, Rule]

sugar over mask (an effector.Rule or a rule string); mutually exclusive with mask.

None

Returns:

Type Description
ndarray

the heterogeneity curve h(xs), (T,), non-negative.

Source code in effector/global_effect.py
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
def eval_heter(
    self,
    feature: Union[int, str],
    xs: np.ndarray,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

    ```python
    h = pdp.eval_heter("hr", xs)          # (T,) variance around the mean
    band = np.sqrt(h)                     # std-like band, plot-ready
    ```

    !!! note "It's a variance, and it's method-specific (R2)"
        PDP: variance of centered ICE; DerPDP: of d-ICE slopes; ALE/RHALE:
        per-bin slope variance as a step function; ShapDP: interpolated
        per-bin φ variance. Take the square root for a band.

    !!! note "No `centering` argument — by design"
        Heterogeneity is invariant to centering; the signature enforces it.

    Args:
        feature: index or name of the feature of interest.
        xs: where to evaluate, `(T,)`.
        mask: optional boolean `(N,)` subregion — re-summarized from cached
            local effects, zero model calls.
        rule: sugar over `mask` (an `effector.Rule` or a rule string);
            mutually exclusive with `mask`.

    Returns:
        the heterogeneity curve h(xs), `(T,)`, non-negative.
    """
    feature = self._resolve_feature(feature)
    mask = self._resolve_mask(mask, rule)
    params = self._summary(feature, mask)
    return self._eval_payload(feature, params, xs, heterogeneity=True)[1]

grid(feature)

The evaluation grid on which this feature's effect is model-free.

The observed levels for a discrete feature; otherwise helpers.NOF_INTERNAL_POINTS equally spaced points inside the feature's axis limits — the cache grid explain evaluates every reported curve on.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name.

required

Returns:

Type Description
ndarray

(T,) array of evaluation positions.

Source code in effector/global_effect.py
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
def grid(self, feature: Union[int, str]) -> np.ndarray:
    """The evaluation grid on which this feature's effect is model-free.

    The observed levels for a discrete feature; otherwise
    `helpers.NOF_INTERNAL_POINTS` equally spaced points inside the
    feature's axis limits — the cache grid `explain` evaluates every
    reported curve on.

    Args:
        feature: index or name.

    Returns:
        `(T,)` array of evaluation positions.
    """
    feature = self._resolve_feature(feature)
    if self._is_cat(feature):
        return np.unique(self.data[:, feature])
    return np.linspace(
        self.axis_limits[0, feature],
        self.axis_limits[1, feature],
        helpers.NOF_INTERNAL_POINTS,
    )

heter_score(feature, mask=None, rule=None)

One number for a feature's heterogeneity — the scalar find_regions minimizes.

pdp.heter_score("hr")                            # global
pdp.heter_score("hr", rule="workingday == 0")    # within a subregion

In output units (units contract, method_semantics.md): the RMS of eval_heter over the feature's own (masked) data values (frequency-weighted over levels for categorical features), bridged by the feature's dispersion for the derivative-based methods (ALE/RHALE/DerPDP) so every feature type and every method lands on the same y-unit scale — "a typical instance's effect deviates from the mean effect by about this much". eval_heter itself stays a variance curve in the method's native units.

Pair it with importance

importance measures the mean effect's strength; heter_score measures the spread around it — same units, mean/spread twins. High importance + high heterogeneity = the top-right corner of effector.plot_triage — where find_regions should look.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
mask Optional[ndarray]

optional boolean (N,) subregion — model-free.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
float

a non-negative scalar, in output units.

Source code in effector/global_effect.py
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
def heter_score(
    self,
    feature: Union[int, str],
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> float:
    """One number for a feature's heterogeneity — the scalar `find_regions` minimizes.

    ```python
    pdp.heter_score("hr")                            # global
    pdp.heter_score("hr", rule="workingday == 0")    # within a subregion
    ```

    In **output units** (units contract, method_semantics.md): the RMS of
    `eval_heter` over the feature's own (masked) data values
    (frequency-weighted over levels for categorical features), bridged by
    the feature's dispersion for the derivative-based methods
    (ALE/RHALE/DerPDP) so every feature type and every method lands on the
    same y-unit scale — "a typical instance's effect deviates from the
    mean effect by about this much". `eval_heter` itself stays a variance
    curve in the method's native units.

    !!! tip "Pair it with `importance`"
        `importance` measures the *mean* effect's strength; `heter_score`
        measures the spread around it — same units, mean/spread twins.
        High importance + high heterogeneity = the top-right corner of
        `effector.plot_triage` — where `find_regions` should look.

    Args:
        feature: index or name of the feature of interest.
        mask: optional boolean `(N,)` subregion — model-free.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        a non-negative scalar, in output units.
    """
    feature = self._resolve_feature(feature)
    self._check_feature_type_supported(feature)
    mask = self._resolve_mask(mask, rule)
    return float(self._heter(feature, mask))

payload(feature)

The raw fitted object behind eval/eval_heter — pure numpy, yours to inspect.

p = ale.payload("hr")     # e.g. {"limits": ..., "bin_effect": ..., "bin_variance": ...}

Per method: per-bin effects and variances for (RH)ALE and ShapDP, the grid summaries for (d-)PDP. A copy — mutate freely.

Source code in effector/global_effect.py
743
744
745
746
747
748
749
750
751
752
753
def payload(self, feature: Union[int, str]) -> dict:
    """The raw fitted object behind `eval`/`eval_heter` — pure numpy, yours to inspect.

    ```python
    p = ale.payload("hr")     # e.g. {"limits": ..., "bin_effect": ..., "bin_variance": ...}
    ```

    Per method: per-bin effects and variances for (RH)ALE and ShapDP, the
    grid summaries for (d-)PDP. A copy — mutate freely.
    """
    return dict(self._summary(self._resolve_feature(feature), None))

importance(feature, mask=None, rule=None)

How much a feature's mean effect moves the prediction (R13).

pdp.importance("temp")                           # scalar
pdp.importance("temp", rule="workingday == 1")   # within a subregion

The dispersion of the mean effect in output units — the μ-twin of heter_score (which measures per-instance spread on the same scale). A flat curve scores ~0; a swinging curve scores high. Per method: std of the mean effect over the (masked) data values (PDP/ALE/RHALE/ ShapDP; for a linear model this is |coefficient| * std(x)), mean(|derivative|) * std(x) (DerPDP). Comparable across feature types and, in magnitude, across methods.

No y, ever

effector never sees ground-truth labels — this is a property of the fitted effect, not a loss/permutation importance.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
mask Optional[ndarray]

optional boolean (N,) subregion — model-free.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
float

a non-negative scalar.

Source code in effector/global_effect.py
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
def importance(
    self,
    feature: Union[int, str],
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> float:
    """How much a feature's mean effect moves the prediction (R13).

    ```python
    pdp.importance("temp")                           # scalar
    pdp.importance("temp", rule="workingday == 1")   # within a subregion
    ```

    The dispersion of the **mean** effect in output units — the μ-twin of
    `heter_score` (which measures per-instance spread on the same scale).
    A flat curve scores ~0; a swinging curve scores high. Per method: std
    of the mean effect over the (masked) data values (PDP/ALE/RHALE/
    ShapDP; for a linear model this is `|coefficient| * std(x)`),
    `mean(|derivative|) * std(x)` (DerPDP). Comparable across feature
    types and, in magnitude, across methods.

    !!! note "No `y`, ever"
        effector never sees ground-truth labels — this is a property of
        the fitted effect, not a loss/permutation importance.

    Args:
        feature: index or name of the feature of interest.
        mask: optional boolean `(N,)` subregion — model-free.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        a non-negative scalar.
    """
    feature = self._resolve_feature(feature)
    self._check_feature_type_supported(feature)
    mask = self._resolve_mask(mask, rule)
    if mask is None:
        # all-ones ≡ None (M1); the concrete array makes the per-method
        # `_importance` implementations mask-index without a null check
        mask = np.ones(self.data.shape[0], dtype=bool)
    self._ensure_local(feature)
    return float(self._importance(feature, mask))

importances(mask=None, rule=None)

The whole importance vector — rank your features in one call.

imp = pdp.importances()                       # (D,)
order = np.argsort(-np.nan_to_num(imp))       # most important first

NaN means unsupported, not unimportant

Feature types this method cannot explain (e.g. DerPDP on a nominal feature) return NaN, with one UserWarning naming them.

Parameters:

Name Type Description Default
mask Optional[ndarray]

optional boolean (N,) subregion.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
ndarray

the per-feature importance vector, (D,).

Source code in effector/global_effect.py
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
def importances(
    self,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The whole importance vector — rank your features in one call.

    ```python
    imp = pdp.importances()                       # (D,)
    order = np.argsort(-np.nan_to_num(imp))       # most important first
    ```

    !!! warning "NaN means unsupported, not unimportant"
        Feature types this method cannot explain (e.g. DerPDP on a
        nominal feature) return `NaN`, with one `UserWarning` naming them.

    Args:
        mask: optional boolean `(N,)` subregion.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        the per-feature importance vector, `(D,)`.
    """
    mask = self._resolve_mask(mask, rule)
    out = np.full(self.dim, np.nan)
    skipped = []
    for f in range(self.dim):
        try:
            out[f] = self.importance(f, mask=mask)
        except ValueError:
            skipped.append(self.feature_names[f])
    if skipped:
        warnings.warn(
            f"importance is undefined for feature(s) {skipped} — this "
            f"method does not support their feature type; returned NaN.",
            UserWarning,
            stacklevel=2,
        )
    return out

find_regions(feature=None, *, features=None, finder='best', candidate_conditioning_features='all')

Search for subregions that resolve a feature's heterogeneity.

part = pdp.find_regions("hr")                       # one feature -> Partition
part.show()                                         # the tree + level stats
pdp.plot("hr", rule=part.leaves[0].rule)            # drill into a leaf

parts = pdp.find_regions(features="heterogeneous")  # several -> {name: Partition}
effector.plot_triage(pdp, partitions=parts)         # the before/after picture

A query, not a mutation (R12)

The result is a value — nothing is stored on the effect. Don't like a partition? Search again with different finder kwargs; nothing needs resetting.

Model-free

Every candidate split is scored by heter_score(feature, mask) on the cached local effects — zero model calls, whatever the grid size. Binning/scope are those the feature was fitted with, replayed.

Parameters:

Name Type Description Default
feature Union[int, str, None]

index or name of the one feature to partition (→ Partition).

None
features Union[list, str, None]

several at once — a list, "all", or "heterogeneous" (heter_score at/above the median, the same convention effector.explain uses) → {feature_name: Partition}. Exactly one of feature/features must be given.

None
finder

"best" (default), "best_level_wise", or a configured finder instance (e.g. effector.space_partitioning.Best(...)).

'best'
candidate_conditioning_features

features allowed to define splits ("all" or a list of indices/names).

'all'

Returns:

Type Description

a Partition bound to this effect — or {feature_name: Partition}

with features=.

Source code in effector/global_effect.py
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
def find_regions(
    self,
    feature: Union[int, str, None] = None,
    *,
    features: Union[list, str, None] = None,
    finder="best",
    candidate_conditioning_features="all",
):
    """Search for subregions that resolve a feature's heterogeneity.

    ```python
    part = pdp.find_regions("hr")                       # one feature -> Partition
    part.show()                                         # the tree + level stats
    pdp.plot("hr", rule=part.leaves[0].rule)            # drill into a leaf

    parts = pdp.find_regions(features="heterogeneous")  # several -> {name: Partition}
    effector.plot_triage(pdp, partitions=parts)         # the before/after picture
    ```

    !!! note "A query, not a mutation (R12)"
        The result is a value — nothing is stored on the effect. Don't
        like a partition? Search again with different finder kwargs;
        nothing needs resetting.

    !!! note "Model-free"
        Every candidate split is scored by `heter_score(feature, mask)`
        on the cached local effects — zero model calls, whatever the grid
        size. Binning/scope are those the feature was fitted with,
        replayed.

    Args:
        feature: index or name of the one feature to partition
            (→ `Partition`).
        features: several at once — a list, `"all"`, or `"heterogeneous"`
            (heter_score at/above the median, the same convention
            `effector.explain` uses) → `{feature_name: Partition}`.
            Exactly one of `feature`/`features` must be given.
        finder: `"best"` (default), `"best_level_wise"`, or a configured
            finder instance (e.g. `effector.space_partitioning.Best(...)`).
        candidate_conditioning_features: features allowed to define splits
            (`"all"` or a list of indices/names).

    Returns:
        a `Partition` bound to this effect — or `{feature_name: Partition}`
        with `features=`.
    """
    if (feature is None) == (features is None):
        raise ValueError(
            "find_regions takes exactly one of `feature` (singular -> "
            "Partition) or `features` (plural -> {name: Partition})"
        )
    if features is not None:
        return self._find_regions_plural(
            features,
            finder=finder,
            candidate_conditioning_features=candidate_conditioning_features,
        )

    from effector import space_partitioning  # lazy: one-way dep guard

    feature = self._resolve_feature(feature)
    if isinstance(candidate_conditioning_features, list):
        candidate_conditioning_features = [
            self._resolve_feature(f) for f in candidate_conditioning_features
        ]
    self._check_feature_type_supported(feature)
    self._ensure_local(feature)

    if isinstance(finder, str):
        finder = space_partitioning.return_default(finder)

    def score_fn(mask):
        return self.heter_score(feature, mask=mask)  # RAW; guard is the finder's

    partition = finder.find_regions(
        feature,
        self.data,
        score_fn,
        axis_limits=self.axis_limits,
        feature_types=self.feature_types,
        cat_limit=self.cat_limit,
        candidate_conditioning_features=candidate_conditioning_features,
        feature_names=self.feature_names,
        target_name=self.target_name,
    )
    return partition.bind(self)

select_regions(partitions=None, *, features='heterogeneous', finder='best', candidate_conditioning_features='all', min_r2_gain=0.01)

Greedily select which partitions earn their complexity — the CALM chain.

chain = pdp.select_regions()      # search + select in one call
chain.show()                      # GAM R2, each accepted split, the rejected
chain.final                       # the last CALM — the regional analysis
chain[0]                          # the GAM snapshot

find_regions proposes one candidate Partition per feature; this verb decides across features which of them actually explain the model: starting from the GAM (all features global), each round applies the split with the largest explained-variance gain — the surrogate R² against , measured on top of the splits already applied — and stops when no remaining split adds at least min_r2_gain. Every accepted round is a snapshot (CALM) of increased complexity; the whole chain is the report's ledger.

A query, not a mutation (R12)

The result is a value — nothing is stored on the effect.

One prediction pass

Beyond fit, the only model touch is one f̂(X) pass for the variance denominator (cached on the effect); the search, the scoring, and every snapshot's summaries are model-free.

Parameters:

Name Type Description Default
partitions Optional[dict]

pre-computed candidates — {feature_index_or_name: Partition}, exactly what find_regions(features=...) returns. None (default) runs the search here first.

None
features Union[list, str]

which features to search when partitions is None — a list, "all", or "heterogeneous" (default; heter_score at/above the median).

'heterogeneous'
finder

region finder, as in find_regions.

'best'
candidate_conditioning_features

features allowed to define splits ("all" or a list of indices/names).

'all'
min_r2_gain float

smallest explained-variance marginal (fraction of Var(f̂), default 0.01 = 1 pt) a split must add — on top of the splits already applied — to earn a snapshot.

0.01

Returns:

Type Description

a CalmSequence[GAM, calm1, ...], R² non-decreasing along

it, with the rejected splits in .skipped

("redundant"/"below_threshold").

Raises:

Type Description
ValueError

the method is derivative-scale (no output-scale surrogate) or Var(f̂) == 0.

Source code in effector/global_effect.py
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
def select_regions(
    self,
    partitions: Optional[dict] = None,
    *,
    features: Union[list, str] = "heterogeneous",
    finder="best",
    candidate_conditioning_features="all",
    min_r2_gain: float = 0.01,
):
    """Greedily select which partitions earn their complexity — the CALM chain.

    ```python
    chain = pdp.select_regions()      # search + select in one call
    chain.show()                      # GAM R2, each accepted split, the rejected
    chain.final                       # the last CALM — the regional analysis
    chain[0]                          # the GAM snapshot
    ```

    `find_regions` proposes one candidate `Partition` per feature;
    this verb decides *across* features which of them actually explain
    the model: starting from the GAM (all features global), each round
    applies the split with the largest explained-variance gain — the
    surrogate R² against `f̂`, measured on top of the splits already
    applied — and stops when no remaining split adds at least
    `min_r2_gain`. Every accepted round is a snapshot (`CALM`) of
    increased complexity; the whole chain is the report's ledger.

    !!! note "A query, not a mutation (R12)"
        The result is a value — nothing is stored on the effect.

    !!! note "One prediction pass"
        Beyond `fit`, the only model touch is one `f̂(X)` pass for the
        variance denominator (cached on the effect); the search, the
        scoring, and every snapshot's summaries are model-free.

    Args:
        partitions: pre-computed candidates — `{feature_index_or_name:
            Partition}`, exactly what `find_regions(features=...)`
            returns. `None` (default) runs the search here first.
        features: which features to search when `partitions` is `None` —
            a list, `"all"`, or `"heterogeneous"` (default; heter_score
            at/above the median).
        finder: region finder, as in `find_regions`.
        candidate_conditioning_features: features allowed to define
            splits (`"all"` or a list of indices/names).
        min_r2_gain: smallest explained-variance marginal (fraction of
            `Var(f̂)`, default 0.01 = 1 pt) a split must add — on top of
            the splits already applied — to earn a snapshot.

    Returns:
        a `CalmSequence` — `[GAM, calm1, ...]`, R² non-decreasing along
        it, with the rejected splits in `.skipped`
        (`"redundant"`/`"below_threshold"`).

    Raises:
        ValueError: the method is derivative-scale (no output-scale
            surrogate) or `Var(f̂) == 0`.
    """
    from effector import explained_variance as _ev  # lazy: one-way dep guard

    if partitions is None:
        partitions = self.find_regions(
            features=features,
            finder=finder,
            candidate_conditioning_features=candidate_conditioning_features,
        )
    parts = {}
    for key, p in partitions.items():
        f = self._resolve_feature(key)
        parts[f] = p if p._effect is not None else p.bind(self)

    supported = []
    for f in range(self.dim):
        try:
            self._check_feature_type_supported(f)
            supported.append(f)
        except ValueError:
            continue
    return _ev.select(self, parts, supported, min_gain=min_r2_gain)

explain(*, y=None, top_k=5, coverage=0.8, heter_threshold=None, min_r2_gain=0.01, finder='best', candidate_conditioning_features='all')

The one-liner on this engine — effector.explain without leaving the session.

pdp = effector.PDP(X, model, schema=schema)
report = pdp.explain()            # same Report as effector.explain

Runs the same pipeline as effector.explain on the already-built engine: features you have fit with custom config keep it (missing ones are computed with the defaults), and every cache the pipeline warms stays on the engine for your follow-up queries.

Parameters:

Name Type Description Default
y Optional[ndarray]

optional ground truth aligned with the original data (or this engine's subsample); when given, the report header states the model's score on the explained subsample.

None
top_k int

hard ceiling on how many features get curve plots.

5
coverage float

stop plotting once the shown features carry this share of the total importance mass (default 0.8).

0.8
heter_threshold Optional[float]

minimum heter_score to enter the region search; None (default) = the median convention.

None
min_r2_gain float

smallest explained-variance marginal a split must add to earn a snapshot in the CALM chain.

0.01
finder

region finder, as in find_regions.

'best'
candidate_conditioning_features

features allowed to define splits.

'all'

Returns:

Type Description

a Report bound to this engine.

Source code in effector/global_effect.py
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
def explain(
    self,
    *,
    y: Optional[np.ndarray] = None,
    top_k: int = 5,
    coverage: float = 0.8,
    heter_threshold: Optional[float] = None,
    min_r2_gain: float = 0.01,
    finder="best",
    candidate_conditioning_features="all",
):
    """The one-liner on *this* engine — `effector.explain` without leaving the session.

    ```python
    pdp = effector.PDP(X, model, schema=schema)
    report = pdp.explain()            # same Report as effector.explain
    ```

    Runs the same pipeline as `effector.explain` on the already-built
    engine: features you have `fit` with custom config keep it (missing
    ones are computed with the defaults), and every cache the pipeline
    warms stays on the engine for your follow-up queries.

    Args:
        y: optional ground truth aligned with the original `data` (or
            this engine's subsample); when given, the report header
            states the model's score on the explained subsample.
        top_k: hard ceiling on how many features get curve plots.
        coverage: stop plotting once the shown features carry this share
            of the total importance mass (default 0.8).
        heter_threshold: minimum `heter_score` to enter the region
            search; `None` (default) = the median convention.
        min_r2_gain: smallest explained-variance marginal a split must
            add to earn a snapshot in the CALM chain.
        finder: region finder, as in `find_regions`.
        candidate_conditioning_features: features allowed to define
            splits.

    Returns:
        a `Report` bound to this engine.
    """
    from effector import report as _report  # lazy: one-way dep guard

    return _report._explain_effect(
        self,
        y=y,
        top_k=top_k,
        coverage=coverage,
        heter_threshold=heter_threshold,
        min_r2_gain=min_r2_gain,
        finder=finder,
        candidate_conditioning_features=candidate_conditioning_features,
    )

plot(feature, heterogeneity=True, centering=True, scale_x=None, scale_y=None, show_avg_output=False, y_limits=None, dy_limits=None, show_only_aggregated=False, show_plot=True, mask=None, rule=None, feature_label=None)

Plot the (RH)ALE effect of feature.

ale.plot("hr")                          # curve + heterogeneity
ale.plot("hr", rule="workingday == 0")  # within a subregion

For a continuous feature the figure has two panels: the accumulated curve on top, the per-bin average local effect (± std) below. Categorical features get one bar per level with std whiskers.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature to plot.

required
heterogeneity Union[bool, str]

False for the mean effect only; True or "std" (default) adds the per-bin std of the local effects.

True
centering Union[bool, str]

False (none), True/"zero_integral" (center around the y axis), or "zero_start" (start at y=0).

True
scale_x Optional[dict]

None or {"mean": m, "std": s} to undo a standardization of the x axis for display.

None
scale_y Optional[dict]

same, for the y axis.

None
show_avg_output bool

draw the model's average output as a horizontal line.

False
y_limits Optional[List]

(low, high) for the y axis; None = automatic.

None
dy_limits Optional[List]

(low, high) for the bottom (local-effect) panel; None = automatic.

None
show_only_aggregated bool

draw only the accumulated curve, without the bottom panel.

False
show_plot bool

if False, return the figure and axes instead of showing.

True
mask Optional[ndarray]

boolean (N,) selecting a subregion — plot the effect within it (re-binned from the cached local effects, no model calls), x axis windowed to the subregion's own interval.

None
rule

sugar over mask — an effector.Rule or a rule string, applied to the effect's data. Mutually exclusive with mask.

None
feature_label Optional[str]

display title for the figure (e.g. a regional node's label with its rule); defaults to the feature name. The x-axis always keeps the plain feature name.

None
Source code in effector/global_effect_ale.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
def plot(
    self,
    feature: Union[int, str],
    heterogeneity: Union[bool, str] = True,
    centering: Union[bool, str] = True,
    scale_x: Optional[dict] = None,
    scale_y: Optional[dict] = None,
    show_avg_output: bool = False,
    y_limits: Optional[List] = None,
    dy_limits: Optional[List] = None,
    show_only_aggregated: bool = False,
    show_plot: bool = True,
    mask: Optional[np.ndarray] = None,
    rule=None,
    feature_label: Optional[str] = None,
):
    """Plot the (RH)ALE effect of `feature`.

    ```python
    ale.plot("hr")                          # curve + heterogeneity
    ale.plot("hr", rule="workingday == 0")  # within a subregion
    ```

    For a continuous feature the figure has two panels: the accumulated
    curve on top, the per-bin average local effect (± std) below.
    Categorical features get one bar per level with std whiskers.

    Args:
        feature: index or name of the feature to plot.
        heterogeneity: `False` for the mean effect only; `True` or
            `"std"` (default) adds the per-bin std of the local effects.
        centering: `False` (none), `True`/`"zero_integral"` (center
            around the y axis), or `"zero_start"` (start at `y=0`).
        scale_x: `None` or `{"mean": m, "std": s}` to undo a
            standardization of the x axis for display.
        scale_y: same, for the y axis.
        show_avg_output: draw the model's average output as a
            horizontal line.
        y_limits: `(low, high)` for the y axis; `None` = automatic.
        dy_limits: `(low, high)` for the bottom (local-effect) panel;
            `None` = automatic.
        show_only_aggregated: draw only the accumulated curve, without
            the bottom panel.
        show_plot: if `False`, return the figure and axes instead of
            showing.
        mask: boolean `(N,)` selecting a subregion — plot the effect
            *within* it (re-binned from the cached local effects, no
            model calls), x axis windowed to the subregion's own
            interval.
        rule: sugar over `mask` — an `effector.Rule` or a rule string,
            applied to the effect's data. Mutually exclusive with `mask`.
        feature_label: display title for the figure (e.g. a regional
            node's label with its rule); defaults to the feature name.
            The x-axis always keeps the plain feature name.
    """
    feature = self._resolve_feature(feature)
    heterogeneity = helpers.prep_confidence_interval(heterogeneity)
    centering = helpers.prep_centering(centering)
    scale_x = helpers.resolve_scale(
        scale_x, self.scale_x_list[feature] if self.scale_x_list else None
    )
    scale_y = helpers.resolve_scale(scale_y, self.scale_y)
    mask = self._resolve_mask(mask, rule)
    feature_names = self.feature_names
    # C2: title = feature (or leaf label with its rule); method · scope
    # context moves to the corner tag
    plot_title = (
        feature_label if feature_label is not None else feature_names[feature]
    )
    tag = (
        f"{'ALE' if self.method_name == 'ale' else 'RHALE'}"
        f" · {'regional' if mask is not None else 'global'}"
    )

    # one path for global and masked alike (R14): pick the payload, read it
    is_cat = self._is_cat(feature)
    params = self._summary(feature, mask)
    x_window = (
        self._effective_limits(feature, mask)
        if mask is not None and not is_cat
        else None
    )

    def centered_eval(xs):
        y = self._eval_payload(feature, params, xs)
        if centering is not False:
            y = y - self._centering_const(feature, mask, centering)
        return y

    # the accumulated curve is piecewise linear between bin limits, so
    # evaluating exactly at the limits draws it exactly (no resampling).
    # categoricals are drawn by the is_cat branch below (at their observed
    # level values); their limits are positional codes 0..K-1 that eval
    # would reject, so only build this grid for continuous features.
    if not is_cat:
        x = np.asarray(params["limits"], dtype=float)
        y = centered_eval(x)

    avg_output = self._avg_output(mask, scale_y) if show_avg_output else None

    if is_cat:
        # bars = accumulated per-level values (in fit order); whiskers =
        # the variance of the step into each level (method_semantics.md)
        levels, labels = self._level_display(feature, params["levels"])
        y_levels = centered_eval(levels)
        variances = (
            self._eval_payload(feature, params, levels, heterogeneity=True)[1]
            if heterogeneity is not False
            else None
        )
        level_kind = self.feature_types[feature]
        level_counts = self._level_counts_for(feature, mask, levels)
        positions = np.asarray(levels, dtype=float)
        plot_scale_x, sort = scale_x, None
        if level_kind == "ordinal" and np.any(np.diff(positions) < 0):
            # custom (declared/induced) order: draw by rank in fit order —
            # ranks are display geometry, so the feature scale must not
            # touch them — and label by level
            if labels is None:
                labels = [f"{v:g}" for v in positions]
            positions = np.arange(len(positions), dtype=float)
            plot_scale_x, sort = None, False
        return vis.plot_categorical_effect(
            positions,
            y_levels,
            variances,
            feature,
            heterogeneity,
            title=plot_title,
            level_labels=labels,
            scale_x=plot_scale_x,
            scale_y=scale_y,
            avg_output=avg_output,
            feature_names=feature_names,
            target_name=self.target_name,
            y_limits=y_limits,
            # the accumulation path is meaningful only along an ordered
            # axis; sorted nominal bars would fake an interpolation
            connect_line=level_kind == "ordinal",
            show_plot=show_plot,
            tag=tag,
            level_kind=level_kind,
            sort=sort,
            level_counts=level_counts,
        )
    return vis.ale_plot(
        x,
        y,
        bin_effect=params["bin_effect"],
        bin_variance=params["bin_variance"],
        limits=params["limits"],
        dx=params["dx"],
        feature=feature,
        heterogeneity=heterogeneity,
        scale_x=scale_x,
        scale_y=scale_y,
        title=plot_title,
        avg_output=avg_output,
        feature_names=feature_names,
        target_name=self.target_name,
        y_limits=y_limits,
        dy_limits=dy_limits,
        show_only_aggregated=show_only_aggregated,
        show_plot=show_plot,
        x_limits=x_window,
        tag=tag,
    )

effector.global_effect_ale.RHALE(data, model, model_jac=None, *, data_effect=None, nof_instances=10000, axis_limits=None, schema=None, random_state=21)

Bases: ALEBase

Robust and Heterogeneity-aware ALE: ALE computed from the model Jacobian, with automatic variable-size binning.

rhale = effector.RHALE(X, model, model_jac)
rhale.plot("hr")

The local effect of an instance is the pointwise derivative instead of ALE's bin secant, so it does not depend on the bin width — bins can be sized automatically (dynamic programming by default) to balance bias and variance. Accumulation and heterogeneity are then identical to ALE:

\[ \hat{f}^{RHALE}(x) = \sum_{k=1}^{k_x - 1} (z_k - z_{k-1})\, \mu_k + (x - z_{k_x - 1})\, \mu_{k_x}, \qquad \mu_k = \frac{1}{|S_k|} \sum_{i \in S_k} \frac{\partial f}{\partial x_s}(x^i) \]

Needs derivatives

Pass model_jac (or a precomputed data_effect); otherwise the Jacobian is estimated with slower, less exact numerical differentiation. The Jacobian only serves the continuous features: ordinal ones use discrete differences with adaptive level grouping, and nominal ones fall back to ALE exactly (one bin per transition, no grouping — "adjacent" is not real under an arbitrary order).

Build a RHALE explainer. No model calls happen here.

Heterogeneity

eval_heter returns a step function: the variance of the per-instance derivatives within the bin containing \(x\),

\[ h(x) = \sigma^2_{k_x}, \qquad \sigma^2_k = \frac{1}{|S_k|} \sum_{i \in S_k} (\mathtt{effect}_i - \mu_k)^2 \]

The bin plot draws \(\sqrt{\sigma^2_k}\) as error bars.

Parameters:

Name Type Description Default
data ndarray

the design matrix, shape (N, D) — numpy only.

required
model callable

the black-box model — a Callable mapping (N, D) arrays to (N,) predictions.

required
model_jac Union[None, callable]

the model Jacobian — a Callable mapping (N, D) arrays to (N, D) derivatives. If None (and no data_effect), the Jacobian is computed numerically.

None
data_effect Optional[ndarray]

precomputed Jacobian on data, shape (N, D); skips calling model_jac.

None
nof_instances Union[int, str]

max instances kept (default 10_000) — an int subsamples randomly, "all" keeps everything.

10000
axis_limits Optional[ndarray]

per-feature plot limits, shape (2, D); None (default) infers them from data.

None
schema Optional[Union[Schema, dict]]

input metadata — an effector.Schema or a plain dict with any of feature_names, feature_types, cat_limit, target_name, scale_x_list, scale_y; omitted fields are inferred from data, explicit ones win. Coming from a DataFrame? Use effector.from_dataframe.

None
random_state Optional[int]

seed for every internal random step (default 21, reproducible); None for non-deterministic behavior.

21

Methods:

Name Description
fit

Declare per-feature defaults and warm the caches.

eval

The mean effect of a feature at positions xs.

eval_heter

The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

grid

The evaluation grid on which this feature's effect is model-free.

heter_score

One number for a feature's heterogeneity — the scalar find_regions minimizes.

payload

The raw fitted object behind eval/eval_heter — pure numpy, yours to inspect.

importance

How much a feature's mean effect moves the prediction (R13).

importances

The whole importance vector — rank your features in one call.

find_regions

Search for subregions that resolve a feature's heterogeneity.

select_regions

Greedily select which partitions earn their complexity — the CALM chain.

explain

The one-liner on this engine — effector.explain without leaving the session.

plot

Plot the (RH)ALE effect of feature.

Source code in effector/global_effect_ale.py
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
def __init__(
    self,
    data: np.ndarray,
    model: callable,
    model_jac: typing.Union[None, callable] = None,
    *,
    data_effect: typing.Optional[np.ndarray] = None,
    nof_instances: typing.Union[int, str] = 10_000,
    axis_limits: typing.Optional[np.ndarray] = None,
    schema: Optional[Union[ingestion.Schema, dict]] = None,
    random_state: typing.Optional[int] = 21,
):
    r"""Build a RHALE explainer. No model calls happen here.

    ??? note "Heterogeneity"
        `eval_heter` returns a step function: the variance of the
        per-instance derivatives within the bin containing $x$,

        $$
        h(x) = \sigma^2_{k_x},
        \qquad
        \sigma^2_k = \frac{1}{|S_k|} \sum_{i \in S_k}
        (\mathtt{effect}_i - \mu_k)^2
        $$

        The bin plot draws $\sqrt{\sigma^2_k}$ as error bars.

    Args:
        data: the design matrix, shape `(N, D)` — numpy only.
        model: the black-box model — a `Callable` mapping `(N, D)`
            arrays to `(N,)` predictions.
        model_jac: the model Jacobian — a `Callable` mapping `(N, D)`
            arrays to `(N, D)` derivatives. If `None` (and no
            `data_effect`), the Jacobian is computed numerically.
        data_effect: precomputed Jacobian on `data`, shape `(N, D)`;
            skips calling `model_jac`.
        nof_instances: max instances kept (default `10_000`) — an `int`
            subsamples randomly, `"all"` keeps everything.
        axis_limits: per-feature plot limits, shape `(2, D)`; `None`
            (default) infers them from `data`.
        schema: input metadata — an `effector.Schema` or a plain `dict`
            with any of `feature_names`, `feature_types`, `cat_limit`,
            `target_name`, `scale_x_list`, `scale_y`; omitted fields are
            inferred from `data`, explicit ones win. Coming from a
            DataFrame? Use `effector.from_dataframe`.
        random_state: seed for every internal random step (default `21`,
            reproducible); `None` for non-deterministic behavior.
    """
    super(RHALE, self).__init__(
        data,
        model,
        model_jac,
        data_effect=data_effect,
        nof_instances=nof_instances,
        axis_limits=axis_limits,
        schema=schema,
        random_state=random_state,
        method_name="RHALE",
    )

fit(features='all', *, centering=True, binning_method='dp', order=None, binning_scope='global')

Declare per-feature defaults and warm the caches.

rhale.fit("hr", binning_method="dp")

fit is optional

eval, plot, heter_score compute what they need lazily with these defaults; fit declares the config once and pays the model cost upfront.

Parameters:

Name Type Description Default
features Union[int, str, list]

feature(s) to fit — index, name, list, or "all".

'all'
centering Union[bool, str]

default centering for this feature's queries — False (none), True/"zero_integral" (center around the y axis), or "zero_start" (start at y=0).

True
binning_method Union[str, DynamicProgramming, Agglomerative, Quantile, Fixed]

how the axis is split into bins:

  • "dp" (default): dynamic programming — optimal variable-size bins
  • "agglomerative": bottom-up merging of small bins ("greedy" is a deprecated alias)
  • "quantile": equal-frequency bins
  • "fixed": equal-width bins

For custom parameters pass an instance from effector.axis_partitioning, e.g. DynamicProgramming(max_nof_bins=30).

'dp'
order Union[None, str, list]

level order for a categorical feature of interest:

  • None (default): ascending encoded order
  • "similarity": induce the order from the other features (KS-distance seriation, Molnar/iml)
  • a list of the levels: declare it explicitly (applies to exactly one categorical feature)

Changing order invalidates the cached local effects: the next query recomputes them; re-fitting the same order is a cache hit.

None
binning_scope str

the x-range the binner covers when a masked summary re-bins a subregion (eval/eval_heter/plot/ heter_score with mask=; the regional split search):

  • "global" (default): the frozen global axis_limits — one frame for every subregion, directly comparable
  • "effective": the masked column's own [min, max] — bins packed into the subregion, finer resolution

Recorded at fit and replayed by every masked call. Ignored when no mask is involved.

'global'
Source code in effector/global_effect_ale.py
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
def fit(
    self,
    features: typing.Union[int, str, list] = "all",
    *,
    centering: typing.Union[bool, str] = True,
    binning_method: typing.Union[
        str, ap.DynamicProgramming, ap.Agglomerative, ap.Quantile, ap.Fixed
    ] = "dp",
    order: typing.Union[None, str, list] = None,
    binning_scope: str = "global",
) -> None:
    """Declare per-feature defaults and warm the caches.

    ```python
    rhale.fit("hr", binning_method="dp")
    ```

    !!! note "fit is optional"
        `eval`, `plot`, `heter_score` compute what they need lazily with
        these defaults; `fit` declares the config once and pays the model
        cost upfront.

    Args:
        features: feature(s) to fit — index, name, list, or `"all"`.
        centering: default centering for this feature's queries —
            `False` (none), `True`/`"zero_integral"` (center around the
            y axis), or `"zero_start"` (start at `y=0`).
        binning_method: how the axis is split into bins:

            - `"dp"` (default): dynamic programming — optimal
              variable-size bins
            - `"agglomerative"`: bottom-up merging of small bins
              (`"greedy"` is a deprecated alias)
            - `"quantile"`: equal-frequency bins
            - `"fixed"`: equal-width bins

            For custom parameters pass an instance from
            `effector.axis_partitioning`, e.g.
            `DynamicProgramming(max_nof_bins=30)`.

        order: level order for a *categorical* feature of interest:

            - `None` (default): ascending encoded order
            - `"similarity"`: induce the order from the other features
              (KS-distance seriation, Molnar/iml)
            - a list of the levels: declare it explicitly (applies to
              exactly one categorical feature)

            Changing `order` invalidates the cached local effects: the
            next query recomputes them; re-fitting the same `order` is a
            cache hit.

        binning_scope: the x-range the binner covers when a *masked*
            summary re-bins a subregion (`eval`/`eval_heter`/`plot`/
            `heter_score` with `mask=`; the regional split search):

            - `"global"` (default): the frozen global `axis_limits` —
              one frame for every subregion, directly comparable
            - `"effective"`: the masked column's own `[min, max]` —
              bins packed into the subregion, finer resolution

            Recorded at fit and replayed by every masked call. Ignored
            when no mask is involved.
    """
    # validation is the resolver's job (R6): one table, one error message
    binning_method = ap.return_default(binning_method)
    self._validate_order_arg(features, order)
    check_binning_scope(binning_scope)

    self._fit_loop(
        features,
        centering,
        binning_method=binning_method,
        order=order,
        binning_scope=binning_scope,
    )

eval(feature, xs, centering=None, mask=None, rule=None)

The mean effect of a feature at positions xs.

xs = np.linspace(0, 24, 100)
y = pdp.eval("hr", xs)                            # (100,) mean effect
y_wd = pdp.eval("hr", xs, rule="workingday == 0") # same, on a subregion

One array, one type (R1)

eval always returns the mean effect only. The spread around it has its own ladder: eval_heter (curve), heter_score (scalar), payload (the raw fitted object).

Discrete features

Ordinal/nominal features are evaluated only at observed levels — any other xs value raises ValueError.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
xs ndarray

where to evaluate, (T,).

required
centering Union[None, bool, str]

None (class default), False, True/"zero_integral", or "zero_start".

None
mask Optional[ndarray]

optional boolean (N,) selecting a subregion — the effect within it, re-summarized from cached local effects with zero model calls. Nothing is stored.

None
rule Union[None, str, Rule]

sugar over mask — an effector.Rule or a string like "temp < 3 and season == 0". Mutually exclusive with mask.

None

Returns:

Type Description
ndarray

the mean effect at xs, shape (T,).

Source code in effector/global_effect.py
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
def eval(
    self,
    feature: Union[int, str],
    xs: np.ndarray,
    centering: Union[None, bool, str] = None,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The mean effect of a feature at positions `xs`.

    ```python
    xs = np.linspace(0, 24, 100)
    y = pdp.eval("hr", xs)                            # (100,) mean effect
    y_wd = pdp.eval("hr", xs, rule="workingday == 0") # same, on a subregion
    ```

    !!! note "One array, one type (R1)"
        `eval` always returns the mean effect only. The spread around it
        has its own ladder: `eval_heter` (curve), `heter_score` (scalar),
        `payload` (the raw fitted object).

    !!! warning "Discrete features"
        Ordinal/nominal features are evaluated **only at observed
        levels** — any other `xs` value raises `ValueError`.

    Args:
        feature: index or name of the feature of interest.
        xs: where to evaluate, `(T,)`.
        centering: `None` (class default), `False`,
            `True`/`"zero_integral"`, or `"zero_start"`.
        mask: optional boolean `(N,)` selecting a subregion — the effect
            *within* it, re-summarized from cached local effects with zero
            model calls. Nothing is stored.
        rule: sugar over `mask` — an `effector.Rule` or a string like
            `"temp < 3 and season == 0"`. Mutually exclusive with `mask`.

    Returns:
        the mean effect at `xs`, shape `(T,)`.
    """
    feature = self._resolve_feature(feature)
    centering = self.DEFAULT_CENTERING if centering is None else centering
    centering = helpers.prep_centering(centering)
    mask = self._resolve_mask(mask, rule)

    if not self._is_cat(feature):
        if mask is not None:
            self._effective_limits(feature, mask)  # degeneracy guard
        elif not self.axis_limits[0, feature] < self.axis_limits[1, feature]:
            raise ValueError(
                f"Feature {feature} has a degenerate axis interval "
                f"[{self.axis_limits[0, feature]}, {self.axis_limits[1, feature]}]"
            )

    params = self._summary(feature, mask)
    y = self._eval_mean(feature, xs, params, mask)
    if centering is not False:
        y = y - self._mean_norm_const(
            self._centering_const(feature, mask, centering)
        )
    return y

eval_heter(feature, xs, mask=None, rule=None)

The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

h = pdp.eval_heter("hr", xs)          # (T,) variance around the mean
band = np.sqrt(h)                     # std-like band, plot-ready

It's a variance, and it's method-specific (R2)

PDP: variance of centered ICE; DerPDP: of d-ICE slopes; ALE/RHALE: per-bin slope variance as a step function; ShapDP: interpolated per-bin φ variance. Take the square root for a band.

No centering argument — by design

Heterogeneity is invariant to centering; the signature enforces it.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
xs ndarray

where to evaluate, (T,).

required
mask Optional[ndarray]

optional boolean (N,) subregion — re-summarized from cached local effects, zero model calls.

None
rule Union[None, str, Rule]

sugar over mask (an effector.Rule or a rule string); mutually exclusive with mask.

None

Returns:

Type Description
ndarray

the heterogeneity curve h(xs), (T,), non-negative.

Source code in effector/global_effect.py
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
def eval_heter(
    self,
    feature: Union[int, str],
    xs: np.ndarray,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

    ```python
    h = pdp.eval_heter("hr", xs)          # (T,) variance around the mean
    band = np.sqrt(h)                     # std-like band, plot-ready
    ```

    !!! note "It's a variance, and it's method-specific (R2)"
        PDP: variance of centered ICE; DerPDP: of d-ICE slopes; ALE/RHALE:
        per-bin slope variance as a step function; ShapDP: interpolated
        per-bin φ variance. Take the square root for a band.

    !!! note "No `centering` argument — by design"
        Heterogeneity is invariant to centering; the signature enforces it.

    Args:
        feature: index or name of the feature of interest.
        xs: where to evaluate, `(T,)`.
        mask: optional boolean `(N,)` subregion — re-summarized from cached
            local effects, zero model calls.
        rule: sugar over `mask` (an `effector.Rule` or a rule string);
            mutually exclusive with `mask`.

    Returns:
        the heterogeneity curve h(xs), `(T,)`, non-negative.
    """
    feature = self._resolve_feature(feature)
    mask = self._resolve_mask(mask, rule)
    params = self._summary(feature, mask)
    return self._eval_payload(feature, params, xs, heterogeneity=True)[1]

grid(feature)

The evaluation grid on which this feature's effect is model-free.

The observed levels for a discrete feature; otherwise helpers.NOF_INTERNAL_POINTS equally spaced points inside the feature's axis limits — the cache grid explain evaluates every reported curve on.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name.

required

Returns:

Type Description
ndarray

(T,) array of evaluation positions.

Source code in effector/global_effect.py
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
def grid(self, feature: Union[int, str]) -> np.ndarray:
    """The evaluation grid on which this feature's effect is model-free.

    The observed levels for a discrete feature; otherwise
    `helpers.NOF_INTERNAL_POINTS` equally spaced points inside the
    feature's axis limits — the cache grid `explain` evaluates every
    reported curve on.

    Args:
        feature: index or name.

    Returns:
        `(T,)` array of evaluation positions.
    """
    feature = self._resolve_feature(feature)
    if self._is_cat(feature):
        return np.unique(self.data[:, feature])
    return np.linspace(
        self.axis_limits[0, feature],
        self.axis_limits[1, feature],
        helpers.NOF_INTERNAL_POINTS,
    )

heter_score(feature, mask=None, rule=None)

One number for a feature's heterogeneity — the scalar find_regions minimizes.

pdp.heter_score("hr")                            # global
pdp.heter_score("hr", rule="workingday == 0")    # within a subregion

In output units (units contract, method_semantics.md): the RMS of eval_heter over the feature's own (masked) data values (frequency-weighted over levels for categorical features), bridged by the feature's dispersion for the derivative-based methods (ALE/RHALE/DerPDP) so every feature type and every method lands on the same y-unit scale — "a typical instance's effect deviates from the mean effect by about this much". eval_heter itself stays a variance curve in the method's native units.

Pair it with importance

importance measures the mean effect's strength; heter_score measures the spread around it — same units, mean/spread twins. High importance + high heterogeneity = the top-right corner of effector.plot_triage — where find_regions should look.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
mask Optional[ndarray]

optional boolean (N,) subregion — model-free.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
float

a non-negative scalar, in output units.

Source code in effector/global_effect.py
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
def heter_score(
    self,
    feature: Union[int, str],
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> float:
    """One number for a feature's heterogeneity — the scalar `find_regions` minimizes.

    ```python
    pdp.heter_score("hr")                            # global
    pdp.heter_score("hr", rule="workingday == 0")    # within a subregion
    ```

    In **output units** (units contract, method_semantics.md): the RMS of
    `eval_heter` over the feature's own (masked) data values
    (frequency-weighted over levels for categorical features), bridged by
    the feature's dispersion for the derivative-based methods
    (ALE/RHALE/DerPDP) so every feature type and every method lands on the
    same y-unit scale — "a typical instance's effect deviates from the
    mean effect by about this much". `eval_heter` itself stays a variance
    curve in the method's native units.

    !!! tip "Pair it with `importance`"
        `importance` measures the *mean* effect's strength; `heter_score`
        measures the spread around it — same units, mean/spread twins.
        High importance + high heterogeneity = the top-right corner of
        `effector.plot_triage` — where `find_regions` should look.

    Args:
        feature: index or name of the feature of interest.
        mask: optional boolean `(N,)` subregion — model-free.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        a non-negative scalar, in output units.
    """
    feature = self._resolve_feature(feature)
    self._check_feature_type_supported(feature)
    mask = self._resolve_mask(mask, rule)
    return float(self._heter(feature, mask))

payload(feature)

The raw fitted object behind eval/eval_heter — pure numpy, yours to inspect.

p = ale.payload("hr")     # e.g. {"limits": ..., "bin_effect": ..., "bin_variance": ...}

Per method: per-bin effects and variances for (RH)ALE and ShapDP, the grid summaries for (d-)PDP. A copy — mutate freely.

Source code in effector/global_effect.py
743
744
745
746
747
748
749
750
751
752
753
def payload(self, feature: Union[int, str]) -> dict:
    """The raw fitted object behind `eval`/`eval_heter` — pure numpy, yours to inspect.

    ```python
    p = ale.payload("hr")     # e.g. {"limits": ..., "bin_effect": ..., "bin_variance": ...}
    ```

    Per method: per-bin effects and variances for (RH)ALE and ShapDP, the
    grid summaries for (d-)PDP. A copy — mutate freely.
    """
    return dict(self._summary(self._resolve_feature(feature), None))

importance(feature, mask=None, rule=None)

How much a feature's mean effect moves the prediction (R13).

pdp.importance("temp")                           # scalar
pdp.importance("temp", rule="workingday == 1")   # within a subregion

The dispersion of the mean effect in output units — the μ-twin of heter_score (which measures per-instance spread on the same scale). A flat curve scores ~0; a swinging curve scores high. Per method: std of the mean effect over the (masked) data values (PDP/ALE/RHALE/ ShapDP; for a linear model this is |coefficient| * std(x)), mean(|derivative|) * std(x) (DerPDP). Comparable across feature types and, in magnitude, across methods.

No y, ever

effector never sees ground-truth labels — this is a property of the fitted effect, not a loss/permutation importance.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
mask Optional[ndarray]

optional boolean (N,) subregion — model-free.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
float

a non-negative scalar.

Source code in effector/global_effect.py
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
def importance(
    self,
    feature: Union[int, str],
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> float:
    """How much a feature's mean effect moves the prediction (R13).

    ```python
    pdp.importance("temp")                           # scalar
    pdp.importance("temp", rule="workingday == 1")   # within a subregion
    ```

    The dispersion of the **mean** effect in output units — the μ-twin of
    `heter_score` (which measures per-instance spread on the same scale).
    A flat curve scores ~0; a swinging curve scores high. Per method: std
    of the mean effect over the (masked) data values (PDP/ALE/RHALE/
    ShapDP; for a linear model this is `|coefficient| * std(x)`),
    `mean(|derivative|) * std(x)` (DerPDP). Comparable across feature
    types and, in magnitude, across methods.

    !!! note "No `y`, ever"
        effector never sees ground-truth labels — this is a property of
        the fitted effect, not a loss/permutation importance.

    Args:
        feature: index or name of the feature of interest.
        mask: optional boolean `(N,)` subregion — model-free.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        a non-negative scalar.
    """
    feature = self._resolve_feature(feature)
    self._check_feature_type_supported(feature)
    mask = self._resolve_mask(mask, rule)
    if mask is None:
        # all-ones ≡ None (M1); the concrete array makes the per-method
        # `_importance` implementations mask-index without a null check
        mask = np.ones(self.data.shape[0], dtype=bool)
    self._ensure_local(feature)
    return float(self._importance(feature, mask))

importances(mask=None, rule=None)

The whole importance vector — rank your features in one call.

imp = pdp.importances()                       # (D,)
order = np.argsort(-np.nan_to_num(imp))       # most important first

NaN means unsupported, not unimportant

Feature types this method cannot explain (e.g. DerPDP on a nominal feature) return NaN, with one UserWarning naming them.

Parameters:

Name Type Description Default
mask Optional[ndarray]

optional boolean (N,) subregion.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
ndarray

the per-feature importance vector, (D,).

Source code in effector/global_effect.py
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
def importances(
    self,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The whole importance vector — rank your features in one call.

    ```python
    imp = pdp.importances()                       # (D,)
    order = np.argsort(-np.nan_to_num(imp))       # most important first
    ```

    !!! warning "NaN means unsupported, not unimportant"
        Feature types this method cannot explain (e.g. DerPDP on a
        nominal feature) return `NaN`, with one `UserWarning` naming them.

    Args:
        mask: optional boolean `(N,)` subregion.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        the per-feature importance vector, `(D,)`.
    """
    mask = self._resolve_mask(mask, rule)
    out = np.full(self.dim, np.nan)
    skipped = []
    for f in range(self.dim):
        try:
            out[f] = self.importance(f, mask=mask)
        except ValueError:
            skipped.append(self.feature_names[f])
    if skipped:
        warnings.warn(
            f"importance is undefined for feature(s) {skipped} — this "
            f"method does not support their feature type; returned NaN.",
            UserWarning,
            stacklevel=2,
        )
    return out

find_regions(feature=None, *, features=None, finder='best', candidate_conditioning_features='all')

Search for subregions that resolve a feature's heterogeneity.

part = pdp.find_regions("hr")                       # one feature -> Partition
part.show()                                         # the tree + level stats
pdp.plot("hr", rule=part.leaves[0].rule)            # drill into a leaf

parts = pdp.find_regions(features="heterogeneous")  # several -> {name: Partition}
effector.plot_triage(pdp, partitions=parts)         # the before/after picture

A query, not a mutation (R12)

The result is a value — nothing is stored on the effect. Don't like a partition? Search again with different finder kwargs; nothing needs resetting.

Model-free

Every candidate split is scored by heter_score(feature, mask) on the cached local effects — zero model calls, whatever the grid size. Binning/scope are those the feature was fitted with, replayed.

Parameters:

Name Type Description Default
feature Union[int, str, None]

index or name of the one feature to partition (→ Partition).

None
features Union[list, str, None]

several at once — a list, "all", or "heterogeneous" (heter_score at/above the median, the same convention effector.explain uses) → {feature_name: Partition}. Exactly one of feature/features must be given.

None
finder

"best" (default), "best_level_wise", or a configured finder instance (e.g. effector.space_partitioning.Best(...)).

'best'
candidate_conditioning_features

features allowed to define splits ("all" or a list of indices/names).

'all'

Returns:

Type Description

a Partition bound to this effect — or {feature_name: Partition}

with features=.

Source code in effector/global_effect.py
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
def find_regions(
    self,
    feature: Union[int, str, None] = None,
    *,
    features: Union[list, str, None] = None,
    finder="best",
    candidate_conditioning_features="all",
):
    """Search for subregions that resolve a feature's heterogeneity.

    ```python
    part = pdp.find_regions("hr")                       # one feature -> Partition
    part.show()                                         # the tree + level stats
    pdp.plot("hr", rule=part.leaves[0].rule)            # drill into a leaf

    parts = pdp.find_regions(features="heterogeneous")  # several -> {name: Partition}
    effector.plot_triage(pdp, partitions=parts)         # the before/after picture
    ```

    !!! note "A query, not a mutation (R12)"
        The result is a value — nothing is stored on the effect. Don't
        like a partition? Search again with different finder kwargs;
        nothing needs resetting.

    !!! note "Model-free"
        Every candidate split is scored by `heter_score(feature, mask)`
        on the cached local effects — zero model calls, whatever the grid
        size. Binning/scope are those the feature was fitted with,
        replayed.

    Args:
        feature: index or name of the one feature to partition
            (→ `Partition`).
        features: several at once — a list, `"all"`, or `"heterogeneous"`
            (heter_score at/above the median, the same convention
            `effector.explain` uses) → `{feature_name: Partition}`.
            Exactly one of `feature`/`features` must be given.
        finder: `"best"` (default), `"best_level_wise"`, or a configured
            finder instance (e.g. `effector.space_partitioning.Best(...)`).
        candidate_conditioning_features: features allowed to define splits
            (`"all"` or a list of indices/names).

    Returns:
        a `Partition` bound to this effect — or `{feature_name: Partition}`
        with `features=`.
    """
    if (feature is None) == (features is None):
        raise ValueError(
            "find_regions takes exactly one of `feature` (singular -> "
            "Partition) or `features` (plural -> {name: Partition})"
        )
    if features is not None:
        return self._find_regions_plural(
            features,
            finder=finder,
            candidate_conditioning_features=candidate_conditioning_features,
        )

    from effector import space_partitioning  # lazy: one-way dep guard

    feature = self._resolve_feature(feature)
    if isinstance(candidate_conditioning_features, list):
        candidate_conditioning_features = [
            self._resolve_feature(f) for f in candidate_conditioning_features
        ]
    self._check_feature_type_supported(feature)
    self._ensure_local(feature)

    if isinstance(finder, str):
        finder = space_partitioning.return_default(finder)

    def score_fn(mask):
        return self.heter_score(feature, mask=mask)  # RAW; guard is the finder's

    partition = finder.find_regions(
        feature,
        self.data,
        score_fn,
        axis_limits=self.axis_limits,
        feature_types=self.feature_types,
        cat_limit=self.cat_limit,
        candidate_conditioning_features=candidate_conditioning_features,
        feature_names=self.feature_names,
        target_name=self.target_name,
    )
    return partition.bind(self)

select_regions(partitions=None, *, features='heterogeneous', finder='best', candidate_conditioning_features='all', min_r2_gain=0.01)

Greedily select which partitions earn their complexity — the CALM chain.

chain = pdp.select_regions()      # search + select in one call
chain.show()                      # GAM R2, each accepted split, the rejected
chain.final                       # the last CALM — the regional analysis
chain[0]                          # the GAM snapshot

find_regions proposes one candidate Partition per feature; this verb decides across features which of them actually explain the model: starting from the GAM (all features global), each round applies the split with the largest explained-variance gain — the surrogate R² against , measured on top of the splits already applied — and stops when no remaining split adds at least min_r2_gain. Every accepted round is a snapshot (CALM) of increased complexity; the whole chain is the report's ledger.

A query, not a mutation (R12)

The result is a value — nothing is stored on the effect.

One prediction pass

Beyond fit, the only model touch is one f̂(X) pass for the variance denominator (cached on the effect); the search, the scoring, and every snapshot's summaries are model-free.

Parameters:

Name Type Description Default
partitions Optional[dict]

pre-computed candidates — {feature_index_or_name: Partition}, exactly what find_regions(features=...) returns. None (default) runs the search here first.

None
features Union[list, str]

which features to search when partitions is None — a list, "all", or "heterogeneous" (default; heter_score at/above the median).

'heterogeneous'
finder

region finder, as in find_regions.

'best'
candidate_conditioning_features

features allowed to define splits ("all" or a list of indices/names).

'all'
min_r2_gain float

smallest explained-variance marginal (fraction of Var(f̂), default 0.01 = 1 pt) a split must add — on top of the splits already applied — to earn a snapshot.

0.01

Returns:

Type Description

a CalmSequence[GAM, calm1, ...], R² non-decreasing along

it, with the rejected splits in .skipped

("redundant"/"below_threshold").

Raises:

Type Description
ValueError

the method is derivative-scale (no output-scale surrogate) or Var(f̂) == 0.

Source code in effector/global_effect.py
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
def select_regions(
    self,
    partitions: Optional[dict] = None,
    *,
    features: Union[list, str] = "heterogeneous",
    finder="best",
    candidate_conditioning_features="all",
    min_r2_gain: float = 0.01,
):
    """Greedily select which partitions earn their complexity — the CALM chain.

    ```python
    chain = pdp.select_regions()      # search + select in one call
    chain.show()                      # GAM R2, each accepted split, the rejected
    chain.final                       # the last CALM — the regional analysis
    chain[0]                          # the GAM snapshot
    ```

    `find_regions` proposes one candidate `Partition` per feature;
    this verb decides *across* features which of them actually explain
    the model: starting from the GAM (all features global), each round
    applies the split with the largest explained-variance gain — the
    surrogate R² against `f̂`, measured on top of the splits already
    applied — and stops when no remaining split adds at least
    `min_r2_gain`. Every accepted round is a snapshot (`CALM`) of
    increased complexity; the whole chain is the report's ledger.

    !!! note "A query, not a mutation (R12)"
        The result is a value — nothing is stored on the effect.

    !!! note "One prediction pass"
        Beyond `fit`, the only model touch is one `f̂(X)` pass for the
        variance denominator (cached on the effect); the search, the
        scoring, and every snapshot's summaries are model-free.

    Args:
        partitions: pre-computed candidates — `{feature_index_or_name:
            Partition}`, exactly what `find_regions(features=...)`
            returns. `None` (default) runs the search here first.
        features: which features to search when `partitions` is `None` —
            a list, `"all"`, or `"heterogeneous"` (default; heter_score
            at/above the median).
        finder: region finder, as in `find_regions`.
        candidate_conditioning_features: features allowed to define
            splits (`"all"` or a list of indices/names).
        min_r2_gain: smallest explained-variance marginal (fraction of
            `Var(f̂)`, default 0.01 = 1 pt) a split must add — on top of
            the splits already applied — to earn a snapshot.

    Returns:
        a `CalmSequence` — `[GAM, calm1, ...]`, R² non-decreasing along
        it, with the rejected splits in `.skipped`
        (`"redundant"`/`"below_threshold"`).

    Raises:
        ValueError: the method is derivative-scale (no output-scale
            surrogate) or `Var(f̂) == 0`.
    """
    from effector import explained_variance as _ev  # lazy: one-way dep guard

    if partitions is None:
        partitions = self.find_regions(
            features=features,
            finder=finder,
            candidate_conditioning_features=candidate_conditioning_features,
        )
    parts = {}
    for key, p in partitions.items():
        f = self._resolve_feature(key)
        parts[f] = p if p._effect is not None else p.bind(self)

    supported = []
    for f in range(self.dim):
        try:
            self._check_feature_type_supported(f)
            supported.append(f)
        except ValueError:
            continue
    return _ev.select(self, parts, supported, min_gain=min_r2_gain)

explain(*, y=None, top_k=5, coverage=0.8, heter_threshold=None, min_r2_gain=0.01, finder='best', candidate_conditioning_features='all')

The one-liner on this engine — effector.explain without leaving the session.

pdp = effector.PDP(X, model, schema=schema)
report = pdp.explain()            # same Report as effector.explain

Runs the same pipeline as effector.explain on the already-built engine: features you have fit with custom config keep it (missing ones are computed with the defaults), and every cache the pipeline warms stays on the engine for your follow-up queries.

Parameters:

Name Type Description Default
y Optional[ndarray]

optional ground truth aligned with the original data (or this engine's subsample); when given, the report header states the model's score on the explained subsample.

None
top_k int

hard ceiling on how many features get curve plots.

5
coverage float

stop plotting once the shown features carry this share of the total importance mass (default 0.8).

0.8
heter_threshold Optional[float]

minimum heter_score to enter the region search; None (default) = the median convention.

None
min_r2_gain float

smallest explained-variance marginal a split must add to earn a snapshot in the CALM chain.

0.01
finder

region finder, as in find_regions.

'best'
candidate_conditioning_features

features allowed to define splits.

'all'

Returns:

Type Description

a Report bound to this engine.

Source code in effector/global_effect.py
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
def explain(
    self,
    *,
    y: Optional[np.ndarray] = None,
    top_k: int = 5,
    coverage: float = 0.8,
    heter_threshold: Optional[float] = None,
    min_r2_gain: float = 0.01,
    finder="best",
    candidate_conditioning_features="all",
):
    """The one-liner on *this* engine — `effector.explain` without leaving the session.

    ```python
    pdp = effector.PDP(X, model, schema=schema)
    report = pdp.explain()            # same Report as effector.explain
    ```

    Runs the same pipeline as `effector.explain` on the already-built
    engine: features you have `fit` with custom config keep it (missing
    ones are computed with the defaults), and every cache the pipeline
    warms stays on the engine for your follow-up queries.

    Args:
        y: optional ground truth aligned with the original `data` (or
            this engine's subsample); when given, the report header
            states the model's score on the explained subsample.
        top_k: hard ceiling on how many features get curve plots.
        coverage: stop plotting once the shown features carry this share
            of the total importance mass (default 0.8).
        heter_threshold: minimum `heter_score` to enter the region
            search; `None` (default) = the median convention.
        min_r2_gain: smallest explained-variance marginal a split must
            add to earn a snapshot in the CALM chain.
        finder: region finder, as in `find_regions`.
        candidate_conditioning_features: features allowed to define
            splits.

    Returns:
        a `Report` bound to this engine.
    """
    from effector import report as _report  # lazy: one-way dep guard

    return _report._explain_effect(
        self,
        y=y,
        top_k=top_k,
        coverage=coverage,
        heter_threshold=heter_threshold,
        min_r2_gain=min_r2_gain,
        finder=finder,
        candidate_conditioning_features=candidate_conditioning_features,
    )

plot(feature, heterogeneity=True, centering=True, scale_x=None, scale_y=None, show_avg_output=False, y_limits=None, dy_limits=None, show_only_aggregated=False, show_plot=True, mask=None, rule=None, feature_label=None)

Plot the (RH)ALE effect of feature.

ale.plot("hr")                          # curve + heterogeneity
ale.plot("hr", rule="workingday == 0")  # within a subregion

For a continuous feature the figure has two panels: the accumulated curve on top, the per-bin average local effect (± std) below. Categorical features get one bar per level with std whiskers.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature to plot.

required
heterogeneity Union[bool, str]

False for the mean effect only; True or "std" (default) adds the per-bin std of the local effects.

True
centering Union[bool, str]

False (none), True/"zero_integral" (center around the y axis), or "zero_start" (start at y=0).

True
scale_x Optional[dict]

None or {"mean": m, "std": s} to undo a standardization of the x axis for display.

None
scale_y Optional[dict]

same, for the y axis.

None
show_avg_output bool

draw the model's average output as a horizontal line.

False
y_limits Optional[List]

(low, high) for the y axis; None = automatic.

None
dy_limits Optional[List]

(low, high) for the bottom (local-effect) panel; None = automatic.

None
show_only_aggregated bool

draw only the accumulated curve, without the bottom panel.

False
show_plot bool

if False, return the figure and axes instead of showing.

True
mask Optional[ndarray]

boolean (N,) selecting a subregion — plot the effect within it (re-binned from the cached local effects, no model calls), x axis windowed to the subregion's own interval.

None
rule

sugar over mask — an effector.Rule or a rule string, applied to the effect's data. Mutually exclusive with mask.

None
feature_label Optional[str]

display title for the figure (e.g. a regional node's label with its rule); defaults to the feature name. The x-axis always keeps the plain feature name.

None
Source code in effector/global_effect_ale.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
def plot(
    self,
    feature: Union[int, str],
    heterogeneity: Union[bool, str] = True,
    centering: Union[bool, str] = True,
    scale_x: Optional[dict] = None,
    scale_y: Optional[dict] = None,
    show_avg_output: bool = False,
    y_limits: Optional[List] = None,
    dy_limits: Optional[List] = None,
    show_only_aggregated: bool = False,
    show_plot: bool = True,
    mask: Optional[np.ndarray] = None,
    rule=None,
    feature_label: Optional[str] = None,
):
    """Plot the (RH)ALE effect of `feature`.

    ```python
    ale.plot("hr")                          # curve + heterogeneity
    ale.plot("hr", rule="workingday == 0")  # within a subregion
    ```

    For a continuous feature the figure has two panels: the accumulated
    curve on top, the per-bin average local effect (± std) below.
    Categorical features get one bar per level with std whiskers.

    Args:
        feature: index or name of the feature to plot.
        heterogeneity: `False` for the mean effect only; `True` or
            `"std"` (default) adds the per-bin std of the local effects.
        centering: `False` (none), `True`/`"zero_integral"` (center
            around the y axis), or `"zero_start"` (start at `y=0`).
        scale_x: `None` or `{"mean": m, "std": s}` to undo a
            standardization of the x axis for display.
        scale_y: same, for the y axis.
        show_avg_output: draw the model's average output as a
            horizontal line.
        y_limits: `(low, high)` for the y axis; `None` = automatic.
        dy_limits: `(low, high)` for the bottom (local-effect) panel;
            `None` = automatic.
        show_only_aggregated: draw only the accumulated curve, without
            the bottom panel.
        show_plot: if `False`, return the figure and axes instead of
            showing.
        mask: boolean `(N,)` selecting a subregion — plot the effect
            *within* it (re-binned from the cached local effects, no
            model calls), x axis windowed to the subregion's own
            interval.
        rule: sugar over `mask` — an `effector.Rule` or a rule string,
            applied to the effect's data. Mutually exclusive with `mask`.
        feature_label: display title for the figure (e.g. a regional
            node's label with its rule); defaults to the feature name.
            The x-axis always keeps the plain feature name.
    """
    feature = self._resolve_feature(feature)
    heterogeneity = helpers.prep_confidence_interval(heterogeneity)
    centering = helpers.prep_centering(centering)
    scale_x = helpers.resolve_scale(
        scale_x, self.scale_x_list[feature] if self.scale_x_list else None
    )
    scale_y = helpers.resolve_scale(scale_y, self.scale_y)
    mask = self._resolve_mask(mask, rule)
    feature_names = self.feature_names
    # C2: title = feature (or leaf label with its rule); method · scope
    # context moves to the corner tag
    plot_title = (
        feature_label if feature_label is not None else feature_names[feature]
    )
    tag = (
        f"{'ALE' if self.method_name == 'ale' else 'RHALE'}"
        f" · {'regional' if mask is not None else 'global'}"
    )

    # one path for global and masked alike (R14): pick the payload, read it
    is_cat = self._is_cat(feature)
    params = self._summary(feature, mask)
    x_window = (
        self._effective_limits(feature, mask)
        if mask is not None and not is_cat
        else None
    )

    def centered_eval(xs):
        y = self._eval_payload(feature, params, xs)
        if centering is not False:
            y = y - self._centering_const(feature, mask, centering)
        return y

    # the accumulated curve is piecewise linear between bin limits, so
    # evaluating exactly at the limits draws it exactly (no resampling).
    # categoricals are drawn by the is_cat branch below (at their observed
    # level values); their limits are positional codes 0..K-1 that eval
    # would reject, so only build this grid for continuous features.
    if not is_cat:
        x = np.asarray(params["limits"], dtype=float)
        y = centered_eval(x)

    avg_output = self._avg_output(mask, scale_y) if show_avg_output else None

    if is_cat:
        # bars = accumulated per-level values (in fit order); whiskers =
        # the variance of the step into each level (method_semantics.md)
        levels, labels = self._level_display(feature, params["levels"])
        y_levels = centered_eval(levels)
        variances = (
            self._eval_payload(feature, params, levels, heterogeneity=True)[1]
            if heterogeneity is not False
            else None
        )
        level_kind = self.feature_types[feature]
        level_counts = self._level_counts_for(feature, mask, levels)
        positions = np.asarray(levels, dtype=float)
        plot_scale_x, sort = scale_x, None
        if level_kind == "ordinal" and np.any(np.diff(positions) < 0):
            # custom (declared/induced) order: draw by rank in fit order —
            # ranks are display geometry, so the feature scale must not
            # touch them — and label by level
            if labels is None:
                labels = [f"{v:g}" for v in positions]
            positions = np.arange(len(positions), dtype=float)
            plot_scale_x, sort = None, False
        return vis.plot_categorical_effect(
            positions,
            y_levels,
            variances,
            feature,
            heterogeneity,
            title=plot_title,
            level_labels=labels,
            scale_x=plot_scale_x,
            scale_y=scale_y,
            avg_output=avg_output,
            feature_names=feature_names,
            target_name=self.target_name,
            y_limits=y_limits,
            # the accumulation path is meaningful only along an ordered
            # axis; sorted nominal bars would fake an interpolation
            connect_line=level_kind == "ordinal",
            show_plot=show_plot,
            tag=tag,
            level_kind=level_kind,
            sort=sort,
            level_counts=level_counts,
        )
    return vis.ale_plot(
        x,
        y,
        bin_effect=params["bin_effect"],
        bin_variance=params["bin_variance"],
        limits=params["limits"],
        dx=params["dx"],
        feature=feature,
        heterogeneity=heterogeneity,
        scale_x=scale_x,
        scale_y=scale_y,
        title=plot_title,
        avg_output=avg_output,
        feature_names=feature_names,
        target_name=self.target_name,
        y_limits=y_limits,
        dy_limits=dy_limits,
        show_only_aggregated=show_only_aggregated,
        show_plot=show_plot,
        x_limits=x_window,
        tag=tag,
    )

effector.global_effect_pdp.PDP(data, model, *, axis_limits=None, nof_instances=10000, schema=None, random_state=21)

Bases: PDPBase

Partial Dependence Plot: the average prediction as one feature varies.

pdp = effector.PDP(X, model)
pdp.plot("hr")                               # mean effect + ICE curves
y = pdp.eval("hr", np.linspace(0, 23, 100))  # (100,) mean effect

Every instance is forced to each position \(x_s\) and the predictions are averaged:

\[ PDP(x_s) = \frac{1}{N} \sum_{i=1}^N f(x_s, \mathbf{x}_c^i) \]

Each instance's own curve \(ICE^i(x_s) = f(x_s, \mathbf{x}_c^i)\) tells the individual story; the heterogeneity is the variance of the ICE curves around the mean (each ICE centered on its own mean first).

Correlated features

PDP averages over the marginal distribution: with strongly correlated features it queries the model far off the data manifold. Prefer effector.ALE or effector.RHALE there.

Build a PDP explainer. No model calls happen here.

Parameters:

Name Type Description Default
data ndarray

the design matrix, shape (N, D) — numpy only.

required
model Callable

the black-box model — a Callable mapping (N, D) arrays to (N,) predictions.

required
axis_limits Optional[ndarray]

per-feature plot limits, shape (2, D); None (default) infers them from data.

None
nof_instances Union[int, str]

max instances kept (default 10_000) — an int subsamples randomly, "all" keeps everything.

10000
schema Optional[Union[Schema, dict]]

input metadata — an effector.Schema or a plain dict with any of feature_names, feature_types, cat_limit, target_name, scale_x_list, scale_y; omitted fields are inferred from data, explicit ones win. Coming from a DataFrame? Use effector.from_dataframe.

None
random_state Optional[int]

seed for every internal random step (default 21, reproducible); None for non-deterministic behavior.

21

Methods:

Name Description
fit

Declare per-feature defaults and warm the caches.

eval

The mean effect of a feature at positions xs.

eval_heter

The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

grid

The evaluation grid on which this feature's effect is model-free.

heter_score

One number for a feature's heterogeneity — the scalar find_regions minimizes.

payload

The raw fitted object behind eval/eval_heter — pure numpy, yours to inspect.

importance

How much a feature's mean effect moves the prediction (R13).

importances

The whole importance vector — rank your features in one call.

find_regions

Search for subregions that resolve a feature's heterogeneity.

select_regions

Greedily select which partitions earn their complexity — the CALM chain.

explain

The one-liner on this engine — effector.explain without leaving the session.

plot

Plot the PDP of feature, by default with the ICE cloud.

Source code in effector/global_effect_pdp.py
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
def __init__(
    self,
    data: np.ndarray,
    model: Callable,
    *,
    axis_limits: Optional[np.ndarray] = None,
    nof_instances: Union[int, str] = 10_000,
    schema: Optional[Union[ingestion.Schema, dict]] = None,
    random_state: Optional[int] = 21,
):
    r"""Build a PDP explainer. No model calls happen here.

    Args:
        data: the design matrix, shape `(N, D)` — numpy only.
        model: the black-box model — a `Callable` mapping `(N, D)`
            arrays to `(N,)` predictions.
        axis_limits: per-feature plot limits, shape `(2, D)`; `None`
            (default) infers them from `data`.
        nof_instances: max instances kept (default `10_000`) — an `int`
            subsamples randomly, `"all"` keeps everything.
        schema: input metadata — an `effector.Schema` or a plain `dict`
            with any of `feature_names`, `feature_types`, `cat_limit`,
            `target_name`, `scale_x_list`, `scale_y`; omitted fields are
            inferred from `data`, explicit ones win. Coming from a
            DataFrame? Use `effector.from_dataframe`.
        random_state: seed for every internal random step (default `21`,
            reproducible); `None` for non-deterministic behavior.
    """

    super(PDP, self).__init__(
        data,
        model,
        None,
        axis_limits=axis_limits,
        nof_instances=nof_instances,
        schema=schema,
        random_state=random_state,
        method_name="PDP",
    )

fit(features='all', *, centering=False, use_vectorized=True)

Declare per-feature defaults and warm the caches.

pdp.fit("hr", centering="zero_integral")

fit is optional

eval, plot, heter_score compute what they need lazily with these defaults; fit declares the config once and pays the model cost upfront.

Parameters:

Name Type Description Default
features Union[int, str, list]

feature(s) to fit — index, name, list, or "all".

'all'
centering Union[bool, str]

default centering for this feature's queries — False (none), True/"zero_integral" (center around the y axis), or "zero_start" (start at y=0).

False
use_vectorized bool

vectorize the ICE computation — faster, but builds a (T, N, D) array internally; set False to trade speed for memory.

True
Source code in effector/global_effect_pdp.py
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def fit(
    self,
    features: Union[int, str, list] = "all",
    *,
    centering: Union[bool, str] = False,
    use_vectorized: bool = True,
):
    """Declare per-feature defaults and warm the caches.

    ```python
    pdp.fit("hr", centering="zero_integral")
    ```

    !!! note "fit is optional"
        `eval`, `plot`, `heter_score` compute what they need lazily with
        these defaults; `fit` declares the config once and pays the model
        cost upfront.

    Args:
        features: feature(s) to fit — index, name, list, or `"all"`.
        centering: default centering for this feature's queries —
            `False` (none), `True`/`"zero_integral"` (center around the
            y axis), or `"zero_start"` (start at `y=0`).
        use_vectorized: vectorize the ICE computation — faster, but
            builds a `(T, N, D)` array internally; set `False` to trade
            speed for memory.
    """
    self._fit_loop(features, centering, use_vectorized=use_vectorized)

eval(feature, xs, centering=None, mask=None, rule=None)

The mean effect of a feature at positions xs.

xs = np.linspace(0, 24, 100)
y = pdp.eval("hr", xs)                            # (100,) mean effect
y_wd = pdp.eval("hr", xs, rule="workingday == 0") # same, on a subregion

One array, one type (R1)

eval always returns the mean effect only. The spread around it has its own ladder: eval_heter (curve), heter_score (scalar), payload (the raw fitted object).

Discrete features

Ordinal/nominal features are evaluated only at observed levels — any other xs value raises ValueError.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
xs ndarray

where to evaluate, (T,).

required
centering Union[None, bool, str]

None (class default), False, True/"zero_integral", or "zero_start".

None
mask Optional[ndarray]

optional boolean (N,) selecting a subregion — the effect within it, re-summarized from cached local effects with zero model calls. Nothing is stored.

None
rule Union[None, str, Rule]

sugar over mask — an effector.Rule or a string like "temp < 3 and season == 0". Mutually exclusive with mask.

None

Returns:

Type Description
ndarray

the mean effect at xs, shape (T,).

Source code in effector/global_effect.py
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
def eval(
    self,
    feature: Union[int, str],
    xs: np.ndarray,
    centering: Union[None, bool, str] = None,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The mean effect of a feature at positions `xs`.

    ```python
    xs = np.linspace(0, 24, 100)
    y = pdp.eval("hr", xs)                            # (100,) mean effect
    y_wd = pdp.eval("hr", xs, rule="workingday == 0") # same, on a subregion
    ```

    !!! note "One array, one type (R1)"
        `eval` always returns the mean effect only. The spread around it
        has its own ladder: `eval_heter` (curve), `heter_score` (scalar),
        `payload` (the raw fitted object).

    !!! warning "Discrete features"
        Ordinal/nominal features are evaluated **only at observed
        levels** — any other `xs` value raises `ValueError`.

    Args:
        feature: index or name of the feature of interest.
        xs: where to evaluate, `(T,)`.
        centering: `None` (class default), `False`,
            `True`/`"zero_integral"`, or `"zero_start"`.
        mask: optional boolean `(N,)` selecting a subregion — the effect
            *within* it, re-summarized from cached local effects with zero
            model calls. Nothing is stored.
        rule: sugar over `mask` — an `effector.Rule` or a string like
            `"temp < 3 and season == 0"`. Mutually exclusive with `mask`.

    Returns:
        the mean effect at `xs`, shape `(T,)`.
    """
    feature = self._resolve_feature(feature)
    centering = self.DEFAULT_CENTERING if centering is None else centering
    centering = helpers.prep_centering(centering)
    mask = self._resolve_mask(mask, rule)

    if not self._is_cat(feature):
        if mask is not None:
            self._effective_limits(feature, mask)  # degeneracy guard
        elif not self.axis_limits[0, feature] < self.axis_limits[1, feature]:
            raise ValueError(
                f"Feature {feature} has a degenerate axis interval "
                f"[{self.axis_limits[0, feature]}, {self.axis_limits[1, feature]}]"
            )

    params = self._summary(feature, mask)
    y = self._eval_mean(feature, xs, params, mask)
    if centering is not False:
        y = y - self._mean_norm_const(
            self._centering_const(feature, mask, centering)
        )
    return y

eval_heter(feature, xs, mask=None, rule=None)

The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

h = pdp.eval_heter("hr", xs)          # (T,) variance around the mean
band = np.sqrt(h)                     # std-like band, plot-ready

It's a variance, and it's method-specific (R2)

PDP: variance of centered ICE; DerPDP: of d-ICE slopes; ALE/RHALE: per-bin slope variance as a step function; ShapDP: interpolated per-bin φ variance. Take the square root for a band.

No centering argument — by design

Heterogeneity is invariant to centering; the signature enforces it.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
xs ndarray

where to evaluate, (T,).

required
mask Optional[ndarray]

optional boolean (N,) subregion — re-summarized from cached local effects, zero model calls.

None
rule Union[None, str, Rule]

sugar over mask (an effector.Rule or a rule string); mutually exclusive with mask.

None

Returns:

Type Description
ndarray

the heterogeneity curve h(xs), (T,), non-negative.

Source code in effector/global_effect.py
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
def eval_heter(
    self,
    feature: Union[int, str],
    xs: np.ndarray,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

    ```python
    h = pdp.eval_heter("hr", xs)          # (T,) variance around the mean
    band = np.sqrt(h)                     # std-like band, plot-ready
    ```

    !!! note "It's a variance, and it's method-specific (R2)"
        PDP: variance of centered ICE; DerPDP: of d-ICE slopes; ALE/RHALE:
        per-bin slope variance as a step function; ShapDP: interpolated
        per-bin φ variance. Take the square root for a band.

    !!! note "No `centering` argument — by design"
        Heterogeneity is invariant to centering; the signature enforces it.

    Args:
        feature: index or name of the feature of interest.
        xs: where to evaluate, `(T,)`.
        mask: optional boolean `(N,)` subregion — re-summarized from cached
            local effects, zero model calls.
        rule: sugar over `mask` (an `effector.Rule` or a rule string);
            mutually exclusive with `mask`.

    Returns:
        the heterogeneity curve h(xs), `(T,)`, non-negative.
    """
    feature = self._resolve_feature(feature)
    mask = self._resolve_mask(mask, rule)
    params = self._summary(feature, mask)
    return self._eval_payload(feature, params, xs, heterogeneity=True)[1]

grid(feature)

The evaluation grid on which this feature's effect is model-free.

The observed levels for a discrete feature; otherwise helpers.NOF_INTERNAL_POINTS equally spaced points inside the feature's axis limits — the cache grid explain evaluates every reported curve on.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name.

required

Returns:

Type Description
ndarray

(T,) array of evaluation positions.

Source code in effector/global_effect.py
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
def grid(self, feature: Union[int, str]) -> np.ndarray:
    """The evaluation grid on which this feature's effect is model-free.

    The observed levels for a discrete feature; otherwise
    `helpers.NOF_INTERNAL_POINTS` equally spaced points inside the
    feature's axis limits — the cache grid `explain` evaluates every
    reported curve on.

    Args:
        feature: index or name.

    Returns:
        `(T,)` array of evaluation positions.
    """
    feature = self._resolve_feature(feature)
    if self._is_cat(feature):
        return np.unique(self.data[:, feature])
    return np.linspace(
        self.axis_limits[0, feature],
        self.axis_limits[1, feature],
        helpers.NOF_INTERNAL_POINTS,
    )

heter_score(feature, mask=None, rule=None)

One number for a feature's heterogeneity — the scalar find_regions minimizes.

pdp.heter_score("hr")                            # global
pdp.heter_score("hr", rule="workingday == 0")    # within a subregion

In output units (units contract, method_semantics.md): the RMS of eval_heter over the feature's own (masked) data values (frequency-weighted over levels for categorical features), bridged by the feature's dispersion for the derivative-based methods (ALE/RHALE/DerPDP) so every feature type and every method lands on the same y-unit scale — "a typical instance's effect deviates from the mean effect by about this much". eval_heter itself stays a variance curve in the method's native units.

Pair it with importance

importance measures the mean effect's strength; heter_score measures the spread around it — same units, mean/spread twins. High importance + high heterogeneity = the top-right corner of effector.plot_triage — where find_regions should look.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
mask Optional[ndarray]

optional boolean (N,) subregion — model-free.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
float

a non-negative scalar, in output units.

Source code in effector/global_effect.py
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
def heter_score(
    self,
    feature: Union[int, str],
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> float:
    """One number for a feature's heterogeneity — the scalar `find_regions` minimizes.

    ```python
    pdp.heter_score("hr")                            # global
    pdp.heter_score("hr", rule="workingday == 0")    # within a subregion
    ```

    In **output units** (units contract, method_semantics.md): the RMS of
    `eval_heter` over the feature's own (masked) data values
    (frequency-weighted over levels for categorical features), bridged by
    the feature's dispersion for the derivative-based methods
    (ALE/RHALE/DerPDP) so every feature type and every method lands on the
    same y-unit scale — "a typical instance's effect deviates from the
    mean effect by about this much". `eval_heter` itself stays a variance
    curve in the method's native units.

    !!! tip "Pair it with `importance`"
        `importance` measures the *mean* effect's strength; `heter_score`
        measures the spread around it — same units, mean/spread twins.
        High importance + high heterogeneity = the top-right corner of
        `effector.plot_triage` — where `find_regions` should look.

    Args:
        feature: index or name of the feature of interest.
        mask: optional boolean `(N,)` subregion — model-free.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        a non-negative scalar, in output units.
    """
    feature = self._resolve_feature(feature)
    self._check_feature_type_supported(feature)
    mask = self._resolve_mask(mask, rule)
    return float(self._heter(feature, mask))

payload(feature)

The raw fitted object behind eval/eval_heter — pure numpy, yours to inspect.

p = ale.payload("hr")     # e.g. {"limits": ..., "bin_effect": ..., "bin_variance": ...}

Per method: per-bin effects and variances for (RH)ALE and ShapDP, the grid summaries for (d-)PDP. A copy — mutate freely.

Source code in effector/global_effect.py
743
744
745
746
747
748
749
750
751
752
753
def payload(self, feature: Union[int, str]) -> dict:
    """The raw fitted object behind `eval`/`eval_heter` — pure numpy, yours to inspect.

    ```python
    p = ale.payload("hr")     # e.g. {"limits": ..., "bin_effect": ..., "bin_variance": ...}
    ```

    Per method: per-bin effects and variances for (RH)ALE and ShapDP, the
    grid summaries for (d-)PDP. A copy — mutate freely.
    """
    return dict(self._summary(self._resolve_feature(feature), None))

importance(feature, mask=None, rule=None)

How much a feature's mean effect moves the prediction (R13).

pdp.importance("temp")                           # scalar
pdp.importance("temp", rule="workingday == 1")   # within a subregion

The dispersion of the mean effect in output units — the μ-twin of heter_score (which measures per-instance spread on the same scale). A flat curve scores ~0; a swinging curve scores high. Per method: std of the mean effect over the (masked) data values (PDP/ALE/RHALE/ ShapDP; for a linear model this is |coefficient| * std(x)), mean(|derivative|) * std(x) (DerPDP). Comparable across feature types and, in magnitude, across methods.

No y, ever

effector never sees ground-truth labels — this is a property of the fitted effect, not a loss/permutation importance.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
mask Optional[ndarray]

optional boolean (N,) subregion — model-free.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
float

a non-negative scalar.

Source code in effector/global_effect.py
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
def importance(
    self,
    feature: Union[int, str],
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> float:
    """How much a feature's mean effect moves the prediction (R13).

    ```python
    pdp.importance("temp")                           # scalar
    pdp.importance("temp", rule="workingday == 1")   # within a subregion
    ```

    The dispersion of the **mean** effect in output units — the μ-twin of
    `heter_score` (which measures per-instance spread on the same scale).
    A flat curve scores ~0; a swinging curve scores high. Per method: std
    of the mean effect over the (masked) data values (PDP/ALE/RHALE/
    ShapDP; for a linear model this is `|coefficient| * std(x)`),
    `mean(|derivative|) * std(x)` (DerPDP). Comparable across feature
    types and, in magnitude, across methods.

    !!! note "No `y`, ever"
        effector never sees ground-truth labels — this is a property of
        the fitted effect, not a loss/permutation importance.

    Args:
        feature: index or name of the feature of interest.
        mask: optional boolean `(N,)` subregion — model-free.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        a non-negative scalar.
    """
    feature = self._resolve_feature(feature)
    self._check_feature_type_supported(feature)
    mask = self._resolve_mask(mask, rule)
    if mask is None:
        # all-ones ≡ None (M1); the concrete array makes the per-method
        # `_importance` implementations mask-index without a null check
        mask = np.ones(self.data.shape[0], dtype=bool)
    self._ensure_local(feature)
    return float(self._importance(feature, mask))

importances(mask=None, rule=None)

The whole importance vector — rank your features in one call.

imp = pdp.importances()                       # (D,)
order = np.argsort(-np.nan_to_num(imp))       # most important first

NaN means unsupported, not unimportant

Feature types this method cannot explain (e.g. DerPDP on a nominal feature) return NaN, with one UserWarning naming them.

Parameters:

Name Type Description Default
mask Optional[ndarray]

optional boolean (N,) subregion.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
ndarray

the per-feature importance vector, (D,).

Source code in effector/global_effect.py
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
def importances(
    self,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The whole importance vector — rank your features in one call.

    ```python
    imp = pdp.importances()                       # (D,)
    order = np.argsort(-np.nan_to_num(imp))       # most important first
    ```

    !!! warning "NaN means unsupported, not unimportant"
        Feature types this method cannot explain (e.g. DerPDP on a
        nominal feature) return `NaN`, with one `UserWarning` naming them.

    Args:
        mask: optional boolean `(N,)` subregion.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        the per-feature importance vector, `(D,)`.
    """
    mask = self._resolve_mask(mask, rule)
    out = np.full(self.dim, np.nan)
    skipped = []
    for f in range(self.dim):
        try:
            out[f] = self.importance(f, mask=mask)
        except ValueError:
            skipped.append(self.feature_names[f])
    if skipped:
        warnings.warn(
            f"importance is undefined for feature(s) {skipped} — this "
            f"method does not support their feature type; returned NaN.",
            UserWarning,
            stacklevel=2,
        )
    return out

find_regions(feature=None, *, features=None, finder='best', candidate_conditioning_features='all')

Search for subregions that resolve a feature's heterogeneity.

part = pdp.find_regions("hr")                       # one feature -> Partition
part.show()                                         # the tree + level stats
pdp.plot("hr", rule=part.leaves[0].rule)            # drill into a leaf

parts = pdp.find_regions(features="heterogeneous")  # several -> {name: Partition}
effector.plot_triage(pdp, partitions=parts)         # the before/after picture

A query, not a mutation (R12)

The result is a value — nothing is stored on the effect. Don't like a partition? Search again with different finder kwargs; nothing needs resetting.

Model-free

Every candidate split is scored by heter_score(feature, mask) on the cached local effects — zero model calls, whatever the grid size. Binning/scope are those the feature was fitted with, replayed.

Parameters:

Name Type Description Default
feature Union[int, str, None]

index or name of the one feature to partition (→ Partition).

None
features Union[list, str, None]

several at once — a list, "all", or "heterogeneous" (heter_score at/above the median, the same convention effector.explain uses) → {feature_name: Partition}. Exactly one of feature/features must be given.

None
finder

"best" (default), "best_level_wise", or a configured finder instance (e.g. effector.space_partitioning.Best(...)).

'best'
candidate_conditioning_features

features allowed to define splits ("all" or a list of indices/names).

'all'

Returns:

Type Description

a Partition bound to this effect — or {feature_name: Partition}

with features=.

Source code in effector/global_effect.py
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
def find_regions(
    self,
    feature: Union[int, str, None] = None,
    *,
    features: Union[list, str, None] = None,
    finder="best",
    candidate_conditioning_features="all",
):
    """Search for subregions that resolve a feature's heterogeneity.

    ```python
    part = pdp.find_regions("hr")                       # one feature -> Partition
    part.show()                                         # the tree + level stats
    pdp.plot("hr", rule=part.leaves[0].rule)            # drill into a leaf

    parts = pdp.find_regions(features="heterogeneous")  # several -> {name: Partition}
    effector.plot_triage(pdp, partitions=parts)         # the before/after picture
    ```

    !!! note "A query, not a mutation (R12)"
        The result is a value — nothing is stored on the effect. Don't
        like a partition? Search again with different finder kwargs;
        nothing needs resetting.

    !!! note "Model-free"
        Every candidate split is scored by `heter_score(feature, mask)`
        on the cached local effects — zero model calls, whatever the grid
        size. Binning/scope are those the feature was fitted with,
        replayed.

    Args:
        feature: index or name of the one feature to partition
            (→ `Partition`).
        features: several at once — a list, `"all"`, or `"heterogeneous"`
            (heter_score at/above the median, the same convention
            `effector.explain` uses) → `{feature_name: Partition}`.
            Exactly one of `feature`/`features` must be given.
        finder: `"best"` (default), `"best_level_wise"`, or a configured
            finder instance (e.g. `effector.space_partitioning.Best(...)`).
        candidate_conditioning_features: features allowed to define splits
            (`"all"` or a list of indices/names).

    Returns:
        a `Partition` bound to this effect — or `{feature_name: Partition}`
        with `features=`.
    """
    if (feature is None) == (features is None):
        raise ValueError(
            "find_regions takes exactly one of `feature` (singular -> "
            "Partition) or `features` (plural -> {name: Partition})"
        )
    if features is not None:
        return self._find_regions_plural(
            features,
            finder=finder,
            candidate_conditioning_features=candidate_conditioning_features,
        )

    from effector import space_partitioning  # lazy: one-way dep guard

    feature = self._resolve_feature(feature)
    if isinstance(candidate_conditioning_features, list):
        candidate_conditioning_features = [
            self._resolve_feature(f) for f in candidate_conditioning_features
        ]
    self._check_feature_type_supported(feature)
    self._ensure_local(feature)

    if isinstance(finder, str):
        finder = space_partitioning.return_default(finder)

    def score_fn(mask):
        return self.heter_score(feature, mask=mask)  # RAW; guard is the finder's

    partition = finder.find_regions(
        feature,
        self.data,
        score_fn,
        axis_limits=self.axis_limits,
        feature_types=self.feature_types,
        cat_limit=self.cat_limit,
        candidate_conditioning_features=candidate_conditioning_features,
        feature_names=self.feature_names,
        target_name=self.target_name,
    )
    return partition.bind(self)

select_regions(partitions=None, *, features='heterogeneous', finder='best', candidate_conditioning_features='all', min_r2_gain=0.01)

Greedily select which partitions earn their complexity — the CALM chain.

chain = pdp.select_regions()      # search + select in one call
chain.show()                      # GAM R2, each accepted split, the rejected
chain.final                       # the last CALM — the regional analysis
chain[0]                          # the GAM snapshot

find_regions proposes one candidate Partition per feature; this verb decides across features which of them actually explain the model: starting from the GAM (all features global), each round applies the split with the largest explained-variance gain — the surrogate R² against , measured on top of the splits already applied — and stops when no remaining split adds at least min_r2_gain. Every accepted round is a snapshot (CALM) of increased complexity; the whole chain is the report's ledger.

A query, not a mutation (R12)

The result is a value — nothing is stored on the effect.

One prediction pass

Beyond fit, the only model touch is one f̂(X) pass for the variance denominator (cached on the effect); the search, the scoring, and every snapshot's summaries are model-free.

Parameters:

Name Type Description Default
partitions Optional[dict]

pre-computed candidates — {feature_index_or_name: Partition}, exactly what find_regions(features=...) returns. None (default) runs the search here first.

None
features Union[list, str]

which features to search when partitions is None — a list, "all", or "heterogeneous" (default; heter_score at/above the median).

'heterogeneous'
finder

region finder, as in find_regions.

'best'
candidate_conditioning_features

features allowed to define splits ("all" or a list of indices/names).

'all'
min_r2_gain float

smallest explained-variance marginal (fraction of Var(f̂), default 0.01 = 1 pt) a split must add — on top of the splits already applied — to earn a snapshot.

0.01

Returns:

Type Description

a CalmSequence[GAM, calm1, ...], R² non-decreasing along

it, with the rejected splits in .skipped

("redundant"/"below_threshold").

Raises:

Type Description
ValueError

the method is derivative-scale (no output-scale surrogate) or Var(f̂) == 0.

Source code in effector/global_effect.py
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
def select_regions(
    self,
    partitions: Optional[dict] = None,
    *,
    features: Union[list, str] = "heterogeneous",
    finder="best",
    candidate_conditioning_features="all",
    min_r2_gain: float = 0.01,
):
    """Greedily select which partitions earn their complexity — the CALM chain.

    ```python
    chain = pdp.select_regions()      # search + select in one call
    chain.show()                      # GAM R2, each accepted split, the rejected
    chain.final                       # the last CALM — the regional analysis
    chain[0]                          # the GAM snapshot
    ```

    `find_regions` proposes one candidate `Partition` per feature;
    this verb decides *across* features which of them actually explain
    the model: starting from the GAM (all features global), each round
    applies the split with the largest explained-variance gain — the
    surrogate R² against `f̂`, measured on top of the splits already
    applied — and stops when no remaining split adds at least
    `min_r2_gain`. Every accepted round is a snapshot (`CALM`) of
    increased complexity; the whole chain is the report's ledger.

    !!! note "A query, not a mutation (R12)"
        The result is a value — nothing is stored on the effect.

    !!! note "One prediction pass"
        Beyond `fit`, the only model touch is one `f̂(X)` pass for the
        variance denominator (cached on the effect); the search, the
        scoring, and every snapshot's summaries are model-free.

    Args:
        partitions: pre-computed candidates — `{feature_index_or_name:
            Partition}`, exactly what `find_regions(features=...)`
            returns. `None` (default) runs the search here first.
        features: which features to search when `partitions` is `None` —
            a list, `"all"`, or `"heterogeneous"` (default; heter_score
            at/above the median).
        finder: region finder, as in `find_regions`.
        candidate_conditioning_features: features allowed to define
            splits (`"all"` or a list of indices/names).
        min_r2_gain: smallest explained-variance marginal (fraction of
            `Var(f̂)`, default 0.01 = 1 pt) a split must add — on top of
            the splits already applied — to earn a snapshot.

    Returns:
        a `CalmSequence` — `[GAM, calm1, ...]`, R² non-decreasing along
        it, with the rejected splits in `.skipped`
        (`"redundant"`/`"below_threshold"`).

    Raises:
        ValueError: the method is derivative-scale (no output-scale
            surrogate) or `Var(f̂) == 0`.
    """
    from effector import explained_variance as _ev  # lazy: one-way dep guard

    if partitions is None:
        partitions = self.find_regions(
            features=features,
            finder=finder,
            candidate_conditioning_features=candidate_conditioning_features,
        )
    parts = {}
    for key, p in partitions.items():
        f = self._resolve_feature(key)
        parts[f] = p if p._effect is not None else p.bind(self)

    supported = []
    for f in range(self.dim):
        try:
            self._check_feature_type_supported(f)
            supported.append(f)
        except ValueError:
            continue
    return _ev.select(self, parts, supported, min_gain=min_r2_gain)

explain(*, y=None, top_k=5, coverage=0.8, heter_threshold=None, min_r2_gain=0.01, finder='best', candidate_conditioning_features='all')

The one-liner on this engine — effector.explain without leaving the session.

pdp = effector.PDP(X, model, schema=schema)
report = pdp.explain()            # same Report as effector.explain

Runs the same pipeline as effector.explain on the already-built engine: features you have fit with custom config keep it (missing ones are computed with the defaults), and every cache the pipeline warms stays on the engine for your follow-up queries.

Parameters:

Name Type Description Default
y Optional[ndarray]

optional ground truth aligned with the original data (or this engine's subsample); when given, the report header states the model's score on the explained subsample.

None
top_k int

hard ceiling on how many features get curve plots.

5
coverage float

stop plotting once the shown features carry this share of the total importance mass (default 0.8).

0.8
heter_threshold Optional[float]

minimum heter_score to enter the region search; None (default) = the median convention.

None
min_r2_gain float

smallest explained-variance marginal a split must add to earn a snapshot in the CALM chain.

0.01
finder

region finder, as in find_regions.

'best'
candidate_conditioning_features

features allowed to define splits.

'all'

Returns:

Type Description

a Report bound to this engine.

Source code in effector/global_effect.py
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
def explain(
    self,
    *,
    y: Optional[np.ndarray] = None,
    top_k: int = 5,
    coverage: float = 0.8,
    heter_threshold: Optional[float] = None,
    min_r2_gain: float = 0.01,
    finder="best",
    candidate_conditioning_features="all",
):
    """The one-liner on *this* engine — `effector.explain` without leaving the session.

    ```python
    pdp = effector.PDP(X, model, schema=schema)
    report = pdp.explain()            # same Report as effector.explain
    ```

    Runs the same pipeline as `effector.explain` on the already-built
    engine: features you have `fit` with custom config keep it (missing
    ones are computed with the defaults), and every cache the pipeline
    warms stays on the engine for your follow-up queries.

    Args:
        y: optional ground truth aligned with the original `data` (or
            this engine's subsample); when given, the report header
            states the model's score on the explained subsample.
        top_k: hard ceiling on how many features get curve plots.
        coverage: stop plotting once the shown features carry this share
            of the total importance mass (default 0.8).
        heter_threshold: minimum `heter_score` to enter the region
            search; `None` (default) = the median convention.
        min_r2_gain: smallest explained-variance marginal a split must
            add to earn a snapshot in the CALM chain.
        finder: region finder, as in `find_regions`.
        candidate_conditioning_features: features allowed to define
            splits.

    Returns:
        a `Report` bound to this engine.
    """
    from effector import report as _report  # lazy: one-way dep guard

    return _report._explain_effect(
        self,
        y=y,
        top_k=top_k,
        coverage=coverage,
        heter_threshold=heter_threshold,
        min_r2_gain=min_r2_gain,
        finder=finder,
        candidate_conditioning_features=candidate_conditioning_features,
    )

plot(feature, heterogeneity='ice', centering=True, nof_points=100, scale_x=None, scale_y=None, nof_ice=100, show_avg_output=False, y_limits=None, use_vectorized=True, show_plot=True, mask=None, rule=None, feature_label=None)

Plot the PDP of feature, by default with the ICE cloud.

pdp.plot("hr")                          # mean effect + ICE curves
pdp.plot("hr", heterogeneity="std")     # mean ± std band
pdp.plot("hr", rule="workingday == 0")  # PDP within a subregion

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature to plot.

required
heterogeneity Union[bool, str]

what to draw around the mean effect:

  • False: the mean effect only
  • True or "std": ± one std of the ICE curves
  • "std_err": ± the standard error of the mean
  • "ice" (default): the ICE curves themselves
'ice'
centering Union[bool, str]

False (none), True/"zero_integral" (center around the y axis), or "zero_start" (start at y=0).

True
nof_points int

grid size of the x axis (default 100).

100
scale_x Optional[dict]

None or {"mean": m, "std": s} — the x axis is drawn as x = (x + m) * s (undo a standardization).

None
scale_y Optional[dict]

same, for the y axis.

None
nof_ice Union[int, str]

how many ICE curves to draw (default 100), or "all".

100
show_avg_output bool

draw the model's average output as a horizontal line.

False
y_limits Optional[List]

(low, high) for the y axis; None = automatic.

None
use_vectorized bool

vectorized ICE computation (faster, more memory).

True
show_plot bool

if False, return the figure and axes instead of showing.

True
mask Optional[ndarray]

boolean (N,) selecting a subregion — plot the PDP/ICE within it from the cached ICE table (no model calls; nof_points does not apply), x axis windowed to the subregion's own interval.

None
rule

sugar over mask — an effector.Rule or a rule string, applied to the effect's data. Mutually exclusive with mask.

None
feature_label Optional[str]

display title for the figure (e.g. a regional node's label with its rule); defaults to the feature name. The x-axis always keeps the plain feature name.

None
Source code in effector/global_effect_pdp.py
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
def plot(
    self,
    feature: Union[int, str],
    heterogeneity: Union[bool, str] = "ice",
    centering: Union[bool, str] = True,
    nof_points: int = 100,
    scale_x: Optional[dict] = None,
    scale_y: Optional[dict] = None,
    nof_ice: Union[int, str] = 100,
    show_avg_output: bool = False,
    y_limits: Optional[List] = None,
    use_vectorized: bool = True,
    show_plot: bool = True,
    mask: Optional[np.ndarray] = None,
    rule=None,
    feature_label: Optional[str] = None,
):
    """Plot the PDP of `feature`, by default with the ICE cloud.

    ```python
    pdp.plot("hr")                          # mean effect + ICE curves
    pdp.plot("hr", heterogeneity="std")     # mean ± std band
    pdp.plot("hr", rule="workingday == 0")  # PDP within a subregion
    ```

    Args:
        feature: index or name of the feature to plot.
        heterogeneity: what to draw around the mean effect:

            - `False`: the mean effect only
            - `True` or `"std"`: ± one std of the ICE curves
            - `"std_err"`: ± the standard error of the mean
            - `"ice"` (default): the ICE curves themselves

        centering: `False` (none), `True`/`"zero_integral"` (center
            around the y axis), or `"zero_start"` (start at `y=0`).
        nof_points: grid size of the x axis (default `100`).
        scale_x: `None` or `{"mean": m, "std": s}` — the x axis is
            drawn as `x = (x + m) * s` (undo a standardization).
        scale_y: same, for the y axis.
        nof_ice: how many ICE curves to draw (default `100`), or `"all"`.
        show_avg_output: draw the model's average output as a
            horizontal line.
        y_limits: `(low, high)` for the y axis; `None` = automatic.
        use_vectorized: vectorized ICE computation (faster, more memory).
        show_plot: if `False`, return the figure and axes instead of
            showing.
        mask: boolean `(N,)` selecting a subregion — plot the PDP/ICE
            *within* it from the cached ICE table (no model calls;
            `nof_points` does not apply), x axis windowed to the
            subregion's own interval.
        rule: sugar over `mask` — an `effector.Rule` or a rule string,
            applied to the effect's data. Mutually exclusive with `mask`.
        feature_label: display title for the figure (e.g. a regional
            node's label with its rule); defaults to the feature name.
            The x-axis always keeps the plain feature name.
    """
    feature = self._resolve_feature(feature)
    mask = self._resolve_mask(mask, rule)
    ret = self._plot(
        feature,
        heterogeneity,
        centering,
        nof_points,
        scale_x,
        scale_y,
        nof_ice,
        show_avg_output,
        y_limits,
        use_vectorized,
        show_plot,
        mask,
        feature_label,
    )

    if not show_plot:
        return ret

effector.global_effect_pdp.DerPDP(data, model, model_jac=None, *, axis_limits=None, nof_instances=10000, schema=None, random_state=21)

Bases: PDPBase

Derivative-PDP: the model's average derivative as one feature varies.

dpdp = effector.DerPDP(X, model, model_jac)
dpdp.plot("hr")   # y axis in derivative units
\[ dPDP(x_s) = \frac{1}{N} \sum_{i=1}^N \frac{\partial f}{\partial x_s}(x_s, \mathbf{x}_c^i) \]

Flat at zero means no effect; constant non-zero means a linear effect. The heterogeneity is the variance of the d-ICE curves.

Derivative units

The y axis is in \(\partial y / \partial x_s\) units, not output units. Without model_jac, derivatives fall back to slower, less exact numerical differentiation.

On a discrete axis the derivative becomes the finite difference: ordinal features show one bar per adjacent-level transition (the per-instance difference of the plain ICE values at the two levels — the jacobian is never used there); nominal features additionally get order-free scalars from all level pairs (method_semantics.md).

Build a d-PDP explainer. No model calls happen here.

Parameters:

Name Type Description Default
data ndarray

the design matrix, shape (N, D) — numpy only.

required
model Callable

the black-box model — a Callable mapping (N, D) arrays to (N,) predictions.

required
model_jac Optional[Callable]

the model Jacobian — a Callable mapping (N, D) arrays to (N, D) derivatives. If None, derivatives are computed with central finite differences (two model calls per position).

None
axis_limits Optional[ndarray]

per-feature plot limits, shape (2, D); None (default) infers them from data.

None
nof_instances Union[int, str]

max instances kept (default 10_000) — an int subsamples randomly, "all" keeps everything.

10000
schema Optional[Union[Schema, dict]]

input metadata — an effector.Schema or a plain dict with any of feature_names, feature_types, cat_limit, target_name, scale_x_list, scale_y; omitted fields are inferred from data, explicit ones win. Coming from a DataFrame? Use effector.from_dataframe.

None
random_state Optional[int]

seed for every internal random step (default 21, reproducible); None for non-deterministic behavior.

21

Methods:

Name Description
fit

Declare per-feature defaults and warm the caches.

eval

The mean effect of a feature at positions xs.

eval_heter

The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

grid

The evaluation grid on which this feature's effect is model-free.

heter_score

One number for a feature's heterogeneity — the scalar find_regions minimizes.

payload

The raw fitted object behind eval/eval_heter — pure numpy, yours to inspect.

importance

How much a feature's mean effect moves the prediction (R13).

importances

The whole importance vector — rank your features in one call.

find_regions

Search for subregions that resolve a feature's heterogeneity.

select_regions

Greedily select which partitions earn their complexity — the CALM chain.

explain

The one-liner on this engine — effector.explain without leaving the session.

plot

Plot the d-PDP of feature, by default with the d-ICE cloud.

Source code in effector/global_effect_pdp.py
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
def __init__(
    self,
    data: np.ndarray,
    model: Callable,
    model_jac: Optional[Callable] = None,
    *,
    axis_limits: Optional[np.ndarray] = None,
    nof_instances: Union[int, str] = 10_000,
    schema: Optional[Union[ingestion.Schema, dict]] = None,
    random_state: Optional[int] = 21,
):
    r"""Build a d-PDP explainer. No model calls happen here.

    Args:
        data: the design matrix, shape `(N, D)` — numpy only.
        model: the black-box model — a `Callable` mapping `(N, D)`
            arrays to `(N,)` predictions.
        model_jac: the model Jacobian — a `Callable` mapping `(N, D)`
            arrays to `(N, D)` derivatives. If `None`, derivatives are
            computed with central finite differences (two model calls
            per position).
        axis_limits: per-feature plot limits, shape `(2, D)`; `None`
            (default) infers them from `data`.
        nof_instances: max instances kept (default `10_000`) — an `int`
            subsamples randomly, `"all"` keeps everything.
        schema: input metadata — an `effector.Schema` or a plain `dict`
            with any of `feature_names`, `feature_types`, `cat_limit`,
            `target_name`, `scale_x_list`, `scale_y`; omitted fields are
            inferred from `data`, explicit ones win. Coming from a
            DataFrame? Use `effector.from_dataframe`.
        random_state: seed for every internal random step (default `21`,
            reproducible); `None` for non-deterministic behavior.
    """

    super(DerPDP, self).__init__(
        data,
        model,
        model_jac,
        axis_limits=axis_limits,
        nof_instances=nof_instances,
        schema=schema,
        random_state=random_state,
        method_name="d-PDP",
    )

fit(features='all', *, centering=False, use_vectorized=True)

Declare per-feature defaults and warm the caches.

pdp.fit("hr", centering="zero_integral")

fit is optional

eval, plot, heter_score compute what they need lazily with these defaults; fit declares the config once and pays the model cost upfront.

Parameters:

Name Type Description Default
features Union[int, str, list]

feature(s) to fit — index, name, list, or "all".

'all'
centering Union[bool, str]

default centering for this feature's queries — False (none), True/"zero_integral" (center around the y axis), or "zero_start" (start at y=0).

False
use_vectorized bool

vectorize the ICE computation — faster, but builds a (T, N, D) array internally; set False to trade speed for memory.

True
Source code in effector/global_effect_pdp.py
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def fit(
    self,
    features: Union[int, str, list] = "all",
    *,
    centering: Union[bool, str] = False,
    use_vectorized: bool = True,
):
    """Declare per-feature defaults and warm the caches.

    ```python
    pdp.fit("hr", centering="zero_integral")
    ```

    !!! note "fit is optional"
        `eval`, `plot`, `heter_score` compute what they need lazily with
        these defaults; `fit` declares the config once and pays the model
        cost upfront.

    Args:
        features: feature(s) to fit — index, name, list, or `"all"`.
        centering: default centering for this feature's queries —
            `False` (none), `True`/`"zero_integral"` (center around the
            y axis), or `"zero_start"` (start at `y=0`).
        use_vectorized: vectorize the ICE computation — faster, but
            builds a `(T, N, D)` array internally; set `False` to trade
            speed for memory.
    """
    self._fit_loop(features, centering, use_vectorized=use_vectorized)

eval(feature, xs, centering=None, mask=None, rule=None)

The mean effect of a feature at positions xs.

xs = np.linspace(0, 24, 100)
y = pdp.eval("hr", xs)                            # (100,) mean effect
y_wd = pdp.eval("hr", xs, rule="workingday == 0") # same, on a subregion

One array, one type (R1)

eval always returns the mean effect only. The spread around it has its own ladder: eval_heter (curve), heter_score (scalar), payload (the raw fitted object).

Discrete features

Ordinal/nominal features are evaluated only at observed levels — any other xs value raises ValueError.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
xs ndarray

where to evaluate, (T,).

required
centering Union[None, bool, str]

None (class default), False, True/"zero_integral", or "zero_start".

None
mask Optional[ndarray]

optional boolean (N,) selecting a subregion — the effect within it, re-summarized from cached local effects with zero model calls. Nothing is stored.

None
rule Union[None, str, Rule]

sugar over mask — an effector.Rule or a string like "temp < 3 and season == 0". Mutually exclusive with mask.

None

Returns:

Type Description
ndarray

the mean effect at xs, shape (T,).

Source code in effector/global_effect.py
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
def eval(
    self,
    feature: Union[int, str],
    xs: np.ndarray,
    centering: Union[None, bool, str] = None,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The mean effect of a feature at positions `xs`.

    ```python
    xs = np.linspace(0, 24, 100)
    y = pdp.eval("hr", xs)                            # (100,) mean effect
    y_wd = pdp.eval("hr", xs, rule="workingday == 0") # same, on a subregion
    ```

    !!! note "One array, one type (R1)"
        `eval` always returns the mean effect only. The spread around it
        has its own ladder: `eval_heter` (curve), `heter_score` (scalar),
        `payload` (the raw fitted object).

    !!! warning "Discrete features"
        Ordinal/nominal features are evaluated **only at observed
        levels** — any other `xs` value raises `ValueError`.

    Args:
        feature: index or name of the feature of interest.
        xs: where to evaluate, `(T,)`.
        centering: `None` (class default), `False`,
            `True`/`"zero_integral"`, or `"zero_start"`.
        mask: optional boolean `(N,)` selecting a subregion — the effect
            *within* it, re-summarized from cached local effects with zero
            model calls. Nothing is stored.
        rule: sugar over `mask` — an `effector.Rule` or a string like
            `"temp < 3 and season == 0"`. Mutually exclusive with `mask`.

    Returns:
        the mean effect at `xs`, shape `(T,)`.
    """
    feature = self._resolve_feature(feature)
    centering = self.DEFAULT_CENTERING if centering is None else centering
    centering = helpers.prep_centering(centering)
    mask = self._resolve_mask(mask, rule)

    if not self._is_cat(feature):
        if mask is not None:
            self._effective_limits(feature, mask)  # degeneracy guard
        elif not self.axis_limits[0, feature] < self.axis_limits[1, feature]:
            raise ValueError(
                f"Feature {feature} has a degenerate axis interval "
                f"[{self.axis_limits[0, feature]}, {self.axis_limits[1, feature]}]"
            )

    params = self._summary(feature, mask)
    y = self._eval_mean(feature, xs, params, mask)
    if centering is not False:
        y = y - self._mean_norm_const(
            self._centering_const(feature, mask, centering)
        )
    return y

eval_heter(feature, xs, mask=None, rule=None)

The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

h = pdp.eval_heter("hr", xs)          # (T,) variance around the mean
band = np.sqrt(h)                     # std-like band, plot-ready

It's a variance, and it's method-specific (R2)

PDP: variance of centered ICE; DerPDP: of d-ICE slopes; ALE/RHALE: per-bin slope variance as a step function; ShapDP: interpolated per-bin φ variance. Take the square root for a band.

No centering argument — by design

Heterogeneity is invariant to centering; the signature enforces it.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
xs ndarray

where to evaluate, (T,).

required
mask Optional[ndarray]

optional boolean (N,) subregion — re-summarized from cached local effects, zero model calls.

None
rule Union[None, str, Rule]

sugar over mask (an effector.Rule or a rule string); mutually exclusive with mask.

None

Returns:

Type Description
ndarray

the heterogeneity curve h(xs), (T,), non-negative.

Source code in effector/global_effect.py
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
def eval_heter(
    self,
    feature: Union[int, str],
    xs: np.ndarray,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

    ```python
    h = pdp.eval_heter("hr", xs)          # (T,) variance around the mean
    band = np.sqrt(h)                     # std-like band, plot-ready
    ```

    !!! note "It's a variance, and it's method-specific (R2)"
        PDP: variance of centered ICE; DerPDP: of d-ICE slopes; ALE/RHALE:
        per-bin slope variance as a step function; ShapDP: interpolated
        per-bin φ variance. Take the square root for a band.

    !!! note "No `centering` argument — by design"
        Heterogeneity is invariant to centering; the signature enforces it.

    Args:
        feature: index or name of the feature of interest.
        xs: where to evaluate, `(T,)`.
        mask: optional boolean `(N,)` subregion — re-summarized from cached
            local effects, zero model calls.
        rule: sugar over `mask` (an `effector.Rule` or a rule string);
            mutually exclusive with `mask`.

    Returns:
        the heterogeneity curve h(xs), `(T,)`, non-negative.
    """
    feature = self._resolve_feature(feature)
    mask = self._resolve_mask(mask, rule)
    params = self._summary(feature, mask)
    return self._eval_payload(feature, params, xs, heterogeneity=True)[1]

grid(feature)

The evaluation grid on which this feature's effect is model-free.

The observed levels for a discrete feature; otherwise helpers.NOF_INTERNAL_POINTS equally spaced points inside the feature's axis limits — the cache grid explain evaluates every reported curve on.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name.

required

Returns:

Type Description
ndarray

(T,) array of evaluation positions.

Source code in effector/global_effect.py
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
def grid(self, feature: Union[int, str]) -> np.ndarray:
    """The evaluation grid on which this feature's effect is model-free.

    The observed levels for a discrete feature; otherwise
    `helpers.NOF_INTERNAL_POINTS` equally spaced points inside the
    feature's axis limits — the cache grid `explain` evaluates every
    reported curve on.

    Args:
        feature: index or name.

    Returns:
        `(T,)` array of evaluation positions.
    """
    feature = self._resolve_feature(feature)
    if self._is_cat(feature):
        return np.unique(self.data[:, feature])
    return np.linspace(
        self.axis_limits[0, feature],
        self.axis_limits[1, feature],
        helpers.NOF_INTERNAL_POINTS,
    )

heter_score(feature, mask=None, rule=None)

One number for a feature's heterogeneity — the scalar find_regions minimizes.

pdp.heter_score("hr")                            # global
pdp.heter_score("hr", rule="workingday == 0")    # within a subregion

In output units (units contract, method_semantics.md): the RMS of eval_heter over the feature's own (masked) data values (frequency-weighted over levels for categorical features), bridged by the feature's dispersion for the derivative-based methods (ALE/RHALE/DerPDP) so every feature type and every method lands on the same y-unit scale — "a typical instance's effect deviates from the mean effect by about this much". eval_heter itself stays a variance curve in the method's native units.

Pair it with importance

importance measures the mean effect's strength; heter_score measures the spread around it — same units, mean/spread twins. High importance + high heterogeneity = the top-right corner of effector.plot_triage — where find_regions should look.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
mask Optional[ndarray]

optional boolean (N,) subregion — model-free.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
float

a non-negative scalar, in output units.

Source code in effector/global_effect.py
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
def heter_score(
    self,
    feature: Union[int, str],
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> float:
    """One number for a feature's heterogeneity — the scalar `find_regions` minimizes.

    ```python
    pdp.heter_score("hr")                            # global
    pdp.heter_score("hr", rule="workingday == 0")    # within a subregion
    ```

    In **output units** (units contract, method_semantics.md): the RMS of
    `eval_heter` over the feature's own (masked) data values
    (frequency-weighted over levels for categorical features), bridged by
    the feature's dispersion for the derivative-based methods
    (ALE/RHALE/DerPDP) so every feature type and every method lands on the
    same y-unit scale — "a typical instance's effect deviates from the
    mean effect by about this much". `eval_heter` itself stays a variance
    curve in the method's native units.

    !!! tip "Pair it with `importance`"
        `importance` measures the *mean* effect's strength; `heter_score`
        measures the spread around it — same units, mean/spread twins.
        High importance + high heterogeneity = the top-right corner of
        `effector.plot_triage` — where `find_regions` should look.

    Args:
        feature: index or name of the feature of interest.
        mask: optional boolean `(N,)` subregion — model-free.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        a non-negative scalar, in output units.
    """
    feature = self._resolve_feature(feature)
    self._check_feature_type_supported(feature)
    mask = self._resolve_mask(mask, rule)
    return float(self._heter(feature, mask))

payload(feature)

The raw fitted object behind eval/eval_heter — pure numpy, yours to inspect.

p = ale.payload("hr")     # e.g. {"limits": ..., "bin_effect": ..., "bin_variance": ...}

Per method: per-bin effects and variances for (RH)ALE and ShapDP, the grid summaries for (d-)PDP. A copy — mutate freely.

Source code in effector/global_effect.py
743
744
745
746
747
748
749
750
751
752
753
def payload(self, feature: Union[int, str]) -> dict:
    """The raw fitted object behind `eval`/`eval_heter` — pure numpy, yours to inspect.

    ```python
    p = ale.payload("hr")     # e.g. {"limits": ..., "bin_effect": ..., "bin_variance": ...}
    ```

    Per method: per-bin effects and variances for (RH)ALE and ShapDP, the
    grid summaries for (d-)PDP. A copy — mutate freely.
    """
    return dict(self._summary(self._resolve_feature(feature), None))

importance(feature, mask=None, rule=None)

How much a feature's mean effect moves the prediction (R13).

pdp.importance("temp")                           # scalar
pdp.importance("temp", rule="workingday == 1")   # within a subregion

The dispersion of the mean effect in output units — the μ-twin of heter_score (which measures per-instance spread on the same scale). A flat curve scores ~0; a swinging curve scores high. Per method: std of the mean effect over the (masked) data values (PDP/ALE/RHALE/ ShapDP; for a linear model this is |coefficient| * std(x)), mean(|derivative|) * std(x) (DerPDP). Comparable across feature types and, in magnitude, across methods.

No y, ever

effector never sees ground-truth labels — this is a property of the fitted effect, not a loss/permutation importance.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
mask Optional[ndarray]

optional boolean (N,) subregion — model-free.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
float

a non-negative scalar.

Source code in effector/global_effect.py
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
def importance(
    self,
    feature: Union[int, str],
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> float:
    """How much a feature's mean effect moves the prediction (R13).

    ```python
    pdp.importance("temp")                           # scalar
    pdp.importance("temp", rule="workingday == 1")   # within a subregion
    ```

    The dispersion of the **mean** effect in output units — the μ-twin of
    `heter_score` (which measures per-instance spread on the same scale).
    A flat curve scores ~0; a swinging curve scores high. Per method: std
    of the mean effect over the (masked) data values (PDP/ALE/RHALE/
    ShapDP; for a linear model this is `|coefficient| * std(x)`),
    `mean(|derivative|) * std(x)` (DerPDP). Comparable across feature
    types and, in magnitude, across methods.

    !!! note "No `y`, ever"
        effector never sees ground-truth labels — this is a property of
        the fitted effect, not a loss/permutation importance.

    Args:
        feature: index or name of the feature of interest.
        mask: optional boolean `(N,)` subregion — model-free.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        a non-negative scalar.
    """
    feature = self._resolve_feature(feature)
    self._check_feature_type_supported(feature)
    mask = self._resolve_mask(mask, rule)
    if mask is None:
        # all-ones ≡ None (M1); the concrete array makes the per-method
        # `_importance` implementations mask-index without a null check
        mask = np.ones(self.data.shape[0], dtype=bool)
    self._ensure_local(feature)
    return float(self._importance(feature, mask))

importances(mask=None, rule=None)

The whole importance vector — rank your features in one call.

imp = pdp.importances()                       # (D,)
order = np.argsort(-np.nan_to_num(imp))       # most important first

NaN means unsupported, not unimportant

Feature types this method cannot explain (e.g. DerPDP on a nominal feature) return NaN, with one UserWarning naming them.

Parameters:

Name Type Description Default
mask Optional[ndarray]

optional boolean (N,) subregion.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
ndarray

the per-feature importance vector, (D,).

Source code in effector/global_effect.py
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
def importances(
    self,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The whole importance vector — rank your features in one call.

    ```python
    imp = pdp.importances()                       # (D,)
    order = np.argsort(-np.nan_to_num(imp))       # most important first
    ```

    !!! warning "NaN means unsupported, not unimportant"
        Feature types this method cannot explain (e.g. DerPDP on a
        nominal feature) return `NaN`, with one `UserWarning` naming them.

    Args:
        mask: optional boolean `(N,)` subregion.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        the per-feature importance vector, `(D,)`.
    """
    mask = self._resolve_mask(mask, rule)
    out = np.full(self.dim, np.nan)
    skipped = []
    for f in range(self.dim):
        try:
            out[f] = self.importance(f, mask=mask)
        except ValueError:
            skipped.append(self.feature_names[f])
    if skipped:
        warnings.warn(
            f"importance is undefined for feature(s) {skipped} — this "
            f"method does not support their feature type; returned NaN.",
            UserWarning,
            stacklevel=2,
        )
    return out

find_regions(feature=None, *, features=None, finder='best', candidate_conditioning_features='all')

Search for subregions that resolve a feature's heterogeneity.

part = pdp.find_regions("hr")                       # one feature -> Partition
part.show()                                         # the tree + level stats
pdp.plot("hr", rule=part.leaves[0].rule)            # drill into a leaf

parts = pdp.find_regions(features="heterogeneous")  # several -> {name: Partition}
effector.plot_triage(pdp, partitions=parts)         # the before/after picture

A query, not a mutation (R12)

The result is a value — nothing is stored on the effect. Don't like a partition? Search again with different finder kwargs; nothing needs resetting.

Model-free

Every candidate split is scored by heter_score(feature, mask) on the cached local effects — zero model calls, whatever the grid size. Binning/scope are those the feature was fitted with, replayed.

Parameters:

Name Type Description Default
feature Union[int, str, None]

index or name of the one feature to partition (→ Partition).

None
features Union[list, str, None]

several at once — a list, "all", or "heterogeneous" (heter_score at/above the median, the same convention effector.explain uses) → {feature_name: Partition}. Exactly one of feature/features must be given.

None
finder

"best" (default), "best_level_wise", or a configured finder instance (e.g. effector.space_partitioning.Best(...)).

'best'
candidate_conditioning_features

features allowed to define splits ("all" or a list of indices/names).

'all'

Returns:

Type Description

a Partition bound to this effect — or {feature_name: Partition}

with features=.

Source code in effector/global_effect.py
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
def find_regions(
    self,
    feature: Union[int, str, None] = None,
    *,
    features: Union[list, str, None] = None,
    finder="best",
    candidate_conditioning_features="all",
):
    """Search for subregions that resolve a feature's heterogeneity.

    ```python
    part = pdp.find_regions("hr")                       # one feature -> Partition
    part.show()                                         # the tree + level stats
    pdp.plot("hr", rule=part.leaves[0].rule)            # drill into a leaf

    parts = pdp.find_regions(features="heterogeneous")  # several -> {name: Partition}
    effector.plot_triage(pdp, partitions=parts)         # the before/after picture
    ```

    !!! note "A query, not a mutation (R12)"
        The result is a value — nothing is stored on the effect. Don't
        like a partition? Search again with different finder kwargs;
        nothing needs resetting.

    !!! note "Model-free"
        Every candidate split is scored by `heter_score(feature, mask)`
        on the cached local effects — zero model calls, whatever the grid
        size. Binning/scope are those the feature was fitted with,
        replayed.

    Args:
        feature: index or name of the one feature to partition
            (→ `Partition`).
        features: several at once — a list, `"all"`, or `"heterogeneous"`
            (heter_score at/above the median, the same convention
            `effector.explain` uses) → `{feature_name: Partition}`.
            Exactly one of `feature`/`features` must be given.
        finder: `"best"` (default), `"best_level_wise"`, or a configured
            finder instance (e.g. `effector.space_partitioning.Best(...)`).
        candidate_conditioning_features: features allowed to define splits
            (`"all"` or a list of indices/names).

    Returns:
        a `Partition` bound to this effect — or `{feature_name: Partition}`
        with `features=`.
    """
    if (feature is None) == (features is None):
        raise ValueError(
            "find_regions takes exactly one of `feature` (singular -> "
            "Partition) or `features` (plural -> {name: Partition})"
        )
    if features is not None:
        return self._find_regions_plural(
            features,
            finder=finder,
            candidate_conditioning_features=candidate_conditioning_features,
        )

    from effector import space_partitioning  # lazy: one-way dep guard

    feature = self._resolve_feature(feature)
    if isinstance(candidate_conditioning_features, list):
        candidate_conditioning_features = [
            self._resolve_feature(f) for f in candidate_conditioning_features
        ]
    self._check_feature_type_supported(feature)
    self._ensure_local(feature)

    if isinstance(finder, str):
        finder = space_partitioning.return_default(finder)

    def score_fn(mask):
        return self.heter_score(feature, mask=mask)  # RAW; guard is the finder's

    partition = finder.find_regions(
        feature,
        self.data,
        score_fn,
        axis_limits=self.axis_limits,
        feature_types=self.feature_types,
        cat_limit=self.cat_limit,
        candidate_conditioning_features=candidate_conditioning_features,
        feature_names=self.feature_names,
        target_name=self.target_name,
    )
    return partition.bind(self)

select_regions(partitions=None, *, features='heterogeneous', finder='best', candidate_conditioning_features='all', min_r2_gain=0.01)

Greedily select which partitions earn their complexity — the CALM chain.

chain = pdp.select_regions()      # search + select in one call
chain.show()                      # GAM R2, each accepted split, the rejected
chain.final                       # the last CALM — the regional analysis
chain[0]                          # the GAM snapshot

find_regions proposes one candidate Partition per feature; this verb decides across features which of them actually explain the model: starting from the GAM (all features global), each round applies the split with the largest explained-variance gain — the surrogate R² against , measured on top of the splits already applied — and stops when no remaining split adds at least min_r2_gain. Every accepted round is a snapshot (CALM) of increased complexity; the whole chain is the report's ledger.

A query, not a mutation (R12)

The result is a value — nothing is stored on the effect.

One prediction pass

Beyond fit, the only model touch is one f̂(X) pass for the variance denominator (cached on the effect); the search, the scoring, and every snapshot's summaries are model-free.

Parameters:

Name Type Description Default
partitions Optional[dict]

pre-computed candidates — {feature_index_or_name: Partition}, exactly what find_regions(features=...) returns. None (default) runs the search here first.

None
features Union[list, str]

which features to search when partitions is None — a list, "all", or "heterogeneous" (default; heter_score at/above the median).

'heterogeneous'
finder

region finder, as in find_regions.

'best'
candidate_conditioning_features

features allowed to define splits ("all" or a list of indices/names).

'all'
min_r2_gain float

smallest explained-variance marginal (fraction of Var(f̂), default 0.01 = 1 pt) a split must add — on top of the splits already applied — to earn a snapshot.

0.01

Returns:

Type Description

a CalmSequence[GAM, calm1, ...], R² non-decreasing along

it, with the rejected splits in .skipped

("redundant"/"below_threshold").

Raises:

Type Description
ValueError

the method is derivative-scale (no output-scale surrogate) or Var(f̂) == 0.

Source code in effector/global_effect.py
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
def select_regions(
    self,
    partitions: Optional[dict] = None,
    *,
    features: Union[list, str] = "heterogeneous",
    finder="best",
    candidate_conditioning_features="all",
    min_r2_gain: float = 0.01,
):
    """Greedily select which partitions earn their complexity — the CALM chain.

    ```python
    chain = pdp.select_regions()      # search + select in one call
    chain.show()                      # GAM R2, each accepted split, the rejected
    chain.final                       # the last CALM — the regional analysis
    chain[0]                          # the GAM snapshot
    ```

    `find_regions` proposes one candidate `Partition` per feature;
    this verb decides *across* features which of them actually explain
    the model: starting from the GAM (all features global), each round
    applies the split with the largest explained-variance gain — the
    surrogate R² against `f̂`, measured on top of the splits already
    applied — and stops when no remaining split adds at least
    `min_r2_gain`. Every accepted round is a snapshot (`CALM`) of
    increased complexity; the whole chain is the report's ledger.

    !!! note "A query, not a mutation (R12)"
        The result is a value — nothing is stored on the effect.

    !!! note "One prediction pass"
        Beyond `fit`, the only model touch is one `f̂(X)` pass for the
        variance denominator (cached on the effect); the search, the
        scoring, and every snapshot's summaries are model-free.

    Args:
        partitions: pre-computed candidates — `{feature_index_or_name:
            Partition}`, exactly what `find_regions(features=...)`
            returns. `None` (default) runs the search here first.
        features: which features to search when `partitions` is `None` —
            a list, `"all"`, or `"heterogeneous"` (default; heter_score
            at/above the median).
        finder: region finder, as in `find_regions`.
        candidate_conditioning_features: features allowed to define
            splits (`"all"` or a list of indices/names).
        min_r2_gain: smallest explained-variance marginal (fraction of
            `Var(f̂)`, default 0.01 = 1 pt) a split must add — on top of
            the splits already applied — to earn a snapshot.

    Returns:
        a `CalmSequence` — `[GAM, calm1, ...]`, R² non-decreasing along
        it, with the rejected splits in `.skipped`
        (`"redundant"`/`"below_threshold"`).

    Raises:
        ValueError: the method is derivative-scale (no output-scale
            surrogate) or `Var(f̂) == 0`.
    """
    from effector import explained_variance as _ev  # lazy: one-way dep guard

    if partitions is None:
        partitions = self.find_regions(
            features=features,
            finder=finder,
            candidate_conditioning_features=candidate_conditioning_features,
        )
    parts = {}
    for key, p in partitions.items():
        f = self._resolve_feature(key)
        parts[f] = p if p._effect is not None else p.bind(self)

    supported = []
    for f in range(self.dim):
        try:
            self._check_feature_type_supported(f)
            supported.append(f)
        except ValueError:
            continue
    return _ev.select(self, parts, supported, min_gain=min_r2_gain)

explain(*, y=None, top_k=5, coverage=0.8, heter_threshold=None, min_r2_gain=0.01, finder='best', candidate_conditioning_features='all')

The one-liner on this engine — effector.explain without leaving the session.

pdp = effector.PDP(X, model, schema=schema)
report = pdp.explain()            # same Report as effector.explain

Runs the same pipeline as effector.explain on the already-built engine: features you have fit with custom config keep it (missing ones are computed with the defaults), and every cache the pipeline warms stays on the engine for your follow-up queries.

Parameters:

Name Type Description Default
y Optional[ndarray]

optional ground truth aligned with the original data (or this engine's subsample); when given, the report header states the model's score on the explained subsample.

None
top_k int

hard ceiling on how many features get curve plots.

5
coverage float

stop plotting once the shown features carry this share of the total importance mass (default 0.8).

0.8
heter_threshold Optional[float]

minimum heter_score to enter the region search; None (default) = the median convention.

None
min_r2_gain float

smallest explained-variance marginal a split must add to earn a snapshot in the CALM chain.

0.01
finder

region finder, as in find_regions.

'best'
candidate_conditioning_features

features allowed to define splits.

'all'

Returns:

Type Description

a Report bound to this engine.

Source code in effector/global_effect.py
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
def explain(
    self,
    *,
    y: Optional[np.ndarray] = None,
    top_k: int = 5,
    coverage: float = 0.8,
    heter_threshold: Optional[float] = None,
    min_r2_gain: float = 0.01,
    finder="best",
    candidate_conditioning_features="all",
):
    """The one-liner on *this* engine — `effector.explain` without leaving the session.

    ```python
    pdp = effector.PDP(X, model, schema=schema)
    report = pdp.explain()            # same Report as effector.explain
    ```

    Runs the same pipeline as `effector.explain` on the already-built
    engine: features you have `fit` with custom config keep it (missing
    ones are computed with the defaults), and every cache the pipeline
    warms stays on the engine for your follow-up queries.

    Args:
        y: optional ground truth aligned with the original `data` (or
            this engine's subsample); when given, the report header
            states the model's score on the explained subsample.
        top_k: hard ceiling on how many features get curve plots.
        coverage: stop plotting once the shown features carry this share
            of the total importance mass (default 0.8).
        heter_threshold: minimum `heter_score` to enter the region
            search; `None` (default) = the median convention.
        min_r2_gain: smallest explained-variance marginal a split must
            add to earn a snapshot in the CALM chain.
        finder: region finder, as in `find_regions`.
        candidate_conditioning_features: features allowed to define
            splits.

    Returns:
        a `Report` bound to this engine.
    """
    from effector import report as _report  # lazy: one-way dep guard

    return _report._explain_effect(
        self,
        y=y,
        top_k=top_k,
        coverage=coverage,
        heter_threshold=heter_threshold,
        min_r2_gain=min_r2_gain,
        finder=finder,
        candidate_conditioning_features=candidate_conditioning_features,
    )

plot(feature, heterogeneity='ice', centering=False, nof_points=100, scale_x=None, scale_y=None, nof_ice=100, show_avg_output=False, y_limits=None, use_vectorized=True, show_plot=True, mask=None, rule=None, feature_label=None)

Plot the d-PDP of feature, by default with the d-ICE cloud.

dpdp.plot("hr")                       # mean derivative + d-ICE
dpdp.plot("hr", heterogeneity="std")  # mean ± std band

Derivative units

The y axis (and y_limits) is in derivative units d(target)/d(feature), not output units.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature to plot.

required
heterogeneity Union[bool, str]

what to draw around the mean derivative:

  • False: the mean effect only
  • True or "std": ± one std of the d-ICE curves
  • "std_err": ± the standard error of the mean
  • "ice" (default): the d-ICE curves themselves
'ice'
centering Union[bool, str]

False (default, none), True/"zero_integral" (center around the y axis), or "zero_start" (start at y=0).

False
nof_points int

grid size of the x axis (default 100).

100
scale_x Optional[dict]

None or {"mean": m, "std": s} — the x axis is drawn as x = (x + m) * s (undo a standardization).

None
scale_y Optional[dict]

same, for the y axis.

None
nof_ice Union[int, str]

how many d-ICE curves to draw (default 100), or "all".

100
show_avg_output bool

draw the model's average output as a horizontal line.

False
y_limits Optional[List]

(low, high) for the y axis; None = automatic.

None
use_vectorized bool

vectorized ICE computation (faster, more memory).

True
show_plot bool

if False, return the figure and axes instead of showing.

True
mask Optional[ndarray]

boolean (N,) selecting a subregion — plot the d-PDP/d-ICE within it from the cached d-ICE table (no model calls; nof_points does not apply), x axis windowed to the subregion's own interval.

None
rule

sugar over mask — an effector.Rule or a rule string, applied to the effect's data. Mutually exclusive with mask.

None
feature_label Optional[str]

display title for the figure (e.g. a regional node's label with its rule); defaults to the feature name. The x-axis always keeps the plain feature name.

None
Source code in effector/global_effect_pdp.py
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
def plot(
    self,
    feature: Union[int, str],
    heterogeneity: Union[bool, str] = "ice",
    centering: Union[bool, str] = False,
    nof_points: int = 100,
    scale_x: Optional[dict] = None,
    scale_y: Optional[dict] = None,
    nof_ice: Union[int, str] = 100,
    show_avg_output: bool = False,
    y_limits: Optional[List] = None,
    use_vectorized: bool = True,
    show_plot: bool = True,
    mask: Optional[np.ndarray] = None,
    rule=None,
    feature_label: Optional[str] = None,
):
    """Plot the d-PDP of `feature`, by default with the d-ICE cloud.

    ```python
    dpdp.plot("hr")                       # mean derivative + d-ICE
    dpdp.plot("hr", heterogeneity="std")  # mean ± std band
    ```

    !!! warning "Derivative units"
        The y axis (and `y_limits`) is in derivative units
        `d(target)/d(feature)`, not output units.

    Args:
        feature: index or name of the feature to plot.
        heterogeneity: what to draw around the mean derivative:

            - `False`: the mean effect only
            - `True` or `"std"`: ± one std of the d-ICE curves
            - `"std_err"`: ± the standard error of the mean
            - `"ice"` (default): the d-ICE curves themselves

        centering: `False` (default, none), `True`/`"zero_integral"`
            (center around the y axis), or `"zero_start"` (start at
            `y=0`).
        nof_points: grid size of the x axis (default `100`).
        scale_x: `None` or `{"mean": m, "std": s}` — the x axis is
            drawn as `x = (x + m) * s` (undo a standardization).
        scale_y: same, for the y axis.
        nof_ice: how many d-ICE curves to draw (default `100`), or
            `"all"`.
        show_avg_output: draw the model's average output as a
            horizontal line.
        y_limits: `(low, high)` for the y axis; `None` = automatic.
        use_vectorized: vectorized ICE computation (faster, more memory).
        show_plot: if `False`, return the figure and axes instead of
            showing.
        mask: boolean `(N,)` selecting a subregion — plot the
            d-PDP/d-ICE *within* it from the cached d-ICE table (no
            model calls; `nof_points` does not apply), x axis windowed
            to the subregion's own interval.
        rule: sugar over `mask` — an `effector.Rule` or a rule string,
            applied to the effect's data. Mutually exclusive with `mask`.
        feature_label: display title for the figure (e.g. a regional
            node's label with its rule); defaults to the feature name.
            The x-axis always keeps the plain feature name.
    """
    feature = self._resolve_feature(feature)
    mask = self._resolve_mask(mask, rule)
    ret = self._plot(
        feature,
        heterogeneity,
        centering,
        nof_points,
        scale_x,
        scale_y,
        nof_ice,
        show_avg_output,
        y_limits,
        use_vectorized,
        show_plot,
        mask,
        feature_label,
    )

    if not show_plot:
        fig, ax = ret
        return fig, ax

effector.global_effect_shap.ShapDP(data, model, *, axis_limits=None, nof_instances=1000, schema=None, random_state=21, shap_values=None, backend='shap', budget=512, shap_explainer_kwargs=None, shap_explanation_kwargs=None)

Bases: GlobalEffectBase

SHAP Dependence Plot: per-instance SHAP values against the feature value, with a curve fitted through them.

sdp = effector.ShapDP(X, model, nof_instances=500)
sdp.plot("hr")   # SHAP scatter + fitted curve

The curve \(\hat{f}^{SDP}_j(x_j)\) is fit to the SHAP cloud \(\{(x_j^i, \hat{\phi}_j(x_j^i))\}_{i=1}^N\): the axis is binned and the per-bin SHAP means are interpolated piecewise-linearly. The heterogeneity is the per-bin variance of the SHAP values.

The slow one

SHAP values are expensive — cost grows with instances and features. Keep nof_instances modest (default 1_000) and raise budget only if the estimate looks noisy. Requires the shap or shapiq package: pip install effector[shap].

Build a ShapDP explainer. SHAP values are computed lazily, on the first query.

Definition

The value of a coalition \(S\) of features is estimated as: $$ \hat{v}(S) = {1 \over N} \sum_{i=1}^N [f(\mathbf{x}_S \cup \mathbf{x}_C^i) - f(\mathbf{x}^i)] $$ i.e. the average change in the output when the features in \(S\) are set to \(\mathbf{x}_S\) and the rest are left as observed.

The contribution of feature \(j\) added to a coalition \(S\) is: $$ \hat{\Delta}_{S, j} = \hat{v}(S \cup {j}) - \hat{v}(S) $$

The SHAP value of feature \(j\) at value \(x_j\) averages this contribution over all coalitions, weighted so that every coalition size counts equally: $$ \hat{\phi}j(x_j) = \sum{S \subseteq {1, \dots, D} \setminus {j}} w_{S, j} \hat{\Delta}_{S, j} $$

The SHAP-DP curve \(\hat{f}^{SDP}_j(x_j)\) is fit to \(\{(x_j^i, \hat{\phi}_j(x_j^i))\}_{i=1}^N\): the axis is split into bins and the per-bin SHAP means are interpolated piecewise-linearly (linear extrapolation beyond the outer bin centers). See the original paper.

Parameters:

Name Type Description Default
data ndarray

the design matrix, shape (N, D) — numpy only.

required
model Callable

the black-box model — a Callable mapping (N, D) arrays to (N,) predictions.

required
axis_limits Optional[ndarray]

per-feature plot limits, shape (2, D); None (default) infers them from data.

None
nof_instances Union[int, str]

max instances used for SHAP estimation (default 1_000 — deliberately lower than other methods, SHAP is expensive); an int subsamples randomly, "all" keeps everything.

1000
schema Optional[Union[Schema, dict]]

input metadata — an effector.Schema or a plain dict with any of feature_names, feature_types, cat_limit, target_name, scale_x_list, scale_y; omitted fields are inferred from data, explicit ones win. Coming from a DataFrame? Use effector.from_dataframe.

None
random_state Optional[int]

seed for every internal random step, including the shap/shapiq explainer (default 21, reproducible); None for non-deterministic behavior.

21
shap_values Optional[ndarray]

precomputed SHAP values, shape (N, D); if given, the backend is never called.

None
backend str

"shap" (default) or "shapiq" — the package that computes the SHAP values.

'shap'
budget int

max model evaluations per instance for the SHAP approximation (default 512); higher = more accurate, slower.

512
shap_explainer_kwargs Optional[dict]

extra kwargs for shap.Explainer / shapiq.Explainer (they override the defaults, including the seed). See effector.global_effect_shap._compute_shap_values — the single place the explainer is constructed and invoked.

None
shap_explanation_kwargs Optional[dict]

extra kwargs for the explanation call of the chosen backend (same code path as above).

None

Methods:

Name Description
fit

Declare per-feature defaults and warm the caches.

eval

The mean effect of a feature at positions xs.

eval_heter

The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

grid

The evaluation grid on which this feature's effect is model-free.

heter_score

One number for a feature's heterogeneity — the scalar find_regions minimizes.

payload

The raw fitted object behind eval/eval_heter — pure numpy, yours to inspect.

importance

How much a feature's mean effect moves the prediction (R13).

importances

The whole importance vector — rank your features in one call.

find_regions

Search for subregions that resolve a feature's heterogeneity.

select_regions

Greedily select which partitions earn their complexity — the CALM chain.

explain

The one-liner on this engine — effector.explain without leaving the session.

plot

Plot the SHAP-DP of feature, by default with the SHAP scatter.

Source code in effector/global_effect_shap.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def __init__(
    self,
    data: np.ndarray,
    model: Callable,
    *,
    axis_limits: Optional[np.ndarray] = None,
    nof_instances: Union[int, str] = 1_000,
    schema: Optional[Union[ingestion.Schema, dict]] = None,
    random_state: Optional[int] = 21,
    shap_values: Optional[np.ndarray] = None,
    backend: str = "shap",
    budget: int = 512,
    shap_explainer_kwargs: Optional[dict] = None,
    shap_explanation_kwargs: Optional[dict] = None,
):
    r"""Build a ShapDP explainer. SHAP values are computed lazily, on the
    first query.

    ??? note "Definition"

        The value of a coalition $S$ of features is estimated as:
        $$
        \hat{v}(S) = {1 \over N} \sum_{i=1}^N
        [f(\mathbf{x}_S \cup \mathbf{x}_C^i) - f(\mathbf{x}^i)]
        $$
        i.e. the average change in the output when the features in $S$
        are set to $\mathbf{x}_S$ and the rest are left as observed.

        The contribution of feature $j$ added to a coalition $S$ is:
        $$
        \hat{\Delta}_{S, j} = \hat{v}(S \cup \{j\}) - \hat{v}(S)
        $$

        The SHAP value of feature $j$ at value $x_j$ averages this
        contribution over all coalitions, weighted so that every
        coalition size counts equally:
        $$
        \hat{\phi}_j(x_j) = \sum_{S \subseteq \{1, \dots, D\}
        \setminus \{j\}} w_{S, j} \hat{\Delta}_{S, j}
        $$

        The SHAP-DP curve $\hat{f}^{SDP}_j(x_j)$ is fit to
        $\{(x_j^i, \hat{\phi}_j(x_j^i))\}_{i=1}^N$: the axis is split
        into bins and the per-bin SHAP means are interpolated
        piecewise-linearly (linear extrapolation beyond the outer bin
        centers). See the
        [original paper](https://arxiv.org/abs/1705.07874).

    Args:
        data: the design matrix, shape `(N, D)` — numpy only.
        model: the black-box model — a `Callable` mapping `(N, D)`
            arrays to `(N,)` predictions.
        axis_limits: per-feature plot limits, shape `(2, D)`; `None`
            (default) infers them from `data`.
        nof_instances: max instances used for SHAP estimation (default
            `1_000` — deliberately lower than other methods, SHAP is
            expensive); an `int` subsamples randomly, `"all"` keeps
            everything.
        schema: input metadata — an `effector.Schema` or a plain `dict`
            with any of `feature_names`, `feature_types`, `cat_limit`,
            `target_name`, `scale_x_list`, `scale_y`; omitted fields are
            inferred from `data`, explicit ones win. Coming from a
            DataFrame? Use `effector.from_dataframe`.
        random_state: seed for every internal random step, including the
            shap/shapiq explainer (default `21`, reproducible); `None`
            for non-deterministic behavior.
        shap_values: precomputed SHAP values, shape `(N, D)`; if given,
            the backend is never called.
        backend: `"shap"` (default) or `"shapiq"` — the package that
            computes the SHAP values.
        budget: max model evaluations per instance for the SHAP
            approximation (default `512`); higher = more accurate,
            slower.
        shap_explainer_kwargs: extra kwargs for `shap.Explainer` /
            `shapiq.Explainer` (they override the defaults, including
            the seed). See
            `effector.global_effect_shap._compute_shap_values` — the
            single place the explainer is constructed and invoked.
        shap_explanation_kwargs: extra kwargs for the explanation call
            of the chosen backend (same code path as above).
    """
    self.shap_values = shap_values if shap_values is not None else None
    if backend not in ["shap", "shapiq"]:
        raise ValueError(f"Invalid backend: {backend!r}; use 'shap' or 'shapiq'")
    self.backend = backend
    self.budget = budget
    self.shap_explainer_kwargs = shap_explainer_kwargs
    self.shap_explanation_kwargs = shap_explanation_kwargs
    super(ShapDP, self).__init__(
        "SHAP DP",
        data,
        model,
        nof_instances=nof_instances,
        axis_limits=axis_limits,
        schema=schema,
        random_state=random_state,
    )

fit(features='all', *, centering=True, binning_method='dp', binning_scope='global')

Declare per-feature defaults and warm the caches.

sdp.fit("hr", binning_method="dp")

fit is optional — but it pays the SHAP bill

Any query computes what it needs lazily with these defaults; the first one triggers the (expensive) SHAP computation for the whole (N, D) table. fit lets you pay that cost upfront.

The curve is the piecewise-linear interpolation of the per-bin SHAP means (linear extrapolation beyond the outer bin centers); the heterogeneity is the same interpolation of the per-bin SHAP variances, clamped at zero.

Parameters:

Name Type Description Default
features Union[int, str, List]

feature(s) to fit — index, name, list, or "all".

'all'
centering Union[bool, str]

default centering for this feature's queries — False (none), True/"zero_integral" (center around the y axis), or "zero_start" (start at y=0).

True
binning_method Union[str, DynamicProgramming, Agglomerative, Quantile, Fixed]

how the axis is split before fitting the curve:

  • "dp" (default): dynamic programming — optimal variable-size bins
  • "agglomerative": bottom-up merging of small bins ("greedy" is a deprecated alias)
  • "quantile": equal-frequency bins
  • "fixed": equal-width bins

For custom parameters pass an instance from effector.axis_partitioning, e.g. DynamicProgramming(max_nof_bins=30).

'dp'
binning_scope str

the x-range the binner covers when a masked summary re-bins a subregion (eval/eval_heter/plot/ heter_score with mask=; the regional split search):

  • "global" (default): the frozen global axis_limits — one frame for every subregion, directly comparable
  • "effective": the masked column's own [min, max] — bins packed into the subregion, finer resolution

Recorded at fit and replayed by every masked call. Ignored when no mask is involved.

'global'
Source code in effector/global_effect_shap.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
def fit(
    self,
    features: Union[int, str, List] = "all",
    *,
    centering: Union[bool, str] = True,
    binning_method: Union[
        str, ap.DynamicProgramming, ap.Agglomerative, ap.Quantile, ap.Fixed
    ] = "dp",
    binning_scope: str = "global",
) -> None:
    r"""Declare per-feature defaults and warm the caches.

    ```python
    sdp.fit("hr", binning_method="dp")
    ```

    !!! note "fit is optional — but it pays the SHAP bill"
        Any query computes what it needs lazily with these defaults; the
        first one triggers the (expensive) SHAP computation for the whole
        `(N, D)` table. `fit` lets you pay that cost upfront.

    The curve is the piecewise-linear interpolation of the per-bin SHAP
    means (linear extrapolation beyond the outer bin centers); the
    heterogeneity is the same interpolation of the per-bin SHAP
    *variances*, clamped at zero.

    Args:
        features: feature(s) to fit — index, name, list, or `"all"`.
        centering: default centering for this feature's queries —
            `False` (none), `True`/`"zero_integral"` (center around the
            y axis), or `"zero_start"` (start at `y=0`).
        binning_method: how the axis is split before fitting the curve:

            - `"dp"` (default): dynamic programming — optimal
              variable-size bins
            - `"agglomerative"`: bottom-up merging of small bins
              (`"greedy"` is a deprecated alias)
            - `"quantile"`: equal-frequency bins
            - `"fixed"`: equal-width bins

            For custom parameters pass an instance from
            `effector.axis_partitioning`, e.g.
            `DynamicProgramming(max_nof_bins=30)`.

        binning_scope: the x-range the binner covers when a *masked*
            summary re-bins a subregion (`eval`/`eval_heter`/`plot`/
            `heter_score` with `mask=`; the regional split search):

            - `"global"` (default): the frozen global `axis_limits` —
              one frame for every subregion, directly comparable
            - `"effective"`: the masked column's own `[min, max]` —
              bins packed into the subregion, finer resolution

            Recorded at fit and replayed by every masked call. Ignored
            when no mask is involved.
    """
    check_binning_scope(binning_scope)
    self._fit_loop(
        features,
        centering,
        binning_method=binning_method,
        binning_scope=binning_scope,
    )

eval(feature, xs, centering=None, mask=None, rule=None)

The mean effect of a feature at positions xs.

xs = np.linspace(0, 24, 100)
y = pdp.eval("hr", xs)                            # (100,) mean effect
y_wd = pdp.eval("hr", xs, rule="workingday == 0") # same, on a subregion

One array, one type (R1)

eval always returns the mean effect only. The spread around it has its own ladder: eval_heter (curve), heter_score (scalar), payload (the raw fitted object).

Discrete features

Ordinal/nominal features are evaluated only at observed levels — any other xs value raises ValueError.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
xs ndarray

where to evaluate, (T,).

required
centering Union[None, bool, str]

None (class default), False, True/"zero_integral", or "zero_start".

None
mask Optional[ndarray]

optional boolean (N,) selecting a subregion — the effect within it, re-summarized from cached local effects with zero model calls. Nothing is stored.

None
rule Union[None, str, Rule]

sugar over mask — an effector.Rule or a string like "temp < 3 and season == 0". Mutually exclusive with mask.

None

Returns:

Type Description
ndarray

the mean effect at xs, shape (T,).

Source code in effector/global_effect.py
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
def eval(
    self,
    feature: Union[int, str],
    xs: np.ndarray,
    centering: Union[None, bool, str] = None,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The mean effect of a feature at positions `xs`.

    ```python
    xs = np.linspace(0, 24, 100)
    y = pdp.eval("hr", xs)                            # (100,) mean effect
    y_wd = pdp.eval("hr", xs, rule="workingday == 0") # same, on a subregion
    ```

    !!! note "One array, one type (R1)"
        `eval` always returns the mean effect only. The spread around it
        has its own ladder: `eval_heter` (curve), `heter_score` (scalar),
        `payload` (the raw fitted object).

    !!! warning "Discrete features"
        Ordinal/nominal features are evaluated **only at observed
        levels** — any other `xs` value raises `ValueError`.

    Args:
        feature: index or name of the feature of interest.
        xs: where to evaluate, `(T,)`.
        centering: `None` (class default), `False`,
            `True`/`"zero_integral"`, or `"zero_start"`.
        mask: optional boolean `(N,)` selecting a subregion — the effect
            *within* it, re-summarized from cached local effects with zero
            model calls. Nothing is stored.
        rule: sugar over `mask` — an `effector.Rule` or a string like
            `"temp < 3 and season == 0"`. Mutually exclusive with `mask`.

    Returns:
        the mean effect at `xs`, shape `(T,)`.
    """
    feature = self._resolve_feature(feature)
    centering = self.DEFAULT_CENTERING if centering is None else centering
    centering = helpers.prep_centering(centering)
    mask = self._resolve_mask(mask, rule)

    if not self._is_cat(feature):
        if mask is not None:
            self._effective_limits(feature, mask)  # degeneracy guard
        elif not self.axis_limits[0, feature] < self.axis_limits[1, feature]:
            raise ValueError(
                f"Feature {feature} has a degenerate axis interval "
                f"[{self.axis_limits[0, feature]}, {self.axis_limits[1, feature]}]"
            )

    params = self._summary(feature, mask)
    y = self._eval_mean(feature, xs, params, mask)
    if centering is not False:
        y = y - self._mean_norm_const(
            self._centering_const(feature, mask, centering)
        )
    return y

eval_heter(feature, xs, mask=None, rule=None)

The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

h = pdp.eval_heter("hr", xs)          # (T,) variance around the mean
band = np.sqrt(h)                     # std-like band, plot-ready

It's a variance, and it's method-specific (R2)

PDP: variance of centered ICE; DerPDP: of d-ICE slopes; ALE/RHALE: per-bin slope variance as a step function; ShapDP: interpolated per-bin φ variance. Take the square root for a band.

No centering argument — by design

Heterogeneity is invariant to centering; the signature enforces it.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
xs ndarray

where to evaluate, (T,).

required
mask Optional[ndarray]

optional boolean (N,) subregion — re-summarized from cached local effects, zero model calls.

None
rule Union[None, str, Rule]

sugar over mask (an effector.Rule or a rule string); mutually exclusive with mask.

None

Returns:

Type Description
ndarray

the heterogeneity curve h(xs), (T,), non-negative.

Source code in effector/global_effect.py
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
def eval_heter(
    self,
    feature: Union[int, str],
    xs: np.ndarray,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The heterogeneity curve h(xs): how much per-instance effects disagree at each x.

    ```python
    h = pdp.eval_heter("hr", xs)          # (T,) variance around the mean
    band = np.sqrt(h)                     # std-like band, plot-ready
    ```

    !!! note "It's a variance, and it's method-specific (R2)"
        PDP: variance of centered ICE; DerPDP: of d-ICE slopes; ALE/RHALE:
        per-bin slope variance as a step function; ShapDP: interpolated
        per-bin φ variance. Take the square root for a band.

    !!! note "No `centering` argument — by design"
        Heterogeneity is invariant to centering; the signature enforces it.

    Args:
        feature: index or name of the feature of interest.
        xs: where to evaluate, `(T,)`.
        mask: optional boolean `(N,)` subregion — re-summarized from cached
            local effects, zero model calls.
        rule: sugar over `mask` (an `effector.Rule` or a rule string);
            mutually exclusive with `mask`.

    Returns:
        the heterogeneity curve h(xs), `(T,)`, non-negative.
    """
    feature = self._resolve_feature(feature)
    mask = self._resolve_mask(mask, rule)
    params = self._summary(feature, mask)
    return self._eval_payload(feature, params, xs, heterogeneity=True)[1]

grid(feature)

The evaluation grid on which this feature's effect is model-free.

The observed levels for a discrete feature; otherwise helpers.NOF_INTERNAL_POINTS equally spaced points inside the feature's axis limits — the cache grid explain evaluates every reported curve on.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name.

required

Returns:

Type Description
ndarray

(T,) array of evaluation positions.

Source code in effector/global_effect.py
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
def grid(self, feature: Union[int, str]) -> np.ndarray:
    """The evaluation grid on which this feature's effect is model-free.

    The observed levels for a discrete feature; otherwise
    `helpers.NOF_INTERNAL_POINTS` equally spaced points inside the
    feature's axis limits — the cache grid `explain` evaluates every
    reported curve on.

    Args:
        feature: index or name.

    Returns:
        `(T,)` array of evaluation positions.
    """
    feature = self._resolve_feature(feature)
    if self._is_cat(feature):
        return np.unique(self.data[:, feature])
    return np.linspace(
        self.axis_limits[0, feature],
        self.axis_limits[1, feature],
        helpers.NOF_INTERNAL_POINTS,
    )

heter_score(feature, mask=None, rule=None)

One number for a feature's heterogeneity — the scalar find_regions minimizes.

pdp.heter_score("hr")                            # global
pdp.heter_score("hr", rule="workingday == 0")    # within a subregion

In output units (units contract, method_semantics.md): the RMS of eval_heter over the feature's own (masked) data values (frequency-weighted over levels for categorical features), bridged by the feature's dispersion for the derivative-based methods (ALE/RHALE/DerPDP) so every feature type and every method lands on the same y-unit scale — "a typical instance's effect deviates from the mean effect by about this much". eval_heter itself stays a variance curve in the method's native units.

Pair it with importance

importance measures the mean effect's strength; heter_score measures the spread around it — same units, mean/spread twins. High importance + high heterogeneity = the top-right corner of effector.plot_triage — where find_regions should look.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
mask Optional[ndarray]

optional boolean (N,) subregion — model-free.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
float

a non-negative scalar, in output units.

Source code in effector/global_effect.py
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
def heter_score(
    self,
    feature: Union[int, str],
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> float:
    """One number for a feature's heterogeneity — the scalar `find_regions` minimizes.

    ```python
    pdp.heter_score("hr")                            # global
    pdp.heter_score("hr", rule="workingday == 0")    # within a subregion
    ```

    In **output units** (units contract, method_semantics.md): the RMS of
    `eval_heter` over the feature's own (masked) data values
    (frequency-weighted over levels for categorical features), bridged by
    the feature's dispersion for the derivative-based methods
    (ALE/RHALE/DerPDP) so every feature type and every method lands on the
    same y-unit scale — "a typical instance's effect deviates from the
    mean effect by about this much". `eval_heter` itself stays a variance
    curve in the method's native units.

    !!! tip "Pair it with `importance`"
        `importance` measures the *mean* effect's strength; `heter_score`
        measures the spread around it — same units, mean/spread twins.
        High importance + high heterogeneity = the top-right corner of
        `effector.plot_triage` — where `find_regions` should look.

    Args:
        feature: index or name of the feature of interest.
        mask: optional boolean `(N,)` subregion — model-free.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        a non-negative scalar, in output units.
    """
    feature = self._resolve_feature(feature)
    self._check_feature_type_supported(feature)
    mask = self._resolve_mask(mask, rule)
    return float(self._heter(feature, mask))

payload(feature)

The raw fitted object behind eval/eval_heter — pure numpy, yours to inspect.

p = ale.payload("hr")     # e.g. {"limits": ..., "bin_effect": ..., "bin_variance": ...}

Per method: per-bin effects and variances for (RH)ALE and ShapDP, the grid summaries for (d-)PDP. A copy — mutate freely.

Source code in effector/global_effect.py
743
744
745
746
747
748
749
750
751
752
753
def payload(self, feature: Union[int, str]) -> dict:
    """The raw fitted object behind `eval`/`eval_heter` — pure numpy, yours to inspect.

    ```python
    p = ale.payload("hr")     # e.g. {"limits": ..., "bin_effect": ..., "bin_variance": ...}
    ```

    Per method: per-bin effects and variances for (RH)ALE and ShapDP, the
    grid summaries for (d-)PDP. A copy — mutate freely.
    """
    return dict(self._summary(self._resolve_feature(feature), None))

importance(feature, mask=None, rule=None)

How much a feature's mean effect moves the prediction (R13).

pdp.importance("temp")                           # scalar
pdp.importance("temp", rule="workingday == 1")   # within a subregion

The dispersion of the mean effect in output units — the μ-twin of heter_score (which measures per-instance spread on the same scale). A flat curve scores ~0; a swinging curve scores high. Per method: std of the mean effect over the (masked) data values (PDP/ALE/RHALE/ ShapDP; for a linear model this is |coefficient| * std(x)), mean(|derivative|) * std(x) (DerPDP). Comparable across feature types and, in magnitude, across methods.

No y, ever

effector never sees ground-truth labels — this is a property of the fitted effect, not a loss/permutation importance.

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature of interest.

required
mask Optional[ndarray]

optional boolean (N,) subregion — model-free.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
float

a non-negative scalar.

Source code in effector/global_effect.py
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
def importance(
    self,
    feature: Union[int, str],
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> float:
    """How much a feature's mean effect moves the prediction (R13).

    ```python
    pdp.importance("temp")                           # scalar
    pdp.importance("temp", rule="workingday == 1")   # within a subregion
    ```

    The dispersion of the **mean** effect in output units — the μ-twin of
    `heter_score` (which measures per-instance spread on the same scale).
    A flat curve scores ~0; a swinging curve scores high. Per method: std
    of the mean effect over the (masked) data values (PDP/ALE/RHALE/
    ShapDP; for a linear model this is `|coefficient| * std(x)`),
    `mean(|derivative|) * std(x)` (DerPDP). Comparable across feature
    types and, in magnitude, across methods.

    !!! note "No `y`, ever"
        effector never sees ground-truth labels — this is a property of
        the fitted effect, not a loss/permutation importance.

    Args:
        feature: index or name of the feature of interest.
        mask: optional boolean `(N,)` subregion — model-free.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        a non-negative scalar.
    """
    feature = self._resolve_feature(feature)
    self._check_feature_type_supported(feature)
    mask = self._resolve_mask(mask, rule)
    if mask is None:
        # all-ones ≡ None (M1); the concrete array makes the per-method
        # `_importance` implementations mask-index without a null check
        mask = np.ones(self.data.shape[0], dtype=bool)
    self._ensure_local(feature)
    return float(self._importance(feature, mask))

importances(mask=None, rule=None)

The whole importance vector — rank your features in one call.

imp = pdp.importances()                       # (D,)
order = np.argsort(-np.nan_to_num(imp))       # most important first

NaN means unsupported, not unimportant

Feature types this method cannot explain (e.g. DerPDP on a nominal feature) return NaN, with one UserWarning naming them.

Parameters:

Name Type Description Default
mask Optional[ndarray]

optional boolean (N,) subregion.

None
rule Union[None, str, Rule]

sugar over mask; mutually exclusive with it.

None

Returns:

Type Description
ndarray

the per-feature importance vector, (D,).

Source code in effector/global_effect.py
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
def importances(
    self,
    mask: Optional[np.ndarray] = None,
    rule: Union[None, str, "Rule"] = None,
) -> np.ndarray:
    """The whole importance vector — rank your features in one call.

    ```python
    imp = pdp.importances()                       # (D,)
    order = np.argsort(-np.nan_to_num(imp))       # most important first
    ```

    !!! warning "NaN means unsupported, not unimportant"
        Feature types this method cannot explain (e.g. DerPDP on a
        nominal feature) return `NaN`, with one `UserWarning` naming them.

    Args:
        mask: optional boolean `(N,)` subregion.
        rule: sugar over `mask`; mutually exclusive with it.

    Returns:
        the per-feature importance vector, `(D,)`.
    """
    mask = self._resolve_mask(mask, rule)
    out = np.full(self.dim, np.nan)
    skipped = []
    for f in range(self.dim):
        try:
            out[f] = self.importance(f, mask=mask)
        except ValueError:
            skipped.append(self.feature_names[f])
    if skipped:
        warnings.warn(
            f"importance is undefined for feature(s) {skipped} — this "
            f"method does not support their feature type; returned NaN.",
            UserWarning,
            stacklevel=2,
        )
    return out

find_regions(feature=None, *, features=None, finder='best', candidate_conditioning_features='all')

Search for subregions that resolve a feature's heterogeneity.

part = pdp.find_regions("hr")                       # one feature -> Partition
part.show()                                         # the tree + level stats
pdp.plot("hr", rule=part.leaves[0].rule)            # drill into a leaf

parts = pdp.find_regions(features="heterogeneous")  # several -> {name: Partition}
effector.plot_triage(pdp, partitions=parts)         # the before/after picture

A query, not a mutation (R12)

The result is a value — nothing is stored on the effect. Don't like a partition? Search again with different finder kwargs; nothing needs resetting.

Model-free

Every candidate split is scored by heter_score(feature, mask) on the cached local effects — zero model calls, whatever the grid size. Binning/scope are those the feature was fitted with, replayed.

Parameters:

Name Type Description Default
feature Union[int, str, None]

index or name of the one feature to partition (→ Partition).

None
features Union[list, str, None]

several at once — a list, "all", or "heterogeneous" (heter_score at/above the median, the same convention effector.explain uses) → {feature_name: Partition}. Exactly one of feature/features must be given.

None
finder

"best" (default), "best_level_wise", or a configured finder instance (e.g. effector.space_partitioning.Best(...)).

'best'
candidate_conditioning_features

features allowed to define splits ("all" or a list of indices/names).

'all'

Returns:

Type Description

a Partition bound to this effect — or {feature_name: Partition}

with features=.

Source code in effector/global_effect.py
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
def find_regions(
    self,
    feature: Union[int, str, None] = None,
    *,
    features: Union[list, str, None] = None,
    finder="best",
    candidate_conditioning_features="all",
):
    """Search for subregions that resolve a feature's heterogeneity.

    ```python
    part = pdp.find_regions("hr")                       # one feature -> Partition
    part.show()                                         # the tree + level stats
    pdp.plot("hr", rule=part.leaves[0].rule)            # drill into a leaf

    parts = pdp.find_regions(features="heterogeneous")  # several -> {name: Partition}
    effector.plot_triage(pdp, partitions=parts)         # the before/after picture
    ```

    !!! note "A query, not a mutation (R12)"
        The result is a value — nothing is stored on the effect. Don't
        like a partition? Search again with different finder kwargs;
        nothing needs resetting.

    !!! note "Model-free"
        Every candidate split is scored by `heter_score(feature, mask)`
        on the cached local effects — zero model calls, whatever the grid
        size. Binning/scope are those the feature was fitted with,
        replayed.

    Args:
        feature: index or name of the one feature to partition
            (→ `Partition`).
        features: several at once — a list, `"all"`, or `"heterogeneous"`
            (heter_score at/above the median, the same convention
            `effector.explain` uses) → `{feature_name: Partition}`.
            Exactly one of `feature`/`features` must be given.
        finder: `"best"` (default), `"best_level_wise"`, or a configured
            finder instance (e.g. `effector.space_partitioning.Best(...)`).
        candidate_conditioning_features: features allowed to define splits
            (`"all"` or a list of indices/names).

    Returns:
        a `Partition` bound to this effect — or `{feature_name: Partition}`
        with `features=`.
    """
    if (feature is None) == (features is None):
        raise ValueError(
            "find_regions takes exactly one of `feature` (singular -> "
            "Partition) or `features` (plural -> {name: Partition})"
        )
    if features is not None:
        return self._find_regions_plural(
            features,
            finder=finder,
            candidate_conditioning_features=candidate_conditioning_features,
        )

    from effector import space_partitioning  # lazy: one-way dep guard

    feature = self._resolve_feature(feature)
    if isinstance(candidate_conditioning_features, list):
        candidate_conditioning_features = [
            self._resolve_feature(f) for f in candidate_conditioning_features
        ]
    self._check_feature_type_supported(feature)
    self._ensure_local(feature)

    if isinstance(finder, str):
        finder = space_partitioning.return_default(finder)

    def score_fn(mask):
        return self.heter_score(feature, mask=mask)  # RAW; guard is the finder's

    partition = finder.find_regions(
        feature,
        self.data,
        score_fn,
        axis_limits=self.axis_limits,
        feature_types=self.feature_types,
        cat_limit=self.cat_limit,
        candidate_conditioning_features=candidate_conditioning_features,
        feature_names=self.feature_names,
        target_name=self.target_name,
    )
    return partition.bind(self)

select_regions(partitions=None, *, features='heterogeneous', finder='best', candidate_conditioning_features='all', min_r2_gain=0.01)

Greedily select which partitions earn their complexity — the CALM chain.

chain = pdp.select_regions()      # search + select in one call
chain.show()                      # GAM R2, each accepted split, the rejected
chain.final                       # the last CALM — the regional analysis
chain[0]                          # the GAM snapshot

find_regions proposes one candidate Partition per feature; this verb decides across features which of them actually explain the model: starting from the GAM (all features global), each round applies the split with the largest explained-variance gain — the surrogate R² against , measured on top of the splits already applied — and stops when no remaining split adds at least min_r2_gain. Every accepted round is a snapshot (CALM) of increased complexity; the whole chain is the report's ledger.

A query, not a mutation (R12)

The result is a value — nothing is stored on the effect.

One prediction pass

Beyond fit, the only model touch is one f̂(X) pass for the variance denominator (cached on the effect); the search, the scoring, and every snapshot's summaries are model-free.

Parameters:

Name Type Description Default
partitions Optional[dict]

pre-computed candidates — {feature_index_or_name: Partition}, exactly what find_regions(features=...) returns. None (default) runs the search here first.

None
features Union[list, str]

which features to search when partitions is None — a list, "all", or "heterogeneous" (default; heter_score at/above the median).

'heterogeneous'
finder

region finder, as in find_regions.

'best'
candidate_conditioning_features

features allowed to define splits ("all" or a list of indices/names).

'all'
min_r2_gain float

smallest explained-variance marginal (fraction of Var(f̂), default 0.01 = 1 pt) a split must add — on top of the splits already applied — to earn a snapshot.

0.01

Returns:

Type Description

a CalmSequence[GAM, calm1, ...], R² non-decreasing along

it, with the rejected splits in .skipped

("redundant"/"below_threshold").

Raises:

Type Description
ValueError

the method is derivative-scale (no output-scale surrogate) or Var(f̂) == 0.

Source code in effector/global_effect.py
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
def select_regions(
    self,
    partitions: Optional[dict] = None,
    *,
    features: Union[list, str] = "heterogeneous",
    finder="best",
    candidate_conditioning_features="all",
    min_r2_gain: float = 0.01,
):
    """Greedily select which partitions earn their complexity — the CALM chain.

    ```python
    chain = pdp.select_regions()      # search + select in one call
    chain.show()                      # GAM R2, each accepted split, the rejected
    chain.final                       # the last CALM — the regional analysis
    chain[0]                          # the GAM snapshot
    ```

    `find_regions` proposes one candidate `Partition` per feature;
    this verb decides *across* features which of them actually explain
    the model: starting from the GAM (all features global), each round
    applies the split with the largest explained-variance gain — the
    surrogate R² against `f̂`, measured on top of the splits already
    applied — and stops when no remaining split adds at least
    `min_r2_gain`. Every accepted round is a snapshot (`CALM`) of
    increased complexity; the whole chain is the report's ledger.

    !!! note "A query, not a mutation (R12)"
        The result is a value — nothing is stored on the effect.

    !!! note "One prediction pass"
        Beyond `fit`, the only model touch is one `f̂(X)` pass for the
        variance denominator (cached on the effect); the search, the
        scoring, and every snapshot's summaries are model-free.

    Args:
        partitions: pre-computed candidates — `{feature_index_or_name:
            Partition}`, exactly what `find_regions(features=...)`
            returns. `None` (default) runs the search here first.
        features: which features to search when `partitions` is `None` —
            a list, `"all"`, or `"heterogeneous"` (default; heter_score
            at/above the median).
        finder: region finder, as in `find_regions`.
        candidate_conditioning_features: features allowed to define
            splits (`"all"` or a list of indices/names).
        min_r2_gain: smallest explained-variance marginal (fraction of
            `Var(f̂)`, default 0.01 = 1 pt) a split must add — on top of
            the splits already applied — to earn a snapshot.

    Returns:
        a `CalmSequence` — `[GAM, calm1, ...]`, R² non-decreasing along
        it, with the rejected splits in `.skipped`
        (`"redundant"`/`"below_threshold"`).

    Raises:
        ValueError: the method is derivative-scale (no output-scale
            surrogate) or `Var(f̂) == 0`.
    """
    from effector import explained_variance as _ev  # lazy: one-way dep guard

    if partitions is None:
        partitions = self.find_regions(
            features=features,
            finder=finder,
            candidate_conditioning_features=candidate_conditioning_features,
        )
    parts = {}
    for key, p in partitions.items():
        f = self._resolve_feature(key)
        parts[f] = p if p._effect is not None else p.bind(self)

    supported = []
    for f in range(self.dim):
        try:
            self._check_feature_type_supported(f)
            supported.append(f)
        except ValueError:
            continue
    return _ev.select(self, parts, supported, min_gain=min_r2_gain)

explain(*, y=None, top_k=5, coverage=0.8, heter_threshold=None, min_r2_gain=0.01, finder='best', candidate_conditioning_features='all')

The one-liner on this engine — effector.explain without leaving the session.

pdp = effector.PDP(X, model, schema=schema)
report = pdp.explain()            # same Report as effector.explain

Runs the same pipeline as effector.explain on the already-built engine: features you have fit with custom config keep it (missing ones are computed with the defaults), and every cache the pipeline warms stays on the engine for your follow-up queries.

Parameters:

Name Type Description Default
y Optional[ndarray]

optional ground truth aligned with the original data (or this engine's subsample); when given, the report header states the model's score on the explained subsample.

None
top_k int

hard ceiling on how many features get curve plots.

5
coverage float

stop plotting once the shown features carry this share of the total importance mass (default 0.8).

0.8
heter_threshold Optional[float]

minimum heter_score to enter the region search; None (default) = the median convention.

None
min_r2_gain float

smallest explained-variance marginal a split must add to earn a snapshot in the CALM chain.

0.01
finder

region finder, as in find_regions.

'best'
candidate_conditioning_features

features allowed to define splits.

'all'

Returns:

Type Description

a Report bound to this engine.

Source code in effector/global_effect.py
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
def explain(
    self,
    *,
    y: Optional[np.ndarray] = None,
    top_k: int = 5,
    coverage: float = 0.8,
    heter_threshold: Optional[float] = None,
    min_r2_gain: float = 0.01,
    finder="best",
    candidate_conditioning_features="all",
):
    """The one-liner on *this* engine — `effector.explain` without leaving the session.

    ```python
    pdp = effector.PDP(X, model, schema=schema)
    report = pdp.explain()            # same Report as effector.explain
    ```

    Runs the same pipeline as `effector.explain` on the already-built
    engine: features you have `fit` with custom config keep it (missing
    ones are computed with the defaults), and every cache the pipeline
    warms stays on the engine for your follow-up queries.

    Args:
        y: optional ground truth aligned with the original `data` (or
            this engine's subsample); when given, the report header
            states the model's score on the explained subsample.
        top_k: hard ceiling on how many features get curve plots.
        coverage: stop plotting once the shown features carry this share
            of the total importance mass (default 0.8).
        heter_threshold: minimum `heter_score` to enter the region
            search; `None` (default) = the median convention.
        min_r2_gain: smallest explained-variance marginal a split must
            add to earn a snapshot in the CALM chain.
        finder: region finder, as in `find_regions`.
        candidate_conditioning_features: features allowed to define
            splits.

    Returns:
        a `Report` bound to this engine.
    """
    from effector import report as _report  # lazy: one-way dep guard

    return _report._explain_effect(
        self,
        y=y,
        top_k=top_k,
        coverage=coverage,
        heter_threshold=heter_threshold,
        min_r2_gain=min_r2_gain,
        finder=finder,
        candidate_conditioning_features=candidate_conditioning_features,
    )

plot(feature, heterogeneity='shap_values', centering=True, nof_points=100, scale_x=None, scale_y=None, nof_shap_values=100, show_avg_output=False, y_limits=None, only_shap_values=False, show_plot=True, mask=None, rule=None, feature_label=None)

Plot the SHAP-DP of feature, by default with the SHAP scatter.

sdp.plot("hr")                       # fitted curve + SHAP scatter
sdp.plot("hr", heterogeneity="std")  # curve ± std band

Parameters:

Name Type Description Default
feature Union[int, str]

index or name of the feature to plot.

required
heterogeneity Union[bool, str]

what to draw around the fitted curve:

  • False: the curve only
  • True or "std": ± one std of the SHAP values per bin
  • "shap_values" (default): the SHAP values scattered on top of the curve
'shap_values'
centering Union[bool, str]

False (none), True/"zero_integral" (center around the y axis), or "zero_start" (start at y=0).

True
nof_points int

grid size of the x axis (default 100).

100
scale_x Optional[dict]

None or {"mean": m, "std": s} to undo a standardization of the x axis for display.

None
scale_y Optional[dict]

same, for the y axis.

None
nof_shap_values Union[int, str]

how many SHAP values to scatter (default 100), or "all".

100
show_avg_output bool

draw the model's average output as a horizontal line.

False
y_limits Optional[List]

(low, high) for the y axis; None = automatic.

None
only_shap_values bool

scatter the SHAP values without the fitted curve.

False
show_plot bool

if False, return the figure and axes instead of showing.

True
mask Optional[ndarray]

boolean (N,) selecting a subregion — plot the SHAP-DP within it (the masked SHAP values re-binned from the cached attributions, no model calls), x axis windowed to the subregion's own interval.

None
rule

sugar over mask — an effector.Rule or a rule string, applied to the effect's data. Mutually exclusive with mask.

None
feature_label Optional[str]

display title for the figure (e.g. a regional node's label with its rule); defaults to the feature name. The x-axis always keeps the plain feature name.

None
Source code in effector/global_effect_shap.py
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
def plot(
    self,
    feature: Union[int, str],
    heterogeneity: Union[bool, str] = "shap_values",
    centering: Union[bool, str] = True,
    nof_points: int = 100,
    scale_x: Optional[dict] = None,
    scale_y: Optional[dict] = None,
    nof_shap_values: Union[int, str] = 100,
    show_avg_output: bool = False,
    y_limits: Optional[List] = None,
    only_shap_values: bool = False,
    show_plot: bool = True,
    mask: Optional[np.ndarray] = None,
    rule=None,
    feature_label: Optional[str] = None,
) -> Union[Tuple, None]:
    """Plot the SHAP-DP of `feature`, by default with the SHAP scatter.

    ```python
    sdp.plot("hr")                       # fitted curve + SHAP scatter
    sdp.plot("hr", heterogeneity="std")  # curve ± std band
    ```

    Args:
        feature: index or name of the feature to plot.
        heterogeneity: what to draw around the fitted curve:

            - `False`: the curve only
            - `True` or `"std"`: ± one std of the SHAP values per bin
            - `"shap_values"` (default): the SHAP values scattered on
              top of the curve

        centering: `False` (none), `True`/`"zero_integral"` (center
            around the y axis), or `"zero_start"` (start at `y=0`).
        nof_points: grid size of the x axis (default `100`).
        scale_x: `None` or `{"mean": m, "std": s}` to undo a
            standardization of the x axis for display.
        scale_y: same, for the y axis.
        nof_shap_values: how many SHAP values to scatter (default
            `100`), or `"all"`.
        show_avg_output: draw the model's average output as a
            horizontal line.
        y_limits: `(low, high)` for the y axis; `None` = automatic.
        only_shap_values: scatter the SHAP values without the fitted
            curve.
        show_plot: if `False`, return the figure and axes instead of
            showing.
        mask: boolean `(N,)` selecting a subregion — plot the SHAP-DP
            *within* it (the masked SHAP values re-binned from the
            cached attributions, no model calls), x axis windowed to the
            subregion's own interval.
        rule: sugar over `mask` — an `effector.Rule` or a rule string,
            applied to the effect's data. Mutually exclusive with `mask`.
        feature_label: display title for the figure (e.g. a regional
            node's label with its rule); defaults to the feature name.
            The x-axis always keeps the plain feature name.
    """
    feature = self._resolve_feature(feature)
    heterogeneity = helpers.prep_confidence_interval(heterogeneity)
    centering = helpers.prep_centering(centering)
    scale_x = helpers.resolve_scale(
        scale_x, self.scale_x_list[feature] if self.scale_x_list else None
    )
    scale_y = helpers.resolve_scale(scale_y, self.scale_y)
    mask = self._resolve_mask(mask, rule)
    feature_names = self.feature_names
    # C2: title = feature (or leaf label); method · scope = corner tag
    title = feature_label if feature_label is not None else feature_names[feature]
    tag = f"SHAP-DP · {'regional' if mask is not None else 'global'}"

    if mask is not None and not self._is_cat(feature):
        self._effective_limits(feature, mask)  # degeneracy guard

    # one path for global and masked alike (R14): pick the payload, read it
    params = self._summary(feature, mask)
    norm = (
        self._centering_const(feature, mask, centering)
        if centering is not False
        else 0.0
    )
    avg_output = self._avg_output(mask, scale_y) if show_avg_output else None

    if self._is_cat(feature):
        # the payload's frame: the levels observed within the (masked) data
        levels, labels = self._level_display(feature, params["levels"])
        y_levels = self._eval_payload(feature, params, levels) - norm
        level_kind = self.feature_types[feature]
        level_counts = self._level_counts_for(feature, mask, levels)
        if heterogeneity == "shap_values":
            # the scatter cloud comes from cache (a) — the same (masked)
            # φ the payload was summarized from, by construction
            xx, phi = self._masked_phi(feature, mask)
            yy = phi - norm
            return vis.plot_shap_categorical(
                levels,
                y_levels,
                xx,
                yy,
                feature,
                title=title,
                level_labels=labels,
                scale_x=scale_x,
                scale_y=scale_y,
                avg_output=avg_output,
                feature_names=feature_names,
                target_name=self.target_name,
                nof_shap_values=nof_shap_values,
                y_limits=y_limits,
                show_plot=show_plot,
                random_state=self.random_state,
                tag=tag,
                level_kind=level_kind,
                level_counts=level_counts,
            )
        variances = (
            self._eval_payload(feature, params, levels, heterogeneity=True)[1]
            if heterogeneity is not False
            else None
        )
        return vis.plot_categorical_effect(
            levels,
            y_levels,
            variances,
            feature,
            heterogeneity,
            title=title,
            level_labels=labels,
            scale_x=scale_x,
            scale_y=scale_y,
            avg_output=avg_output,
            feature_names=feature_names,
            target_name=self.target_name,
            y_limits=y_limits,
            show_plot=show_plot,
            tag=tag,
            level_kind=level_kind,
            level_counts=level_counts,
        )

    # continuous: the x-axis spans the (effective) interval; the cloud is
    # the (masked) φ from cache (a) — the same instances the payload was
    # summarized from, model-free
    lo, hi = self._effective_limits(feature, mask)
    x = np.linspace(lo, hi, nof_points)
    y = self._eval_payload(feature, params, x) - norm
    y_std = (
        np.sqrt(self._eval_payload(feature, params, x, heterogeneity=True)[1])
        if heterogeneity == "std"
        else None
    )
    col, phi = self._masked_phi(feature, mask)
    _, ind = helpers.prep_nof_instances(
        nof_shap_values, len(phi), self.random_state
    )
    yy = phi[ind] - norm if heterogeneity == "shap_values" else None
    xx = col[ind] if heterogeneity == "shap_values" else None

    ret = vis.plot_shap(
        x,
        y,
        xx,
        yy,
        y_std,
        feature,
        heterogeneity=heterogeneity,
        scale_x=scale_x,
        scale_y=scale_y,
        avg_output=avg_output,
        feature_names=feature_names,
        target_name=self.target_name,
        y_limits=y_limits,
        only_shap_values=only_shap_values,
        show_plot=show_plot,
        title=title,
        tag=tag,
    )

    return ret