Skip to content

Api extras

Models

effector.models

Analytic toy models with exact predict and jacobian.

Each class is a closed-form function of a few features, built to exercise one specific phenomenon (a conditional interaction, a general interaction, a categorical gate). Because the math is known, every effect method's output can be checked against pen-and-paper ground truth — effector.benchmarks pairs these models with data distributions and spells those ground truths out.

model = effector.models.ConditionalInteraction()
y = model.predict(x)        # (N, 3) -> (N,)
dy = model.jacobian(x)      # (N, 3) -> (N, 3)

Classes:

Name Description
ConditionalCategorical
ConditionalInteraction
ConditionalInteraction4Regions
DoubleConditionalInteraction
GeneralInteraction

ConditionalCategorical()

Bases: Base

The categorical-FOI ground-truth model (method_semantics.md tests).

\(f(x) = a_{x_0} + b_{x_0} \, x_1 \, \mathbb{1}_{x_2 > 0}\), with \(x_0 \in \{0, 1, 2\}\), \(a = [0, 1, 3]\), \(b = [1, -1, 0]\).

Every per-level quantity (PDP, ALE transitions, heterogeneity) has a closed form: the per-level PDP is \(a_k + b_k \bar{g}\) and the heterogeneity is \((b_k - \bar{b}_w)^2 \mathrm{Var}(g)\) with \(g_i = x_1^i \mathbb{1}_{x_2^i > 0}\).

Source code in effector/models.py
239
240
241
242
243
244
245
246
247
248
249
250
def __init__(self):
    r"""The categorical-FOI ground-truth model (method_semantics.md tests).

    $f(x) = a_{x_0} + b_{x_0} \, x_1 \, \mathbb{1}_{x_2 > 0}$, with
    $x_0 \in \{0, 1, 2\}$, $a = [0, 1, 3]$, $b = [1, -1, 0]$.

    Every per-level quantity (PDP, ALE transitions, heterogeneity) has a
    closed form: the per-level PDP is $a_k + b_k \bar{g}$ and the
    heterogeneity is $(b_k - \bar{b}_w)^2 \mathrm{Var}(g)$ with
    $g_i = x_1^i \mathbb{1}_{x_2^i > 0}$.
    """
    super().__init__(name=self.__class__.__name__)

ConditionalInteraction()

Bases: Base

The canonical conditional interaction: the sign of \(x_2\) flips the effect of \(x_1\).

\(f(x_1, x_2, x_3) = -x_1^2\mathbb{1}_{x_2 < 0} + x_1^2\mathbb{1}_{x_2 \geq 0} + e^{x_3}\)

The global effect of \(x_1\) averages out to zero while the regional effects (\(\pm x_1^2\)) are strong — the textbook case for find_regions, with one optimal split (\(x_2\) at 0).

Methods:

Name Description
jacobian

Calculate the Jacobian of the model.

predict

Predict.

Source code in effector/models.py
34
35
36
37
38
39
40
41
42
43
def __init__(self):
    r"""The canonical conditional interaction: the sign of $x_2$ flips the effect of $x_1$.

    $f(x_1, x_2, x_3) = -x_1^2\mathbb{1}_{x_2 < 0} + x_1^2\mathbb{1}_{x_2 \geq 0} + e^{x_3}$

    The global effect of $x_1$ averages out to zero while the regional
    effects ($\pm x_1^2$) are strong — the textbook case for
    `find_regions`, with one optimal split ($x_2$ at 0).
    """
    super().__init__(name=self.__class__.__name__)
jacobian(x)

Calculate the Jacobian of the model.

Parameters:

Name Type Description Default
x ndarray

Input data, shape (N, 3)

required

Returns:

Type Description
ndarray

Jacobian of the model, shape (N, 3)

Source code in effector/models.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def jacobian(self, x: np.ndarray) -> np.ndarray:
    """Calculate the Jacobian of the model.

    Args:
        x: Input data, shape (N, 3)

    Returns:
        Jacobian of the model, shape (N, 3)
    """
    y = np.zeros_like(x)
    y[:, 2] = np.exp(x[:, 2])
    ind = x[:, 1] < 0
    y[ind, 0] = -2 * x[ind, 0]
    y[~ind, 0] = 2 * x[~ind, 0]
    return y
predict(x)

Predict.

Parameters:

Name Type Description Default
x ndarray

Input data, shape (N, 3)

required

Returns:

Type Description
ndarray

Output of the model, shape (N,)

Source code in effector/models.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def predict(self, x: np.ndarray) -> np.ndarray:
    """Predict.

    Args:
        x: Input data, shape (N, 3)

    Returns:
        Output of the model, shape (N,)
    """
    y = np.exp(x[:, 2])
    ind = x[:, 1] < 0
    y[ind] += -(x[ind, 0] ** 2)
    y[~ind] += x[~ind, 0] ** 2
    return y

ConditionalInteraction4Regions()

Bases: Base

Two gates on the effect of \(x_1\): \(x_3\) flips its sign, \(x_2\) picks its power.

\(f(x_1, x_2, x_3, x_4) = \begin{cases} -x_1^2 + e^{x_4}, & \text{if } x_2 < 0 \text{ and } x_3 < 0 \\ x_1^2 + e^{x_4}, & \text{if } x_2 < 0 \text{ and } x_3 \geq 0 \\ -x_1^4 + e^{x_4}, & \text{if } x_2 \geq 0 \text{ and } x_3 < 0 \\ x_1^4 + e^{x_4}, & \text{if } x_2 \geq 0 \text{ and } x_3 \geq 0 \end{cases}\)

