Each focal country’s recent standing against the common target profile — how far, and in which direction. The country layer of the target-profile work.
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); library(tibble)font_add_google("Archivo", "Archivo"); showtext_auto(); showtext_opts(dpi =200)NZ_DARK <-"#073309"; NZ_GREEN <-"#3cb54a"; NZ_TEAL <-"#0d9488"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")CATS <-c("Chemicals","Electronics","Industrial Materials","Machinery","Metals")CATS_SHORT <-c(Chemicals="Chem", Electronics="Elec", "Industrial Materials"="Ind.M",Machinery="Mach", Metals="Met")TECHS <-c("Solar","Wind","Batteries","EVs","Nuclear","Geothermal","Heat Pumps","Electrolyzers","Transmission","Magnets","Biofuel")FOCAL <-c("Brazil","India","Mexico","Japan","Vietnam","Spain")# tech colour palette (matches the Atlas) — used to paint each tech's own "need" profileTECH_COLORS <-c(Solar="#eab308", Nuclear="#0ea5e9", Batteries="#f59e0b", Transmission="#06b6d4",Wind="#3b82f6", Biofuel="#84cc16", Electrolyzers="#a855f7", Geothermal="#ef4444","Heat Pumps"="#f97316", Magnets="#ec4899", EVs="#14b8a6")CGREEN <-"#22c55e"# one shiny green for "what the country actually holds"# tidytext::reorder_within shim (avoid the dependency)reorder_within <-function(x, by, within, sep="___") stats::reorder(paste(x, within, sep=sep), by)gap <-read_csv(here::here("analysis","ml","shap_gap_by_country_cluster.csv"), show_col_types =FALSE) |>mutate(tech =factor(tech, TECHS), country =factor(country, FOCAL),cluster =factor(cluster, CATS),# % of target reached, capped. coalesce: when a tech has NO retained features in a# cluster its target weight is 0, so achieved is 0/0=NaN — treat that axis as 0 so the# pentagon closes cleanly instead of breaking into a sliver.ach =pmin(pmax(coalesce(achieved, 0), 0), 1))front <- gap |>group_by(country, tech) |>summarise(frontier =sum(pmax(country_signed, 0)) /sum(target_weight), .groups ="drop")
Where this sits.target_profile.qmd established the target layer — a defensible common per-tech capability profile (the winners’ recipe). This is the gap layer: for each of the six focal countries, its own recent signed SHAP (option A: mean over the last 3 available years) read against that target, per capability cluster. The target is a shared shape, but countries differ enormously in level and direction — which is exactly what the gap surfaces. Built by scripts/ml/target_profile_prep.py from analysis/ml/raw_signed/; no model re-fit.
1 How to read the gap
The whole point of this note is to make one question concrete: if this country wanted to become competitive in this technology, what would it build? The target-profile note gave us the destination — a common per-tech “need” profile, the winners’ recipe across the five capability clusters. Here we lay each country’s own recent capability (its mean signed SHAP over the last three years, per cluster) against that destination. Everything below is two numbers per country × technology × cluster — what the technology needs and what the country currently has — shown two complementary ways.
The building block: an achievement fraction. For each cluster we form
\[\text{achieved} \;=\; \frac{\text{country's recent signed SHAP in the cluster}}{\text{target (winners') weight in the cluster}}\]
read as the share of the frontier the country has reached there. A value of 1.0 means “at the frontier” (the country already has the competitive producers’ level of that capability); below 1.0 is a gap — how far there is to build; above 1.0 is a surplus — capability beyond what the target needs, a candidate to leverage or redeploy.
Two radar views, because they answer different questions. We draw the same data both ways on purpose:
The relative radar normalises every cluster by its own target, so the outer pentagon is always the frontier (100%) and the green fill is the fraction reached. This view puts every technology on the same 0–100% footing, which is what you want when ranking where a country is close versus distant, and when reading build priorities off the short axes. Its cost: it hides how big the underlying need is — a near-full pentagon on a tiny-need technology and on a huge-need one look alike.
The absolute radar keeps the raw magnitudes. It overlays two shapes on a shared per-panel scale: the technology’s need profile, drawn in that technology’s own colour, and what the country actually holds, in green. This view restores the sense of scale the relative view drops — you see the shape of the need and how much of it the country fills — and it reads especially well for technologies where the country is far behind (a large coloured need with only a sliver of green inside).
Together they make the argument the audit set up: because the target is a shared shape (countries rank-agree at ρ≈0.89), what distinguishes countries is level and direction — how full the radar is, and which axes are short — precisely the magnitude and country-specific signal the shipped count-driven radar threw away.
2 Distance to the frontier — the whole board
Before the cluster-by-cluster detail, it helps to see the whole map at once. Collapsing all five clusters into a single number per country × technology — the total share of the target the country has reached — gives an executive summary: which technologies are within a country’s reach, and which are distant. This is the “where to look” view; the radars that follow are the “why.”
Code
hd <- front |>mutate(country =factor(country, rev(FOCAL)))ggplot(hd, aes(tech, country, fill =pmin(frontier, 1.2))) +geom_tile(colour ="white", linewidth = .6) +geom_text(aes(label =sprintf("%.2f", frontier)), family ="Archivo", size =2.6,colour =ifelse(hd$frontier > .6, "white", "#0b1a12")) +scale_fill_gradientn(colours =c("#f7f9f7", "#bfe6c4", NZ_GREEN, NZ_DARK),values =c(0, .4, .8, 1), name ="share of\ntarget reached",limits =c(0, 1.2)) +labs(x =NULL, y =NULL, title ="Distance to the frontier — focal country × technology",subtitle ="Overall share of the target capability each country has reached (recent 3 yrs). 1.0 = at the winners' level; >1 shaded darkest.") +theme(axis.text.x =element_text(angle =25, hjust =1), legend.position ="right")
Code
ggplot(front, aes(x =reorder(interaction(country, tech), frontier), y =pmin(frontier,1.3), fill = country)) +geom_col(width=.7, show.legend =FALSE) +coord_flip() +facet_wrap(~country, scales ="free_y", ncol =3) +geom_hline(yintercept =1, linetype ="22", colour ="#334155") +scale_x_discrete(labels =function(x) sub("\\..*","", sub("^[^.]*\\.","",x))) +labs(x =NULL, y ="share of target reached (capped 1.3; dashed = frontier)",title ="Each country's technologies, nearest-to-frontier first",subtitle ="Where a country is already competitive (≥1) vs where the build is largest (→0).") +theme(strip.text =element_text(face="bold", colour=NZ_DARK), axis.text.y =element_text(size=7))
Reading the board. In the heatmap, rows are countries and columns technologies; each cell is that country’s overall share of the technology’s target — dark green means at or beyond the frontier, pale means a long way to go. The companion panel simply re-sorts the same numbers within each country, so a country’s technologies line up from “already competitive” (bars past the dashed line) to “largest build ahead” (bars near zero). Read together, they turn a country’s industrial-policy question into a short list: the bars nearest the frontier are where an existing base can be defended and extended; the ones near zero are green-field bets. The radars in the next two sections open up any one of these cells to show which capabilities drive the number.
3 Gap radars — relative view (share of the frontier reached)
This is the “how close, and where” view. In every panel the dashed pentagon is the target frontier (100% on all five clusters), and the green fill is the fraction the country has reached on each — so a full pentagon means “already at the winners’ level,” and a short spoke is a build priority. Because every technology is normalised to its own frontier, panels are directly comparable across technologies: a nearly full Solar and a nearly empty Wind tell you, at a glance, where a country’s base is strong and where it starts from scratch. The trade-off, addressed by the absolute view that follows, is that this normalisation hides how large each technology’s underlying need is.
Code
ang <-tibble(cluster =factor(CATS, CATS), a = pi/2-2*pi*(0:4)/5)# close a per-tech polygon back to its first vertex so the pentagon draws shutclose_poly <-function(d, grp) bind_rows(d, d |>filter(cluster == CATS[1]) |>mutate(o =6L)) |>arrange(across(all_of(grp)), o)# shared pentagon grid (rings + spokes + axis labels) for a set of techsgrid_layers <-function(techs) { rings <-expand_grid(tech =factor(techs, TECHS), ring =c(.33,.66,1), cluster =factor(CATS,CATS)) |>left_join(ang, by="cluster") |>mutate(x = ring*cos(a), y = ring*sin(a), o =as.integer(cluster))list(ringsc =bind_rows(rings, rings |>filter(cluster==CATS[1]) |>mutate(o=6L)) |>arrange(tech, ring, o),spokes =expand_grid(tech =factor(techs, TECHS), cluster =factor(CATS,CATS)) |>left_join(ang, by="cluster") |>mutate(x =cos(a), y =sin(a)),labs =expand_grid(tech =factor(techs, TECHS), cluster =factor(CATS,CATS)) |>left_join(ang, by="cluster") |>mutate(x =1.28*cos(a), y =1.28*sin(a),lab = CATS_SHORT[as.character(cluster)]))}base_grid <-function(g)list(geom_path(data = g$ringsc, aes(x,y,group=interaction(tech,ring)), colour="grey88", linewidth=.3),geom_segment(data = g$spokes, aes(0,0,xend=x,yend=y), colour="grey88", linewidth=.3),geom_text(data = g$labs, aes(x,y,label=lab), family="Archivo", size=2.1, colour="#475569"))radar_theme <-function(ttl)list(facet_wrap(~tech, ncol=4), coord_equal(clip="off"), xlim(-1.5,1.5), ylim(-1.4,1.4),labs(title = ttl), theme_void(base_family="Archivo"),theme(strip.text =element_text(face="bold", colour=NZ_DARK, size=10),plot.title =element_text(face="bold", colour=NZ_DARK, size=12.5), legend.position="none"))# RELATIVE view — outer pentagon = frontier (100%); green fill = share of it reachedrel_radar <-function(cty) { d <- gap |>filter(country == cty) |>left_join(ang, by ="cluster") |>mutate(x = ach*cos(a), y = ach*sin(a), o =as.integer(cluster)) poly <-close_poly(d, "tech") g <-grid_layers(levels(droplevels(d$tech))); frame <- g$ringsc |>filter(ring ==1)ggplot() +base_grid(g) +geom_polygon(data = frame, aes(x,y,group=tech), fill=NA, colour="#94a3b8", linewidth=.7, linetype="22") +geom_polygon(data = poly, aes(x,y,group=tech), fill=NZ_GREEN, colour=NZ_TEAL, alpha=.42, linewidth=.7) +radar_theme(paste0(cty, " — RELATIVE: share of the frontier reached (dashed pentagon = 100% target)"))}# ABSOLUTE view — the need in each tech's own colour vs what the country holds in green,# both on a shared per-panel scale so the shapes (and any surplus) are directly comparableabs_radar <-function(cty) { d <- gap |>filter(country == cty) |>transmute(tech, cluster, need = target_weight, hold =pmax(country_signed, 0)) |>pivot_longer(c(need, hold), names_to ="series", values_to ="val") |>group_by(tech) |>mutate(r =coalesce(val /max(val, na.rm=TRUE), 0)) |>ungroup() |>left_join(ang, by ="cluster") |>mutate(x = r*cos(a), y = r*sin(a), o =as.integer(cluster),fillc =ifelse(series =="need", TECH_COLORS[as.character(tech)], CGREEN)) polyN <-close_poly(d |>filter(series =="need"), "tech") polyH <-close_poly(d |>filter(series =="hold"), "tech") g <-grid_layers(levels(droplevels(factor(d$tech, TECHS))))ggplot() +base_grid(g) +geom_polygon(data = polyN, aes(x,y,group=tech, fill=fillc, colour=fillc), alpha=.16, linewidth=.9) +geom_polygon(data = polyH, aes(x,y,group=tech), fill=CGREEN, colour="#15803d", alpha=.5, linewidth=.6) +scale_fill_identity() +scale_colour_identity() +radar_theme(paste0(cty, " — ABSOLUTE: the need (tech colour) vs what the country holds (green)"))}
4 Gap radars — absolute view (the need vs what the country holds)
The relative view above deliberately erased scale so technologies could be compared; this view puts it back. In each panel the coloured pentagon is the technology’s need profile — the winners’ recipe, drawn in that technology’s own Atlas colour — and the green shape is what the country actually holds (its recent positive signed SHAP per cluster), both on a shared per-panel scale. Now the reading is literal: the coloured boundary is the target shape, and the green shows how much of it is filled. Where green falls short of the colour, that is the build; where green pokes beyond it, a surplus. This view earns its keep exactly where the relative one struggles — a technology a country has barely entered shows as a full coloured need with only a sliver of green inside, which is far more legible than a near-empty pentagon. Read the two views as a pair: the relative one ranks how close, the absolute one shows the shape of what is missing.
Positive = deficit (build); negative = surplus (leverage / rotate). Sorted by the largest single-cluster deficit per country.
Code
pb <- gap |>mutate(country =factor(country, FOCAL)) |>group_by(country) |>slice_max(order_by = gap, n =8, with_ties =FALSE) |>ungroup() |>mutate(lab =paste0(tech, " · ", CATS_SHORT[as.character(cluster)]))ggplot(pb, aes(x =reorder_within(lab, gap, country), y = gap, fill = cluster)) +geom_col(width=.72) +coord_flip() +facet_wrap(~country, scales="free_y", ncol=2) +scale_fill_manual(values = CAT_COL, name =NULL) +scale_x_discrete(labels =function(x) sub("___.*","",x)) +labs(x=NULL, y="target − country (deficit to build)",title="Top build priorities per focal country",subtitle="The largest cluster-level gaps to the winners' recipe (recent 3 yrs).") +theme(legend.position="bottom", strip.text=element_text(face="bold",colour=NZ_DARK),axis.text.y=element_text(size=7.5))
Reading the bars. The radars show a country’s whole position at a glance; this chart distils it into an ordered to-do list. Each bar is one technology × cluster, its length the raw deficit target − country — how much capability separates the country from the winners’ recipe in that specific cluster — and the eight longest per country are kept. A tall bar is where the single biggest, most concrete build lies (e.g. “Solar · Electronics” for a machinery-heavy economy); a bar that dips below zero is a surplus the country could lean on. This is the step from diagnosis to agenda: the frontier board says which technologies to prioritise, the radars say why, and these bars name the specific capability clusters a policy would target first.
6 Reading the gaps — three worked cases
The cluster tables below zoom all the way in on three country × technology pairs, so the numbers behind the shapes are visible. Read each as: the Target column is the winners’ recipe (what the technology needs), Country (recent) is what the country holds, and Achieved is their ratio — the same achievement fraction the radars draw, now as a figure you can quote.
Code
show_case <-function(cty, tch) gap |>filter(country==cty, tech==tch) |>transmute(Cluster=cluster, `Country (recent)`=round(country_signed,4), `Target`=round(target_weight,4),`Achieved`=sprintf("%.0f%%", pmin(achieved,9)*100),Read =case_when(achieved>=1.2~"surplus — leverage", achieved>=.8~"at frontier", achieved>=.4~"partial — build", TRUE~"deep gap — priority")) |>arrange(desc(`Target`))kable(show_case("India","Solar"), caption="India · Solar — machinery-heavy economy, but the Solar target is Electronics-led: Electronics is the deep gap, Machinery a surplus.")
India · Solar — machinery-heavy economy, but the Solar target is Electronics-led: Electronics is the deep gap, Machinery a surplus.
How to use this. For any focal country, the radar shows how competitive its capability base already is per technology (fullness) and where the model says the missing pieces are (short axes) — read against a target that is the same for everyone, so the comparison is apples-to-apples. The build-priority bars turn that into a ranked list; the frontier board shows which technologies are within reach vs distant. Unlike the shipped count-driven radar, this is magnitude-preserving, signed, and country-specific — the three things the audit flagged as missing, now supplied by the raw SHAP.
7 Reproducibility
scripts/ml/target_profile_prep.py writes analysis/ml/shap_gap_by_country_cluster.csv (per country × tech × cluster: country recent-3yr signed SHAP, target weight, achieved fraction, gap) from analysis/ml/raw_signed/ and the winners’-recipe target. Country value uses the last 3 available years (option A). No model re-fit.
Source Code
---title: "The Capability Gap"subtitle: "Each focal country's recent standing against the common target profile — how far, and in which direction. The country layer of the target-profile work."author: "Net Zero Industrial Policy Lab (NZIPL) · Johns Hopkins SAIS"date: todayformat: html: toc: true toc-depth: 3 toc-location: left toc-title: "Contents" number-sections: true self-contained: true page-layout: full fig-width: 10 fig-height: 5.5 code-fold: true code-tools: true theme: [cosmo, ../nzipl.scss] grid: sidebar-width: 270px body-width: 1080px docx: toc: true number-sections: trueexecute-dir: projectexecute: echo: true warning: false message: false---```{r setup}#| code-fold: truelibrary(readr); library(dplyr); library(tidyr); library(stringr); library(forcats)library(ggplot2); library(patchwork); library(knitr); library(showtext); library(tibble)font_add_google("Archivo", "Archivo"); showtext_auto(); showtext_opts(dpi =200)NZ_DARK <-"#073309"; NZ_GREEN <-"#3cb54a"; NZ_TEAL <-"#0d9488"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")CATS <-c("Chemicals","Electronics","Industrial Materials","Machinery","Metals")CATS_SHORT <-c(Chemicals="Chem", Electronics="Elec", "Industrial Materials"="Ind.M",Machinery="Mach", Metals="Met")TECHS <-c("Solar","Wind","Batteries","EVs","Nuclear","Geothermal","Heat Pumps","Electrolyzers","Transmission","Magnets","Biofuel")FOCAL <-c("Brazil","India","Mexico","Japan","Vietnam","Spain")# tech colour palette (matches the Atlas) — used to paint each tech's own "need" profileTECH_COLORS <-c(Solar="#eab308", Nuclear="#0ea5e9", Batteries="#f59e0b", Transmission="#06b6d4",Wind="#3b82f6", Biofuel="#84cc16", Electrolyzers="#a855f7", Geothermal="#ef4444","Heat Pumps"="#f97316", Magnets="#ec4899", EVs="#14b8a6")CGREEN <-"#22c55e"# one shiny green for "what the country actually holds"# tidytext::reorder_within shim (avoid the dependency)reorder_within <-function(x, by, within, sep="___") stats::reorder(paste(x, within, sep=sep), by)gap <-read_csv(here::here("analysis","ml","shap_gap_by_country_cluster.csv"), show_col_types =FALSE) |>mutate(tech =factor(tech, TECHS), country =factor(country, FOCAL),cluster =factor(cluster, CATS),# % of target reached, capped. coalesce: when a tech has NO retained features in a# cluster its target weight is 0, so achieved is 0/0=NaN — treat that axis as 0 so the# pentagon closes cleanly instead of breaking into a sliver.ach =pmin(pmax(coalesce(achieved, 0), 0), 1))front <- gap |>group_by(country, tech) |>summarise(frontier =sum(pmax(country_signed, 0)) /sum(target_weight), .groups ="drop")```::: {.callout-note appearance="minimal"}**Where this sits.** `target_profile.qmd` established the **target** layer — a defensible common per-tech capability profile (the winners' recipe). This is the **gap** layer: for each of the six focal countries, its *own* recent signed SHAP (option A: mean over the last 3 available years) read against that target, per capability cluster. The target is a shared *shape*, but countries differ enormously in *level and direction* — which is exactly what the gap surfaces. Built by `scripts/ml/target_profile_prep.py` from `analysis/ml/raw_signed/`; no model re-fit.:::# How to read the gapThe whole point of this note is to make one question concrete: *if this country wanted to become competitive in this technology, what would it build?* The target-profile note gave us the destination — a common per-tech "need" profile, the winners' recipe across the five capability clusters. Here we lay each country's *own* recent capability (its mean signed SHAP over the last three years, per cluster) against that destination. Everything below is two numbers per country × technology × cluster — **what the technology needs** and **what the country currently has** — shown two complementary ways.**The building block: an achievement fraction.** For each cluster we form$$\text{achieved} \;=\; \frac{\text{country's recent signed SHAP in the cluster}}{\text{target (winners') weight in the cluster}}$$read as *the share of the frontier the country has reached there*. A value of **1.0 means "at the frontier"** (the country already has the competitive producers' level of that capability); **below 1.0 is a gap** — how far there is to build; **above 1.0 is a surplus** — capability beyond what the target needs, a candidate to leverage or redeploy.**Two radar views, because they answer different questions.** We draw the same data both ways on purpose:- The **relative** radar normalises every cluster by its own target, so the outer pentagon is *always* the frontier (100%) and the green fill is the fraction reached. This view puts every technology on the same 0–100% footing, which is what you want when *ranking* where a country is close versus distant, and when reading build priorities off the short axes. Its cost: it hides how big the underlying need is — a near-full pentagon on a tiny-need technology and on a huge-need one look alike.- The **absolute** radar keeps the raw magnitudes. It overlays two shapes on a shared per-panel scale: the technology's **need profile, drawn in that technology's own colour**, and **what the country actually holds, in green**. This view restores the sense of scale the relative view drops — you see the *shape* of the need and how much of it the country fills — and it reads especially well for technologies where the country is far behind (a large coloured need with only a sliver of green inside).Together they make the argument the audit set up: because the target is a shared *shape* (countries rank-agree at ρ≈0.89), what distinguishes countries is **level and direction** — how full the radar is, and which axes are short — precisely the magnitude and country-specific signal the shipped count-driven radar threw away.# Distance to the frontier — the whole boardBefore the cluster-by-cluster detail, it helps to see the whole map at once. Collapsing all five clusters into a single number per country × technology — the total share of the target the country has reached — gives an executive summary: which technologies are within a country's reach, and which are distant. This is the "where to look" view; the radars that follow are the "why."```{r frontier-heat, fig.height=4.6}#| code-fold: truehd <- front |>mutate(country =factor(country, rev(FOCAL)))ggplot(hd, aes(tech, country, fill =pmin(frontier, 1.2))) +geom_tile(colour ="white", linewidth = .6) +geom_text(aes(label =sprintf("%.2f", frontier)), family ="Archivo", size =2.6,colour =ifelse(hd$frontier > .6, "white", "#0b1a12")) +scale_fill_gradientn(colours =c("#f7f9f7", "#bfe6c4", NZ_GREEN, NZ_DARK),values =c(0, .4, .8, 1), name ="share of\ntarget reached",limits =c(0, 1.2)) +labs(x =NULL, y =NULL, title ="Distance to the frontier — focal country × technology",subtitle ="Overall share of the target capability each country has reached (recent 3 yrs). 1.0 = at the winners' level; >1 shaded darkest.") +theme(axis.text.x =element_text(angle =25, hjust =1), legend.position ="right")``````{r frontier-rank, fig.height=4.2}#| code-fold: trueggplot(front, aes(x =reorder(interaction(country, tech), frontier), y =pmin(frontier,1.3), fill = country)) +geom_col(width=.7, show.legend =FALSE) +coord_flip() +facet_wrap(~country, scales ="free_y", ncol =3) +geom_hline(yintercept =1, linetype ="22", colour ="#334155") +scale_x_discrete(labels =function(x) sub("\\..*","", sub("^[^.]*\\.","",x))) +labs(x =NULL, y ="share of target reached (capped 1.3; dashed = frontier)",title ="Each country's technologies, nearest-to-frontier first",subtitle ="Where a country is already competitive (≥1) vs where the build is largest (→0).") +theme(strip.text =element_text(face="bold", colour=NZ_DARK), axis.text.y =element_text(size=7))```**Reading the board.** In the heatmap, rows are countries and columns technologies; each cell is that country's overall share of the technology's target — dark green means at or beyond the frontier, pale means a long way to go. The companion panel simply re-sorts the same numbers within each country, so a country's technologies line up from "already competitive" (bars past the dashed line) to "largest build ahead" (bars near zero). Read together, they turn a country's industrial-policy question into a short list: the bars nearest the frontier are where an existing base can be defended and extended; the ones near zero are green-field bets. The radars in the next two sections open up any one of these cells to show *which capabilities* drive the number.# Gap radars — relative view (share of the frontier reached)This is the "how close, and where" view. In every panel the dashed pentagon is the target frontier (100% on all five clusters), and the green fill is the fraction the country has reached on each — so a full pentagon means "already at the winners' level," and a short spoke is a build priority. Because every technology is normalised to its own frontier, panels are directly comparable *across* technologies: a nearly full Solar and a nearly empty Wind tell you, at a glance, where a country's base is strong and where it starts from scratch. The trade-off, addressed by the absolute view that follows, is that this normalisation hides *how large* each technology's underlying need is.```{r radar-helper}#| code-fold: trueang <-tibble(cluster =factor(CATS, CATS), a = pi/2-2*pi*(0:4)/5)# close a per-tech polygon back to its first vertex so the pentagon draws shutclose_poly <-function(d, grp) bind_rows(d, d |>filter(cluster == CATS[1]) |>mutate(o =6L)) |>arrange(across(all_of(grp)), o)# shared pentagon grid (rings + spokes + axis labels) for a set of techsgrid_layers <-function(techs) { rings <-expand_grid(tech =factor(techs, TECHS), ring =c(.33,.66,1), cluster =factor(CATS,CATS)) |>left_join(ang, by="cluster") |>mutate(x = ring*cos(a), y = ring*sin(a), o =as.integer(cluster))list(ringsc =bind_rows(rings, rings |>filter(cluster==CATS[1]) |>mutate(o=6L)) |>arrange(tech, ring, o),spokes =expand_grid(tech =factor(techs, TECHS), cluster =factor(CATS,CATS)) |>left_join(ang, by="cluster") |>mutate(x =cos(a), y =sin(a)),labs =expand_grid(tech =factor(techs, TECHS), cluster =factor(CATS,CATS)) |>left_join(ang, by="cluster") |>mutate(x =1.28*cos(a), y =1.28*sin(a),lab = CATS_SHORT[as.character(cluster)]))}base_grid <-function(g)list(geom_path(data = g$ringsc, aes(x,y,group=interaction(tech,ring)), colour="grey88", linewidth=.3),geom_segment(data = g$spokes, aes(0,0,xend=x,yend=y), colour="grey88", linewidth=.3),geom_text(data = g$labs, aes(x,y,label=lab), family="Archivo", size=2.1, colour="#475569"))radar_theme <-function(ttl)list(facet_wrap(~tech, ncol=4), coord_equal(clip="off"), xlim(-1.5,1.5), ylim(-1.4,1.4),labs(title = ttl), theme_void(base_family="Archivo"),theme(strip.text =element_text(face="bold", colour=NZ_DARK, size=10),plot.title =element_text(face="bold", colour=NZ_DARK, size=12.5), legend.position="none"))# RELATIVE view — outer pentagon = frontier (100%); green fill = share of it reachedrel_radar <-function(cty) { d <- gap |>filter(country == cty) |>left_join(ang, by ="cluster") |>mutate(x = ach*cos(a), y = ach*sin(a), o =as.integer(cluster)) poly <-close_poly(d, "tech") g <-grid_layers(levels(droplevels(d$tech))); frame <- g$ringsc |>filter(ring ==1)ggplot() +base_grid(g) +geom_polygon(data = frame, aes(x,y,group=tech), fill=NA, colour="#94a3b8", linewidth=.7, linetype="22") +geom_polygon(data = poly, aes(x,y,group=tech), fill=NZ_GREEN, colour=NZ_TEAL, alpha=.42, linewidth=.7) +radar_theme(paste0(cty, " — RELATIVE: share of the frontier reached (dashed pentagon = 100% target)"))}# ABSOLUTE view — the need in each tech's own colour vs what the country holds in green,# both on a shared per-panel scale so the shapes (and any surplus) are directly comparableabs_radar <-function(cty) { d <- gap |>filter(country == cty) |>transmute(tech, cluster, need = target_weight, hold =pmax(country_signed, 0)) |>pivot_longer(c(need, hold), names_to ="series", values_to ="val") |>group_by(tech) |>mutate(r =coalesce(val /max(val, na.rm=TRUE), 0)) |>ungroup() |>left_join(ang, by ="cluster") |>mutate(x = r*cos(a), y = r*sin(a), o =as.integer(cluster),fillc =ifelse(series =="need", TECH_COLORS[as.character(tech)], CGREEN)) polyN <-close_poly(d |>filter(series =="need"), "tech") polyH <-close_poly(d |>filter(series =="hold"), "tech") g <-grid_layers(levels(droplevels(factor(d$tech, TECHS))))ggplot() +base_grid(g) +geom_polygon(data = polyN, aes(x,y,group=tech, fill=fillc, colour=fillc), alpha=.16, linewidth=.9) +geom_polygon(data = polyH, aes(x,y,group=tech), fill=CGREEN, colour="#15803d", alpha=.5, linewidth=.6) +scale_fill_identity() +scale_colour_identity() +radar_theme(paste0(cty, " — ABSOLUTE: the need (tech colour) vs what the country holds (green)"))}```::: {.panel-tabset}## Brazil```{r r-bra, fig.height=8.6, fig.width=10}#| code-fold: truerel_radar("Brazil")```## India```{r r-ind, fig.height=8.6, fig.width=10}#| code-fold: truerel_radar("India")```## Mexico```{r r-mex, fig.height=8.6, fig.width=10}#| code-fold: truerel_radar("Mexico")```## Japan```{r r-jpn, fig.height=8.6, fig.width=10}#| code-fold: truerel_radar("Japan")```## Vietnam```{r r-vnm, fig.height=8.6, fig.width=10}#| code-fold: truerel_radar("Vietnam")```## Spain```{r r-esp, fig.height=8.6, fig.width=10}#| code-fold: truerel_radar("Spain")```:::# Gap radars — absolute view (the need vs what the country holds)The relative view above deliberately erased scale so technologies could be compared; this view puts it back. In each panel the coloured pentagon is the technology's **need profile** — the winners' recipe, drawn in that technology's own Atlas colour — and the green shape is **what the country actually holds** (its recent positive signed SHAP per cluster), both on a shared per-panel scale. Now the reading is literal: the coloured boundary is the target *shape*, and the green shows how much of it is filled. Where green falls short of the colour, that is the build; where green pokes beyond it, a surplus. This view earns its keep exactly where the relative one struggles — a technology a country has barely entered shows as a full coloured need with only a sliver of green inside, which is far more legible than a near-empty pentagon. Read the two views as a pair: the relative one ranks *how close*, the absolute one shows *the shape of what is missing*.::: {.panel-tabset}## Brazil```{r a-bra, fig.height=8.6, fig.width=10}#| code-fold: trueabs_radar("Brazil")```## India```{r a-ind, fig.height=8.6, fig.width=10}#| code-fold: trueabs_radar("India")```## Mexico```{r a-mex, fig.height=8.6, fig.width=10}#| code-fold: trueabs_radar("Mexico")```## Japan```{r a-jpn, fig.height=8.6, fig.width=10}#| code-fold: trueabs_radar("Japan")```## Vietnam```{r a-vnm, fig.height=8.6, fig.width=10}#| code-fold: trueabs_radar("Vietnam")```## Spain```{r a-esp, fig.height=8.6, fig.width=10}#| code-fold: trueabs_radar("Spain")```:::# Build priorities — where the deficit isPositive = deficit (build); negative = surplus (leverage / rotate). Sorted by the largest single-cluster deficit per country.```{r priority-bars, fig.height=7.5, fig.width=10}#| code-fold: truepb <- gap |>mutate(country =factor(country, FOCAL)) |>group_by(country) |>slice_max(order_by = gap, n =8, with_ties =FALSE) |>ungroup() |>mutate(lab =paste0(tech, " · ", CATS_SHORT[as.character(cluster)]))ggplot(pb, aes(x =reorder_within(lab, gap, country), y = gap, fill = cluster)) +geom_col(width=.72) +coord_flip() +facet_wrap(~country, scales="free_y", ncol=2) +scale_fill_manual(values = CAT_COL, name =NULL) +scale_x_discrete(labels =function(x) sub("___.*","",x)) +labs(x=NULL, y="target − country (deficit to build)",title="Top build priorities per focal country",subtitle="The largest cluster-level gaps to the winners' recipe (recent 3 yrs).") +theme(legend.position="bottom", strip.text=element_text(face="bold",colour=NZ_DARK),axis.text.y=element_text(size=7.5))```**Reading the bars.** The radars show a country's whole position at a glance; this chart distils it into an ordered to-do list. Each bar is one technology × cluster, its length the raw deficit `target − country` — how much capability separates the country from the winners' recipe in that specific cluster — and the eight longest per country are kept. A tall bar is where the single biggest, most concrete build lies (e.g. "Solar · Electronics" for a machinery-heavy economy); a bar that dips *below* zero is a surplus the country could lean on. This is the step from *diagnosis* to *agenda*: the frontier board says which technologies to prioritise, the radars say why, and these bars name the specific capability clusters a policy would target first.# Reading the gaps — three worked casesThe cluster tables below zoom all the way in on three country × technology pairs, so the numbers behind the shapes are visible. Read each as: the **Target** column is the winners' recipe (what the technology needs), **Country (recent)** is what the country holds, and **Achieved** is their ratio — the same achievement fraction the radars draw, now as a figure you can quote.```{r cases}#| code-fold: trueshow_case <-function(cty, tch) gap |>filter(country==cty, tech==tch) |>transmute(Cluster=cluster, `Country (recent)`=round(country_signed,4), `Target`=round(target_weight,4),`Achieved`=sprintf("%.0f%%", pmin(achieved,9)*100),Read =case_when(achieved>=1.2~"surplus — leverage", achieved>=.8~"at frontier", achieved>=.4~"partial — build", TRUE~"deep gap — priority")) |>arrange(desc(`Target`))kable(show_case("India","Solar"), caption="India · Solar — machinery-heavy economy, but the Solar target is Electronics-led: Electronics is the deep gap, Machinery a surplus.")kable(show_case("Brazil","Biofuel"), caption="Brazil · Biofuel.")kable(show_case("Vietnam","Batteries"), caption="Vietnam · Batteries.")```**How to use this.** For any focal country, the radar shows *how competitive its capability base already is per technology* (fullness) and *where the model says the missing pieces are* (short axes) — read against a target that is the same for everyone, so the comparison is apples-to-apples. The build-priority bars turn that into a ranked list; the frontier board shows which technologies are within reach vs distant. Unlike the shipped count-driven radar, this is **magnitude-preserving, signed, and country-specific** — the three things the audit flagged as missing, now supplied by the raw SHAP.# Reproducibility`scripts/ml/target_profile_prep.py` writes `analysis/ml/shap_gap_by_country_cluster.csv` (per country × tech × cluster: country recent-3yr signed SHAP, target weight, achieved fraction, gap) from `analysis/ml/raw_signed/` and the winners'-recipe target. Country value uses the last 3 available years (option A). No model re-fit.