ShapleyX Documentation
  • Home
  • User Guide

Getting Started

  • Installation
  • Quickstart
  • Deployment

Tutorials

  • Basic Usage
  • Example Workflow
  • Cantilever Beam
  • MC Shapley
  • MC Shapley + Sobol
  • Coalition Truncation
  • Truncated Normal
  • Correlated Ishigami
  • Wing Weight
  • Fire Spread
  • Sobol G50
  • Global Sensitivity Analysis of the SAC-SMA Rainfall-Runoff Model
  • Owen (Grouped) Shapley Effects for the SAC-SMA Model

How-to Guides

  • Common Tasks
  • MC Shapley

Reference

  • API
  • ARD
  • MC Shapley
  • Streaming OMP

Explanation

  • Theory
  • Value Function v(S)
  • RS-HDMR Construction
  • Distribution Classes
  • Arbitrary Marginals
  • Owen (Grouped) Effects
  • Case Studies
  • ARD Theory
  • Streaming Basis Expansion
  • Moment-Free Measures
  • Performance
ShapleyX Documentation
  • Tutorials
  • Example Workflow
  • Edit on GitHub

Case Study: Owen Product Function¶

We validate RS-HDMR Sobol indices and Shapley effects on the product function studied by Owen [1], a benchmark designed to stress-test sensitivity estimators in high-dimensional settings:

$$ f(\mathbf{x}) = \prod_{j=1}^{d} \left( \mu_j + \tau_j g_j(x_j) \right), \quad x_j \in [0, 1], $$

where $g_j(x_j) = \sqrt{12}(x_j - 1/2)$ ensures zero mean and unit variance, $\mu_j = 1$ for all $j$, and $\tau = (1, 1, 0.5, 0.5, 0.25, 0.25)$ for $d = 6$. This creates a hierarchy of variable importance with diminishing contributions from $X_1$ to $X_6$, and analytically tractable Sobol indices $\sigma_u^2 = \prod_{j \in u} \tau_j^2 \prod_{j \notin u} \mu_j^2$.

The first- and second-order HDMR terms capture only ~83% of total variance; the remaining ~17% comes from third- and fourth-order interactions, making this a challenging test for surrogate-based sensitivity analysis.


[1] Owen, A. B. (2013). "Better Estimation of Small Sobol' Sensitivity Indices." ACM Transactions on Modeling and Computer Simulation, 23(2), 1–17.

In [1]:
Copied!
# Import dependencies
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from itertools import combinations
from scipy.stats import qmc

from shapleyx import rshdmr

from importlib.metadata import version
print(f"Running on ShapleyX v{version('shapleyx')}")
# Import dependencies import numpy as np import pandas as pd import matplotlib.pyplot as plt from itertools import combinations from scipy.stats import qmc from shapleyx import rshdmr from importlib.metadata import version print(f"Running on ShapleyX v{version('shapleyx')}")
Running on ShapleyX v0.2

Helper Functions¶

We draw samples using a scrambled Sobol' sequence for good space-filling and define the analytical sensitivity indices for later validation.

In [2]:
Copied!
def owen_product(m, d, tau, mu):
    """Generate samples from the Owen product function.

    Uses a scrambled Sobol' sequence (base-2) for quasi-Monte Carlo
    sampling and returns a DataFrame with columns X1...Xd and Y.
    """
    num_samples = 2 ** m
    sampler = qmc.Sobol(d, scramble=True, seed=123)
    samples = sampler.random_base2(m)

    Y = np.ones(num_samples)
    for dim in range(d):
        g = np.sqrt(12) * (samples[:, dim] - 0.5)
        Y *= (mu[dim] + tau[dim] * g)

    cols = [f'X{i+1}' for i in range(d)]
    return pd.DataFrame(
        np.column_stack([samples, Y]),
        columns=cols + ['Y']
    )


def analytical_sigma2(u, tau, mu):
    """Analytical Sobol' index for subset u."""
    d = len(tau)
    prod_tau = np.prod([tau[j] ** 2 for j in u])
    prod_mu = np.prod([mu[j] ** 2 for j in range(d) if j not in u])
    return prod_tau * prod_mu


