Skip to content

Triage, comparison & theme

Summary

Two free functions stand above the engines. They take fitted effect objects, query their public verbs, and draw — they compute nothing themselves and store nothing.

plot_triage is the survey: importance (x) against heterogeneity (y), one labeled point per feature. Bottom-left is unimportant; bottom-right is important and fully described by its mean effect; top-right — important and heterogeneous — is where find_regions should look. With partitions=effect.find_regions(features="heterogeneous") it becomes the before/after story: an arrow runs from each partitioned feature's global point to each leaf point — leaves of a good partition move right (more decisive) and down (heterogeneity explained).

pdp = effector.PDP(X, model, schema=schema)

effector.plot_triage(pdp)                          # step (b): the survey
parts = pdp.find_regions(features="heterogeneous") # step (d)
effector.plot_triage(pdp, partitions=parts)        # step (e): the arrows

compare is the cross-examination: overlay the mean effect of several fitted engines — different methods, or even different models over the same columns — on one feature, always centered.

effector.compare(pdp, rhale, shapdp, feature="temp")

FeatureEffect is the single-model shortcut with the same look: it ingests once and builds its own engines (same data, subsample, limits, and schema for every method), so the only difference between the overlaid curves is the method itself.

fe = effector.FeatureEffect(X, model, schema=schema)
fe.plot("temp", methods=["PDP", "ALE", "RHALE"])

set_theme switches the house matplotlib style for every figure the package produces:

effector.set_theme("dark")     # "light" | "dark" | "paper" | "default"

API

effector.visualization.plot_triage(effect, partitions=None, threshold=None, features='all', title=None, show_plot=True)

The triage plane of one fitted effect: importance (x) against heterogeneity (y), one labeled point per feature.

effector.plot_triage(pdp)                       # survey every feature
parts = pdp.find_regions(features="heterogeneous")
effector.plot_triage(pdp, partitions=parts)     # before/after arrows

Read it as a to-do list: bottom-left is unimportant and honest, the bottom-right features are important and fully described by their mean effect, and the top-right corner — important and heterogeneous — is where the mean effect hides something and find_regions should look.

The before/after story

With partitions, an arrow runs from every partitioned feature's global point to each of its leaf points (the leaf's importance/heter_score under its rule, computed model-free from the caches). Leaves of a good partition land right and down — more decisive, less heterogeneous.

Parameters:

Name Type Description Default
effect

a fitted effect object (PDP/RHALE/...); its importance and heter_score are queried per feature.

required
partitions Union[None, dict]

optional {feature_name_or_index: Partition} — exactly what effect.find_regions(features=...) returns. Root-only partitions are skipped.

None
threshold Union[None, bool, float]

the heterogeneity threshold line. None (default) draws the median heterogeneity of the plotted features — the same convention effector.explain and find_regions (features="heterogeneous") use; a float draws that value, given in heter_score units (it is rescaled together with the points when the schema declares a scale_y); False draws nothing.

None
features Union[str, list]

which features to plot — "all" or a list of indices/names. Feature types the method does not support are skipped with one UserWarning.

'all'
title Union[None, str]

figure title.

None
show_plot bool

if True, show the figure and return None; if False, return (fig, ax).

