Replicating the SHAP Need-Axis from Source

An 11-technology reproduction of the shipped importances, and an EDA of what the pipeline discards — sign, magnitude, and country-year variation

Author

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

Published

July 7, 2026

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

NZ_DARK <- "#073309"; NZ_GREEN <- "#3cb54a"
theme_nr <- function(...) theme_minimal(base_family = "Archivo", base_size = 12) +
  theme(plot.title = element_text(size = 14, face = "bold", colour = NZ_DARK),
        plot.subtitle = element_text(size = 11, colour = "#475569"),
        panel.grid.minor = element_blank(), ...)
theme_set(theme_nr())
CAT_COL <- c(Chemicals="#2563eb", Electronics="#7c3aed", Metals="#ef4444",
             Machinery="#10b981", "Industrial Materials"="#f59e0b", Other="#6b7280", "Non-HS"="#94a3b8")
CATS <- c("Chemicals","Electronics","Industrial Materials","Machinery","Metals")
TECHS <- c("Solar","Wind","Batteries","EVs","Nuclear","Geothermal",
           "Heat Pumps","Electrolyzers","Transmission","Magnets","Biofuel")

ml <- here::here("analysis","ml")
feat  <- read_csv(file.path(ml,"shap_replication_features.csv"), show_col_types = FALSE) |>
  mutate(retained = as.logical(retained), tech = factor(tech, TECHS))
catt  <- read_csv(file.path(ml,"shap_replication_by_category.csv"), show_col_types = FALSE)
diag  <- read_csv(file.path(ml,"shap_replication_diagnostics.csv"), show_col_types = FALSE)
val   <- read_csv(file.path(ml,"shap_replication_validation.csv"), show_col_types = FALSE)
disp  <- read_csv(file.path(ml,"shap_replication_cy_dispersion.csv"), show_col_types = FALSE)
ex    <- read_csv(file.path(ml,"shap_replication_cy_examples.csv"), show_col_types = FALSE)
kept  <- feat |> filter(retained)

What this is. A from-source replication of the Atlas SHAP “need” axis for all 11 shipped technologies, produced by scripts/ml/regen_shap.py off Ishana Ratan’s notebooks (~/Documents/R/CICE), with every hardcoded path repointed and the Random Forests pinned to the notebooks’ own hyperparameters and seed (random_state=28). For each tech the script re-fits the RF, computes SHAP, and writes both the raw signed per-country-year matrix (never persisted upstream) and a faithful reconstruction of the shipped absolute-valued, year-z-scored, 0.5-thresholded shap_mean_z. This note validates the reconstruction against the shipped data/pc/pc_features.csv, then uses the raw SHAP to show what the shipped axis discards: sign, magnitude, and country-year variation. Companion to capability_model_audit.qmd.

1 Did the reproduction work?

Before any critique lands, the reproduction has to earn trust: if our re-fit Random Forest doesn’t reproduce the shipped importances, nothing downstream is credible. The table below is that check, one row per technology. N countries / N predictors size each model’s panel. Class bal. % is the share of country-years that are actually competitive (RCA>1) — around 5%, which tells you the model is learning a rare event, a fact that matters greatly for the sign analysis later. Test F1 / AUC are out-of-sample fit on the held-out 25% split. HS matched (ours/shipped) is how many of our retained HS-code features coincide with the shipped pc_features; max / mean |diff| is how far our reconstructed shap_mean_z lands from the shipped number on those matched codes — zero means bit-for-bit identical.

Code
diag |> left_join(val, by="tech") |>
  transmute(Tech = factor(tech, TECHS), `N countries`=n_countries, `N predictors`=n_predictors,
            `Class bal. %`=class_balance_pct, `Test F1`=test_f1, `Test AUC`=test_auc,
            `HS matched (ours/shipped)` = paste0(n_matched,"/",n_shipped),
            `max |diff|`=max_abs_diff, `mean |diff|`=mean_abs_diff) |>
  arrange(Tech) |>
  kable(caption = "Reproduction fidelity. Every tech's retained HS-code SET matches the shipped set 1:1. Solar is bit-exact (max|diff|=0); the others drift modestly — their <tech>_trade.csv inputs were refreshed ('updated to 2024 models') after the shipped pc_features was built. F1/AUC all sit inside the appendix's stated ranges (F1 0.65-0.89, AUC 0.92-0.98).")
