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
    • The Ishigami function
    • Fit the RS-HDMR surrogate model
  • 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
  • Correlated Ishigami
  • Edit on GitHub

Ishigami Function with Correlated Inputs¶

This notebook reproduces the analysis from Iooss & Prieur (2019) [1, Figure 5], examining how Shapley effects for the Ishigami function evolve when the inputs are correlated. A Gaussian copula is used to impose a correlation $\rho$ between $X_1$ and $X_3$, the two variables that interact in the $b X_3^4 \sin(X_1)$ term.

The workflow:

  1. Fit an RS-HDMR surrogate model to 256 independent Ishigami samples.
  2. For a range of $\rho$ values, compute Monte Carlo Shapley effects via the Owen–Prieur formulation using the fitted surrogate.
  3. Compare the exhaustive (exact, all $2^d-1$ subsets) and random permutation (approximate) estimation methods.
  4. Display results with 96 % bootstrap confidence intervals.

[1] Iooss, B., & Prieur, C. (2019). “Shapley effects for sensitivity analysis with correlated inputs: comparison with Sobol’ indices, numerical estimation and applications.” International Journal for Uncertainty Quantification, 9(5), 493–514.

In [1]:
Copied!
# Import dependencies
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import time

from shapleyx import rshdmr
from shapleyx.utilities.mc_shapley import GaussianCopulaUniform

from importlib.metadata import version
print(f"Running on ShapleyX v{version('shapleyx')}")

# Set random seed for reproducibility
np.random.seed(123)
# Import dependencies import numpy as np import pandas as pd import matplotlib.pyplot as plt import time from shapleyx import rshdmr from shapleyx.utilities.mc_shapley import GaussianCopulaUniform from importlib.metadata import version print(f"Running on ShapleyX v{version('shapleyx')}") # Set random seed for reproducibility np.random.seed(123)
Running on ShapleyX v0.5.1
The Ishigami function¶

$$ f(\mathbf{x}) = \sin(x_1) + 7\sin^2(x_2) + 0.1\, x_3^4 \sin(x_1), \qquad x_i \in [-\pi, \pi] $$

In [2]:
Copied!
def ishigami_vectorised(x1, x2, x3, a=7.0, b=0.1):
    """Ishigami function for vectorised evaluation."""
    return np.sin(x1) + a * np.sin(x2)**2 + b * x3**4 * np.sin(x1)


d = 3
N_train = 256

# Generate training data with a Sobol' sequence
from scipy.stats import qmc
sampler = qmc.Sobol(d, scramble=True, seed=42)
X_raw = sampler.random_base2(int(np.log2(N_train)))
X_train = -np.pi + 2 * np.pi * X_raw  # scale to [-pi, pi]
Y_train = ishigami_vectorised(X_train[:, 0], X_train[:, 1], X_train[:, 2])

dataframe = pd.DataFrame(X_train, columns=['X1', 'X2', 'X3'])
dataframe['Y'] = Y_train
print(f'{len(dataframe)} training samples generated')
def ishigami_vectorised(x1, x2, x3, a=7.0, b=0.1): """Ishigami function for vectorised evaluation.""" return np.sin(x1) + a * np.sin(x2)**2 + b * x3**4 * np.sin(x1) d = 3 N_train = 256 # Generate training data with a Sobol' sequence from scipy.stats import qmc sampler = qmc.Sobol(d, scramble=True, seed=42) X_raw = sampler.random_base2(int(np.log2(N_train))) X_train = -np.pi + 2 * np.pi * X_raw # scale to [-pi, pi] Y_train = ishigami_vectorised(X_train[:, 0], X_train[:, 1], X_train[:, 2]) dataframe = pd.DataFrame(X_train, columns=['X1', 'X2', 'X3']) dataframe['Y'] = Y_train print(f'{len(dataframe)} training samples generated')
256 training samples generated
Fit the RS-HDMR surrogate model¶
In [3]:
Copied!
model = rshdmr(
    dataframe,
    polys=[10, 5],
    n_iter=300,
    method='ard_cv',
    cv_method='bayesian',
    cv_tol=0.01,
    resampling=True,
    number_of_resamples=500,
)

sob, shap, total = model.run_all()

# Display the HDMR-based Shapley effects for independent inputs
print("\nRS-HDMR Shapley effects (independent inputs):")
shap
model = rshdmr( dataframe, polys=[10, 5], n_iter=300, method='ard_cv', cv_method='bayesian', cv_tol=0.01, resampling=True, number_of_resamples=500, ) sob, shap, total = model.run_all() # Display the HDMR-based Shapley effects for independent inputs print("\nRS-HDMR Shapley effects (independent inputs):") shap
Found a DataFrame

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