Four leaf regions with deterministic effects; the optimal split order is dictated by the math (first \(x_3\), the sign gate, then \(x_2\), the power gate) — see benchmarks.ConditionalInteraction4RegionsUniform.

Methods:

Name Description
jacobian

Calculate the Jacobian of the model.

predict

Predict.

Source code in effector/models.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def __init__(self):
    r"""Two gates on the effect of $x_1$: $x_3$ flips its sign, $x_2$ picks its power.

    $f(x_1, x_2, x_3, x_4) =
    \begin{cases}
    -x_1^2 + e^{x_4}, & \text{if } x_2 < 0 \text{ and } x_3 < 0 \\
    x_1^2 + e^{x_4}, & \text{if } x_2 < 0 \text{ and } x_3 \geq 0 \\
    -x_1^4 + e^{x_4}, & \text{if } x_2 \geq 0 \text{ and } x_3 < 0 \\
    x_1^4 + e^{x_4}, & \text{if } x_2 \geq 0 \text{ and } x_3 \geq 0
    \end{cases}$

    Four leaf regions with deterministic effects; the optimal split order
    is dictated by the math (first $x_3$, the sign gate, then $x_2$, the
    power gate) — see `benchmarks.ConditionalInteraction4RegionsUniform`.
    """
    super().__init__(name=self.__class__.__name__)
jacobian(x)

Calculate the Jacobian of the model.

Parameters:

Name Type Description Default
x ndarray

Input data, shape (N, 4)

required

Returns:

Type Description
ndarray

Jacobian of the model, shape (N, 4)

Source code in effector/models.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def jacobian(self, x: np.ndarray) -> np.ndarray:
    """Calculate the Jacobian of the model.

    Args:
        x: Input data, shape (N, 4)

    Returns:
        Jacobian of the model, shape (N, 4)
    """
    y = np.zeros(x.shape)

    y[:, 3] = np.exp(x[:, 3])

    mask1 = (x[:, 1] < 0) & (x[:, 2] < 0)
    mask2 = (x[:, 1] < 0) & (x[:, 2] >= 0)
    mask3 = (x[:, 1] >= 0) & (x[:, 2] < 0)
    mask4 = (x[:, 1] >= 0) & (x[:, 2] >= 0)

    y[mask1, 0] = -2 * x[mask1, 0]
    y[mask2, 0] = 2 * x[mask2, 0]
    y[mask3, 0] = -4 * x[mask3, 0] ** 3
    y[mask4, 0] = 4 * x[mask4, 0] ** 3

    return y
predict(x)

Predict.

Parameters:

Name Type Description Default
x ndarray

Input data, shape (N, 4)

required

Returns:

Type Description
ndarray

Output of the model, shape (N,)

Source code in effector/models.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def predict(self, x: np.ndarray) -> np.ndarray:
    """Predict.

    Args:
        x: Input data, shape (N, 4)

    Returns:
        Output of the model, shape (N,)
    """
    y = np.exp(x[:, 3])

    mask1 = (x[:, 1] < 0) & (x[:, 2] < 0)
    mask2 = (x[:, 1] < 0) & (x[:, 2] >= 0)
    mask3 = (x[:, 1] >= 0) & (x[:, 2] < 0)
    mask4 = (x[:, 1] >= 0) & (x[:, 2] >= 0)

    y[mask1] += -(x[mask1, 0] ** 2)
    y[mask2] += x[mask2, 0] ** 2
    y[mask3] += -(x[mask3, 0] ** 4)
    y[mask4] += x[mask4, 0] ** 4

    return y

DoubleConditionalInteraction()

Bases: Base

A two-level conditional interaction: the signs of \(x_2\) and \(x_3\) jointly gate the effect of \(x_1\).

\(f(x_1, x_2, x_3) = -3x_1^2\mathbb{1}_{x_2 < 0}\mathbb{1}_{x_3 < 0} +x_1^2\mathbb{1}_{x_2 < 0}\mathbb{1}_{x_3 \geq 0} -e^{x_1}\mathbb{1}_{x_2 \geq 0}\mathbb{1}_{x_3 < 0} +e^{3x_1}\mathbb{1}_{x_2 \geq 0}\mathbb{1}_{x_3 \geq 0}\)

Four quadrants, four different effects of \(x_1\) — resolving the heterogeneity requires a two-level partition.

Methods:

Name Description
jacobian

Calculate the Jacobian of the model.

predict

Predict.

Source code in effector/models.py
78
79
80
81
82
83
84
85
86
87
88
89
def __init__(self):
    r"""A two-level conditional interaction: the signs of $x_2$ and $x_3$ jointly gate the effect of $x_1$.

    $f(x_1, x_2, x_3) = -3x_1^2\mathbb{1}_{x_2 < 0}\mathbb{1}_{x_3 < 0}
                        +x_1^2\mathbb{1}_{x_2 < 0}\mathbb{1}_{x_3 \geq 0}
                        -e^{x_1}\mathbb{1}_{x_2 \geq 0}\mathbb{1}_{x_3 < 0}
                        +e^{3x_1}\mathbb{1}_{x_2 \geq 0}\mathbb{1}_{x_3 \geq 0}$

    Four quadrants, four different effects of $x_1$ — resolving the
    heterogeneity requires a two-level partition.
    """
    super().__init__(name=self.__class__.__name__)