Reproduction fidelity. Every tech’s retained HS-code SET matches the shipped set 1:1. Solar is bit-exact (max|diff|=0); the others drift modestly — their _trade.csv inputs were refreshed (‘updated to 2024 models’) after the shipped pc_features was built. F1/AUC all sit inside the appendix’s stated ranges (F1 0.65-0.89, AUC 0.92-0.98).
Tech N countries N predictors Class bal. % Test F1 Test AUC HS matched (ours/shipped) max |diff| mean |diff|
Solar 155 80 5.84 0.7816 0.9674 34/34 0.000000 0.000000
Wind 155 136 4.81 0.7037 0.9689 50/50 0.113412 0.037384
Batteries 155 93 5.01 0.6857 0.9305 31/31 0.133883 0.031845
EVs 158 197 6.88 0.6667 0.9737 53/53 0.283339 0.086272
Nuclear 155 51 6.25 0.7216 0.9695 22/22 0.154080 0.035296
Geothermal 155 102 13.17 0.8357 0.9720 54/54 0.226161 0.056213
Heat Pumps 155 34 13.96 0.8372 0.9634 26/26 0.122548 0.039322
Electrolyzers 155 76 8.01 0.5524 0.9382 42/42 0.191601 0.037242
Transmission 155 34 20.67 0.8089 0.9519 24/24 0.097247 0.014523
Magnets 155 24 5.16 0.8000 0.9729 8/8 0.083164 0.032510
Biofuel 155 86 21.23 0.7524 0.9343 66/66 0.188475 0.045374

Read: the selection step reproduces exactly everywhere — the same features clear the Cohen’s-d bar for every technology — so the pipeline is faithfully replicated. Solar reproduces the shipped shap_mean_z to six decimals, confirming the shipped artifact came from this exact code path. The small magnitude drift on the other ten is a data-vintage effect, not a method discrepancy; the directional and distributional findings below come from our freshly computed SHAP and don’t depend on it.

2 The two sets of SHAP weights

This table is the conceptual spine of the whole note, so it is worth reading slowly. There are two distinct objects. The raw SHAP (right column) is what the model actually produces: for every country, every year, and every feature, a signed number saying how much that capability pushed that country’s predicted competitiveness up or down. The shipped shap_mean_z (left column) is what the Atlas radar is built from — and it is not the raw SHAP, it is the raw SHAP after three deliberate reductions: take the absolute value (drop the sign), z-score within each year and average (drop the magnitude), and average over all country-years (drop the country dimension). Each row below is one of those reductions. The argument of this note is simply that all three discard information a “what to build” reading needs.

Shipped shap_mean_z (the radar axis) Raw SHAP (what we regenerated)
Granularity one number per feature one signed value per country × year × feature
Sign absolute-valued — unsigned signed (+ toward competitive / − away)
Magnitude z-scored per year → magnitude divided out raw contribution retained
Country dimension averaged over all country-years preserved
Selection thresholded at mean|z| ≥ 0.5 (same features, for comparability)

Three things are stripped between the two columns — sign, magnitude, and the country dimension. The rest of this note quantifies each.

3 EDA 1 — the two importances decouple

If the z-scored axis tracked genuine magnitude, the shipped mean|z| and the raw mean|SHAP| would rank features similarly. They do not.