Feature: X1, Min Value: -3.1345, Max Value: 3.1291
Feature: X2, Min Value: -3.1386, Max Value: 3.1342
Feature: X3, Min Value: -3.1204, Max Value: 3.1227

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

Basis functions of 1 order : 30
Basis functions of 2 order : 75
Total basis functions in basis set : 105
Total number of features in basis set is 105

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

running ARD
Fit Execution Time : 2.570700
--
 
 Model complete 
 

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

variance of data        : 13.575
sum of coefficients^2   : 13.546
variance ratio          : 0.998
===============================
mae error on test set   : 0.007
mse error on test set   : 0.000
explained variance score: 1.000
===============================
slope     :  0.9999932607502641
r value   :  0.9999971332956006
r^2       :  0.9999942665994191
p value   :  0.0
std error :  0.00015024070156228166

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

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

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


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

If your actions inspire others to dream more, learn more,
do more and become more, you are a leader.  John Quincy
Adams
============================================================


RS-HDMR Shapley effects (independent inputs):
Out[3]:
label effect scaled effect lower upper std
0 X1 0.429820 0.429822 0.428999 0.430507 0.000410
1 X2 0.451437 0.451439 0.450455 0.452864 0.000649
2 X3 0.118738 0.118738 0.118011 0.119191 0.000303

MC Shapley Effects as a Function of Correlation $\rho$¶

We impose a correlation $\rho$ between $X_1$ and $X_3$ using a Gaussian copula. The surrogate model is evaluated on samples drawn from this correlated distribution.

The permutation method is used across the full sweep for efficiency; the exhaustive method is evaluated at three anchor values ($\rho = -0.9, 0, 0.9$) for validation.

In [4]:
Copied!
# Correlation sweep: rho between X1 and X3
rho_values = [-0.9, -0.7, -0.5, -0.2, 0.0, 0.2, 0.5, 0.7, 0.9]
# Anchor values for exhaustive comparison
exhaustive_rho = [-0.9, 0.0, 0.9]

# Storage
results_perm = []   # permutation results for all rho
results_exh = []    # exhaustive results for anchor rho

# MC parameters (permutation sweep — lighter settings for speed)
N_perm = 5000      # Monte Carlo sample size per subset
B_perm = 50        # bootstrap replications
n_perm_val = 1000  # random permutations
alpha_96 = 0.04    # 96% CI

# MC parameters (exhaustive anchors — heavier settings for accuracy)
N_exh = 5000
B_exh = 100
# Correlation sweep: rho between X1 and X3 rho_values = [-0.9, -0.7, -0.5, -0.2, 0.0, 0.2, 0.5, 0.7, 0.9] # Anchor values for exhaustive comparison exhaustive_rho = [-0.9, 0.0, 0.9] # Storage results_perm = [] # permutation results for all rho results_exh = [] # exhaustive results for anchor rho # MC parameters (permutation sweep — lighter settings for speed) N_perm = 5000 # Monte Carlo sample size per subset B_perm = 50 # bootstrap replications n_perm_val = 1000 # random permutations alpha_96 = 0.04 # 96% CI # MC parameters (exhaustive anchors — heavier settings for accuracy) N_exh = 5000 B_exh = 100
In [5]:
Copied!
print("=" * 60)
print("Permutation method sweep")
print("=" * 60)

for rho in rho_values:
    corr = np.array([
        [1.0, 0.0, rho],
        [0.0, 1.0, 0.0],
        [rho, 0.0, 1.0],
    ])
    t0 = time.time()
    mc_df = model.get_mc_shapley(
        corr=corr,
        N=N_perm,
        method='permutation',
        n_perm=n_perm_val,
        B=B_perm,
        alpha=alpha_96,
        random_state=42,
        progress=False,
    )
    elapsed = time.time() - t0
    mc_df['rho'] = rho
    results_perm.append(mc_df)
    print(f"  rho = {rho:+.1f}  |  "
          f"X1={mc_df['effect'].iloc[0]:.3f}  "
          f"X2={mc_df['effect'].iloc[1]:.3f}  "
          f"X3={mc_df['effect'].iloc[2]:.3f}  "
          f"({elapsed:.0f}s)")