jacobian(x)

Calculate the Jacobian of the model.

Parameters:

Name Type Description Default
x ndarray

Input data, shape (N, 3)

required

Returns:

Type Description
ndarray

Jacobian of the model, shape (N, 3)

Source code in effector/models.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def jacobian(self, x: np.ndarray) -> np.ndarray:
    """Calculate the Jacobian of the model.

    Args:
        x: Input data, shape (N, 3)

    Returns:
        Jacobian of the model, shape (N, 3)
    """
    y = np.zeros_like(x)
    ind1 = x[:, 1] < 0
    ind2 = x[:, 2] < 0
    y[ind1 & ind2, 0] = -2 * 3 * x[ind1 & ind2, 0]
    y[ind1 & ~ind2, 0] = 2 * x[ind1 & ~ind2, 0]
    y[~ind1 & ind2, 0] = -np.exp(x[~ind1 & ind2, 0])
    y[~ind1 & ~ind2, 0] = 3 * np.exp(3 * x[~ind1 & ~ind2, 0])
    return y
predict(x)

Predict.

Parameters:

Name Type Description Default
x ndarray

Input data, shape (N, 3)

required

Returns:

Type Description
ndarray

Output of the model, shape (N,)

Source code in effector/models.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def predict(self, x: np.ndarray) -> np.ndarray:
    """Predict.

    Args:
        x: Input data, shape (N, 3)

    Returns:
        Output of the model, shape (N,)
    """
    y = np.zeros(x.shape[0])
    ind1 = x[:, 1] < 0
    ind2 = x[:, 2] < 0
    y[ind1 & ind2] = -3 * x[ind1 & ind2, 0] ** 2
    y[ind1 & ~ind2] = x[ind1 & ~ind2, 0] ** 2
    y[~ind1 & ind2] = -np.exp(x[~ind1 & ind2, 0])
    y[~ind1 & ~ind2] = np.exp(3 * x[~ind1 & ~ind2, 0])
    return y

GeneralInteraction()

Bases: Base

A smooth (non-conditional) interaction: \(x_1\) and \(x_2\) interact multiplicatively.

\(f(x_1, x_2, x_3) = x_1 x_2^2 + e^{x_3}\)

Under a zero-mean \(x_1\), the interaction is invisible in the mean effect of \(x_2\) but shows up in its heterogeneity — no crisp region split resolves it (contrast with ConditionalInteraction).

Methods:

Name Description
jacobian

Calculate the Jacobian of the model.

predict

Predict.

Source code in effector/models.py
196
197
198
199
200
201
202
203
204
205
def __init__(self):
    r"""A smooth (non-conditional) interaction: $x_1$ and $x_2$ interact multiplicatively.

    $f(x_1, x_2, x_3) = x_1 x_2^2 + e^{x_3}$

    Under a zero-mean $x_1$, the interaction is invisible in the mean
    effect of $x_2$ but shows up in its heterogeneity — no crisp region
    split resolves it (contrast with `ConditionalInteraction`).
    """
    super().__init__(name=self.__class__.__name__)
jacobian(x)

Calculate the Jacobian of the model.

Parameters:

Name Type Description Default
x ndarray

Input data, shape (N, 3)

required

Returns:

Type Description
ndarray

Jacobian of the model, shape (N, 3)

Source code in effector/models.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def jacobian(self, x: np.ndarray) -> np.ndarray:
    """Calculate the Jacobian of the model.

    Args:
        x: Input data, shape (N, 3)

    Returns:
        Jacobian of the model, shape (N, 3)
    """
    y = np.zeros_like(x)
    y[:, 0] = x[:, 1] ** 2
    y[:, 1] = 2 * x[:, 0] * x[:, 1]
    y[:, 2] = np.exp(x[:, 2])
    return y
predict(x)

Predict.

Parameters:

Name Type Description Default
x ndarray

Input data, shape (N, 3)

required

Returns:

Type Description
ndarray

Output of the model, shape (N,)

Source code in effector/models.py
207
208
209
210
211
212
213
214
215
216
217
def predict(self, x: np.ndarray) -> np.ndarray:
    """Predict.

    Args:
        x: Input data, shape (N, 3)

    Returns:
        Output of the model, shape (N,)
    """
    y = x[:, 0] * x[:, 1] ** 2 + np.exp(x[:, 2])
    return y

Datasets

effector.datasets

Data generators and real datasets used in the examples and tests.

Synthetic generators (IndependentUniform) produce numpy arrays with known distributions — pair them with effector.models for ground-truth checks. Real datasets (BikeSharing, MedicalCosts, AirfoilSelfNoise, AdultIncome) fetch, split, and optionally standardize a public dataset into ready-to-use x_train / y_train / x_test / y_test numpy arrays. Datasets with categorical columns also expose feature_types and category_names, ready to feed effector.Schema.

Classes:

Name Description
AdultIncome

The Adult (census income) dataset (UCI id 2) — a classification

AirfoilSelfNoise

The NASA Airfoil Self-Noise dataset (UCI id 291).

Base
BikeSharing

The UCI Bike Sharing dataset (hourly) — effector's canonical real example.

IndependentUniform

dim independent features, each uniform on [low, high].