Code
cor_hs <- kept |> filter(category %in% CATS)
rr <- round(cor(cor_hs$mean_abs_z, cor_hs$raw_mean_abs_shap, method="spearman"), 2)
ggplot(cor_hs, aes(raw_mean_abs_shap, mean_abs_z, colour = category)) +
  geom_point(alpha = .6, size = 1.6) +
  scale_x_log10() + scale_colour_manual(values = CAT_COL, name = NULL) +
  labs(x = "raw mean|SHAP|  (magnitude, log scale)", y = "shipped mean|z|  (radar axis)",
       title = "Shipped importance vs raw magnitude — nearly flat on the y-axis",
       subtitle = paste0("Retained HS features, all 11 techs. Spearman rho = ", rr,
                         ". The z-score compresses every retained feature into a narrow 0.5-0.85 band,")) +
  theme(legend.position = "bottom")

Reading the plot. Every dot is one retained HS feature, pooled across all 11 technologies. The horizontal axis (log scale) is its raw average importance — the size of its SHAP contribution — spanning more than an order of magnitude from left to right. The vertical axis is the shipped importance the radar actually uses. If the two measured the same thing, the cloud would climb from lower-left to upper-right; instead it is almost perfectly flat, and the Spearman rank correlation of just 0.48 confirms the two barely order the features the same way. In words: a feature the model leans on ten times harder than another can end up with the same radar value.

The shipped axis lives in a compressed 0.5–0.85 band regardless of raw magnitude — the Cohen’s-d cut plus z-scoring flatten features that differ by an order of magnitude in raw contribution onto nearly the same value. So when clusters are formed by summing these near-equal values, the cluster totals track feature counts, not importance.

4 EDA 2 — the “most needed” cluster flips for 9 of 11 techs

Because the shipped axis sums near-equal values, the biggest cluster wins; because raw magnitude varies, a different cluster often wins once magnitude is preserved.

Code
topcl <- catt |> group_by(tech) |>
  summarise(`Shipped top (sum of z)` = category[which.max(pct_shipped_sum_z)],
            `Magnitude top (raw mean)` = category[which.max(pct_raw_mean)], .groups="drop") |>
  mutate(Flip = ifelse(`Shipped top (sum of z)`!=`Magnitude top (raw mean)`, "⚠ flips",""),
         tech = factor(tech, TECHS)) |> arrange(tech)
nfl <- sum(topcl$Flip=="⚠ flips")
kable(topcl, caption = sprintf("Top 'need' cluster under the shipped sum-of-z axis vs a magnitude-preserving raw mean|SHAP|. The top priority flips for %d of 11 technologies. The shipped axis makes Machinery the top cluster for 8 of 11 (it holds the most product codes); magnitude spreads the winners across Electronics, Chemicals, Metals, and Industrial Materials.", nfl))
Top ‘need’ cluster under the shipped sum-of-z axis vs a magnitude-preserving raw mean|SHAP|. The top priority flips for 9 of 11 technologies. The shipped axis makes Machinery the top cluster for 8 of 11 (it holds the most product codes); magnitude spreads the winners across Electronics, Chemicals, Metals, and Industrial Materials.
tech Shipped top (sum of z) Magnitude top (raw mean) Flip
Solar Chemicals Electronics ⚠ flips
Wind Machinery Chemicals ⚠ flips
Batteries Machinery Electronics ⚠ flips
EVs Machinery Industrial Materials ⚠ flips
Nuclear Metals Chemicals ⚠ flips
Geothermal Machinery Machinery
Heat Pumps Machinery Machinery
Electrolyzers Machinery Metals ⚠ flips
Transmission Machinery Electronics ⚠ flips
Magnets Metals Chemicals ⚠ flips
Biofuel Machinery Chemicals ⚠ flips
Code
hd <- catt |> select(tech, category, pct_shipped_sum_z, pct_raw_mean) |>
  pivot_longer(c(pct_shipped_sum_z, pct_raw_mean), names_to="axis", values_to="pct") |>
  mutate(axis = recode(axis, pct_shipped_sum_z="Shipped (sum of z)", pct_raw_mean="Magnitude (raw mean)"),
         tech = factor(tech, rev(TECHS)), category = factor(category, CATS))