# Concatenate all permutation results
df_perm = pd.concat(results_perm, ignore_index=True)
print("=" * 60) print("Permutation method sweep") print("=" * 60) for rho in rho_values: corr = np.array([ [1.0, 0.0, rho], [0.0, 1.0, 0.0], [rho, 0.0, 1.0], ]) t0 = time.time() mc_df = model.get_mc_shapley( corr=corr, N=N_perm, method='permutation', n_perm=n_perm_val, B=B_perm, alpha=alpha_96, random_state=42, progress=False, ) elapsed = time.time() - t0 mc_df['rho'] = rho results_perm.append(mc_df) print(f" rho = {rho:+.1f} | " f"X1={mc_df['effect'].iloc[0]:.3f} " f"X2={mc_df['effect'].iloc[1]:.3f} " f"X3={mc_df['effect'].iloc[2]:.3f} " f"({elapsed:.0f}s)") # Concatenate all permutation results df_perm = pd.concat(results_perm, ignore_index=True)
============================================================
Permutation method sweep
============================================================
  rho = -0.9  |  X1=0.199  X2=0.591  X3=0.210  (5s)
  rho = -0.7  |  X1=0.266  X2=0.518  X3=0.216  (5s)
  rho = -0.5  |  X1=0.338  X2=0.486  X3=0.176  (4s)
  rho = -0.2  |  X1=0.415  X2=0.465  X3=0.120  (4s)
  rho = +0.0  |  X1=0.418  X2=0.461  X3=0.120  (4s)
  rho = +0.2  |  X1=0.415  X2=0.464  X3=0.121  (5s)
  rho = +0.5  |  X1=0.337  X2=0.483  X3=0.180  (5s)
  rho = +0.7  |  X1=0.265  X2=0.515  X3=0.220  (4s)
  rho = +0.9  |  X1=0.197  X2=0.590  X3=0.213  (4s)
In [6]:
Copied!
print("\n" + "=" * 60)
print("Exhaustive method (anchor values)")
print("=" * 60)

for rho in exhaustive_rho:
    corr = np.array([
        [1.0, 0.0, rho],
        [0.0, 1.0, 0.0],
        [rho, 0.0, 1.0],
    ])
    t0 = time.time()
    mc_df = model.get_mc_shapley(
        corr=corr,
        N=N_exh,
        method='exhaustive',
        B=B_exh,
        alpha=alpha_96,
        random_state=42,
        progress=False,
    )
    elapsed = time.time() - t0
    mc_df['rho'] = rho
    results_exh.append(mc_df)
    print(f"  rho = {rho:+.1f}  |  "
          f"X1={mc_df['effect'].iloc[0]:.3f}  "
          f"X2={mc_df['effect'].iloc[1]:.3f}  "
          f"X3={mc_df['effect'].iloc[2]:.3f}  "
          f"({elapsed:.0f}s)")

df_exh = pd.concat(results_exh, ignore_index=True)
print("\n" + "=" * 60) print("Exhaustive method (anchor values)") print("=" * 60) for rho in exhaustive_rho: corr = np.array([ [1.0, 0.0, rho], [0.0, 1.0, 0.0], [rho, 0.0, 1.0], ]) t0 = time.time() mc_df = model.get_mc_shapley( corr=corr, N=N_exh, method='exhaustive', B=B_exh, alpha=alpha_96, random_state=42, progress=False, ) elapsed = time.time() - t0 mc_df['rho'] = rho results_exh.append(mc_df) print(f" rho = {rho:+.1f} | " f"X1={mc_df['effect'].iloc[0]:.3f} " f"X2={mc_df['effect'].iloc[1]:.3f} " f"X3={mc_df['effect'].iloc[2]:.3f} " f"({elapsed:.0f}s)") df_exh = pd.concat(results_exh, ignore_index=True)
============================================================
Exhaustive method (anchor values)
============================================================
  rho = -0.9  |  X1=0.196  X2=0.587  X3=0.217  (0s)
  rho = +0.0  |  X1=0.425  X2=0.453  X3=0.122  (0s)
  rho = +0.9  |  X1=0.199  X2=0.582  X3=0.220  (0s)

Figure 5 — Shapley Effects vs Correlation¶

The left panel shows Shapley effects for each variable as a function of $\rho$ (permutation method, with 96 % CI bands). The right panel compares permutation and exhaustive estimates at the three anchor values.

Key observations (consistent with [1]):

  • $X_1$ and $X_3$ are directly linked through the interaction term $0.1\,x_3^4\sin(x_1)$; their Shapley effects converge as $\rho \to 1$ (they become near-perfect substitutes).
  • $X_2$ is independent of the correlation structure (it only appears in an additive term) — its Shapley effect remains essentially constant.
  • Exhaustive and permutation methods agree within sampling error, validating the permutation approximation.
In [7]:
Copied!
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6.5))

