macroforecast.interpretation#
macroforecast.interpretation owns post-fit interpretation helpers. These
functions consume fitted models and feature matrices. They do not fit models,
construct features, choose forecast windows, or run forecast-comparison tests.
Use the namespace form:
import macroforecast as mf
fit = mf.models.ridge(X_train, y_train)
mf.interpretation.linear_coefficients(fit)
Every table returned by this module carries
attrs["macroforecast_metadata_schema"] with kind, version, method,
model, n_features, output columns, and function-specific metadata.
Review Status#
interpretation functions are not all the same kind of evidence. Use this
classification before treating a number as a paper table or structural claim.
Class |
Functions |
Reference implementation |
Status |
|---|---|---|---|
Native/backend extraction |
|
scikit-learn-style |
Direct attribute extraction. Validity depends on the fitted estimator. |
SHAP backend |
|
Python |
Backend call plus pandas reshaping. |
Forecast-accuracy Shapley |
|
Python |
User-facing callables use oShapley/PBSV/MAS names. The optional backend is the Python |
Captum backend |
|
Captum |
Direct Captum call. |
Standard model-agnostic diagnostics |
|
scikit-learn permutation/PDP/ICE, R |
Implements standard diagnostic definitions directly. Interpretation is predictive/associational, not causal. |
Approximate or fixed-model diagnostics |
|
Strobl conditional-permutation idea, LOFO refit idea, macroforecast fixed-model diagnostics |
Metadata records approximation/fixed-model mode. Do not describe these as exact refit or additive decompositions. |
VAR/macroeconomic interpretation |
|
statsmodels VAR result API when available; Pesaran-Shin GIRF formula; internal statsmodels-like adapter for macroforecast VAR |
|
Neural manual attribution |
|
Captum-style torch-gradient methods |
Manual torch autograd implementation, except |
OLS as attention |
|
Goulet Coulombe (2026), “Ordinary Least Squares as an Attention Mechanism” |
Closed-form linear algebra. |
Dual interpretation |
|
Goulet Coulombe, Goebel, and Klieber (2024), plus local |
Episode-based explanation: which historical observations the model uses. Use Dual Interpretation for the full contract. Ridge/KRR/RF routes are implemented directly; boosted-tree AXIL, NN embedding, and classification log-odds routes are documented as future extensions. |
Aggregation/report plumbing |
|
No external fitting backend; table aggregation and user callables |
Aggregates existing evidence. |
Every returned table includes this review classification in
attrs["macroforecast_metadata_schema"]["reference"].
Function Summary#
Function |
Input |
Output |
Meaning |
|---|---|---|---|
|
|
|
Native linear coefficient table. |
|
|
|
Legacy-name alias for native linear coefficients. |
|
|
|
Native tree feature importance table. |
|
|
|
Legacy-name alias for native tree importance. |
|
fitted predictor, feature frame, target vector |
|
Loss degradation after permuting each feature. |
|
fitted predictor, feature frame, target vector |
|
Conditional permutation within bins of correlated features. |
|
fitted predictor or refit callable, feature frame, target vector |
|
Leave-one-feature-out or prediction-drop importance. |
|
fitted predictor and feature frame |
|
One-way manual partial-dependence curves. |
|
fitted predictor and feature frame |
long |
One-way individual conditional expectation curves. |
|
fitted predictor and feature frame |
long |
Alias for |
|
fitted predictor and feature frame |
|
First-order accumulated local effect curve. |
|
fitted predictor and feature frame |
|
Pairwise Friedman-Popescu H interaction statistics. |
|
fitted predictor and feature frame |
long |
SHAP attribution values using optional |
|
fitted predictor and feature frame |
|
Mean absolute SHAP feature importance. |
|
fitted predictor and feature frame |
|
Convenience SHAP-importance wrappers. |
|
aligned feature matrix, target, model specs |
|
Convenience path that precomputes the backend object and returns forecast explanation, oShapley-VI, and PBSV tables. |
|
|
|
Build an oShapley/PBSV sidecar for a completed runner result. |
|
|
|
Select one oShapley/PBSV output table without manually opening the sidecar. |
|
same as |
same as |
Descriptive alias for |
|
aligned feature matrix, target, model specs |
|
Build the provider required by the optional backend precompute loop. |
|
same as |
backend object |
Build provider and run the optional backend precompute. |
|
|
backend subsets |
Convert macroforecast train/test origins to exact backend subsets. |
|
raw forecast/loss output name or callable |
|
Build the optional backend transformer used only inside forecast-accuracy anatomy explanations. |
|
precomputed backend object or saved path |
|
Low-level backend wrapper for raw Shapley explanations. |
|
|
|
Wrap a macroforecast fit as an anatomy prediction function. |
|
|
|
Convert macroforecast train/test origins to exact anatomy subsets. |
|
aligned feature matrix, target, model specs |
|
Build the provider required by the upstream anatomy precompute loop. |
|
same as |
|
Build provider and run |
|
aligned feature matrix, target, model specs |
|
Convenience path that precomputes anatomy and returns forecast explanation, oShapley-VI, and PBSV tables. |
|
|
|
Build an anatomy sidecar for a completed runner result. |
|
precomputed |
|
Backend oShapley-VI: mean absolute raw-forecast Shapley contribution. |
|
precomputed |
|
Backend PBSV_p loss decomposition. |
|
same as |
|
Alias for backend PBSV_p. |
|
in-sample contribution table |
|
Explicit iShapley-VI_p table adapter for MAS input. |
|
local contribution table |
|
Mean absolute Shapley VI table, usable for in-sample or out-of-sample VI. |
|
VI table/Series and PBSV table/Series |
one-row |
Backend MAS and optional MAS p-value. |
|
additive forecast-contribution table and aligned target |
|
Package-native PBSV-style point-loss Shapley fallback from additive forecast contributions. |
|
fitted predictor and feature frame |
|
Linear contribution of each feature to one prediction. |
|
fitted predictor, feature frame, target vector |
|
Sequential R-squared contribution under zero-filled inactive features. |
|
fitted predictor, feature frame, target vector |
|
Recompute importance over rolling evaluation windows. |
|
fitted predictor or refit callable, feature frame, target vector |
|
Bootstrap uncertainty summary for importance. |
|
feature-importance table |
|
Aggregate feature importance by user or inferred groups. |
|
feature-importance table and lineage mapping |
|
Aggregate importance by feature-engineering lineage metadata. |
|
evaluation score table |
|
Attribute score improvements to mutually exclusive preprocessing/feature pipelines. |
|
training and test feature frames |
long |
OLS attention matrix weights. |
|
training and test feature frames |
long |
Exact OLS-as-attention weights from Goulet Coulombe (2026). |
|
training and test feature frames |
long |
Ridge-stabilized attention weights using the standard unpenalized-intercept ridge convention. |
|
training and test feature frames |
long |
Whitened train/test embeddings whose inner products reconstruct attention weights. |
|
train features/outcomes, optional test features, optional reference predictions |
|
Audit that standard OLS/ridge predictions equal attention-weight predictions. |
|
train features/outcomes and optional test features |
long |
OLS prediction as weighted training-outcome contributions. |
|
output from |
result object |
Bundles observation weights, observation contributions, forecast diagnostics, top observations, group weights, and metadata. |
|
model, train/test feature frames, train target |
|
Run the dedicated dual interpretation path. Full contract: Dual Interpretation. |
|
completed |
|
Build a dual sidecar for a completed runner result. |
|
fitted model when needed, train/test feature frames |
long |
DualML-style historical-observation weights for ridge, KRR, and random forest. |
|
observation-weight table and train target |
long |
Convert observation weights into observation contributions whose rows sum to each forecast. |
|
observation-weight table |
|
Forecast concentration, short position, leverage, gross leverage, and turnover. |
|
observation weights or contributions |
long |
Highest-weight historical observations per forecast date. |
|
observation weights and user episode groups |
|
Aggregate weights/contributions over regimes such as recessions or inflation episodes. |
|
same as preferred dual names |
|
Backward-compatible aliases. Prefer the paper-aligned names above in new code. |
|
fitted |
long |
Time-varying coefficient path from MacroRandomForest |
|
fitted VAR model |
|
Pesaran-Shin generalized IRF importance. |
|
fitted VAR model |
|
Cholesky orthogonalised IRF importance. |
|
fitted VAR model |
|
Forecast error variance decomposition importance. |
|
fitted VAR model |
|
Reduced-form MA residual contribution summary. |
|
lasso-style fit, optionally refit callable |
|
Single-fit or bootstrap nonzero coefficient frequency. |
|
fitted torch LSTM/GRU model and feature frame |
|
Hidden-unit activation importance. |
|
fitted torch model and feature frame |
|
Gradient-based neural attribution. |
|
fitted predictor, feature frame, user callable |
|
User-defined interpretation table with metadata attrs. |
Native Model Interpretation#
linear_coefficients#
macroforecast.interpretation.linear_coefficients(model, *, sort=True)
Input: a ModelFit or estimator exposing coef_.
Output columns:
Column |
Meaning |
|---|---|
|
Feature name. |
|
Signed coefficient. |
|
Absolute coefficient used for sorting. |
tree_importance#
macroforecast.interpretation.tree_importance(model, *, sort=True)
Input: a ModelFit or estimator exposing feature_importances_.
Output columns:
Column |
Meaning |
|---|---|
|
Feature name. |
|
Native estimator importance. |
Model-Agnostic Importance#
permutation_importance#
macroforecast.interpretation.permutation_importance(
model,
X,
y,
*,
metric="mse",
n_repeats=5,
random_state=None,
)
Input: fitted predictor, feature DataFrame, and aligned target vector.
Output columns:
Column |
Meaning |
|---|---|
|
Permuted feature. |
|
Mean loss increase after permutation. |
|
Standard deviation across repeats. |
|
Loss before permutation. |
|
Repeat count. |
Supported metric names: mse, mae. A custom callable can also be supplied.
permutation_importance_strobl#
macroforecast.interpretation.permutation_importance_strobl(
model,
X,
y,
*,
metric="mse",
n_repeats=5,
n_bins=5,
random_state=None,
)
Input: fitted predictor, feature DataFrame, and aligned target vector.
Output columns: feature, importance, std, baseline_loss,
n_repeats, conditioning_feature, and n_bins.
This is a Strobl-style conditional-permutation approximation. For each
feature, the function finds the most correlated companion feature and permutes
inside quantile bins of that companion. The exact Strobl conditional
permutation literature can condition on richer structures; this callable uses
a single-companion quantile-bin rule and records
exact_reference_implementation=False in metadata.
lofo_importance#
macroforecast.interpretation.lofo_importance(
model,
X,
y,
*,
fit_func=None,
metric="mse",
sort=True,
)
Input: feature DataFrame, target vector, and either an already fitted model
or a refit callable. If fit_func is supplied, it must have signature
fit_func(X_train, y_train) -> fitted_model.
Output columns: feature, importance, baseline_loss, heldout_loss,
and mode.
mode="refit" means true leave-one-feature-out refitting.
mode="prediction_drop" means the fitted model is reused and the held-out
feature is set to zero. The second mode is a fixed-model diagnostic, not an
exact LOFO refit experiment.
Effect Curves#
partial_dependence#
macroforecast.interpretation.partial_dependence(
model,
X,
*,
features,
grid_size=20,
)
Input: fitted predictor, feature DataFrame, and one feature or list of
features.
Output columns: feature, value, prediction.
Grid strategy: linear_min_max. For each selected feature, macroforecast uses
grid_size equally spaced values between the observed minimum and maximum in
X. This is the same brute-force averaging definition as sklearn/R PDP, with
a simpler explicit grid rule.
individual_conditional_expectation / ice_curves#
macroforecast.interpretation.individual_conditional_expectation(
model,
X,
*,
features,
grid_size=20,
center=False,
)
macroforecast.interpretation.ice_curves(
model,
X,
*,
features,
grid_size=20,
center=False,
)
Input: fitted predictor, feature DataFrame, and one feature or list of
features.
Output columns:
Column |
Meaning |
|---|---|
|
Feature varied along the grid. |
|
Original row position. |
|
Original pandas index value. |
|
Grid value assigned to the feature. |
|
Individual prediction for that row and grid value. |
|
Prediction minus the first grid prediction when |
This matches the brute-force ICE idea used by scikit-learn’s partial
dependence with kind="individual" and R pdp::partial(..., ice=TRUE).
partial_dependence() averages over rows. ICE keeps the row dimension.
The grid strategy is also linear_min_max.
accumulated_local_effect#
macroforecast.interpretation.accumulated_local_effect(
model,
X,
*,
feature,
bins=10,
)
Input: fitted predictor, feature DataFrame, and one feature name.
Output columns: feature, bin, center, ale, local_effect.
Binning strategy: empirical quantile bins. The returned ale curve is
centered to mean zero after accumulating local finite differences.
friedman_h_interaction#
macroforecast.interpretation.friedman_h_interaction(
model,
X,
*,
features=None,
grid_size=10,
)
Input: fitted predictor and feature DataFrame. features=None checks all
feature pairs.
Output columns: feature_1, feature_2, h_statistic,
joint_variance, interaction_variance, and grid_size.
The statistic is computed from manual one-way and two-way partial dependence. It is an interaction diagnostic, not a forecast-accuracy test.
SHAP Values#
shap_values#
macroforecast.interpretation.shap_values(
model,
X,
*,
background=None,
explainer="auto",
check_additivity=True,
**kwargs,
)
Input: a fitted ModelFit or estimator plus a feature DataFrame. background
defaults to X. For stable out-of-sample interpretation, pass a training or
reference sample as background.
Output: long DataFrame, one row per (observation, feature).
Column |
Meaning |
|---|---|
|
Integer row position in |
|
Original pandas index value. |
|
Feature name. |
|
Observed feature value. |
|
SHAP contribution. |
|
SHAP base value when provided by the backend. |
Supported explainer values:
Value |
Backend behavior |
|---|---|
|
Uses |
|
Uses |
|
Uses |
SHAP is optional:
pip install "macroforecast[interpretation]"
shap_importance and SHAP wrappers#
macroforecast.interpretation.shap_importance(model, X, background=None, explainer="auto")
macroforecast.interpretation.shap_tree(model, X, background=None)
macroforecast.interpretation.shap_linear(model, X, background=None)
macroforecast.interpretation.shap_kernel(model, X, background=None)
macroforecast.interpretation.shap_deep(model, X, background=None)
Input: fitted predictor and feature DataFrame.
Output columns: feature, importance, mean_shap, and std_shap.
importance is mean absolute SHAP value.
shap_tree forces the tree explainer. shap_kernel uses the permutation SHAP
path. shap_linear and shap_deep use the generic explainer path because the
installed shap backend determines the exact implementation.
Forecast Accuracy Anatomy#
This section covers the Shapley-based forecast-accuracy anatomy in Borup,
Goulet Coulombe, Rapach, Montes Schutte, and Schwenk-Nebbe (2022), The
Anatomy of Out-of-Sample Forecasting Accuracy, Federal Reserve Bank of
Atlanta Working Paper 2022-16, DOI 10.29338/wp2022-16, SSRN 4278745.
The paper separates several related quantities:
Quantity |
Meaning |
|---|---|
iShapley-VI |
In-sample Shapley variable importance over a training/reference sample. The upstream |
oShapley-VI |
Out-of-sample Shapley variable importance over the forecast sample. In the upstream README this is computed by anatomizing raw forecasts and averaging absolute local contributions. |
PBSV_p |
Performance-based Shapley value for predictor |
MAS |
Model-adaptation score comparing variable-importance and performance-based rankings. |
Backend coverage in macroforecast:
Method |
Callable |
Backend status |
|---|---|---|
Raw anatomy explanations |
|
Direct |
oShapley-VI |
|
Direct backend raw-forecast explanation plus the upstream README aggregation rule. |
PBSV_p |
|
Direct backend loss transformer through |
MAS |
|
Direct |
iShapley-VI input |
|
Table adapter. Use |
Additive fallback |
|
macroforecast-native point-loss Shapley adapter for SHAP/decomposition tables; not the full |
Window support#
The upstream Python anatomy package supports both single-forecast and
multi-window anatomy designs.
Design |
How it is represented upstream |
macroforecast wrapper behavior |
|---|---|---|
Single forecast |
Provider with one period, or |
|
Expanding window |
|
macroforecast consumes the precomputed |
Rolling window |
|
Same as expanding: the window scheme is fixed when the upstream |
Full OOS or subperiod summary |
|
`pbsv(loss=“rmse” |
Important boundary: macroforecast.interpretation does not decide the rolling
or expanding split. The anatomy.Anatomy object already contains the provider
loop, fitted models, forecast periods, and masking logic. For an end-to-end
macroforecast-native path, use anatomy_provider(...),
precompute_anatomy(...), or anatomy_pipeline(...) with a WindowSpec.
oShapley/PBSV bridge utilities#
The smooth user-facing path uses method names rather than backend package names:
result = mf.forecasting.run(feature_set, "ridge", window=window)
result = mf.interpretation.oshapley_from_forecast_result(
result,
feature_set.X,
feature_set.y.iloc[:, 0],
{"ridge": "ridge"},
window=window,
losses=("squared_error", "rmse"),
)
mf.output.write_artifacts(result, "results/oshapley_run")
Once the sidecar is attached, choose the exact output from
macroforecast.interpretation:
vi = mf.interpretation.oshapley_output(result, output="oshapley")
loss = mf.interpretation.oshapley_output(result, output="pbsv", loss="rmse")
local = mf.interpretation.oshapley_output(result, output="forecast")
tables = mf.interpretation.oshapley_output(result, output="tables")
meta = mf.interpretation.oshapley_output(result, output="metadata")
Supported output values:
Output |
Meaning |
|---|---|
|
oShapley-VI table, equivalent aliases: |
|
PBSV/loss-decomposition table for |
|
Raw forecast-explanation Shapley rows, equivalent aliases: |
|
Dictionary of all CSV/JSON-ready output tables. |
|
Flattened metadata table for the oShapley/PBSV run. |
|
JSON-ready dictionary with metadata, forecast explanations, oShapley-VI, and PBSV tables. |
If a ForecastResult has exactly one sidecar, the selector uses it. If
multiple sidecars are attached and none has the preferred names oshapley,
forecast_shapley, or anatomy, pass sidecar_name=....
The anatomy_* functions below are backend aliases. Use them only when the
backend object itself is the object being handled.
backend bridge utilities#
macroforecast.interpretation.window_to_anatomy_subsets(
window,
index,
*,
train_source="fit",
)
The user-facing alias is:
macroforecast.interpretation.window_to_oshapley_subsets(...)
Input: a WindowSpec, method name, or None, plus the feature/target index.
Output: upstream anatomy.AnatomySubsets. Unlike
AnatomySubsets.generate(...), this conversion follows the exact
macroforecast WindowSpec plan, including rolling/expanding/fixed estimation,
calendar or positional test steps, and retrain cadence. train_source="fit"
uses the model-fit sample at each origin; train_source="estimation" uses the
full estimation sample.
macroforecast.interpretation.anatomy_provider(
X,
y,
models,
*,
window=None,
params=None,
target_name=None,
train_source="fit",
)
The user-facing alias is:
macroforecast.interpretation.oshapley_provider(...)
Input: an already aligned forecast-row feature matrix X and target y.
models can be a model name, ModelSpec, sequence, or alias mapping. The
target must already represent the forecast object being explained, such as a
direct y[t+h] target or a precomputed growth target. Panel-to-feature
construction remains the job of macroforecast.feature_engineering or
macroforecast.forecasting.
Output: upstream AnatomyModelProvider. The provider fits the requested
macroforecast model at each anatomy period and wraps the fit with
anatomy_model(...).
macroforecast.interpretation.precompute_anatomy(
X,
y,
models,
*,
window=None,
n_iterations=32,
n_jobs=1,
save_path=None,
)
The user-facing alias is:
macroforecast.interpretation.precompute_oshapley(...)
Output: precomputed upstream anatomy.Anatomy object.
macroforecast.interpretation.anatomy_pipeline(
X,
y,
models,
*,
window=None,
losses=("rmse",),
n_iterations=32,
n_jobs=1,
)
The user-facing alias is:
macroforecast.interpretation.oshapley_pipeline(...)
Output: ForecastShapleyResult (AnatomyPipelineResult backend alias) with:
Attribute |
Meaning |
|---|---|
|
Precomputed backend object. |
|
Raw-forecast anatomy table from |
|
oShapley-VI table from |
|
PBSV table for each requested loss. |
|
Window/model/loss settings used to build the provider. |
AnatomyPipelineResult.to_tables(prefix="anatomy") returns named output tables
for raw forecast explanations, oShapley-VI, PBSV/loss decompositions, and
metadata. macroforecast.output uses this method when writing anatomy
sidecars attached to a ForecastResult.
Example:
window = mf.window.from_cutoffs(
test_start=X.index[120],
estimation_min_size=120,
horizon=1,
step=1,
)
anat = mf.interpretation.anatomy_pipeline(
X,
y_direct,
{"ridge": "ridge", "forest": "random_forest"},
window=window,
losses=("squared_error", "rmse"),
n_iterations=64,
n_jobs=4,
)
anat.variable_importance
anat.performance_values["rmse"]
macroforecast.interpretation.oshapley_from_forecast_result(
result,
X,
y,
models,
*,
window,
attach=True,
sidecar_name="anatomy",
)
Input: a completed ForecastResult plus the aligned X, y, model specs, and
the exact WindowSpec used for the anatomy refit loop. The explicit X/y and
window are required because a forecast table alone cannot reconstruct the
feature matrix, train/test subsets, or model-provider path without risking
look-ahead mistakes.
Output: if attach=True, a copy of ForecastResult with an oShapley sidecar
attached under sidecar_name. If attach=False, returns the standalone
ForecastShapleyResult.
result = mf.forecasting.run(feature_set, "ridge", window=window)
result = mf.interpretation.oshapley_from_forecast_result(
result,
feature_set.X,
feature_set.y.iloc[:, 0],
{"ridge": "ridge"},
window=window,
losses=("squared_error", "rmse"),
)
mf.output.write_artifacts(result, "results/anatomy_run")
Dual sidecars use the same completed-result pattern, but they do not refit the forecasting pipeline. The user passes the fitted model and exact train/test feature matrices:
result = mf.forecasting.run(feature_set, "ridge", window=window)
result = mf.interpretation.dual_from_forecast_result(
result,
fit,
X_train,
y_train,
X_test,
method="ridge",
)
mf.output.write_artifacts(result, "results/dual_run", layout="grouped")
anatomy_explain#
macroforecast.interpretation.anatomy_explain(
anatomy,
*,
model_groups=None,
transformer=None,
metric="forecast",
explanation_subset=None,
output="long",
)
Input: a precomputed anatomy.Anatomy object or a path saved by the Python
anatomy package. The upstream package owns the expensive provider loop:
rolling or expanding forecast periods, train/test extraction, model retrieval
or fitting, background masking, model-combination handling, and Shapley
precomputation.
Output:
Column |
Meaning |
|---|---|
|
Model or model-combination name from the anatomy object. |
|
Forecast-date index for local explanations, or the global subset label for aggregated explanations. |
|
|
|
Shapley contribution returned by |
|
Whether the row is the base contribution. |
Set output="wide" to receive the backend’s original wide table with
base_contribution plus feature columns.
Supported built-in metric values:
Metric |
Explained value |
|---|---|
|
Raw forecast, matching the backend default transformer. |
|
Local point squared error |
|
Local point absolute error |
|
Aggregated mean squared error over the selected forecast subset. |
|
Aggregated root mean squared error over the selected forecast subset. |
|
Aggregated mean absolute error over the selected forecast subset. |
You can pass a custom transformer directly. If it is callable,
macroforecast wraps it as anatomy.AnatomyModelOutputTransformer. The callable
must follow the upstream API and use a named y_hat argument, optionally with
y.
Example:
table = mf.interpretation.anatomy_explain(
"run_artifacts/anatomy.pkl",
model_groups={"forest_plus_linear": ["rf", "ridge"]},
metric="squared_error",
)
This is the preferred route when the goal is to reproduce the paper’s full forecast-anatomy design. It preserves the upstream provider’s rolling or expanding training windows and masking logic.
oshapley_vi#
macroforecast.interpretation.oshapley_vi(
anatomy,
*,
model_groups=None,
explanation_subset=None,
exclude_base=True,
)
Input: a precomputed anatomy.Anatomy object or saved path.
Output columns:
Column |
Meaning |
|---|---|
|
Model or model-combination name. |
|
Predictor name. |
|
Mean absolute raw-forecast Shapley contribution. |
|
Signed mean contribution. |
|
Standard deviation of local contributions. |
|
Number of local contribution rows. |
|
Rank within |
This matches the upstream README recipe:
df_oshapley = anatomy.explain(
model_sets=AnatomyModelCombination(groups=groups),
transformer=AnatomyModelOutputTransformer(transform=lambda y_hat: y_hat),
)
vi = df_oshapley.loc["model_name"].abs().mean(axis=0)
macroforecast performs that sequence through the backend and returns a tidy table.
pbsv / performance_based_shapley_value#
macroforecast.interpretation.pbsv(
anatomy,
*,
model_groups=None,
loss="rmse",
transformer=None,
explanation_subset=None,
output="long",
)
Input: a precomputed anatomy.Anatomy object or saved path.
Output: same shape as anatomy_explain(...), with metadata kind pbsv.
performance_based_shapley_value(...) is an alias for pbsv(...).
Built-in loss choices:
Loss |
Scope |
|---|---|
|
Global/subperiod PBSV_p for RMSE. |
|
Global/subperiod PBSV_p for MSE. |
|
Global/subperiod PBSV_p for MAE. |
|
Local PBSV_p for each forecast row. |
|
Local PBSV_p for each forecast row. |
For R-squared, gains, utility, custom benchmarks, or portfolio-style losses,
pass transformer= directly. The callable must follow the backend API:
transform(y_hat, y) or transform(y_hat).
ishapley_vi / shapley_variable_importance#
macroforecast.interpretation.ishapley_vi(
contributions,
*,
contribution_col=None,
feature_col="feature",
group_col=None,
exclude_base=True,
)
Input: an in-sample/training Shapley contribution table with a feature column and a contribution column. This is the explicit macroforecast callable for iShapley-VI_p.
Output: a mean-absolute in-sample Shapley VI table with metadata kind
ishapley_vi.
macroforecast.interpretation.shapley_variable_importance(
contributions,
*,
contribution_col=None,
feature_col="feature",
group_col=None,
exclude_base=True,
source="contribution_table",
)
Input: any local contribution table with a feature column and a contribution
column. The function infers common contribution names such as contribution,
shap_value, and forecast_contribution.
Output: a mean-absolute Shapley VI table. Use it in two places:
Use |
Example |
|---|---|
iShapley-VI |
Prefer |
oShapley-VI |
Run on an out-of-sample raw-forecast contribution table. |
model_accordance_score#
macroforecast.interpretation.model_accordance_score(
is_vi,
oos_pbsv,
*,
loss_type="lower_is_better",
mas_type="importance_weighted",
hypothesis_test=True,
h0_alpha=0.5,
n_samples=1000000,
random_state=None,
)
Input:
Argument |
Meaning |
|---|---|
|
Feature-indexed Series or VI table. This can be iShapley-VI or oShapley-VI. |
|
Feature-indexed Series or PBSV table. |
|
|
|
|
Output columns: mas, mas_p_value, mas_type, loss_type,
hypothesis_test, h0_alpha, n_samples, and n_features.
The function calls anatomy.MAS(...).compute(...) directly after converting
macroforecast tables to the feature-indexed Series expected by the backend.
When random_state is supplied, macroforecast temporarily seeds NumPy around
the backend hypothesis-test simulation and then restores the previous random
state.
performance_shapley_value#
macroforecast.interpretation.performance_shapley_value(
contributions,
y,
*,
loss="squared_error",
row_col=None,
feature_col="feature",
contribution_col=None,
base_col="base_value",
base_value=0.0,
n_permutations=None,
max_exact_features=8,
random_state=0,
return_local=False,
)
Input: an additive forecast-contribution table and an aligned target vector.
The contribution table can be the output of shap_values(...),
forecast_decomposition(...), or any custom table with:
Required item |
Default column inference |
|---|---|
Row identity |
|
Feature name |
|
Additive forecast contribution |
|
Base forecast |
|
If the table has no row or index column, macroforecast treats it as a
single-forecast contribution table. This is useful for one-row output from
forecast_decomposition(...).
For each forecast row, the function evaluates the cooperative game:
v(S) = L(y_t, base_t + sum_{j in S} contribution_{t,j})
and assigns feature j:
PBSV_{t,j} =
sum_{S subset N \ {j}}
|S|! (p - |S| - 1)! / p!
[v(S union {j}) - v(S)]
The base row is the empty-coalition loss L(y_t, base_t). Therefore:
base_loss_t + sum_j PBSV_{t,j} = full_loss_t
A negative pbsv means the feature reduced the selected loss for that
forecast. A positive pbsv means it increased the loss. This sign convention
is the useful paper-table convention for forecast accuracy: beneficial
variables are negative when the metric is a loss.
Output with return_local=False:
Column |
Meaning |
|---|---|
|
Feature name or |
|
Whether the row is the base loss. |
|
Mean PBSV across forecast rows. |
|
Mean absolute PBSV. |
|
Number of forecast rows. |
|
Mean empty-coalition loss. |
|
Mean full-contribution loss. |
|
Display rank in the returned table. |
Output with return_local=True keeps one row per forecast and feature, with
actual, base_prediction, full_prediction, baseline_loss, full_loss,
shapley_mode, and effective_permutations.
Exact Shapley weights are used when the number of features in a forecast row is
at most max_exact_features and n_permutations=None. For larger rows, the
function samples random feature orderings. Metadata records
max_efficiency_error, which should be near zero when the contribution table
is additive and row alignment is correct.
Example from SHAP output:
shap_table = mf.interpretation.shap_values(
fit,
X_test,
background=X_train,
explainer="tree",
)
pbsv = mf.interpretation.performance_shapley_value(
shap_table,
y_test,
contribution_col="shap_value",
loss="squared_error",
)
This adapter is useful when you already have additive local contributions and
want a PBSV-style loss view inside the macroforecast flow. It does not rebuild
the full Borup et al. rolling provider, background replacement, or iShapley /
oShapley machinery. Use anatomy_explain(...) for that full backend path.
Forecast And Model Decomposition#
forecast_decomposition#
macroforecast.interpretation.forecast_decomposition(model, X, row=-1)
Input: fitted model and feature DataFrame. row can be an integer position
or an index label.
Output columns for linear models: feature, feature_value, coefficient,
contribution, and abs_contribution. For linear models, the contribution
sum equals the selected row’s prediction up to numerical precision.
If the model has no coefficients but does expose tree importance, the function
returns a non-additive fallback. In that case contribution is NaN,
status="tree_importance_fallback_not_additive", and metadata records
prediction_additivity=False. Do not report that fallback as a forecast
decomposition.
cumulative_r2_contribution#
macroforecast.interpretation.cumulative_r2_contribution(
model,
X,
y,
*,
feature_order=None,
)
Input: fitted predictor, feature DataFrame, and target vector.
Output columns: step, feature, r2, incremental_r2, and
cumulative_features.
Inactive features are set to zero. The default order is native coefficient ranking when available, otherwise one-repeat permutation importance. This is a fixed-model masking diagnostic; the model is not refit as features enter.
rolling_recompute#
macroforecast.interpretation.rolling_recompute(
model,
X,
y,
*,
window=None,
step=None,
method="permutation_importance",
n_repeats=1,
random_state=None,
)
Input: fitted predictor, feature DataFrame, and target vector.
Output: feature-importance rows with window_id, window_start, and
window_end columns added.
Supported methods:
Method |
Meaning |
|---|---|
|
Vanilla loss-degradation permutation within each rolling window. |
|
Conditional permutation within each rolling window. |
This function does not refit the model. It asks whether the already fitted
model relies on different variables in different evaluation windows. Metadata
records refits_model=False.
bootstrap_jackknife#
macroforecast.interpretation.bootstrap_jackknife(
model,
X,
y,
*,
fit_func=None,
n_replications=50,
random_state=None,
)
Input: fitted predictor, feature frame, target vector, and optionally a refit
callable. With fit_func, each bootstrap sample is refit and native
coefficient importance is summarized. Without it, each bootstrap sample uses
fixed-model permutation importance. The current implementation is bootstrap
with replacement; metadata records jackknife=False.
Output columns: feature, importance, std, lower, upper, and
n_replications.
lasso_inclusion_frequency#
macroforecast.interpretation.lasso_inclusion_frequency(
model,
X=None,
y=None,
*,
fit_func=None,
n_bootstraps=50,
random_state=None,
)
Input: a lasso-style fitted model. With X, y, and fit_func, the function
bootstraps the sample and refits to estimate nonzero coefficient frequency.
Without those arguments, it reports the single fitted model’s nonzero
coefficient indicator.
Output columns include feature, inclusion_frequency, and importance.
Group And Pipeline Attribution#
group_aggregate#
macroforecast.interpretation.group_aggregate(
table,
*,
groups=None,
group_column=None,
value_column=None,
aggregation="sum",
)
Input: a feature-importance table with a feature column. groups can be
either {feature: group} or {group: [features...]}.
Output columns: group and importance.
Supported aggregations: sum, mean, max_abs, and signed_sum.
lineage_attribution#
macroforecast.interpretation.lineage_attribution(
table,
lineage,
*,
level="pipeline_name",
value_column=None,
aggregation="sum",
)
Input: a feature-importance table and a lineage mapping such as
{"pc1": {"pipeline_name": "pca_block"}}.
Output columns: the selected lineage level and importance.
transformation_attribution#
macroforecast.interpretation.transformation_attribution(
evaluation,
*,
pipeline_column=None,
metric=None,
method="shapley_over_pipelines",
target_columns=("target", "horizon"),
lower_is_better=True,
baseline="worst",
)
Input: an evaluation table with a pipeline/model column and a loss metric
column. If not supplied, the function infers common names such as model,
model_id, pipeline, mse, rmse, and mae.
Output columns: grouping columns when present, pipeline, loss, utility,
contribution, baseline, method, metric, and lower_is_better.
For the default loss setting, utility is:
utility_i = baseline_loss - loss_i
where baseline="worst" uses the worst observed pipeline in each
target/horizon group. method="shapley_over_pipelines" applies exact Shapley
weights to the utility game whose coalition value is average utility. This is
a coherent way to summarize mutually exclusive pipeline alternatives, but it
is not a causal component decomposition unless the evaluation table itself was
constructed from component-removal experiments.
Supported methods:
Method |
Meaning |
|---|---|
|
Exact Shapley contribution over average-utility coalitions. |
|
Pipeline utility relative to the selected baseline. |
|
Average-utility change after removing one pipeline. |
Dual Interpretation#
This block covers the dual interpretation route in Goulet Coulombe, Goebel,
and Klieber (2024), “Dual Interpretation of Machine Learning Forecasts”
(arXiv:2412.13076). Variable-importance tools ask which predictors matter.
Dual interpretation asks which historical observations the model is using as
analogues for the current forecast.
The preferred public surface is the dedicated namespace
macroforecast.interpretation.dual. The short aliases in
macroforecast.interpretation remain available for compatibility. See
Dual Interpretation for the full input/output
contract, DualInterpretationResult, and grouped output layout.
The core object is a data-portfolio weight:
yhat_new = sum_i w_i(new) y_i
Positive weights mean the model borrows from similar historical outcomes. Negative weights mean the model uses an observation by contrast or symmetry. Concentrated weights mean the model relies on a few episodes. High leverage means the forecast is extrapolative rather than a simple average. High turnover means the historical analogies change quickly over time.
The local reference code reviewed for this implementation is:
Reference path |
Used for |
|---|---|
|
Python RR/KRR/RF weight formulas and DualML diagnostics. |
|
R |
|
Model-route inventory: OLS, RF, LGB, RR, KRR, NN. |
Implemented routes:
Route |
Formula / logic |
macroforecast callable |
|---|---|---|
Ridge / OLS |
|
|
Kernel ridge |
|
|
Random forest |
For each tree, assign test and train rows to leaves; train rows in the same leaf share weight; average across trees. For sklearn forests, bootstrap sample counts are used when recoverable. |
|
Not yet implemented:
Route |
Status |
|---|---|
Boosted trees / LightGBM AXIL |
Needs a validated AXIL-style path and model-specific tree-channel storage. |
LGB+ / LGBA+ channel-specific weights |
Should wait until the LGB+ estimator stores separate linear/tree channels. |
Neural networks |
Needs penultimate-layer embeddings and ridge-on-embedding approximation error reporting. |
Classification |
Should default to log-odds-space contributions; probability-scale decomposition is order-dependent. |
dual.observation_weights#
macroforecast.interpretation.dual.observation_weights(
model,
X_train,
X_test=None,
*,
method="auto",
lambda_=1e-8,
kernel="linear",
sigma=1.0,
add_intercept=False,
ridge_penalty_scale="n_train",
normalize=False,
)
Input:
Argument |
Meaning |
|---|---|
|
Required for |
|
Training feature |
|
Forecast-row feature |
|
|
|
Ridge/KRR regularization. Ridge defaults to the DualML convention |
|
|
|
Kernel bandwidth convention used by the reference code: |
|
Adds an unpenalized intercept for ridge/OLS. The paper/reference code usually assumes standardized no-intercept design. |
|
Re-normalize row weights to sum to one. Default is |
Output columns:
Column |
Meaning |
|---|---|
|
Forecast row position and index. |
|
Historical observation position and index. |
|
Data-portfolio weight and absolute weight. |
|
Implemented route: |
The full dense matrix is also attached at attrs["weight_matrix"] with shape
(n_test, n_train).
Example:
weights = mf.interpretation.observation_weights(
None,
X_train,
X_test,
method="krr",
kernel="laplace",
sigma=1e-4,
lambda_=0.1,
)
dual.observation_contributions#
macroforecast.interpretation.dual.observation_contributions(
weights,
y_train,
*,
center=False,
include_base=False,
)
Input: an observation_weights(...) table and the aligned in-sample target.
If y_train is a Series, it is reindexed to train_index when possible.
Output columns add:
Column |
Meaning |
|---|---|
|
Realized historical outcome. |
|
|
|
|
|
Sum of contributions for the forecast row. |
|
|
Default center=False preserves the exact identity
prediction = weights @ y_train. Set center=True for plots that resemble
the cumulative centered paths in the DualML reference code.
dual.forecast_diagnostics#
macroforecast.interpretation.dual.forecast_diagnostics(
weights,
*,
top_q=0.05,
)
Output columns:
Column |
Meaning |
|---|---|
|
Sum of the top |
|
Signed sum of negative weights. This matches the R |
|
Absolute value version of the short side. |
|
Sum of signed weights. |
|
Sum of absolute weights. |
|
Sum of absolute weight changes from the previous forecast row. The first row is |
|
Diagnostic settings. |
Negative weights are not automatically errors. In this paper they can mean symmetry-based inference or extrapolation. The caution is economic: macro shocks are often asymmetric, so a mirror-image historical analogy can be misleading.
dual.top_observations#
macroforecast.interpretation.dual.top_observations(
weights,
*,
y_train=None,
n=10,
sort_by="abs_weight",
)
Input: an observation-weight table or contribution table. If y_train is
supplied and the input has no contribution column, contributions are built
first.
Supported sort_by values: abs_weight, weight, contribution, and
abs_contribution.
Output: top historical observations per forecast row, with a rank column.
This is the most direct table for “which past episodes drove this forecast?”
dual.group_observation_weights#
macroforecast.interpretation.dual.group_observation_weights(
weights,
groups,
*,
y_train=None,
)
Input: observation weights or contributions plus a mapping from group names to training-index labels, for example:
groups = {
"volcker": pd.period_range("1979Q3", "1982Q4", freq="Q").to_timestamp("Q"),
"gfc": pd.period_range("2007Q4", "2009Q2", freq="Q").to_timestamp("Q"),
"covid": pd.period_range("2020Q1", "2021Q2", freq="Q").to_timestamp("Q"),
}
Output columns: test_row, test_index, episode_group, weight,
abs_weight, n_episodes, and, when available, contribution and
abs_contribution.
Use this when a paper table needs weights assigned to recessions, inflation surges, tightening cycles, COVID episodes, or user-defined regimes.
OLS As Attention#
This block implements the algebra in Philippe Goulet Coulombe (2026), “Ordinary Least Squares as an Attention Mechanism.” It is not a separate forecasting model. Standard OLS already produces the forecast. These functions make the same forecast inspectable as a query-key-value style weighted average of historical outcomes.
The paper identity is:
beta_hat = (X_train' X_train)^-1 X_train' y_train
y_hat_test = X_test beta_hat
= X_test (X_train' X_train)^-1 X_train' y_train
= A y_train
where:
Object |
Meaning |
|---|---|
|
training feature matrix, already preprocessed and feature-engineered |
|
forecast-row feature matrix with the same columns |
|
realized training outcomes |
|
test-by-train attention matrix |
|
signed weight placed on training observation |
Positive weights borrow from a historical outcome. Negative weights use that historical outcome by contrast. The row weights do not need to be non-negative, so this is not softmax transformer attention. It is the linear OLS/ridge attention operator implied by the regression geometry.
Recommended use:
fit = mf.models.ols(X_train, y_train)
weights = mf.interpretation.ols_attention_weights(X_train, X_test)
audit = mf.interpretation.ols_attention_equivalence(
X_train,
y_train,
X_test,
reference_predictions=fit.predict(X_test),
)
embedding = mf.interpretation.ols_attention_embedding(X_train, X_test)
For high-dimensional macro panels, use the ridge version:
fit = mf.models.ridge(X_train, y_train, alpha=0.1)
weights = mf.interpretation.ridge_attention_weights(X_train, X_test, alpha=0.1)
With add_intercept=True, the intercept column is included in the design but
is not penalized. This matches standard ridge practice in sklearn/R-style
linear regression and keeps the affine identity for intercept models.
ols_attention_weights#
macroforecast.interpretation.ols_attention_weights(
X_train,
X_test=None,
*,
add_intercept=True,
)
Input:
Argument |
Type |
Default |
Meaning |
|---|---|---|---|
|
pandas |
required |
Training feature matrix. Index labels become |
|
pandas |
|
Forecast-row feature matrix. If omitted, training rows are explained against themselves. Missing columns are filled with zero after reindexing to |
|
bool |
|
Add an intercept column before forming the attention matrix. |
Output: long pandas DataFrame.
Column |
Meaning |
|---|---|
|
forecast-row position and label |
|
training-row position and label |
|
attention weight |
The dense matrix A is attached at attrs["attention_matrix"].
ridge_attention_weights#
macroforecast.interpretation.ridge_attention_weights(
X_train,
X_test=None,
*,
alpha=1.0,
add_intercept=True,
)
Input is the same as ols_attention_weights, plus:
Argument |
Type |
Default |
Meaning |
|---|---|---|---|
|
float |
|
Ridge penalty in |
Output: same long attention-weight table. This function is useful when the
feature matrix is nearly singular or when the fitted forecasting model is
mf.models.ridge(..., alpha=alpha).
ols_attention_embedding#
macroforecast.interpretation.ols_attention_embedding(
X_train,
X_test=None,
*,
add_intercept=True,
ridge=0.0,
tol=1e-12,
)
This returns the whitened embedding view emphasized in the paper. In the full-rank OLS case:
(X_train'X_train)^-1 = U Lambda^-1 U'
F_train = X_train U Lambda^-1/2
F_test = X_test U Lambda^-1/2
A = F_test F_train'
For ridge or rank-deficient cases, macroforecast uses the symmetric
pseudoinverse precision matrix and keeps positive precision components above
tol.
Output: long pandas DataFrame.
Column |
Meaning |
|---|---|
|
|
|
row position and label |
|
whitened embedding component |
|
embedded coordinate |
|
eigenvalue of the precision matrix used for that component |
Attached arrays:
Attr key |
Meaning |
|---|---|
|
dense |
|
dense |
|
reconstructed |
|
symmetric inverse or ridge inverse of the design Gram matrix |
|
retained positive precision eigenvalues |
ols_attention_equivalence#
macroforecast.interpretation.ols_attention_equivalence(
X_train,
y_train,
X_test=None,
*,
reference_predictions=None,
add_intercept=True,
ridge=0.0,
)
Input:
Argument |
Type |
Default |
Meaning |
|---|---|---|---|
|
pandas |
required |
Training feature matrix. |
|
pandas |
required |
Training outcomes. If a |
|
pandas |
|
Forecast rows. |
|
sequence or |
|
Optional predictions from an already fitted model. If omitted, macroforecast computes the closed-form normal-equation prediction. |
|
bool |
|
Same convention as the weight functions. |
|
float |
|
Set to the ridge model’s |
Output: one row per test observation.
Column |
Meaning |
|---|---|
|
|
|
closed-form or user-supplied prediction |
|
signed difference |
|
absolute difference |
Use this as the auditable paper check: if OLS/Ridge and attention are aligned,
abs_equivalence_error should be near machine precision.
attention_weights#
macroforecast.interpretation.attention_weights(
X_train,
X_test=None,
*,
add_intercept=True,
ridge=1e-8,
)
Input: training feature frame and optional test feature frame.
Output columns: test_row, test_index, train_row, train_index, and
weight. The full attention matrix is also attached at
attrs["attention_matrix"].
The implemented formula is:
Omega = X_test (X_train' X_train)^-1 X_train'
with a small ridge term for numerical stability. When add_intercept=True,
the ridge term is not applied to the intercept column, matching the standard
regression convention.
Prefer ols_attention_weights() for exact paper-style OLS and
ridge_attention_weights() for the ridge-stabilized variant. The older
attention_weights() helper remains available as the generic numerically
stabilized route.
dual_decomposition#
macroforecast.interpretation.dual_decomposition(
X_train,
y_train,
X_test=None,
*,
add_intercept=True,
ridge=1e-8,
)
Input: training features, training target, and optional test features.
Output columns add train_y and contribution to the attention-weight table.
The per-test prediction summary is attached at attrs["prediction_summary"].
Macroeconomic Random Forest GTVP#
mrf_gtvp#
macroforecast.interpretation.mrf_gtvp(model, X=None)
Input: a fitted mf.models.macro_random_forest(...) result. The MRF reference
backend only creates time-varying coefficients after prediction, so call
fit.predict(X_test) first or pass X to mrf_gtvp() so it can trigger
prediction.
Output columns: row, index, feature, coefficient,
abs_coefficient, and importance.
feature="__intercept__" is the time-varying intercept. Other features are
the coefficient columns returned by the MacroRandomForest reference backend’s
betas output. A compact feature-level summary is attached at
attrs["summary"].
VAR Interpretation#
generalized_irf#
macroforecast.interpretation.generalized_irf(model, n_periods=12, target=None)
Input: fitted VAR-style model, including mf.models.var(...).
Output columns: feature, importance, coefficient, and status.
The function implements the Pesaran-Shin generalized impulse response:
GIRF_h(j) = sigma_jj^(-1/2) A_h Sigma e_j
importance is the sum of absolute target responses across horizons.
orthogonalised_irf#
macroforecast.interpretation.orthogonalised_irf(model, n_periods=12, target=None)
Input and output shape match generalized_irf, but responses come from the
Cholesky orthogonalised IRF path.
fevd#
macroforecast.interpretation.fevd(model, n_periods=12, target=None)
Input: fitted VAR-style model.
Output columns: feature, importance, coefficient, and status.
importance is the summed forecast-error variance contribution over the
requested horizon range.
historical_decomposition#
macroforecast.interpretation.historical_decomposition(model, max_lag=12, target=None)
Input: fitted VAR-style model.
Output columns: feature, importance, mean_contribution, and
max_abs_contribution. The implementation uses reduced-form MA coefficients
and fitted residuals to summarize historical shock contributions.
Custom Interpretation#
macroforecast.interpretation.custom_interpretation(
model,
X,
func,
*,
y=None,
name=None,
metadata=None,
**params,
)
Input: a fitted ModelFit or predictor, feature DataFrame, optional target,
and a user callable.
Callable signature:
func(model, X, *, y=None, metadata=None, **params)
Accepted callable outputs are DataFrame, Series, mapping, or a sequence
convertible to a DataFrame. The wrapper attaches:
Attr |
Meaning |
|---|---|
|
Always |
|
|
|
User parameters passed to the callable. |
|
User-supplied metadata mapping. |
Example:
def signed_mean_effect(model, X, *, y=None, metadata=None, scale=1.0):
pred = model.predict(X)
return {"signed_mean_prediction": float(pred.mean() * scale)}
custom = mf.interpretation.custom_interpretation(
fit,
X_test,
signed_mean_effect,
name="signed_mean_effect",
scale=100.0,
)
Examples#
fit = mf.models.ridge(X_train, y_train)
coef = mf.interpretation.linear_coefficients(fit)
perm = mf.interpretation.permutation_importance(
fit,
X_test,
y_test,
n_repeats=20,
random_state=123,
)
pdp = mf.interpretation.partial_dependence(fit, X_test, features=["PAYEMS"])
ice = mf.interpretation.ice_curves(fit, X_test, features=["PAYEMS"])
ale = mf.interpretation.accumulated_local_effect(fit, X_test, feature="PAYEMS")
tree = mf.models.random_forest(X_train, y_train, random_state=123)
shap_table = mf.interpretation.shap_values(
tree,
X_test,
background=X_train,
explainer="tree",
)
var_impulse_response– VAR impulse responses with Monte-Carlo bootstrap confidence bands (statsmodels errband_mc; vars::irf), as a tidy horizon/impulse/response table.accumulated_local_effect_2d– second-order (two-feature) accumulated local effect: the pure interaction ALE surface (Apley-Zhu 2020; ALEPlot 2D).