True
Source code in effector/visualization.py
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
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
def plot_triage(
    effect,
    partitions: typing.Union[None, dict] = None,
    threshold: typing.Union[None, bool, float] = None,
    features: typing.Union[str, list] = "all",
    title: typing.Union[None, str] = None,
    show_plot: bool = True,
):
    """The triage plane of one fitted effect: importance (x) against heterogeneity (y), one labeled point per feature.

    ```python
    effector.plot_triage(pdp)                       # survey every feature
    parts = pdp.find_regions(features="heterogeneous")
    effector.plot_triage(pdp, partitions=parts)     # before/after arrows
    ```

    Read it as a to-do list: bottom-left is unimportant and honest, the
    bottom-right features are important and fully described by their mean
    effect, and the top-right corner — important *and* heterogeneous — is
    where the mean effect hides something and `find_regions` should look.

    !!! tip "The before/after story"
        With `partitions`, an arrow runs from every partitioned feature's
        global point to each of its leaf points (the leaf's
        `importance`/`heter_score` under its rule, computed model-free from
        the caches). Leaves of a good partition land right and down — more
        decisive, less heterogeneous.

    Args:
        effect: a fitted effect object (`PDP`/`RHALE`/...); its `importance`
            and `heter_score` are queried per feature.
        partitions: optional `{feature_name_or_index: Partition}` — exactly
            what `effect.find_regions(features=...)` returns. Root-only
            partitions are skipped.
        threshold: the heterogeneity threshold line. `None` (default) draws
            the median heterogeneity of the plotted features — the same
            convention `effector.explain` and `find_regions
            (features="heterogeneous")` use; a float draws that value, given
            in `heter_score` units (it is rescaled together with the points
            when the schema declares a `scale_y`); `False` draws nothing.
        features: which features to plot — `"all"` or a list of
            indices/names. Feature types the method does not support are
            skipped with one `UserWarning`.
        title: figure title.
        show_plot: if `True`, show the figure and return `None`; if `False`,
            return `(fig, ax)`.
    """
    if isinstance(features, str):
        if features != "all":
            raise ValueError(
                f"Invalid features argument: {features!r}; use 'all' or a "
                "list of indices/names"
            )
        candidates = list(range(effect.dim))
    else:
        candidates = [effect._resolve_feature(f) for f in features]

    plotted, skipped = [], []
    for f in candidates:
        try:
            effect._check_feature_type_supported(f)
            plotted.append(f)
        except ValueError:
            skipped.append(effect.feature_names[f])
    if skipped:
        warnings.warn(
            f"plot_triage skipped feature(s) {skipped} — this method does "
            f"not support their feature type.",
            UserWarning,
            stacklevel=2,
        )
    if not plotted:
        raise ValueError("plot_triage: no supported features to plot")

    # the verbs speak model-output units; the axes claim the target's units,
    # so both scalars (spread-type quantities: std only, no mean shift) are
    # bridged by the schema's y-std — the same convention the curve plots use
    sy = effect.scale_y["std"] if effect.scale_y is not None else 1.0
    imp = {f: effect.importance(f) * sy for f in plotted}
    het = {f: effect.heter_score(f) * sy for f in plotted}

    if threshold is None:
        threshold = float(np.median([het[f] for f in plotted]))
        thr_label = "heterogeneity threshold (median)"
    else:
        thr_label = "heterogeneity threshold"
        if threshold is not False:
            threshold = float(threshold) * sy

    arrows = []
    if partitions:
        for key, partition in partitions.items():
            f = effect._resolve_feature(key)
            if len(partition) <= 1:
                continue  # root-only: nothing was found
            start = (imp[f], het[f])
            for leaf in partition.leaves:
                arrows.append(
                    (
                        start,
                        (
                            effect.importance(f, rule=leaf.rule) * sy,
                            effect.heter_score(f, rule=leaf.rule) * sy,
                        ),
                    )
                )

    fig, ax = plt.subplots()
    points = [(effect.feature_names[f], imp[f], het[f]) for f in plotted]
    unit = f" ({effect.target_name} units)"
    _draw_triage(fig, ax, points, arrows, threshold, thr_label, unit, title)
    return _finalize(fig, ax, show_plot)

effector.visualization.compare(*effects, feature, labels=None, centering=True, nof_points=100, scale_x=None, scale_y=None, y_limits=None, title=None, show_plot=True)

Overlay the mean effect of several fitted effect objects on one feature.

effector.compare(pdp, ale, rhale, feature="hr")
effector.compare(pdp_a, pdp_b, feature="hr", labels=["model A", "model B"])

The cross-examination verb above the engines: you hold the effect objects (different methods, or even different models over the same columns); compare queries each one's eval on a shared grid and overlays the curves — one theme color per effect. It computes nothing itself and stores nothing.

Always centered

A comparison is only meaningful for centered effects (each method uses a different reference level), so centering=False is coerced to "zero_integral" with a warning.

Derivative units don't mix

DerPDP effects (dy/dx) can only be compared with each other — never on the same axis as the output-unit methods (PDP/ALE/RHALE/ShapDP).

The single-model shortcut with the same look is effector.FeatureEffect(data, model).plot(feature, methods=[...]), which builds its own engines; compare overlays engines you already have.

Parameters:

Name Type Description Default
*effects

two or more fitted effect objects (PDP/ALE/RHALE/ ShapDP/DerPDP) over data with the same columns.

()
feature Union[int, str]

index or name of the feature to compare on.

required
labels Union[None, list]

one legend label per effect; defaults to the method display names, deduped ("PDP", "PDP (2)", ...).