variables = ['X1', 'X2', 'X3']
colors = ['#2166ac', '#b2182b', '#4daf4a']
markers = ['s', 'o', 'D']

# --- Left panel: permutation sweep with 96 % CI bands ---
for i, var in enumerate(variables):
    mask = df_perm['variable'] == var
    rho_vals = df_perm.loc[mask, 'rho'].values
    eff = df_perm.loc[mask, 'effect'].values
    lo = df_perm.loc[mask, 'lower'].values
    hi = df_perm.loc[mask, 'upper'].values

    # Sort by rho for clean lines
    order = np.argsort(rho_vals)
    ax1.plot(rho_vals[order], eff[order],
             color=colors[i], marker=markers[i], markersize=7,
             linewidth=1.8, label=var)
    ax1.fill_between(rho_vals[order], lo[order], hi[order],
                     color=colors[i], alpha=0.12)

ax1.axhline(0, color='gray', linewidth=0.5, linestyle='--')
ax1.set_xlabel(r'Correlation $\rho$ between $X_1$ and $X_3$')
ax1.set_ylabel('Normalised Shapley Effect')
ax1.set_title('Permutation method (96 % CI bands)')
ax1.legend(loc='best')
ax1.set_ylim(0, None)
ax1.grid(True, alpha=0.3)

# --- Right panel: exhaustive vs permutation at anchor rho ---
bar_width = 0.08
x_pos = np.arange(len(variables))

for j, rho in enumerate(exhaustive_rho):
    # Exhaustive (bars)
    exh_sub = df_exh[df_exh['rho'] == rho]
    offset_exh = (j - 3/3) * bar_width * 1.5
    ax2.bar(
        x_pos + offset_exh, exh_sub['effect'].values, bar_width,
        yerr=[exh_sub['effect'].values - exh_sub['lower'].values,
              exh_sub['upper'].values - exh_sub['effect'].values],
        capsize=3, color=colors[j % len(colors)], alpha=0.75,
        label=f'Exh. ρ={rho:+.1f}',
    )
    # Permutation (scatter markers)
    perm_sub = df_perm[df_perm['rho'] == rho]
    ax2.scatter(
        x_pos + offset_exh, perm_sub['effect'].values,
        marker='x', s=60, color='black', linewidths=1.5, zorder=5,
    )

ax2.set_xticks(x_pos)
ax2.set_xticklabels(variables)
ax2.set_xlabel('Input Variable')
ax2.set_ylabel('Normalised Shapley Effect')
ax2.set_title('Exhaustive (bars) vs Permutation (×) — 96 % CI')
ax2.legend(loc='upper right', fontsize=8)
ax2.set_ylim(0, None)
ax2.grid(True, alpha=0.3, axis='y')

fig.suptitle(
    'Shapley Effects for the Ishigami Function under Correlation\n'
    '(cf. Iooss & Prieur, 2019, Figure 5)',
    fontsize=13, fontweight='bold', y=1.01
)
plt.tight_layout()
plt.show()
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6.5)) variables = ['X1', 'X2', 'X3'] colors = ['#2166ac', '#b2182b', '#4daf4a'] markers = ['s', 'o', 'D'] # --- Left panel: permutation sweep with 96 % CI bands --- for i, var in enumerate(variables): mask = df_perm['variable'] == var rho_vals = df_perm.loc[mask, 'rho'].values eff = df_perm.loc[mask, 'effect'].values lo = df_perm.loc[mask, 'lower'].values hi = df_perm.loc[mask, 'upper'].values # Sort by rho for clean lines order = np.argsort(rho_vals) ax1.plot(rho_vals[order], eff[order], color=colors[i], marker=markers[i], markersize=7, linewidth=1.8, label=var) ax1.fill_between(rho_vals[order], lo[order], hi[order], color=colors[i], alpha=0.12) ax1.axhline(0, color='gray', linewidth=0.5, linestyle='--') ax1.set_xlabel(r'Correlation $\rho$ between $X_1$ and $X_3$') ax1.set_ylabel('Normalised Shapley Effect') ax1.set_title('Permutation method (96 % CI bands)') ax1.legend(loc='best') ax1.set_ylim(0, None) ax1.grid(True, alpha=0.3) # --- Right panel: exhaustive vs permutation at anchor rho --- bar_width = 0.08 x_pos = np.arange(len(variables)) for j, rho in enumerate(exhaustive_rho): # Exhaustive (bars) exh_sub = df_exh[df_exh['rho'] == rho] offset_exh = (j - 3/3) * bar_width * 1.5 ax2.bar( x_pos + offset_exh, exh_sub['effect'].values, bar_width, yerr=[exh_sub['effect'].values - exh_sub['lower'].values, exh_sub['upper'].values - exh_sub['effect'].values], capsize=3, color=colors[j % len(colors)], alpha=0.75, label=f'Exh. ρ={rho:+.1f}', ) # Permutation (scatter markers) perm_sub = df_perm[df_perm['rho'] == rho] ax2.scatter( x_pos + offset_exh, perm_sub['effect'].values, marker='x', s=60, color='black', linewidths=1.5, zorder=5, ) ax2.set_xticks(x_pos) ax2.set_xticklabels(variables) ax2.set_xlabel('Input Variable') ax2.set_ylabel('Normalised Shapley Effect') ax2.set_title('Exhaustive (bars) vs Permutation (×) — 96 % CI') ax2.legend(loc='upper right', fontsize=8) ax2.set_ylim(0, None) ax2.grid(True, alpha=0.3, axis='y') fig.suptitle( 'Shapley Effects for the Ishigami Function under Correlation\n' '(cf. Iooss & Prieur, 2019, Figure 5)', fontsize=13, fontweight='bold', y=1.01 ) plt.tight_layout() plt.show()
No description has been provided for this image