def analytical_shapley(tau, mu):
    """Compute analytical Shapley effects for the product function.

    For the Owen product function the Shapley value for variable i is
    the sum of all Sobol' indices involving i, each divided by its
    interaction order (Owen 2014).  Normalised by the true total
    variance of the product function.
    """
    d = len(tau)
    all_vars = list(range(d))
    shapleys = np.zeros(d)

    # True total variance: prod(mu_j^2 + tau_j^2) - prod(mu_j^2)
    total_var = (np.prod([mu[j]**2 + tau[j]**2 for j in range(d)])
                 - np.prod([mu[j]**2 for j in range(d)]))

    for r in range(1, d + 1):
        for subset in combinations(all_vars, r):
            sig2 = analytical_sigma2(subset, tau, mu)
            share = sig2 / len(subset)
            for i in subset:
                shapleys[i] += share

    return shapleys / total_var


print("Helper functions defined.")
def owen_product(m, d, tau, mu): """Generate samples from the Owen product function. Uses a scrambled Sobol' sequence (base-2) for quasi-Monte Carlo sampling and returns a DataFrame with columns X1...Xd and Y. """ num_samples = 2 ** m sampler = qmc.Sobol(d, scramble=True, seed=123) samples = sampler.random_base2(m) Y = np.ones(num_samples) for dim in range(d): g = np.sqrt(12) * (samples[:, dim] - 0.5) Y *= (mu[dim] + tau[dim] * g) cols = [f'X{i+1}' for i in range(d)] return pd.DataFrame( np.column_stack([samples, Y]), columns=cols + ['Y'] ) def analytical_sigma2(u, tau, mu): """Analytical Sobol' index for subset u.""" d = len(tau) prod_tau = np.prod([tau[j] ** 2 for j in u]) prod_mu = np.prod([mu[j] ** 2 for j in range(d) if j not in u]) return prod_tau * prod_mu def analytical_shapley(tau, mu): """Compute analytical Shapley effects for the product function. For the Owen product function the Shapley value for variable i is the sum of all Sobol' indices involving i, each divided by its interaction order (Owen 2014). Normalised by the true total variance of the product function. """ d = len(tau) all_vars = list(range(d)) shapleys = np.zeros(d) # True total variance: prod(mu_j^2 + tau_j^2) - prod(mu_j^2) total_var = (np.prod([mu[j]**2 + tau[j]**2 for j in range(d)]) - np.prod([mu[j]**2 for j in range(d)])) for r in range(1, d + 1): for subset in combinations(all_vars, r): sig2 = analytical_sigma2(subset, tau, mu) share = sig2 / len(subset) for i in subset: shapleys[i] += share return shapleys / total_var print("Helper functions defined.")
Helper functions defined.

Generate Training Data¶

The sample size is $2^m$; $m = 8$ gives 256 samples — sufficient for a well-behaved function at $d = 6$.

In [3]:
Copied!
m = 8
d = 6
tau = [1.0, 1.0, 0.5, 0.5, 0.25, 0.25]
mu  = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0]

dataframe = owen_product(m, d, tau, mu)
print(f"{len(dataframe)} samples generated")
m = 8 d = 6 tau = [1.0, 1.0, 0.5, 0.5, 0.25, 0.25] mu = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0] dataframe = owen_product(m, d, tau, mu) print(f"{len(dataframe)} samples generated")
256 samples generated

Fit RS-HDMR Surrogate Model¶

We use ARD with Bayesian cross-validation. The polynomial orders are tapered ($8, 6, 4, 4$) to avoid overfitting higher-order interactions given the modest sample size.

In [4]:
Copied!
model = rshdmr(
    dataframe,
    polys=[8, 6, 4, 4],
    n_iter=100,
    method='ard_cv',
    cv_tol=0.005,
)

sob, shap, total = model.run_all()
model = rshdmr( dataframe, polys=[8, 6, 4, 4], n_iter=100, method='ard_cv', cv_tol=0.005, ) sob, shap, total = model.run_all()
Found a DataFrame

============================================================
Transforming data to unit hypercube
============================================================