None
centering Union[bool, str]

how to center — True/"zero_integral" (around the y axis) or "zero_start" (each curve starts at y=0).

True
nof_points int

size of the shared grid (continuous features); the grid spans the intersection of the effects' axis limits.

100
scale_x Union[None, dict]

None or a {"mean", "std"} dict for the x axis; defaults to the first effect's schema scaling.

None
scale_y Union[None, dict]

same, for the y axis.

None
y_limits Union[None, tuple]

None or tuple, manual y-axis limits.

None
title Union[None, str]

figure title.

None
show_plot bool

if True, show the figure and return None; if False, return (fig, ax).

True

Raises:

Type Description
ValueError

fewer than two effects; effects disagree on columns, feature resolution, categorical status, or observed level sets; derivative and output units are mixed; or the axis intervals do not overlap.

Source code in effector/visualization.py
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 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
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
def compare(
    *effects,
    feature: typing.Union[int, str],
    labels: typing.Union[None, list] = None,
    centering: typing.Union[bool, str] = True,
    nof_points: int = 100,
    scale_x: typing.Union[None, dict] = None,
    scale_y: typing.Union[None, dict] = None,
    y_limits: typing.Union[None, tuple] = None,
    title: typing.Union[None, str] = None,
    show_plot: bool = True,
):
    """Overlay the mean effect of several *fitted* effect objects on one feature.

    ```python
    effector.compare(pdp, ale, rhale, feature="hr")
    effector.compare(pdp_a, pdp_b, feature="hr", labels=["model A", "model B"])
    ```

    The cross-examination verb above the engines: you hold the effect objects
    (different methods, or even different models over the same columns);
    `compare` queries each one's `eval` on a shared grid and overlays the
    curves — one theme color per effect. It computes nothing itself and
    stores nothing.

    !!! warning "Always centered"
        A comparison is only meaningful for centered effects (each method
        uses a different reference level), so `centering=False` is coerced to
        `"zero_integral"` with a warning.

    !!! note "Derivative units don't mix"
        `DerPDP` effects (dy/dx) can only be compared with each other — never
        on the same axis as the output-unit methods
        (`PDP`/`ALE`/`RHALE`/`ShapDP`).

    The single-model shortcut with the same look is
    `effector.FeatureEffect(data, model).plot(feature, methods=[...])`, which
    builds its own engines; `compare` overlays engines you already have.

    Args:
        *effects: two or more fitted effect objects (`PDP`/`ALE`/`RHALE`/
            `ShapDP`/`DerPDP`) over data with the same columns.
        feature: index or name of the feature to compare on.
        labels: one legend label per effect; defaults to the method display
            names, deduped (`"PDP"`, `"PDP (2)"`, ...).
        centering: how to center — `True`/`"zero_integral"` (around the y
            axis) or `"zero_start"` (each curve starts at `y=0`).
        nof_points: size of the shared grid (continuous features); the grid
            spans the intersection of the effects' axis limits.
        scale_x: `None` or a `{"mean", "std"}` dict for the x axis; defaults
            to the first effect's schema scaling.
        scale_y: same, for the y axis.
        y_limits: `None` or tuple, manual y-axis limits.
        title: figure title.
        show_plot: if `True`, show the figure and return `None`; if `False`,
            return `(fig, ax)`.

    Raises:
        ValueError: fewer than two effects; effects disagree on columns,
            feature resolution, categorical status, or observed level sets;
            derivative and output units are mixed; or the axis intervals do
            not overlap.
    """
    if len(effects) < 2:
        raise ValueError(
            f"compare needs at least two effect objects, got {len(effects)}"
        )

    dims = {e.dim for e in effects}
    if len(dims) != 1:
        raise ValueError(
            f"compare needs effects over data with the same columns; "
            f"got dims {sorted(dims)}"
        )
    idxs = [e._resolve_feature(feature) for e in effects]
    if len(set(idxs)) != 1:
        raise ValueError(
            f"feature {feature!r} resolves to different columns across the "
            f"effects: {idxs}; align the schemas or pass an index"
        )
    f = idxs[0]

    is_cat = {bool(e._is_cat(f)) for e in effects}
    if len(is_cat) != 1:
        raise ValueError(
            f"the effects disagree on whether feature {feature!r} is "
            "categorical; align the schemas"
        )
    discrete = is_cat.pop()

    is_der = [e.method_name == "d-pdp" for e in effects]
    if any(is_der) and not all(is_der):
        raise ValueError(
            "cannot mix derivative-unit effects (DerPDP, dy/dx) with "
            "level-unit effects (PDP/ALE/RHALE/ShapDP) on one axis"
        )

    centering = helpers.prep_centering(centering)
    if centering is False:
        warnings.warn(
            "Comparing methods without centering is not meaningful (each "
            "method uses a different reference level). Using "
            "centering='zero_integral'.",
            stacklevel=2,
        )
        centering = "zero_integral"

    first = effects[0]
    if discrete:
        level_sets = [tuple(np.unique(e.data[:, f]).tolist()) for e in effects]
        if len(set(level_sets)) != 1:
            raise ValueError(
                f"the effects observe different level sets for feature "
                f"{feature!r}: {sorted(set(level_sets))}"
            )
        xs, level_labels = first._level_display(f)
        xs = np.asarray(xs, dtype=float)
    else:
        lo = max(e.axis_limits[0, f] for e in effects)
        hi = min(e.axis_limits[1, f] for e in effects)
        if not lo < hi:
            raise ValueError(
                f"the effects' axis intervals for feature {feature!r} do not "
                f"overlap (intersection [{lo}, {hi}])"
            )
        xs = np.linspace(lo, hi, nof_points)
        level_labels = None

    if labels is None:
        labels = _method_labels(effects)
    elif len(labels) != len(effects):
        raise ValueError(f"got {len(labels)} labels for {len(effects)} effects")

    curves = {
        label: e.eval(f, xs, centering=centering) for label, e in zip(labels, effects)
    }

    scale_x = helpers.resolve_scale(
        scale_x, first.scale_x_list[f] if first.scale_x_list else None
    )
    scale_y = helpers.resolve_scale(scale_y, first.scale_y)

    return plot_effect_comparison(
        xs,
        f,
        curves,
        scale_x=scale_x,
        scale_y=scale_y,
        avg_output=None,  # possibly different models: no shared baseline
        feature_names=first.feature_names,
        target_name="dy/dx" if all(is_der) else first.target_name,
        y_limits=y_limits,
        title=title,
        discrete=discrete,
        level_labels=level_labels,
        show_plot=show_plot,
        level_kind=first.feature_types[f] if discrete else "nominal",
    )

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