Numerical Comparison: Permutation vs Exhaustive¶

At each anchor $\rho$, the table below quantifies the agreement.

In [8]:
Copied!
rows = []
for rho in exhaustive_rho:
    exh_sub = df_exh[df_exh['rho'] == rho].set_index('variable')
    perm_sub = df_perm[df_perm['rho'] == rho].set_index('variable')
    for var in variables:
        rows.append({
            'rho': rho,
            'Variable': var,
            'Exhaustive': f"{exh_sub.loc[var, 'effect']:.4f}",
            'Permutation': f"{perm_sub.loc[var, 'effect']:.4f}",
            'Diff': f"{abs(exh_sub.loc[var, 'effect'] - perm_sub.loc[var, 'effect']):.4f}",
            'Exh. 96% CI': f"[{exh_sub.loc[var, 'lower']:.3f}, {exh_sub.loc[var, 'upper']:.3f}]",
            'Perm. 96% CI': f"[{perm_sub.loc[var, 'lower']:.3f}, {perm_sub.loc[var, 'upper']:.3f}]",
        })

pd.DataFrame(rows)
rows = [] for rho in exhaustive_rho: exh_sub = df_exh[df_exh['rho'] == rho].set_index('variable') perm_sub = df_perm[df_perm['rho'] == rho].set_index('variable') for var in variables: rows.append({ 'rho': rho, 'Variable': var, 'Exhaustive': f"{exh_sub.loc[var, 'effect']:.4f}", 'Permutation': f"{perm_sub.loc[var, 'effect']:.4f}", 'Diff': f"{abs(exh_sub.loc[var, 'effect'] - perm_sub.loc[var, 'effect']):.4f}", 'Exh. 96% CI': f"[{exh_sub.loc[var, 'lower']:.3f}, {exh_sub.loc[var, 'upper']:.3f}]", 'Perm. 96% CI': f"[{perm_sub.loc[var, 'lower']:.3f}, {perm_sub.loc[var, 'upper']:.3f}]", }) pd.DataFrame(rows)
Out[8]:
rho Variable Exhaustive Permutation Diff Exh. 96% CI Perm. 96% CI
0 -0.9 X1 0.1955 0.1991 0.0036 [0.180, 0.211] [0.184, 0.214]
1 -0.9 X2 0.5874 0.5913 0.0039 [0.572, 0.606] [0.574, 0.609]
2 -0.9 X3 0.2171 0.2096 0.0075 [0.198, 0.237] [0.189, 0.230]
3 0.0 X1 0.4251 0.4182 0.0069 [0.406, 0.446] [0.398, 0.437]
4 0.0 X2 0.4527 0.4615 0.0088 [0.431, 0.472] [0.447, 0.480]
5 0.0 X3 0.1222 0.1203 0.0019 [0.097, 0.142] [0.099, 0.137]
6 0.9 X1 0.1987 0.1975 0.0012 [0.185, 0.215] [0.183, 0.213]
7 0.9 X2 0.5817 0.5899 0.0082 [0.565, 0.600] [0.569, 0.609]
8 0.9 X3 0.2197 0.2126 0.0070 [0.201, 0.236] [0.193, 0.230]

Figure 6 — Sobol Indices vs Correlation¶