ggplot(hd, aes(category, tech, fill = pct)) +
  geom_tile(colour="white", linewidth=.6) +
  geom_text(aes(label = ifelse(is.na(pct)|pct<1,"",sprintf("%.0f",pct))), family="Archivo", size=2.7, colour="#0b1a12") +
  facet_wrap(~axis) +
  scale_fill_gradient(low="#f0f7f0", high=NZ_GREEN, na.value="grey93", name="% of need") +
  labs(x=NULL, y=NULL, title="Cluster need under the two aggregations",
       subtitle="Left: shipped sum-of-z lights up the Machinery column (count-driven). Right: raw magnitude redistributes.") +
  theme(axis.text.x = element_text(angle=20, hjust=1), legend.position="bottom")

Reading the table and the heatmap. The table pairs, for each technology, the single top-priority cluster under the shipped axis (summed z-scores) against the top cluster once raw magnitude is preserved; the flag marks disagreement, and they disagree for 9 of 11 technologies. The heatmap shows the full field behind that headline: rows are technologies, the five columns are the capability clusters, and each cell is that cluster’s share of the technology’s total “need.” On the left (shipped) the Machinery column is uniformly bright — Machinery simply contains the most retained product codes, and because summing near-equal z-scores rewards whichever cluster has the most of them, Machinery wins almost everywhere. On the right (raw magnitude) the brightness redistributes toward whichever cluster actually carries the heaviest SHAP contributions, which is frequently a different one. The implication is pointed: the cluster the Atlas presents as a technology’s “most needed” capability is, for most technologies, an artifact of how many products happen to fall in that bucket — and once you correct for it, the headline priority flips.

5 EDA 3 — which weights are negative? (the sign the axis discards)

The shipped axis is |SHAP|, so it cannot tell whether a capability pushes a country toward or away from predicted competitiveness. The raw SHAP is signed, so we can ask directly.

Code
neg <- kept |> summarise(
  `retained features` = n(),
  `net-negative (mean SHAP < 0)` = sum(mean_shap_signed < 0),
  `% net-negative` = round(mean(mean_shap_signed < 0)*100, 1),
  `median % of country-years negative` = round(median(pct_negative), 1))
kable(neg, caption = "Sign of the retained features. Nearly half carry a net-negative mean SHAP, and the median retained feature is negative in ~73% of country-years — all invisible under |SHAP|.")
Sign of the retained features. Nearly half carry a net-negative mean SHAP, and the median retained feature is negative in ~73% of country-years — all invisible under |SHAP|.
retained features net-negative (mean SHAP < 0) % net-negative median % of country-years negative
448 209 46.7 73.3

This small table is the headline of the section: of the features the radar keeps, it counts how many have a net-negative average SHAP, and for the typical feature, in what share of country-years its contribution is negative. Both numbers are large — and both are erased by the |SHAP| step before anyone sees the radar.

Code
p1 <- ggplot(kept, aes(pct_negative, fill = mean_shap_signed < 0)) +
  geom_histogram(bins = 30, colour="white", linewidth=.2) +
  geom_vline(xintercept = 50, linetype="22", colour="#334155") +
  scale_fill_manual(values = c(`TRUE`="#ef4444", `FALSE`="#3cb54a"),
                    labels = c("net positive","net negative"), name=NULL) +
  labs(x="% of country-years with negative SHAP", y="retained features",
       title="Direction is pervasive, not incidental",
       subtitle="Most retained features are negative in the majority of country-years") +
  theme(legend.position = "bottom")
p2 <- ggplot(kept, aes(x = fct_reorder(category, pct_negative), y = pct_negative, fill = category)) +
  geom_boxplot(alpha=.75, outlier.size=.6) + coord_flip() +
  scale_fill_manual(values = CAT_COL, guide="none") +
  labs(x=NULL, y="% country-years negative", title="…across every cluster", subtitle=" ")
