Where the SHAP Disagreement Actually Lives

Not the model — one post-hoc aggregation step, checked against Lundberg et al. and the group-Shapley literature

Author

Net Zero Industrial Policy Lab (NZIPL) · Johns Hopkins SAIS

Published

July 31, 2026

Code
library(readr); library(dplyr); library(tidyr); library(tibble)
library(knitr); library(ggplot2); library(showtext)
font_add_google("Archivo", "Archivo"); showtext_auto(); showtext_opts(dpi = 200)

CATS <- c("Chemicals","Electronics","Industrial Materials","Machinery","Metals")
feat <- read_csv(here::here("data/pc/pc_features.csv"), show_col_types = FALSE)

theme_nr <- function(...) {
  theme_minimal(base_family = "Archivo", base_size = 12) +
    theme(plot.title    = element_text(size = 14, face = "bold", color = "#073309"),
          plot.subtitle = element_text(size = 11, color = "#475569"),
          panel.grid.minor = element_blank(), ...)
}
theme_set(theme_nr())
NZ_DARK <- "#073309"; NZ_GREEN <- "#3cb54a"; NZ_LIGHT <- "#f0f7f0"; NZ_AMBER <- "#f59e0b"

What this is. A narrow companion to model_critical_review.qmd. That document audits the capability model broadly (macro features, thresholds, the RCA target). This one isolates a single question: is the arithmetic performed on the SHAP output — after the Random Forest and SHAP have already run — standard practice, and if not, what does the methods literature actually say instead? It does not revisit whether SHAP or the RF target are appropriate; that part of the model is not in dispute.

Bottom line. Ishana is correct that summing feature-level SHAP magnitudes into a group score has a theoretical basis in the literature. She is not correct that it applies here unmodified. The theorem that licenses summing (i) requires the values being summed to still carry SHAP’s additive/efficiency property, which the z-scoring step destroys before the sum ever happens, and (ii) is only exact under feature independence, which HS-6 codes inside one capability cluster — co-exported, complementary industrial inputs — are unlikely to satisfy. Neither condition holds for the shipped pipeline.

1 Scope: the one step in dispute

The Random Forest, the binary RCA>1 target, and the use of SHAP to explain it are not what this document is about. The dispute is confined to one arrow in the pipeline below — everything left of it is common ground:

Code
W <- 1.95; H <- 0.9
bx <- tibble(
  cx = seq(1, by = 2.3, length.out = 6), cy = 0,
  label = c("Inputs\n(features)", "Random Forest\n(many trees)",
            "SHAP\nper-feature\nmean(|SHAP|)", "Yearly\nz-score",
            "Sum within\ncluster", "\"Need\"\naxis value")) |>
  mutate(xmin = cx - W/2, xmax = cx + W/2, ymin = cy - H/2, ymax = cy + H/2,
         disputed = row_number() >= 4,
         fill = ifelse(disputed, "#fde8e8", NZ_LIGHT),
         border = ifelse(disputed, "#dc2626", "#cbd5c9"),
         txt  = NZ_DARK)
arr <- tibble(x = bx$xmax[1:5], xend = bx$xmin[2:6], y = 0,
              disputed = seq_len(5) >= 3)
ggplot() +
  geom_segment(data = arr, aes(x, y, xend = xend, yend = y,
                                colour = ifelse(disputed, "#dc2626", NZ_DARK)),
               arrow = arrow(length = unit(0.16, "cm"), type = "closed"), linewidth = 0.6) +
  geom_rect(data = bx, aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax,
                           fill = fill, colour = border), linewidth = 0.7) +
  geom_text(data = bx, aes(cx, cy, label = label, colour = txt),
            family = "Archivo", fontface = "bold", size = 2.6, lineheight = 0.95) +
  scale_fill_identity() + scale_colour_identity() +
  coord_cartesian(ylim = c(-1.7, 1.7), clip = "off") + theme_void()
Figure 1: Common ground (grey) vs. the disputed step (red). The RF, the SHAP computation, and even per-feature mean(|SHAP|) are standard. The disagreement starts at the z-score, and compounds at the sum.

2 What the two source papers actually say

Ishana cites Lundberg & Lee (2017) and Lundberg et al. (2020) — the SHAP and TreeSHAP papers — as her authority. Both were read cover to cover for this review (~/Documents/R/NZIPL/reference/ml_papers/; kept outside the repo, as docs/ publishes world-readable).

What they confirm as standard. The 2020 paper’s canonical “global feature importance” figure (Fig. 4a) is a bar chart of mean(|SHAP value|) per feature, computed on the raw SHAP values, plotted in the model’s native output units (log-odds / relative risk). This part of Ishana’s pipeline — mean absolute SHAP as an importance score — is exactly what the papers do. No dispute here.