MedicalCosts

The Medical Cost Personal dataset — annual medical charges billed by

RealDatasetBase

Shared plumbing for real datasets: fetch, seeded train/test split,

AdultIncome(pcg_train=0.8, standardize=False, seed=21)

Bases: RealDatasetBase

The Adult (census income) dataset (UCI id 2) — a classification example: explain predict_proba of the positive class.

45,222 census records after dropping rows with missing values; the target is binary — whether yearly income exceeds $50K. 12 features remain after dropping fnlwgt (a sampling weight) and education (duplicated by education-num). Categorical columns are encoded to integer codes with the level names recorded in category_names; levels rarer than 50 rows are bucketed into "Other" (a level that rare can vanish from a train split, invalidating the schema). Kept in natural units by default (standardize=False); the 0/1 target is never a regression target — pair it with a classifier and explain the predicted probability.

data = effector.datasets.AdultIncome()
data.x_train, data.y_train    # (36177, 12), (36177,) with y in {0, 1}

Fetch and prepare the dataset.

Parameters:

Name Type Description Default
pcg_train

Fraction of samples in the train split.

0.8
standardize

Standardize the features (default False — natural units keep the partition rules readable). The 0/1 target is standardized too when True; leave False for classification.

False
seed int

Random seed of the train/test shuffle.

21
Source code in effector/datasets.py
370
371
372
373
374
375
376
377
378
379
380
381
382
def __init__(self, pcg_train=0.8, standardize=False, seed: int = 21):
    """Fetch and prepare the dataset.

    Args:
        pcg_train: Fraction of samples in the train split.
        standardize: Standardize the features (default False — natural
            units keep the partition rules readable). The 0/1 target is
            standardized too when True; leave False for classification.
        seed: Random seed of the train/test shuffle.
    """
    super().__init__(
        name="AdultIncome", pcg_train=pcg_train, standardize=standardize, seed=seed
    )

AirfoilSelfNoise(pcg_train=0.8, standardize=False, seed=21)

Bases: RealDatasetBase

The NASA Airfoil Self-Noise dataset (UCI id 291).

1,503 wind-tunnel measurements of NACA 0012 airfoil sections; the target is the scaled sound pressure level in dB. Five continuous features: frequency (Hz), angle of attack (deg), chord length (m), free-stream velocity (m/s), suction-side displacement thickness (m). Kept in natural units by default (standardize=False).

data = effector.datasets.AirfoilSelfNoise()
data.x_train, data.y_train    # (1202, 5), (1202,)

Fetch and prepare the dataset.

Parameters:

Name Type Description Default
pcg_train

Fraction of samples in the train split.

0.8
standardize

Standardize features and target (default False — natural units keep the partition rules readable).

False
seed int

Random seed of the train/test shuffle.

21
Source code in effector/datasets.py
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
def __init__(self, pcg_train=0.8, standardize=False, seed: int = 21):
    """Fetch and prepare the dataset.

    Args:
        pcg_train: Fraction of samples in the train split.
        standardize: Standardize features and target (default False —
            natural units keep the partition rules readable).
        seed: Random seed of the train/test shuffle.
    """
    super().__init__(
        name="AirfoilSelfNoise",
        pcg_train=pcg_train,
        standardize=standardize,
        seed=seed,
    )

Base(name, dim, axis_limits)

Methods:

Name Description
generate_data

Generate n samples.

Source code in effector/datasets.py
18
19
20
21
def __init__(self, name: str, dim: int, axis_limits: np.ndarray):
    self.name = helpers.camel_to_snake(name)
    self.dim = dim
    self.axis_limits = axis_limits
generate_data(n, seed=21)

Generate n samples.

Parameters:

Name Type Description Default
n int

Number of samples.

required
seed int

Random seed, for reproducibility.

21

Returns:

Type Description
ndarray

The samples, shape (n, dim).

Source code in effector/datasets.py
23
24
25
26
27
28
29
30
31
32
33
def generate_data(self, n: int, seed: int = 21) -> np.ndarray:
    """Generate `n` samples.

    Args:
        n: Number of samples.
        seed: Random seed, for reproducibility.

    Returns:
        The samples, shape `(n, dim)`.
    """
    raise NotImplementedError

BikeSharing(pcg_train=0.8, standardize=True, seed=21)

Bases: RealDatasetBase

The UCI Bike Sharing dataset (hourly) — effector's canonical real example.

17,379 hourly records of the Capital Bikeshare system; the target is the hourly rental count. Fetched via ucimlrepo (UCI id 275), with dteday and atemp dropped — 11 features remain (season, yr, mnth, hr, holiday, weekday, workingday, weathersit, temp, hum, windspeed). Data is split into train/test with a seeded shuffle and (by default) standardized; the UCI normalization constants of temp/hum/windspeed are folded into the stored mu/std, so scale_x/scale_y map plots back to natural units.

data = effector.datasets.BikeSharing()
data.x_train, data.y_train    # (13903, 11), (13903,)
data.feature_names, data.target_name

Fetch and prepare the dataset.

Parameters:

Name Type Description Default
pcg_train

Fraction of samples in the train split.

0.8
standardize

Standardize features and target to zero mean, unit variance (the mu/std used are stored on the object).

True
seed int

Random seed of the train/test shuffle.

