Worked examples#

These complete scripts run from the repository with python examples/<name>.py. The core examples need only a normal UnifiedIG installation. The documentation checker executes them in separate processes, and the site includes the same source files rather than maintaining copied snippets.

Regression with CBaseline#

import numpy as np
from sklearn.linear_model import Ridge

from cbaseline import background
import unifiedig as uig

rng = np.random.default_rng(0)
X_train = rng.normal(size=(200, 4))
y_train = 2.0 * X_train[:, 0] - X_train[:, 1] + 0.5 * X_train[:, 2]
model = Ridge(alpha=0.5).fit(X_train, y_train)

# Choose a reference prediction and construct observed baseline inputs whose
# weighted mean model prediction equals that reference.
f_train = model.predict(X_train)
f0 = float(f_train.mean())
bg = background(
    predictions=f_train,
    f0=f0,
    features=X_train,
    weighting="calibrated",
)
X_eval = X_train[100:105]

explanation = uig.Explainer(model, bg)(X_eval)

np.testing.assert_allclose(
    explanation.base_values + explanation.values.sum(axis=1),
    model.predict(X_eval),
)

Multiclass classification#

The background is calibrated on centered scores. Contributions reconstruct those scores; pairwise contrasts require no second model pass.

import numpy as np
from sklearn.linear_model import LogisticRegression

from cbaseline import background
import unifiedig as uig


rng = np.random.default_rng(7)
X = rng.normal(size=(300, 4))
latent_scores = np.column_stack(
    (X[:, 0], X[:, 1] - 0.5 * X[:, 2], -X[:, 0] - X[:, 1])
)
y = np.argmax(latent_scores, axis=1)
model = LogisticRegression(max_iter=1000).fit(X, y)

# Construct one reference distribution for the complete centered score vector.
training_scores = model.decision_function(X)
centered_training_scores = training_scores - training_scores.mean(
    axis=1, keepdims=True
)
f0 = centered_training_scores.mean(axis=0)
bg = background(
    predictions=centered_training_scores,
    f0=f0,
    features=X,
    weighting="calibrated",
)

# A deliberate single starting point is also valid: uig.Explainer(model, x0).
explanation = uig.Explainer(model, bg)(X[100:105])

raw_scores = model.decision_function(X[100:105])
centered_scores = raw_scores - raw_scores.mean(axis=1, keepdims=True)
np.testing.assert_allclose(
    explanation.base_values + explanation.values.sum(axis=1),
    centered_scores,
)

# Pairwise score-margin attribution is derived without another model pass.
first_vs_second = explanation.contrast(
    str(model.classes_[0]), str(model.classes_[1])
)
print(first_vs_second.values)

Pipeline feature spaces#

Compare original features with outputs of named preprocessing steps. Both the evaluation data and baseline rows are transformed together.

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import Ridge
import unifiedig as uig

rng = np.random.default_rng(4)
X = rng.normal(size=(100, 3)) * [1, 5, 10]
y = X[:, 0] - 0.3 * X[:, 1] + 0.1 * X[:, 2]
model = Pipeline([
    ("scale", StandardScaler()), ("pca", PCA(2)), ("regressor", Ridge())
]).fit(X, y)
baseline = X[10:15]
evaluation = X[:4]

# Both calls accept ORIGINAL observations and ORIGINAL baseline rows.
original = uig.Explainer(model, baseline)(evaluation)
standardized = uig.Explainer(model, baseline, attribute_after="scale")(evaluation)
components = uig.Explainer(model, baseline, attribute_after="pca")(evaluation)

# Featurewise affine scaling changes gradient units but not IG contributions.
np.testing.assert_allclose(original.values, standardized.values, atol=1e-12)
np.testing.assert_allclose(standardized.data, model[:1].transform(evaluation))
assert original.values.shape == (4, 3)
assert components.values.shape == (4, 2)
assert components.feature_names == ["pca0", "pca1"]
for result in (original, standardized, components):
    np.testing.assert_allclose(result.base_values + result.values.sum(axis=1),
                               model.predict(evaluation), atol=1e-12)
    print("After:", result.attribute_after, "features:", result.feature_names,
          "shape:", result.values.shape)

Smooth numerical fallback#

A Gaussian-process regressor exercises the explicit finite-difference route. Specialized model backends always take precedence over a requested fallback.

import numpy as np
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import RBF

from cbaseline import background
import unifiedig as uig


rng = np.random.default_rng(8)
X = rng.normal(size=(80, 4))
y = np.sin(X[:, 0]) + 0.5 * X[:, 1] ** 2 - X[:, 2]
model = GaussianProcessRegressor(
    kernel=RBF(1.2), alpha=1e-6, optimizer=None
).fit(X, y)
predictions = model.predict(X)
f0 = float(predictions.mean())
bg = background(
    predictions=predictions,
    f0=f0,
    features=X,
    weighting="calibrated",
)

# A deliberate single starting point is also valid: uig.Explainer(model, x0).
explainer = uig.Explainer(
    model,
    bg,
    fallback="finite_difference",
    n_steps=32,
)
explanation = explainer(X[20:25])

print(explanation.values)
print(explanation.max_abs_completeness_error)

More examples#