What normalization actually appears, and where. The word “normalized” appears exactly once across both papers, in Fig. 6a: “rows are features’ normalized SHAP values” — a per-row rescaling used only to make a patient-similarity heatmap for a clustering visualization readable. It is not proposed as an importance measure, it is not summed across features afterward, and it is not used to rank or compare groups. It is a different operation for a different purpose. Neither paper describes z-scoring a feature’s importance and then summing it with other features’ z-scores to build a composite score. That operation is not in either source Ishana cites — it doesn’t appear there to be defended or attacked; it simply isn’t discussed.

3 What the pipeline actually does, precisely

Per scripts/ml/regen_shap.py and qmd/ml/model_critical_review.qmd §5–6, the shipped shap_mean_z for one HS-6 code \(p\) is:

\[ z_p \;=\; \frac{\overline{|\text{SHAP}_p|} - \mu_{\text{year}}}{\sigma_{\text{year}}} \quad \text{(z-scored per year, then time-averaged)} \]

and the shipped category “need” score for cluster \(c\) within a technology is:

\[ \text{SHAP\%}_{c} \;=\; \frac{\sum_{p \in c} z_p}{\sum_{c'} \sum_{p \in c'} z_p} \times 100 \]

Two distinct operations are stacked here, and they fail for two different reasons.

3.1 Why the sum is dominated by count, not magnitude

This is already fully demonstrated with the shipped data in model_critical_review.qmd §6; reproduced here in miniature because it’s the empirical anchor for everything below.

Code
fc <- feat |> filter(category %in% CATS)
shp <- fc |> group_by(tech, category) |>
  summarise(s = sum(shap_mean_z), m = mean(shap_mean_z), n = n(), .groups = "drop") |>
  group_by(tech) |>
  mutate(pct_sum  = s / sum(s) * 100,
         pct_mean = m / sum(m) * 100,
         pct_cnt  = n / sum(n) * 100) |>
  ungroup()

tibble(
  comparison = c("SHAP% (sum)  vs  feature-count %",
                 "SHAP% (sum)  vs  SHAP% (mean-based)"),
  correlation = c(round(cor(shp$pct_sum, shp$pct_cnt), 3),
                  round(cor(shp$pct_sum, shp$pct_mean), 3))
) |> kable(caption = "The shipped 'need' axis correlates 0.99 with raw code count, ~0 with per-code importance. It is measuring dictionary granularity, not model signal.")
The shipped ‘need’ axis correlates 0.99 with raw code count, ~0 with per-code importance. It is measuring dictionary granularity, not model signal.
comparison correlation
SHAP% (sum) vs feature-count % 0.999
SHAP% (sum) vs SHAP% (mean-based) 0.243
Code
flips <- shp |> group_by(tech) |>
  summarise(`top driver (sum)`  = category[which.max(pct_sum)],
            `top driver (mean)` = category[which.max(pct_mean)], .groups = "drop") |>
  mutate(flipped = ifelse(`top driver (sum)` != `top driver (mean)`, "flips", ""))
n_flip <- sum(flips$flipped == "flips")
flips |> kable(caption = sprintf(
  "Switching sum -> mean flips the identified 'top driver' capability for %d of %d technologies. Not a rounding difference — a different headline finding.",
  n_flip, nrow(flips)))
Switching sum -> mean flips the identified ‘top driver’ capability for 9 of 11 technologies. Not a rounding difference — a different headline finding.
tech top driver (sum) top driver (mean) flipped
Batteries Chemicals Machinery flips
Biofuel Machinery Machinery
EVs Machinery Industrial Materials flips
Electrolyzers Machinery Chemicals flips
Geothermal Machinery Electronics flips
Heat Pumps Machinery Machinery
Magnets Metals Chemicals flips
Nuclear Metals Machinery flips
Solar Chemicals Machinery flips
Transmission Machinery Industrial Materials flips
Wind Machinery Chemicals flips

4 What the group-Shapley literature actually validates

A literature search (Consensus, three queries, ten technologies of ML explainability papers reviewed) was run specifically to check whether z-score-then-sum, or anything resembling it, has a citable precedent. It doesn’t — but the search surfaced the methods family that actually does govern this exact question, and it draws a sharper, stricter line than “sum is wrong, use mean instead.”

4.1 The one theorem that licenses summing at all

groupShapley (Jullum, Redelmeier & Aas, 2021) proves that summing individual feature-level Shapley values within a group equals the true group-level Shapley value — under stated conditions — and shows via simulation what happens when those conditions fail. This is the only theoretical basis in the literature for treating “sum of per-feature SHAP” as a legitimate group score at all.