Iooss & Prieur (2019) also compare first-order ($S_i$) and total-order ($T_i$) Sobol indices as a function of correlation. Under correlation, the standard inequality $S_i \leq T_i$ can break down — the two indices provide complementary information about how dependence interacts with the model structure.

The updated MCShapley.compute() returns both sobol_first and sobol_total alongside Shapley effects in a single call. We re-run the exhaustive method across all $\rho$ values to capture Sobol indices (the permutation method does not compute them since it may not evaluate all singleton subsets).

In [9]:
Copied!
print("Exhaustive sweep for Sobol indices (all rho values)")
print("=" * 60)

results_sobol = []

for rho in rho_values:
    corr = np.array([
        [1.0, 0.0, rho],
        [0.0, 1.0, 0.0],
        [rho, 0.0, 1.0],
    ])
    t0 = time.time()
    mc_df = model.get_mc_shapley(
        corr=corr,
        N=5000,
        method='exhaustive',
        B=100,
        alpha=0.04,
        random_state=42,
        progress=False,
    )
    elapsed = time.time() - t0
    mc_df["rho"] = rho
    results_sobol.append(mc_df)
    print(f"  rho = {rho:+.1f}  |  "
          f"S: X1={mc_df["sobol_first"].iloc[0]:.3f}  "
          f"X2={mc_df["sobol_first"].iloc[1]:.3f}  "
          f"X3={mc_df["sobol_first"].iloc[2]:.3f}  "
          f"  T: X1={mc_df["sobol_total"].iloc[0]:.3f}  "
          f"X2={mc_df["sobol_total"].iloc[1]:.3f}  "
          f"X3={mc_df["sobol_total"].iloc[2]:.3f}  "
          f"({elapsed:.0f}s)")

df_sobol = pd.concat(results_sobol, ignore_index=True)
print("Exhaustive sweep for Sobol indices (all rho values)") print("=" * 60) results_sobol = [] for rho in rho_values: corr = np.array([ [1.0, 0.0, rho], [0.0, 1.0, 0.0], [rho, 0.0, 1.0], ]) t0 = time.time() mc_df = model.get_mc_shapley( corr=corr, N=5000, method='exhaustive', B=100, alpha=0.04, random_state=42, progress=False, ) elapsed = time.time() - t0 mc_df["rho"] = rho results_sobol.append(mc_df) print(f" rho = {rho:+.1f} | " f"S: X1={mc_df["sobol_first"].iloc[0]:.3f} " f"X2={mc_df["sobol_first"].iloc[1]:.3f} " f"X3={mc_df["sobol_first"].iloc[2]:.3f} " f" T: X1={mc_df["sobol_total"].iloc[0]:.3f} " f"X2={mc_df["sobol_total"].iloc[1]:.3f} " f"X3={mc_df["sobol_total"].iloc[2]:.3f} " f"({elapsed:.0f}s)") df_sobol = pd.concat(results_sobol, ignore_index=True)
Exhaustive sweep for Sobol indices (all rho values)
============================================================
  rho = -0.9  |  S: X1=0.299  X2=0.608  X3=0.342    T: X1=0.124  X2=0.599  X3=0.124  (0s)
  rho = -0.7  |  S: X1=0.288  X2=0.540  X3=0.274    T: X1=0.244  X2=0.523  X3=0.199  (0s)
  rho = -0.5  |  S: X1=0.295  X2=0.496  X3=0.173    T: X1=0.364  X2=0.492  X3=0.231  (0s)
  rho = -0.2  |  S: X1=0.302  X2=0.456  X3=0.035    T: X1=0.518  X2=0.477  X3=0.244  (0s)
  rho = +0.0  |  S: X1=0.290  X2=0.453  X3=0.014    T: X1=0.558  X2=0.450  X3=0.228  (0s)
  rho = +0.2  |  S: X1=0.302  X2=0.454  X3=0.035    T: X1=0.518  X2=0.477  X3=0.243  (0s)
  rho = +0.5  |  S: X1=0.300  X2=0.491  X3=0.172    T: X1=0.364  X2=0.491  X3=0.234  (0s)
  rho = +0.7  |  S: X1=0.300  X2=0.533  X3=0.274    T: X1=0.244  X2=0.523  X3=0.208  (0s)
  rho = +0.9  |  S: X1=0.310  X2=0.602  X3=0.341    T: X1=0.125  X2=0.599  X3=0.136  (0s)
In [10]:
Copied!
fig, axes = plt.subplots(1, 3, figsize=(18, 5.5))