Feature: X1, Min Value: 0.0036, Max Value: 0.9980
Feature: X2, Min Value: 0.0037, Max Value: 0.9969
Feature: X3, Min Value: 0.0020, Max Value: 0.9983
Feature: X4, Min Value: 0.0013, Max Value: 0.9995
Feature: X5, Min Value: 0.0030, Max Value: 0.9968
Feature: X6, Min Value: 0.0031, Max Value: 0.9999

============================================================
Building basis functions
============================================================

Basis functions of 1 order : 48
Basis functions of 2 order : 540
Basis functions of 3 order : 1280
Basis functions of 4 order : 3840
Total basis functions in basis set : 5708
Total number of features in basis set is 5708

============================================================
Running regression analysis
============================================================

running ARD
Fit Execution Time : 3.691583
--
 
 Model complete 
 

============================================================
RS-HDMR model performance statistics
============================================================

variance of data        : 7.271
sum of coefficients^2   : 5.904
variance ratio          : 0.812
===============================
mae error on test set   : 0.021
mse error on test set   : 0.001
explained variance score: 1.000
===============================
slope     :  0.9998316395119596
r value   :  0.9999503175360412
r^2       :  0.9999006375404297
p value   :  0.0
std error :  0.0006253782336377583

No description has been provided for this image
============================================================
Running bootstrap resampling 1000 samples for 95.0% CI
============================================================

 |████████████████████████████████████████████████████████████████████████████████████████████████████| 100.0% 

============================================================
Completed bootstrap resampling
============================================================


============================================================
                  Completed all analysis
                 ------------------------

Silence is deep as Eternity, Speech is shallow as Time.
Carlyle
============================================================


RS-HDMR Shapley Effects¶

In [5]:
Copied!
shap
shap
Out[5]:
label effect scaled effect lower upper std
0 X1 0.352569 0.352604 0.349102 0.356469 0.001851
1 X2 0.347385 0.347419 0.344002 0.351083 0.001776
2 X3 0.120009 0.120021 0.117509 0.122956 0.001372
3 X4 0.117150 0.117162 0.113978 0.119686 0.001443
4 X5 0.029022 0.029025 0.027588 0.030496 0.000732
5 X6 0.033765 0.033768 0.032160 0.035400 0.000822

Analytical Shapley Effects¶

The product function has closed-form Sobol' indices, from which exact Shapley effects can be computed (Owen 2014).

In [6]:
Copied!
# Add analytical column to the Shapley DataFrame
shap['analytical'] = analytical_shapley(tau, mu)
shap
# Add analytical column to the Shapley DataFrame shap['analytical'] = analytical_shapley(tau, mu) shap
Out[6]:
label effect scaled effect lower upper std analytical
0 X1 0.352569 0.352604 0.349102 0.356469 0.001851 0.346867
1 X2 0.347385 0.347419 0.344002 0.351083 0.001776 0.346867
2 X3 0.120009 0.120021 0.117509 0.122956 0.001372 0.119793
3 X4 0.117150 0.117162 0.113978 0.119686 0.001443 0.119793
4 X5 0.029022 0.029025 0.027588 0.030496 0.000732 0.033339
5 X6 0.033765 0.033768 0.032160 0.035400 0.000822 0.033339

Comparison: RS-HDMR vs Analytical¶

In [7]:
Copied!
comparison = pd.DataFrame({
    'Variable': shap['label'],
    'RS-HDMR': shap['scaled effect'].round(4),
    'Analytical': shap['analytical'].round(4),
})
comparison['Abs. Difference'] = np.abs(
    comparison['RS-HDMR'] - comparison['Analytical']
).round(4)
comparison
comparison = pd.DataFrame({ 'Variable': shap['label'], 'RS-HDMR': shap['scaled effect'].round(4), 'Analytical': shap['analytical'].round(4), }) comparison['Abs. Difference'] = np.abs( comparison['RS-HDMR'] - comparison['Analytical'] ).round(4) comparison
Out[7]:
Variable RS-HDMR Analytical Abs. Difference
0 X1 0.3526 0.3469 0.0057
1 X2 0.3474 0.3469 0.0005
2 X3 0.1200 0.1198 0.0002
3 X4 0.1172 0.1198 0.0026
4 X5 0.0290 0.0333 0.0043
5 X6 0.0338 0.0333 0.0005
In [8]:
Copied!
fig, ax = plt.subplots(figsize=(10, 6))
bar_width = 0.3
x = np.arange(d)