Compare global feature-effect methods on a single figure — one model, one object.

fe = effector.FeatureEffect(X, model, schema=schema)
fe.plot(feature=3)                             # PDP vs ALE vs RHALE overlaid
fe.plot(feature=3, methods=["PDP", "ShapDP"])  # ShapDP is opt-in (slow)

Holds the shared ingredients (data, model, metadata, axis limits) once and lazily builds the underlying method objects (PDP, ALE, RHALE, ShapDP) on demand. eval returns each method's mean effect on a shared grid; plot overlays them on one axis.

Same background data for every method

The data is filtered to axis_limits and subsampled to nof_instances once; every method object is built on that identical subset. The only difference between the curves is the method itself.

DerPDP is not in the pool

It lives in derivative units (dy/dx) and cannot share an axis with the output-unit methods.

To overlay effect objects you already hold — different subsets, even different models — use effector.compare instead; FeatureEffect always builds its own.

Parameters:

Name Type Description Default
data

the design matrix — a (N, D) numeric numpy array (start from a DataFrame via effector.from_dataframe).

required
model Callable

the black-box model, a numpy->numpy Callable (N, D) -> (N,) (see effector.adapters for wrappers).

required
model_jac Optional[Callable]

the model Jacobian (N, D) -> (N, D), optional. Used by RHALE; if omitted, RHALE falls back to numerical differentiation (slower and approximate).

None
axis_limits Optional[ndarray]

(2, D) array of per-feature limits, or None to infer from the data.

None
nof_instances Union[int, str]

number of instances shared across all methods.

  • use an int to subsample
  • use "all" to use every instance
10000
schema Optional[Union[Schema, dict]]

input metadata — an effector.Schema or a plain dict with any of its keys (feature_names, feature_types, cat_limit, target_name, scale_x_list, scale_y, category_names)

  • omitted fields are auto-inferred from data (numpy heuristics) or synthesized (["x_0", ...], "y")
  • explicit fields always win over inference
None
random_state Optional[int]