# Define which CI columns to use for each panel
panel_info = [
    (axes[0], "sobol_first", "First-order Sobol $S_i$",
     "sobol_first_lower", "sobol_first_upper"),
    (axes[1], "effect", "Shapley Effect",
     "lower", "upper"),
    (axes[2], "sobol_total", "Total-order Sobol $T_i$",
     "sobol_total_lower", "sobol_total_upper"),
]

for ax, col, title, ci_lo, ci_hi in panel_info:
    colors = ["#2166ac", "#b2182b", "#4daf4a"]
    markers = ["s", "o", "D"]
    for i, var in enumerate(variables):
        mask = df_sobol["variable"] == var
        rho_vals = df_sobol.loc[mask, "rho"].values
        vals = df_sobol.loc[mask, col].values
        lo = df_sobol.loc[mask, ci_lo].values
        hi = df_sobol.loc[mask, ci_hi].values

        order = np.argsort(rho_vals)
        ax.plot(rho_vals[order], vals[order],
                color=colors[i], marker=markers[i], markersize=7,
                linewidth=1.8, label=var)
        ax.fill_between(rho_vals[order], lo[order], hi[order],
                        color=colors[i], alpha=0.12)

    ax.axhline(0, color="gray", linewidth=0.5, linestyle="--")
    ax.set_xlabel(r"Correlation $\rho$ between $X_1$ and $X_3$")
    ax.set_ylabel("Sensitivity Index")
    ax.set_title(title)
    if ax == axes[0]:
        ax.legend(loc="best")
    ax.set_ylim(0, None)
    ax.grid(True, alpha=0.3)

fig.suptitle(
    "Sobol and Shapley Indices vs Correlation — Ishigami Function\n"
    "(Exhaustive method, 96 % CI bands, cf. Iooss & Prieur 2019)",
    fontsize=12, fontweight="bold", y=1.02
)
plt.tight_layout()
plt.show()
fig, axes = plt.subplots(1, 3, figsize=(18, 5.5)) # Define which CI columns to use for each panel panel_info = [ (axes[0], "sobol_first", "First-order Sobol $S_i$", "sobol_first_lower", "sobol_first_upper"), (axes[1], "effect", "Shapley Effect", "lower", "upper"), (axes[2], "sobol_total", "Total-order Sobol $T_i$", "sobol_total_lower", "sobol_total_upper"), ] for ax, col, title, ci_lo, ci_hi in panel_info: colors = ["#2166ac", "#b2182b", "#4daf4a"] markers = ["s", "o", "D"] for i, var in enumerate(variables): mask = df_sobol["variable"] == var rho_vals = df_sobol.loc[mask, "rho"].values vals = df_sobol.loc[mask, col].values lo = df_sobol.loc[mask, ci_lo].values hi = df_sobol.loc[mask, ci_hi].values order = np.argsort(rho_vals) ax.plot(rho_vals[order], vals[order], color=colors[i], marker=markers[i], markersize=7, linewidth=1.8, label=var) ax.fill_between(rho_vals[order], lo[order], hi[order], color=colors[i], alpha=0.12) ax.axhline(0, color="gray", linewidth=0.5, linestyle="--") ax.set_xlabel(r"Correlation $\rho$ between $X_1$ and $X_3$") ax.set_ylabel("Sensitivity Index") ax.set_title(title) if ax == axes[0]: ax.legend(loc="best") ax.set_ylim(0, None) ax.grid(True, alpha=0.3) fig.suptitle( "Sobol and Shapley Indices vs Correlation — Ishigami Function\n" "(Exhaustive method, 96 % CI bands, cf. Iooss & Prieur 2019)", fontsize=12, fontweight="bold", y=1.02 ) plt.tight_layout() plt.show()
No description has been provided for this image
Interpreting the Three Panels¶
Panel What it shows Key observation

| $S_i$ (left) | Main effect of each $X_i$ alone | $S_1$ and $S_3$ converge as $\rho\to\pm1$ — correlation makes them proxies for each other | | Shapley (centre) | Fair attribution including interactions | $X_1$ and $X_3$ converge to a shared value; $X_2$ is unaffected | | $T_i$ (right) | Total effect including all interactions | $T_1$ and $T_3$ show the largest gap at $\rho=0$, narrow as correlation increases |

For uncorrelated inputs ($\rho=0$), $S_i \leq$ Shapley $\leq T_i$ holds. Under strong correlation, this ordering can reverse — see the dedicated MC Shapley + Sobol notebook for a detailed explanation.


Supplementary: Varying Correlation Between $X_1$ and $X_2$¶

For completeness, we also examine the case where the correlation is imposed between $X_1$ and $X_2$ (which do not share an interaction term). In this scenario the Shapley effects are expected to be far less sensitive to $\rho$.