p1 | p2

Reading the panels. In the left panel each retained feature contributes one observation; the horizontal axis is the share of its country-years in which its SHAP is negative — i.e. in which having that capability was associated with lower predicted competitiveness — and the dashed line marks 50%. Most of the mass sits to the right of that line, meaning most features are negative more often than not; the bar colour flags whether the feature’s overall average is net-negative. The right panel breaks the same quantity out by cluster, showing the negativity is spread across all five capability groups rather than hiding in one. The takeaway is that direction is not a fringe phenomenon in a few odd features — it is the normal state of most of the axis.

Code
kept |> arrange(mean_shap_signed) |> slice_head(n = 10) |>
  transmute(Tech = tech, Feature = feature, Cluster = category,
            `Mean SHAP (signed)` = round(mean_shap_signed, 4),
            `% country-yrs negative` = round(pct_negative, 0),
            `Shipped mean|z|` = round(mean_abs_z, 2)) |>
  kable(caption = "The ten most net-negative retained features. Each ships as a positive-magnitude 'need'; the sign that says the model reads them as pushing AWAY from competitiveness (on net) is discarded.")
The ten most net-negative retained features. Each ships as a positive-magnitude ‘need’; the sign that says the model reads them as pushing AWAY from competitiveness (on net) is discarded.
Tech Feature Cluster Mean SHAP (signed) % country-yrs negative Shipped mean|z|
Transmission t_854690 Electronics -0.0031 71 0.73
Biofuel nb_940390 Machinery -0.0023 64 0.80
Biofuel nb_844190 Machinery -0.0022 63 0.79
Biofuel b_381590 Chemicals -0.0021 55 0.85
Biofuel b_150790 Other -0.0018 78 0.60
Biofuel b_281121 Chemicals -0.0016 74 0.72
Transmission t_850490 Electronics -0.0016 71 0.82
Geothermal ng_340319 Chemicals -0.0013 82 0.59
Electrolyzers ne_390710 Chemicals -0.0012 72 0.71
Nuclear n_284410 Chemicals -0.0011 88 0.51

Each row here is a concrete example of the reversal: a feature whose average signed contribution is among the most negative of all — the model, on net, reads it as pulling countries away from competitiveness — yet it enters the radar as a positive-magnitude “need,” because |SHAP| keeps only the size of the contribution and drops the minus sign. The last column shows the magnitude these features carry onto the radar despite their negative direction.

How to read the sign. A negative SHAP for a country-year means that country’s value on the feature pushed its predicted competitiveness down. That ~47% of retained features are net-negative — and the median is negative in ~73% of country-years — is partly structural: RCA>1 is rare (~5% base rate), so for the large non-competitive majority most inputs read as “not enough,” i.e. negative. That is exactly why the sign matters: a magnitude-only axis presents “capability the model finds predictive” without saying whether, for a given country, having more of it is associated with closing the gap or not. For a “what to build” reading, that direction is first-order — and it is thrown away.

6 EDA 4 — the mean collapses country-year variation

Each mean|SHAP| (and hence mean|z|) averages over ~3,199 country-years. How much variation does that single number hide?

Code
s <- disp |> summarise(`median CV` = round(median(cv),2),
                       `median max/mean` = round(median(max_over_mean),1),
                       `median top-2% share` = round(median(top2pct_share)*100,0),
                       `median % below ½ mean` = round(median(frac_below_half_mean)*100,0))
p1 <- ggplot(disp, aes(cv)) + geom_histogram(bins=30, fill=NZ_GREEN, colour="white", linewidth=.2) +
  geom_vline(xintercept=1, linetype="22", colour="#334155") +
  annotate("text", x=1.02, y=Inf, vjust=1.5, hjust=0, label="CV = 1\n(spread = mean)", family="Archivo", size=2.6, colour="#334155") +
  labs(x="CV of |SHAP| across country-years", y="retained features",
       title="Importance is not a stable per-feature number",
       subtitle=paste0("Median CV = ", s$`median CV`, " — the spread across countries exceeds the mean"))