ax.bar(x - bar_width/2, shap['scaled effect'], bar_width,
       yerr=[shap['scaled effect'] - shap['lower'],
             shap['upper'] - shap['scaled effect']],
       capsize=5, color='steelblue', alpha=0.85,
       label='RS-HDMR surrogate')
ax.bar(x + bar_width/2, shap['analytical'], bar_width,
       color='darkorange', alpha=0.85,
       label='Analytical (exact)')

ax.set_xlabel('Input Variable')
ax.set_ylabel('Shapley Effect')
ax.set_title('RS-HDMR vs Analytical Shapley Effects\n(Owen product function, d = 6)')
ax.set_xticks(x)
ax.set_xticklabels(shap['label'])
ax.legend()
ax.set_ylim(0, None)
plt.tight_layout()
plt.show()
fig, ax = plt.subplots(figsize=(10, 6)) bar_width = 0.3 x = np.arange(d) ax.bar(x - bar_width/2, shap['scaled effect'], bar_width, yerr=[shap['scaled effect'] - shap['lower'], shap['upper'] - shap['scaled effect']], capsize=5, color='steelblue', alpha=0.85, label='RS-HDMR surrogate') ax.bar(x + bar_width/2, shap['analytical'], bar_width, color='darkorange', alpha=0.85, label='Analytical (exact)') ax.set_xlabel('Input Variable') ax.set_ylabel('Shapley Effect') ax.set_title('RS-HDMR vs Analytical Shapley Effects\n(Owen product function, d = 6)') ax.set_xticks(x) ax.set_xticklabels(shap['label']) ax.legend() ax.set_ylim(0, None) plt.tight_layout() plt.show()
No description has been provided for this image

First-Order Sobol' Indices¶

The RS-HDMR surrogate also provides Sobol' indices to arbitrary order. Below are the first-order indices with bootstrap confidence intervals.

In [9]:
Copied!
# First-order Sobol' indices
first_order = sob[~sob['derived_labels'].str.contains('_')].copy()
first_order = first_order.rename(columns={'derived_labels': 'variable'})
first_order[['variable', 'index', 'lower', 'upper']].round(4)
# First-order Sobol' indices first_order = sob[~sob['derived_labels'].str.contains('_')].copy() first_order = first_order.rename(columns={'derived_labels': 'variable'}) first_order[['variable', 'index', 'lower', 'upper']].round(4)
Out[9]:
variable index lower upper
0 X1 0.1693 0.1666 0.1733
26 X2 0.1694 0.1666 0.1728
41 X3 0.0435 0.0420 0.0451
48 X4 0.0425 0.0407 0.0442
51 X5 0.0094 0.0087 0.0100
53 X6 0.0110 0.0102 0.0120

Key Takeaways¶

  1. RS-HDMR recovers the exact Shapley ranking ($X_1 \approx X_2 > X_3 \approx X_4 > X_5 \approx X_6$) from only 256 samples, despite 17% of variance coming from third- and fourth-order interactions.
  2. High-order interactions are captured through the sparse ARD expansion — 54 basis terms are selected from 5708 candidates.
  3. Bootstrap CIs are tight for the dominant variables, wider for the small contributors — reflecting genuine estimation uncertainty.
  4. Sobol' indices confirm the hierarchy: first-order effects follow the same $\tau_j^2$ pattern as the analytical solution.
In [10]:
Copied!
%load_ext watermark
%watermark -n -u -v -iv -w
%load_ext watermark %watermark -n -u -v -iv -w
Last updated: Fri, 01 May 2026

Python implementation: CPython
Python version       : 3.12.12
IPython version      : 9.13.0

matplotlib: 3.10.8
numpy     : 2.3.5
pandas    : 2.3.3
scipy     : 1.17.1
shapleyx  : 0.2

Watermark: 2.6.0

Previous Next

Built with MkDocs using a theme provided by Read the Docs.
GitHub « Previous Next »