In [11]:
Copied!
rho_supp = [-0.9, -0.5, -0.2, 0.0, 0.2, 0.5, 0.9]
results_supp = []

for rho in rho_supp:
    corr = np.array([
        [1.0, rho, 0.0],
        [rho, 1.0, 0.0],
        [0.0, 0.0, 1.0],
    ])
    mc_df = model.get_mc_shapley(
        corr=corr,
        N=3000,
        method='permutation',
        n_perm=1000,
        B=50,
        alpha=0.04,
        random_state=42,
        progress=False,
    )
    mc_df['rho'] = rho
    results_supp.append(mc_df)

df_supp = pd.concat(results_supp, ignore_index=True)
rho_supp = [-0.9, -0.5, -0.2, 0.0, 0.2, 0.5, 0.9] results_supp = [] for rho in rho_supp: corr = np.array([ [1.0, rho, 0.0], [rho, 1.0, 0.0], [0.0, 0.0, 1.0], ]) mc_df = model.get_mc_shapley( corr=corr, N=3000, method='permutation', n_perm=1000, B=50, alpha=0.04, random_state=42, progress=False, ) mc_df['rho'] = rho results_supp.append(mc_df) df_supp = pd.concat(results_supp, ignore_index=True)
In [12]:
Copied!
fig, ax = plt.subplots(figsize=(8, 5))

for i, var in enumerate(variables):
    mask = df_supp['variable'] == var
    rho_vals = df_supp.loc[mask, 'rho'].values
    eff = df_supp.loc[mask, 'effect'].values
    lo = df_supp.loc[mask, 'lower'].values
    hi = df_supp.loc[mask, 'upper'].values
    order = np.argsort(rho_vals)
    ax.plot(rho_vals[order], eff[order],
            color=colors[i], marker=markers[i], markersize=7,
            linewidth=1.8, label=var)
    ax.fill_between(rho_vals[order], lo[order], hi[order],
                    color=colors[i], alpha=0.12)

ax.axhline(0, color='gray', linewidth=0.5, linestyle='--')
ax.set_xlabel(r'Correlation $\rho$ between $X_1$ and $X_2$')
ax.set_ylabel('Normalised Shapley Effect')
ax.set_title(
    'Shapley Effects — correlation between $X_1$ and $X_2$\n'
    '(Permutation method, 96 % CI)',
    fontsize=11
)
ax.legend(loc='best')
ax.set_ylim(0, None)
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
fig, ax = plt.subplots(figsize=(8, 5)) for i, var in enumerate(variables): mask = df_supp['variable'] == var rho_vals = df_supp.loc[mask, 'rho'].values eff = df_supp.loc[mask, 'effect'].values lo = df_supp.loc[mask, 'lower'].values hi = df_supp.loc[mask, 'upper'].values order = np.argsort(rho_vals) ax.plot(rho_vals[order], eff[order], color=colors[i], marker=markers[i], markersize=7, linewidth=1.8, label=var) ax.fill_between(rho_vals[order], lo[order], hi[order], color=colors[i], alpha=0.12) ax.axhline(0, color='gray', linewidth=0.5, linestyle='--') ax.set_xlabel(r'Correlation $\rho$ between $X_1$ and $X_2$') ax.set_ylabel('Normalised Shapley Effect') ax.set_title( 'Shapley Effects — correlation between $X_1$ and $X_2$\n' '(Permutation method, 96 % CI)', fontsize=11 ) ax.legend(loc='best') ax.set_ylim(0, None) ax.grid(True, alpha=0.3) plt.tight_layout() plt.show()
No description has been provided for this image

Summary¶

  • The RS-HDMR surrogate faithfully reproduces the sensitivity structure of the Ishigami function under correlation.
  • When $X_1$ and $X_3$ are strongly correlated, their Shapley effects converge toward a shared value, reflecting the fact that they carry overlapping information through the $x_3^4 \sin(x_1)$ interaction.
  • $X_2$ remains unaffected because it enters the model only through an additive, non-interacting term.
  • Correlating $X_1$ and $X_2$ (no shared interaction) has a much smaller impact on the Shapley allocation — as expected from theory.
  • The permutation method agrees well with the exhaustive method, supporting its use for higher-dimensional problems where exhaustive enumeration becomes infeasible.
In [13]:
Copied!
%load_ext watermark
%watermark -n -u -v -iv -w
%load_ext watermark %watermark -n -u -v -iv -w
Last updated: Wed, 06 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.5.1

Watermark: 2.6.0

In [ ]:
Copied!

Previous Next

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