p2 <- ggplot(disp, aes(max_over_mean)) + geom_histogram(bins=30, fill="#0d9488", colour="white", linewidth=.2) +
  scale_x_log10() +
  labs(x="(max country-year |SHAP|) / (mean)  — log scale", y="retained features",
       title="…and it peaks far above its own mean",
       subtitle=paste0("Median peak = ", s$`median max/mean`, "× the mean the pipeline reports"))
p1 | p2

Reading the two histograms. Each retained feature contributes one observation to each panel. On the left is the coefficient of variation (CV) of a feature’s |SHAP| across all its country-years — its spread divided by its own mean. A CV above 1 (dashed line) means the feature varies more across countries than its average value is large, and the bulk of features sit there. On the right is how many times bigger the single most-important country-year is than the mean the pipeline reports; a median of roughly 17× says the reported number badly understates the feature’s importance for the country where it matters most. Both panels make the same point from two angles: the one averaged number the pipeline keeps is not a stable property of the feature — it is a smear over wildly unequal country-years.

Code
kable(s, caption = "The single mean|SHAP| collapses a heavy-tailed country-year distribution: spread larger than the center (CV~1.5), a peak ~17x the mean, ~17% of the mass in the top 2% of country-years, and ~40% of country-years below half the mean.")
The single mean|SHAP| collapses a heavy-tailed country-year distribution: spread larger than the center (CV~1.5), a peak ~17x the mean, ~17% of the mass in the top 2% of country-years, and ~40% of country-years below half the mean.
median CV median max/mean median top-2% share median % below ½ mean
1.46 17.3 17 41

The distribution is heavy-tailed and country-specific. Concretely, for the top retained features of three technologies, the per-country-year |SHAP| fans out across two orders of magnitude while the pipeline reports only the diamond:

Code
exd <- ex |> mutate(lab = paste0(str_trunc(feature, 12), "\n", category))
ggplot(exd, aes(x = reorder(lab, abs_shap, FUN=median), y = abs_shap + 1e-6)) +
  geom_jitter(aes(colour = category), width=.18, alpha=.18, size=.5) +
  stat_summary(fun = mean, geom = "point", shape=18, size=4, colour = NZ_DARK) +
  scale_y_log10() + coord_flip() +
  scale_colour_manual(values = CAT_COL, guide="none") +
  facet_wrap(~tech, scales="free_y") +
  labs(x=NULL, y="|SHAP| per country-year (log)  ·  diamond = the mean the axis keeps",
       title="What the mean throws away",
       subtitle="Each point is one country-year; the axis reports only the diamond. Importance is concentrated in a few countries.")

Implication. The radar’s “need” is a portfolio average that fits almost no individual country — it over-credits the ~40% of countries for which a feature is near-inert and under-credits ~17× the country for which it is decisive. Since a country’s gap analysis is inherently about that country, the need axis would be far more informative computed per country from its own SHAP row than from the pooled mean. (The concentration itself is expected — RCA>1 is rare so the model leans on the few threshold countries — which is the point: the country dimension carries the signal, and averaging discards it.)

7 EDA 5 — count-driven, confirmed from source

Closing the loop on the audit’s central claim, now on regenerated data: the shipped cluster axis is almost perfectly the product count.

Code
cc <- catt |> group_by(tech) |>
  mutate(count_share = n_features/sum(n_features, na.rm=TRUE)*100) |> ungroup() |>
  filter(!is.na(pct_shipped_sum_z))