Two things about that theorem matter for this pipeline:

  1. It is stated for raw Shapley values, because the reason summing can equal a group value is Shapley’s efficiency property: \(\sum_i \phi_i = f(x) - E[f(x)]\) — individual contributions sum exactly to the model’s output. Z-scoring destroys this property before the sum happens. A z-scored value no longer decomposes anything; it’s a standardized rank, not a share of the prediction. The shipped pipeline’s sum is therefore not the groupShapley sum — it has already left the one theoretical foundation that would justify summing, one step before the sum is even taken.
  2. The equivalence holds under feature independence. Aas et al. (2019) and Basu et al. (2020), “Multicollinearity Correction and Combined Feature Effect in Shapley Values” both exist specifically to correct this equivalence when features are correlated — which HS-6 codes inside one shap_category (e.g. all “Machinery” codes for a single technology) are, being complementary, frequently co-exported industrial inputs. This precondition is unlikely to hold here either, independent of the z-score problem.

4.2 What is actually validated for “I want one number per capability cluster”

When the literature’s own stated purpose is exactly ours — a single importance score for a group of features rather than each one individually — the field does not recommend summing (or averaging) pre-computed individual values at all. It recomputes a genuine group-level value by treating the whole cluster as one player in a re-run coalitional game:

  • Group Shapley with Robust Significance Testing (Wang et al., 2025) — reports via Lorenz curves and Gini indices that Group Shapley assigns importance more equitably across groups of different sizes than naive aggregation of individual Shapley values does. This is a direct, independent confirmation of the count-driven bias found empirically in §3 above — a different paper, a different domain (bond recovery-rate prediction), the identical pathology.
  • Baseline Group Shapley for tree models (Xu et al., 2024) — same lineage, specifically for tree ensembles (i.e., directly applicable to a Random Forest, no adaptation needed).
  • Grouped feature importance and combined features effect plot (Au et al., 2021) — surveys the broader family (permutation-based, refitting, Shapley-based) and treats “importance of a feature group” as a distinct estimation problem from single-feature importance, not a summary statistic derived from it.

None of these compute a group score by transforming and combining values that were already computed per-feature. They re-touch the model.

4.3 Independent corroboration that naive SHAP aggregation is a known failure mode

This isn’t a fringe objection specific to trade data. Two 2025 papers make the general case that SHAP-derived rankings are routinely over-trusted without additional statistical scrutiny:

5 The honest comparison table

Common practice (papers Ishana cites + broader literature) Shipped pipeline
Per-feature importance mean(\|SHAP\|), raw units, matches Fig. 4a exactly Same, ✅ no dispute
Combining across a group Only licensed if raw (unstandardized) values, and features independent (groupShapley, Jullum et al. 2021) Z-scored first (breaks efficiency property) — the licensing theorem no longer applies
If features are correlated (likely true here) Recompute a genuine group-level Shapley value against the model (Wang et al. 2025; Xu et al. 2024) Not done at any point — no group-level recomputation exists in the pipeline
Result when done wrong Predicted and measured: importance collapses toward group size (Gini/Lorenz evidence, Wang et al. 2025) Observed directly: 0.99 correlation with code count, top-driver flips sum→mean for most techs

6 What “mean instead of sum” is, and isn’t

The diagnostic already run in model_critical_review.qmd — replacing sum with mean — is a useful stress test: it isolates how much of the axis’s apparent structure is manufactured by cluster size versus signal. It is not, however, the literature’s endorsed fix. It’s a simpler patch that happens to remove the specific artifact demonstrated here. The literature’s actual answer, when it exists, is more work and more rigorous than either sum or mean of pre-computed values: a real Group Shapley computation against the trained Random Forest.

7 Recommendations

  1. Immediate, no rebuild required: stop shipping the z-scored sum as “need” without the raw→z-score step disclosed; the axis currently conflates two independent operations (a defensible importance measure, and an aggregation with no theoretical cover).
  2. If keeping the current axis for now: report it as a breadth measure (“how much of this cluster cleared the importance bar”), not a depth measure (“how decisive this cluster is”) — per model_critical_review.qmd §8.
  3. If a rigorous fix is wanted: implement Group Shapley (Wang et al. 2025) or Baseline Group Shapley for tree models (Xu et al. 2024) directly against the Random Forest — this is the only approach in the literature with a stated justification for producing one number per capability cluster.
  4. Either way: drop the z-score step before any group aggregation. It is the one operation with zero support anywhere in the literature surveyed, on either side of this disagreement.

8 Reproducibility note

model_critical_review.qmd remains the source of the recomputed statistics in §3; this document adds only the literature cross-check (external, not recomputed from repo data) and re-derives the mathematical argument for why the two pathologies (z-scoring, then summing over non-independent features) are separately unsupported. Literature searched via Consensus (three queries, 2026-07-28); citations are abstract-level reads, not full-text-verified — flag before this goes into anything Ishana/Bentley/Alon will scrutinize line-by-line, and pull full text on Jullum et al. (2021) first, since its stated conditions are the crux of point 2 in the comparison table.