seed for every internal random step (e.g. the shared nof_instances subsampling), inherited by every lazily built method object. Use an int (default: 21) for reproducible output, or None for non-deterministic behavior.

21

Methods:

Name Description
eval

Evaluate the mean effect of several methods on one shared grid.

plot

Overlay the mean effect of several methods for one feature.

Source code in effector/feature_effect.py
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def __init__(
    self,
    data,
    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,
):
    """
    Args:
        data: the design matrix — a `(N, D)` numeric numpy array (start
            from a DataFrame via `effector.from_dataframe`).
        model: the black-box model, a numpy->numpy `Callable`
            `(N, D) -> (N,)` (see `effector.adapters` for wrappers).
        model_jac: the model Jacobian `(N, D) -> (N, D)`, optional. Used by
            `RHALE`; if omitted, `RHALE` falls back to numerical
            differentiation (slower and approximate).
        axis_limits: `(2, D)` array of per-feature limits, or `None` to
            infer from the data.
        nof_instances: number of instances shared across all methods.

            - use an `int` to subsample
            - use `"all"` to use every instance

        schema: input metadata — an `effector.Schema` or a plain `dict`
            with any of its keys (`feature_names`, `feature_types`,
            `cat_limit`, `target_name`, `scale_x_list`, `scale_y`,
            `category_names`)

            - omitted fields are auto-inferred from `data` (numpy
              heuristics) or synthesized (`["x_0", ...]`, `"y"`)
            - explicit fields always win over inference

        random_state: seed for every internal random step (e.g. the shared
            `nof_instances` subsampling), inherited by every lazily built
            method object. Use an `int` (default: `21`) for reproducible
            output, or `None` for non-deterministic behavior.
    """
    self.random_state = random_state

    # the one door for data + metadata (R10)
    ing = ingestion.ingest(data, model, model_jac, schema=schema)
    data = ing.data
    self.model = ing.model
    self.model_jac = ing.model_jac
    self.feature_metadata = ing.meta
    self.dim = data.shape[1]

    # shared preprocessing (helpers.prep_data): filter to axis limits (or
    # infer them), then subsample ONCE so every method sees the exact same
    # background data.
    self.data, _, self.axis_limits, _, _ = helpers.prep_data(
        data, axis_limits, nof_instances, random_state=random_state
    )

    # flat mirrors of the resolved metadata
    self.feature_names = list(ing.meta.feature_names)
    self.feature_types = list(ing.meta.feature_types)
    self.cat_limit = ing.meta.cat_limit
    self.target_name = ing.meta.target_name
    self.scale_x_list = ing.meta.scale_x_list
    self.scale_y = ing.meta.scale_y

    # lazily instantiated method objects, keyed by canonical name
    self._methods: dict = {}

eval(feature, xs, methods=None, centering=True, method_kwargs=None)

Evaluate the mean effect of several methods on one shared grid.

xs = np.linspace(fe.axis_limits[0, 3], fe.axis_limits[1, 3], 100)
curves = fe.eval(feature=3, xs=xs)   # {"PDP": y, "ALE": y, "RHALE": y}

Methods that do not support the feature's type (e.g. RHALE on a nominal feature) are dropped with a warning.

Parameters:

Name Type Description Default
feature int

index of the feature.

required
xs ndarray

the grid to evaluate on, shape (T,). For a categorical feature pass its observed levels.

required
methods Optional[List[str]]

list of method names. Supported: "PDP", "ALE", "RHALE", "ShapDP" (alias "SHAP"). Defaults to ["PDP", "ALE", "RHALE"]ShapDP is opt-in as it is slower and needs the shap package.

None
centering Union[bool, str]

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

True
method_kwargs Optional[dict]

optional {method_name: {**constructor_kwargs}} to customize individual methods.

None

Returns:

Type Description
dict

{display_name: y} with one (T,) mean-effect array per method.