21
Source code in effector/datasets.py
171
172
173
174
175
176
177
178
179
180
181
182
def __init__(self, pcg_train=0.8, standardize=True, seed: int = 21):
    """Fetch and prepare the dataset.

    Args:
        pcg_train: Fraction of samples in the train split.
        standardize: Standardize features and target to zero mean, unit
            variance (the mu/std used are stored on the object).
        seed: Random seed of the train/test shuffle.
    """
    super().__init__(
        name="BikeSharing", pcg_train=pcg_train, standardize=standardize, seed=seed
    )

IndependentUniform(dim=2, low=0, high=1)

Bases: Base

dim independent features, each uniform on [low, high].

The simplest possible distribution — no correlations, flat marginals — so any structure in an effect plot comes from the model alone. The default data source of the synthetic benchmarks.

Initialize the generator.

Parameters:

Name Type Description Default
dim int

Number of features.

2
low float

Lower bound of every feature.

0
high float

Upper bound of every feature.

1

Methods:

Name Description
generate_data

Generate n samples.

Source code in effector/datasets.py
44
45
46
47
48
49
50
51
52
53
def __init__(self, dim: int = 2, low: float = 0, high: float = 1):
    """Initialize the generator.

    Args:
        dim: Number of features.
        low: Lower bound of every feature.
        high: Upper bound of every feature.
    """
    axis_limits = np.array([[low, high] for _ in range(dim)]).T
    super().__init__(name=self.__class__.__name__, dim=dim, axis_limits=axis_limits)
generate_data(n, seed=21)

Generate n samples.

Parameters:

Name Type Description Default
n int

Number of samples.

required
seed int

Random seed, for reproducibility.

21

Returns:

Type Description
ndarray

The samples, shape (n, dim).

Source code in effector/datasets.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def generate_data(self, n: int, seed: int = 21) -> np.ndarray:
    """Generate `n` samples.

    Args:
        n: Number of samples.
        seed: Random seed, for reproducibility.

    Returns:
        The samples, shape `(n, dim)`.
    """
    np.random.seed(seed)
    x = np.random.uniform(
        self.axis_limits[0, :], self.axis_limits[1, :], (n, self.dim)
    )
    np.random.shuffle(x)
    return x

MedicalCosts(pcg_train=0.8, standardize=False, seed=21)

Bases: RealDatasetBase

The Medical Cost Personal dataset — annual medical charges billed by an insurer, the textbook smoker × bmi interaction.

1,338 policyholders with 6 features (age, sex, bmi, children, smoker, region); the target is the individual's yearly medical charges in USD. Fetched from the dataset's canonical mirror (stedy/Machine-Learning-with-R-datasets). Categorical columns are encoded to integer codes; feature_types and category_names are populated for effector.Schema. Kept in natural units by default (standardize=False) so partition rules read directly (e.g. bmi < 30).

data = effector.datasets.MedicalCosts()
data.x_train, data.y_train        # (1070, 6), (1070,)
schema = effector.Schema(
    feature_names=data.feature_names,
    feature_types=data.feature_types,
    category_names=data.category_names,
    target_name=data.target_name,
)

Fetch and prepare the dataset.

Parameters:

Name Type Description Default
pcg_train

Fraction of samples in the train split.

0.8
standardize

Standardize features and target (default False — natural units keep the partition rules readable).

False
seed int

Random seed of the train/test shuffle.

21
Source code in effector/datasets.py
246
247
248
249
250
251
252
253
254
255
256
257
def __init__(self, pcg_train=0.8, standardize=False, seed: int = 21):
    """Fetch and prepare the dataset.

    Args:
        pcg_train: Fraction of samples in the train split.
        standardize: Standardize features and target (default False —
            natural units keep the partition rules readable).
        seed: Random seed of the train/test shuffle.
    """
    super().__init__(
        name="MedicalCosts", pcg_train=pcg_train, standardize=standardize, seed=seed
    )

RealDatasetBase(name, pcg_train, standardize, seed=21)

Shared plumbing for real datasets: fetch, seeded train/test split, optional standardization (storing the per-feature mu/std for mapping plots back to natural units).

Source code in effector/datasets.py
 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
113
114
115
116
117
118
119
def __init__(self, name: str, pcg_train, standardize, seed: int = 21):
    self.name = helpers.camel_to_snake(name)

    self.dataset: np.ndarray = None

    self.feature_names = None
    self.target_name = None

    # train set
    self.x_train: np.ndarray = None
    self.y_train: np.ndarray = None
    self.x_train_mu = None
    self.x_train_std = None
    self.y_train_mu = None
    self.y_train_std = None

    # test set
    self.x_test: np.ndarray = None
    self.y_test: np.ndarray = None
    self.x_test_mu = None
    self.x_test_std = None
    self.y_test_mu = None
    self.y_test_std = None

    # main logic
    self.fetch_and_preprocess()

    self.x_train, self.x_test, self.y_train, self.y_test = self.split(
        self.dataset[:, :-1], self.dataset[:, -1], pcg_train, seed
    )

    if standardize:
        self.x_train, self.x_train_mu, self.x_train_std = self.standardize(
            self.x_train
        )
        self.x_test, self.x_test_mu, self.x_test_std = self.standardize(self.x_test)
        self.y_train, self.y_train_mu, self.y_train_std = self.standardize(
            self.y_train
        )
        self.y_test, self.y_test_mu, self.y_test_std = self.standardize(self.y_test)

    self.postprocess()

Benchmarks