rc <- round(cor(cc$pct_shipped_sum_z, cc$count_share), 3)
ggplot(cc, aes(count_share, pct_shipped_sum_z, colour = category)) +
  geom_abline(slope=1, intercept=0, linetype="22", colour="grey70") +
  geom_point(alpha=.8, size=2) +
  scale_colour_manual(values = CAT_COL, name=NULL) +
  labs(x="cluster's share of retained product COUNT (%)", y="shipped need share (sum of z, %)",
       title=paste0("The shipped need axis is the product count (r = ", rc, ")"),
       subtitle="Every point is a tech × cluster; the dashed line is exact equality") +
  theme(legend.position="bottom")

Reading the plot. Each point is one technology × cluster. The horizontal axis is that cluster’s share of the retained product count — literally the fraction of the technology’s kept HS codes that fall in it — and the vertical axis is its share of the shipped “need.” The dashed line is exact equality. The points sit almost on it (r = 0.999), which means the need a cluster is assigned is nearly a deterministic function of how many products it contains. This is the mechanical endpoint of EDA 1: once the year-z-score flattens every retained feature to nearly the same value, summing those values within a cluster is arithmetically almost the same as counting them. Implication: the shipped axis is, to a very good approximation, answering “which cluster holds the most HS codes” — a question about the composition of the green dictionary, not about which capability a technology most needs. That gap between the two questions is the entire reason this note exists.

8 Assessment — how the two weight sets differ

The reproduction settles the factual questions and sharpens the audit:

  1. Selection is faithful; magnitudes are vintage-sensitive. The retained feature set reproduces 1:1 for all 11 techs (Solar bit-exact), so the model and its Cohen’s-d selection are exactly as documented. Only the precise z-scored magnitudes drift on the ten techs whose trade inputs were refreshed after shipping — a reason to rebuild pc_features from the current inputs, not a method flaw.

  2. The shipped axis and a magnitude axis disagree about priorities. Shipped mean|z| decouples from raw mean|SHAP| (low rank correlation), so the sum-by-cluster axis tracks product counts (r ≈ 0.999) and the top-priority cluster flips for 9 of 11 technologies once magnitude is preserved. The shipped axis systematically favours Machinery, the largest-count cluster.

  3. Sign is discarded. ~47% of retained features carry a net-negative mean SHAP (median feature negative in ~73% of country-years). The |SHAP| step erases whether a capability, for a given country, is associated with closing the competitiveness gap — first-order information for a “what to build” reading.

  4. Country-year variation is collapsed. The mean hides a heavy-tailed, country-specific distribution (CV ≈ 1.5; peak ≈ 17× the mean; ~40% of country-years below half the mean). The country-invariant “standard profile” fits few actual countries.

Bottom line. The shipped shap_mean_z is a faithful feature-selection summary — but as a quantitative, country-facing need axis it has been stripped of the three things that reading needs: magnitude (→ it counts products), sign (→ it can’t say help vs. hurt), and the country dimension (→ it fits no one country). All three are recoverable from the raw signed SHAP regenerated here (analysis/ml/raw_signed/), which is exactly what a magnitude-preserving, signed, per-country need axis would be built from.

Constructive follow-on → target_profile.qmd. This note is diagnostic; the companion target_profile.qmd builds the fix. It shows (a) a common “standard profile” is defensible — countries rank-agree on the shape of each tech’s needs (median ρ ≈ 0.89), so EDA 4’s dispersion is a scale fact for the gap layer, not a refutation of the target; and (b) the negative signs above are a base-rate artifact, not “avoid” — split by the RCA>1 class, zero of the retained radar features point away among competitive producers. It then rebuilds the axis from the winners’ signed SHAP (magnitude- and direction-preserving), which moves the top-priority cluster for 5 of 11 techs.

9 Reproducibility

scripts/ml/regen_shap.py --tech <Tech> regenerates every artifact under analysis/ml/ from the CICE clone (~/Documents/R/CICE); requirements.txt pins the environment. The per-tech config (input file, target dv_*, excluded_cols, pinned n_estimators/max_depth) is transcribed verbatim from the notebooks; seed 28 is used throughout. Re-run against a refreshed clone to update the fidelity check.