Source code in effector/feature_effect.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def eval(
    self,
    feature: int,
    xs: np.ndarray,
    methods: Optional[List[str]] = None,
    centering: Union[bool, str] = True,
    method_kwargs: Optional[dict] = None,
) -> dict:
    """Evaluate the mean effect of several methods on one shared grid.

    ```python
    xs = np.linspace(fe.axis_limits[0, 3], fe.axis_limits[1, 3], 100)
    curves = fe.eval(feature=3, xs=xs)   # {"PDP": y, "ALE": y, "RHALE": y}
    ```

    Methods that do not support the feature's type (e.g. `RHALE` on a
    nominal feature) are dropped with a warning.

    Args:
        feature: index of the feature.
        xs: the grid to evaluate on, shape `(T,)`. For a categorical
            feature pass its observed levels.
        methods: list of method names. Supported: `"PDP"`, `"ALE"`,
            `"RHALE"`, `"ShapDP"` (alias `"SHAP"`). Defaults to
            `["PDP", "ALE", "RHALE"]` — `ShapDP` is opt-in as it is slower
            and needs the `shap` package.
        centering: `True` / `"zero_integral"` (center around the y axis),
            `"zero_start"` (start each curve at `y=0`), or `False` (raw).
        method_kwargs: optional `{method_name: {**constructor_kwargs}}` to
            customize individual methods.

    Returns:
        `{display_name: y}` with one `(T,)` mean-effect array per method.
    """
    if methods is None:
        methods = ["PDP", "ALE", "RHALE"]
    methods = self._supported_methods(feature, methods)
    centering = helpers.prep_centering(centering)

    curves = {}
    for name in methods:
        mk = method_kwargs.get(name) if method_kwargs else None
        obj = self._get_method(name, mk)
        label = method_registry.resolve(self._canonical(name)).display_name
        curves[label] = obj.eval(feature, xs, centering=centering)
    return curves

plot(feature, methods=None, centering=True, nof_points=100, scale_x=None, scale_y=None, y_limits=None, show_avg_output=False, method_kwargs=None, show_plot=True)

Overlay the mean effect of several methods for one feature.

fe.plot(feature=3)                                  # PDP vs ALE vs RHALE
fe.plot(feature=3, methods=["PDP", "ALE", "ShapDP"])  # add ShapDP (slow)

The grid is shared: a linspace over the feature's axis limits, or the observed levels for a categorical feature.

Always centered

The comparison is only meaningful for centered effects (each method uses a different reference level), so centering=False is coerced to "zero_integral" with a warning.

Parameters:

Name Type Description Default
feature int

index of the feature to plot.

required
methods Optional[List[str]]

list of method names to compare. Supported: "PDP", "ALE", "RHALE", "ShapDP" (alias "SHAP"). Defaults to ["PDP", "ALE", "RHALE"]ShapDP is opt-in as it is slower and needs the shap package.

None
centering Union[bool, str]

how to center the curves

  • True / "zero_integral": center around the y axis
  • "zero_start": start each curve from y=0
True
nof_points int

size of the shared evaluation grid (continuous features).

100
scale_x Optional[dict]

None or a {"mean", "std"} dict to map the x axis back to original units; defaults to the schema's scaling.

None
scale_y Optional[dict]

same, for the y axis.

None
y_limits Optional[List]

None or tuple, manual y-axis limits.

None
show_avg_output bool

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

False
method_kwargs Optional[dict]

optional {method_name: {**constructor_kwargs}} to customize individual methods (e.g. {"ShapDP": {"nof_instances": 300}}).

None
show_plot bool

if True, show the figure and return None; if False, return (fig, ax).