effector.benchmarks

Synthetic (model, distribution) pairs with closed-form effect ground truths.

A feature-effect ground truth is a property of the pair (prediction function, data distribution) — never of the model alone: the same model under a different distribution has different PDP/ALE/SHAP curves. Each class below bundles one such pair and exposes its closed-form effects, so the class name states the exact scope of validity of every *_gt method.

The derivations live in notebooks/synthetic-examples/ (05, 06, 07) and, for CorrelatedInteraction, in the RHALE paper (https://arxiv.org/abs/2309.11193); the notebooks and tests/test_functional_*.py both consume the functions defined here, so the two can never disagree about the right answer.

All *_gt(feature, xs) methods return the effect centered to zero integral over the feature's axis range (matching eval(..., centering=True)), unless stated otherwise in their docstring.

Classes:

Name Description
ConditionalInteraction4RegionsUniform

models.ConditionalInteraction4Regions under iid U[-1, 1]^4.

ConditionalInteractionUniform

models.ConditionalInteraction under iid U[-1, 1]^3.

CorrelatedInteraction

The Gkolemis et al. 2023 (RHALE paper) example — correlated features.

GeneralInteractionUniform

models.GeneralInteraction under iid U[-1, 1]^3.

ConditionalInteraction4RegionsUniform()

models.ConditionalInteraction4Regions under iid U[-1, 1]^4.

\(f(x) = \pm x_1^2 / \pm x_1^4\) gated by the signs of \(x_2, x_3\), plus \(e^{x_4}\).

Derivations: notebooks/synthetic-examples/07_conditional_interaction_4_regions_*.ipynb

Methods:

Name Description
regional_effect_gt

Centered effect of x1 inside each of the 4 leaf regions.

Source code in effector/benchmarks.py
200
201
202
203
def __init__(self):
    self.model = models.ConditionalInteraction4Regions()
    self.dataset = datasets.IndependentUniform(dim=self.dim, low=-1, high=1)
    self.axis_limits = self.dataset.axis_limits
regional_effect_gt(x2_side, x3_side, xs)

Centered effect of x1 inside each of the 4 leaf regions.

Source code in effector/benchmarks.py
249
250
251
252
253
254
255
256
257
258
259
def regional_effect_gt(
    self, x2_side: str, x3_side: str, xs: np.ndarray
) -> np.ndarray:
    """Centered effect of x1 inside each of the 4 leaf regions."""
    sign = -1.0 if x3_side == "left" else 1.0
    power = 2 if x2_side == "left" else 4

    def ff(x):
        return sign * x**power

    return ff(xs) - _center(ff, -1, 1)

ConditionalInteractionUniform()

models.ConditionalInteraction under iid U[-1, 1]^3.

\(f(x) = -x_1^2 1_{x_2 < 0} + x_1^2 1_{x_2 \geq 0} + e^{x_3}\)

Derivations: notebooks/synthetic-examples/05_conditional_interaction_*.ipynb

Methods:

Name Description
ale_bin_variance_gt

Per-bin variance of the ALE local effects, Fixed(nof_bins) on [-1, 1].

pdp_heter_gt

Variance of the centered ICE curves at each grid point.

regional_effect_gt

Centered effect of x1 inside each region of the optimal split.

rhale_bin_variance_gt

Per-bin variance of the derivative effects, Fixed(nof_bins) on [-1, 1].

Source code in effector/benchmarks.py
39
40
41
42
def __init__(self):
    self.model = models.ConditionalInteraction()
    self.dataset = datasets.IndependentUniform(dim=self.dim, low=-1, high=1)
    self.axis_limits = self.dataset.axis_limits
ale_bin_variance_gt(feature, nof_bins=31)

Per-bin variance of the ALE local effects, Fixed(nof_bins) on [-1, 1].

The central bin of feature 1 (the one containing the jump at 0) is NaN: its variance is an artifact of the discontinuity, not a ground truth.

Source code in effector/benchmarks.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def ale_bin_variance_gt(self, feature: int, nof_bins: int = 31) -> np.ndarray:
    """Per-bin variance of the ALE local effects, Fixed(nof_bins) on [-1, 1].

    The central bin of feature 1 (the one containing the jump at 0) is NaN:
    its variance is an artifact of the discontinuity, not a ground truth.
    """
    bin_centers = np.linspace(-1 + 1 / nof_bins, 1 - 1 / nof_bins, nof_bins)
    if feature == 0:
        return 4 * bin_centers**2
    elif feature == 1:
        y = np.zeros_like(bin_centers)
        y[nof_bins // 2] = np.nan
        return y
    elif feature == 2:
        return np.zeros_like(bin_centers)
pdp_heter_gt(feature, xs)

Variance of the centered ICE curves at each grid point.

Source code in effector/benchmarks.py
78
79
80
81
82
83
84
85
def pdp_heter_gt(self, feature: int, xs: np.ndarray) -> np.ndarray:
    """Variance of the centered ICE curves at each grid point."""
    if feature == 0:
        return xs**4 - 2 / 3 * xs**2 + 1 / 9  # = (x^2 - 1/3)^2
    elif feature == 1:
        return np.full_like(xs, 4 / 45)  # ~ 0.089
    elif feature == 2:
        return np.zeros_like(xs)
regional_effect_gt(side, xs)

Centered effect of x1 inside each region of the optimal split.

side: "left" (x2 < 0) -> -x1^2, "right" (x2 >= 0) -> +x1^2. Per-region heterogeneity is 0: the effect is deterministic within a region.

Source code in effector/benchmarks.py
120
121
122
123
124
125
126
127
128
129
130
131
def regional_effect_gt(self, side: str, xs: np.ndarray) -> np.ndarray:
    """Centered effect of x1 inside each region of the optimal split.

    side: "left" (x2 < 0) -> -x1^2, "right" (x2 >= 0) -> +x1^2.
    Per-region heterogeneity is 0: the effect is deterministic within a region.
    """
    sign = -1.0 if side == "left" else 1.0

    def ff(x):
        return sign * x**2

    return ff(xs) - _center(ff, -1, 1)
rhale_bin_variance_gt(feature, nof_bins=31)

Per-bin variance of the derivative effects, Fixed(nof_bins) on [-1, 1].

df/dx1 = -+2 x1 with sign(x2) equiprobable -> variance 4 c_k^2 at the bin center; df/dx2 = 0 a.e. (no NaN bin here, unlike the ALE version: the derivative never sees the jump); df/dx3 = e^{x3} is deterministic.

Source code in effector/benchmarks.py
103
104
105
106
107
108
109
110
111
112
113
def rhale_bin_variance_gt(self, feature: int, nof_bins: int = 31) -> np.ndarray:
    """Per-bin variance of the derivative effects, Fixed(nof_bins) on [-1, 1].

    df/dx1 = -+2 x1 with sign(x2) equiprobable -> variance 4 c_k^2 at the
    bin center; df/dx2 = 0 a.e. (no NaN bin here, unlike the ALE version:
    the derivative never sees the jump); df/dx3 = e^{x3} is deterministic.
    """
    bin_centers = np.linspace(-1 + 1 / nof_bins, 1 - 1 / nof_bins, nof_bins)
    if feature == 0:
        return 4 * bin_centers**2
    return np.zeros_like(bin_centers)

CorrelatedInteraction()

The Gkolemis et al. 2023 (RHALE paper) example — correlated features.

\(f(x) = \sin(2\pi x_1) (1_{x_1 < 0} - 2 \cdot 1_{x_3 < 0}) + x_1 x_2 + x_2\)

with \(x_1\) mixture-uniform on [-0.5, 0.5] (5/6 of the mass below 0), \(x_2 \sim N(0, 2)\) and \(x_3 = x_1 + N(0, 0.01)\) — strongly correlated.

The only pair where PDP, ALE, RHALE and SHAP provably differ; each *_gt is the correct answer for that method's own definition.

Derivations: Gkolemis et al. 2023, https://arxiv.org/abs/2309.11193

Methods:

Name Description
ale_gt

ALE conditions on x3 ~ x1, so the -2 sin term only acts where x1 < 0

d_pdp_gt

Derivative-PDP (uncentered): E over the marginal of df/dx1.

generate_data

Mixture sampler: 5/6 of x1 mass on [-0.5, 0], 1/6 on [0, 0.5].

pdp_gt

PDP averages over the marginal of x3, ignoring its correlation

rhale_heter_gt

Effect-space heterogeneity band (std) accumulated from the x2 noise

shap_gt

Mean SHAP curve for x1 as derived in notebook 02: -5/6 sin(2 pi x).

Source code in effector/benchmarks.py
280
281
def __init__(self):
    self.axis_limits = np.array([[-0.5, 0.5], [-5.0, 5.0], [-0.5, 0.5]]).T
ale_gt(xs)

ALE conditions on x3 ~ x1, so the -2 sin term only acts where x1 < 0 ... and there it flips the sign: sin - 2 sin = -sin.

Source code in effector/benchmarks.py
342
343
344
345
346
347
348
349
def ale_gt(self, xs: np.ndarray) -> np.ndarray:
    """ALE conditions on x3 ~ x1, so the -2 sin term only acts where x1 < 0
    ... and there it flips the sign: sin - 2 sin = -sin."""

    def ff(x):
        return -np.sin(2 * np.pi * x) * (x < 0)

    return ff(xs) - _center(ff, -0.5, 0.5)
d_pdp_gt(xs)

Derivative-PDP (uncentered): E over the marginal of df/dx1.

df/dx1 = 2 pi cos(2 pi x1) 1{x1<0} - 4 pi cos(2 pi x1) 1{x3<0} + x2, and E[1{x3<0}] = 5/6, E[x2] = 0. (Notebook 02's gt_d_pdp carries a spurious +1 — a slip of dy/dx2 = x1 + 1; fixed here.)

Source code in effector/benchmarks.py
332
333
334
335
336
337
338
339
340
def d_pdp_gt(self, xs: np.ndarray) -> np.ndarray:
    """Derivative-PDP (uncentered): E over the marginal of df/dx1.

    df/dx1 = 2 pi cos(2 pi x1) 1{x1<0} - 4 pi cos(2 pi x1) 1{x3<0} + x2,
    and E[1{x3<0}] = 5/6, E[x2] = 0. (Notebook 02's gt_d_pdp carries a
    spurious +1 — a slip of dy/dx2 = x1 + 1; fixed here.)"""
    return 2 * np.pi * np.cos(2 * np.pi * xs) * (xs < 0) - (
        10 * np.pi / 3
    ) * np.cos(2 * np.pi * xs)
generate_data(n, seed=21)

Mixture sampler: 5/6 of x1 mass on [-0.5, 0], 1/6 on [0, 0.5].

Source code in effector/benchmarks.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
def generate_data(self, n: int, seed: int = 21) -> np.ndarray:
    """Mixture sampler: 5/6 of x1 mass on [-0.5, 0], 1/6 on [0, 0.5]."""
    rng = np.random.default_rng(seed)
    n1 = int(5 * n / 6)
    n2 = n - n1
    x1 = np.concatenate(
        [
            np.array([-0.5]),
            rng.uniform(-0.5, 0, size=n1 - 2),
            np.array([-1e-5]),
            np.array([0.0]),
            rng.uniform(0, 0.5, size=n2 - 2),
            np.array([0.5]),
        ]
    )
    x2 = rng.normal(0, self.sigma_2, n)
    x3 = x1 + rng.normal(0, self.sigma_3, n)
    return np.stack([x1, x2, x3], -1)
pdp_gt(xs)

PDP averages over the marginal of x3, ignoring its correlation with x1: the -2 sin term contributes with probability 5/6 everywhere.

Source code in effector/benchmarks.py
323
324
325
326
327
328
329
330
def pdp_gt(self, xs: np.ndarray) -> np.ndarray:
    """PDP averages over the *marginal* of x3, ignoring its correlation
    with x1: the -2 sin term contributes with probability 5/6 everywhere."""

    def ff(x):
        return np.sin(2 * np.pi * x) * (x < 0) - 5 / 3 * np.sin(2 * np.pi * x)

    return ff(xs) - _center(ff, -0.5, 0.5)
rhale_heter_gt(xs)

Effect-space heterogeneity band (std) accumulated from the x2 noise in df/dx1 — what notebook 02 draws around the RHALE curve.

Not asserted anywhere yet: RHALE.eval(heterogeneity=True) returns a derivative-space per-bin std, which is a different quantity. Wire this in once the heterogeneity payload API lands (LOGBOOK #4).

Source code in effector/benchmarks.py
354
355
356
357
358
359
360
361
def rhale_heter_gt(self, xs: np.ndarray) -> np.ndarray:
    """Effect-space heterogeneity band (std) accumulated from the x2 noise
    in df/dx1 — what notebook 02 draws around the RHALE curve.

    Not asserted anywhere yet: ``RHALE.eval(heterogeneity=True)`` returns a
    derivative-space per-bin std, which is a different quantity. Wire this
    in once the heterogeneity payload API lands (LOGBOOK #4)."""
    return (xs + 0.5) * self.sigma_2
shap_gt(xs)

Mean SHAP curve for x1 as derived in notebook 02: -5/6 sin(2 pi x).

DISPUTED (2026-07-03): this does not match interventional Shapley — an exact brute-force computation (all coalitions, marginal imputation over the joint background) and shap's PermutationExplainer agree with each other and both sit ~0.29 away from this curve, independent of N. The notebook's derivation needs revisiting; until then nothing asserts against this function (test_functional_correlated_features.py::test_shap is skipped).

Source code in effector/benchmarks.py
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
def shap_gt(self, xs: np.ndarray) -> np.ndarray:
    """Mean SHAP curve for x1 as derived in notebook 02: -5/6 sin(2 pi x).

    DISPUTED (2026-07-03): this does not match interventional Shapley —
    an exact brute-force computation (all coalitions, marginal imputation
    over the joint background) and shap's PermutationExplainer agree with
    each other and both sit ~0.29 away from this curve, independent of N.
    The notebook's derivation needs revisiting; until then nothing asserts
    against this function (test_functional_correlated_features.py::test_shap
    is skipped)."""

    def ff(x):
        return -5 / 6 * np.sin(2 * np.pi * x)

    return ff(xs) - _center(ff, -0.5, 0.5)

GeneralInteractionUniform()

models.GeneralInteraction under iid U[-1, 1]^3.

\(f(x) = x_1 x_2^2 + e^{x_3}\)

Derivations: notebooks/synthetic-examples/06_general_interaction_*.ipynb

Methods:

Name Description
pdp_heter_gt

Variance of the centered ICE curves at each grid point.

Source code in effector/benchmarks.py
144
145
146
147
def __init__(self):
    self.model = models.GeneralInteraction()
    self.dataset = datasets.IndependentUniform(dim=self.dim, low=-1, high=1)
    self.axis_limits = self.dataset.axis_limits
pdp_heter_gt(feature, xs)

Variance of the centered ICE curves at each grid point.

Feature 1 is the reason this pair exists: the x1*x2^2 interaction is invisible in the mean effect (E[x1] = 0) but not in the heterogeneity.

Source code in effector/benchmarks.py
176
177
178
179
180
181
182
183
184
185
186
187
def pdp_heter_gt(self, feature: int, xs: np.ndarray) -> np.ndarray:
    """Variance of the centered ICE curves at each grid point.

    Feature 1 is the reason this pair exists: the x1*x2^2 interaction is
    invisible in the mean effect (E[x1] = 0) but not in the heterogeneity.
    """
    if feature == 0:
        return 4 / 45 * xs**2  # x^2 * Var[x2^2]
    elif feature == 1:
        return (xs**2 - 1 / 3) ** 2 / 3  # (x2^2 - E[x2^2])^2 * E[x1^2]
    elif feature == 2:
        return np.zeros_like(xs)