True
Source code in effector/feature_effect.py
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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
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
def plot(
    self,
    feature: int,
    methods: Optional[List[str]] = None,
    centering: Union[bool, str] = True,
    nof_points: int = 100,
    scale_x: Optional[dict] = None,
    scale_y: Optional[dict] = None,
    y_limits: Optional[List] = None,
    show_avg_output: bool = False,
    method_kwargs: Optional[dict] = None,
    show_plot: bool = True,
):
    """Overlay the mean effect of several methods for one feature.

    ```python
    fe.plot(feature=3)                                  # PDP vs ALE vs RHALE
    fe.plot(feature=3, methods=["PDP", "ALE", "ShapDP"])  # add ShapDP (slow)
    ```

    The grid is shared: a linspace over the feature's axis limits, or the
    observed levels for a categorical feature.

    !!! warning "Always centered"
        The comparison is only meaningful for centered effects (each
        method uses a different reference level), so `centering=False` is
        coerced to `"zero_integral"` with a warning.

    Args:
        feature: index of the feature to plot.
        methods: list of method names to compare. Supported: `"PDP"`,
            `"ALE"`, `"RHALE"`, `"ShapDP"` (alias `"SHAP"`). Defaults to
            `["PDP", "ALE", "RHALE"]` — `ShapDP` is opt-in as it is slower
            and needs the `shap` package.
        centering: how to center the curves

            - `True` / `"zero_integral"`: center around the `y` axis
            - `"zero_start"`: start each curve from `y=0`

        nof_points: size of the shared evaluation grid (continuous
            features).
        scale_x: `None` or a `{"mean", "std"}` dict to map the x axis
            back to original units; defaults to the schema's scaling.
        scale_y: same, for the y axis.
        y_limits: `None` or tuple, manual y-axis limits.
        show_avg_output: whether to draw the model's average output as a
            horizontal line.
        method_kwargs: optional `{method_name: {**constructor_kwargs}}` to
            customize individual methods
            (e.g. `{"ShapDP": {"nof_instances": 300}}`).
        show_plot: if `True`, show the figure and return `None`; if
            `False`, return `(fig, ax)`.
    """
    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)
    if centering is False:
        warnings.warn(
            "Comparing methods without centering is not meaningful (each method "
            "uses a different reference level). Using centering='zero_integral'.",
            stacklevel=2,
        )
        centering = "zero_integral"

    # shared grid: observed levels for a categorical feature (evaluated only
    # at levels, R10), else a continuous linspace
    discrete = self._is_cat(feature)
    if discrete:
        xs = self._levels(feature)
    else:
        xs = np.linspace(
            self.axis_limits[0, feature], self.axis_limits[1, feature], nof_points
        )
    curves = self.eval(
        feature,
        xs,
        methods=methods,
        centering=centering,
        method_kwargs=method_kwargs,
    )

    avg_output = (
        helpers.prep_avg_output(self.data, self.model, None, scale_y)
        if show_avg_output
        else None
    )

    level_labels = None
    if discrete:
        name_map = (self.feature_metadata.category_names or {}).get(feature)
        if name_map is not None:
            level_labels = [name_map.get(float(v), f"{v:g}") for v in xs]

    return vis.plot_effect_comparison(
        xs,
        feature,
        curves,
        scale_x=scale_x,
        scale_y=scale_y,
        avg_output=avg_output,
        feature_names=self.feature_names,
        target_name=self.target_name,
        y_limits=y_limits,
        discrete=discrete,
        level_labels=level_labels,
        show_plot=show_plot,
        level_kind=self.feature_types[feature] if discrete else "nominal",
    )

effector.theme.set_theme(name='light')

Activate a house theme for every figure effector draws after this call.

effector.set_theme("dark")
effector.set_theme("default")   # back to stock matplotlib
Name Look
"light" the default: off-white surface, colorblind-safe palette
"dark" the same palette on a dark surface
"paper" pure white, no grid, 300-dpi savefig — print-ready
"default" restore stock matplotlib rcParams

Global by design

The chrome (background, font, grid, spines) lives in matplotlib's global rcParams — matplotlib re-reads those at draw time, so a local rc_context would revert before render. Palette colors and chrome switch together for new figures; existing figures keep their look.

Parameters:

Name Type Description Default
name

one of the names above (default "light").

'light'

Raises:

Type Description
ValueError

unknown theme name.

Source code in effector/theme.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
def set_theme(name="light"):
    """Activate a house theme for every figure effector draws after this call.

    ```python
    effector.set_theme("dark")
    effector.set_theme("default")   # back to stock matplotlib
    ```

    | Name | Look |
    |---|---|
    | `"light"` | the default: off-white surface, colorblind-safe palette |
    | `"dark"` | the same palette on a dark surface |
    | `"paper"` | pure white, no grid, 300-dpi savefig — print-ready |
    | `"default"` | restore stock matplotlib rcParams |

    !!! note "Global by design"
        The chrome (background, font, grid, spines) lives in matplotlib's
        global ``rcParams`` — matplotlib re-reads those at draw time, so a
        local ``rc_context`` would revert before render. Palette colors and
        chrome switch together for *new* figures; existing figures keep their
        look.

    Args:
        name: one of the names above (default `"light"`).

    Raises:
        ValueError: unknown theme name.
    """
    global _ACTIVE
    if name == "default":
        mpl.rcParams.update(mpl.rcParamsDefault)
        _ACTIVE = LIGHT
        return
    if name not in THEMES:
        valid = sorted(THEMES) + ["default"]
        raise ValueError(f"unknown theme {name!r}; choose from {valid}")
    _ACTIVE = THEMES[name]
    apply_theme(_ACTIVE)