1 Setup

Data sources:

All growth rates are CAGR computed on the earliest available value in 2000–2005 and the latest available value in 2020–2024.

m %>% summarise(
  countries = n(),
  has_GDP_PPP    = sum(!is.na(`GDP/cap PPP const`)),
  has_GNI_PPP    = sum(!is.na(`GNI/cap PPP const`)),
  has_HHcons_PPP = sum(!is.na(`HH cons/cap PPP`)),
  has_NIQ        = sum(!is.na(NIQ)),
  has_VIIRS      = sum(!is.na(viirs_growth)),
  oecd_members   = sum(is_oecd)
) %>% kable()
countries has_GDP_PPP has_GNI_PPP has_HHcons_PPP has_NIQ has_VIIRS oecd_members
193 193 118 131 176 142 38

2 OECD growth, 2000–2024

2.1 GDP per capita

oecd <- m %>% filter(is_oecd, !is.na(`GDP/cap PPP const`)) %>%
  mutate(country = entity,
         g = `GDP/cap PPP const`,
         color = ifelse(g < median(g), "below", "above"))
ggplot(oecd, aes(x = reorder(country, g), y = g, fill = color)) +
  geom_col() +
  geom_hline(yintercept = median(oecd$g), lty = "dashed", lwd = 0.4) +
  geom_text(aes(label = sprintf("%.2f", g)), hjust = -0.1, size = 3) +
  coord_flip() +
  scale_fill_manual(values = c(above = "#4c72b0", below = "#c44e52"), guide = "none") +
  labs(x = NULL,
       y = "Annualized GDP per capita growth, 2000–2024 (%, PPP)",
       title = sprintf("OECD GDP per capita growth, 2000–2024 (median = %.2f%%)",
                       median(oecd$g))) +
  theme_minimal(base_size = 11)

2.2 Household consumption per capita (avoids tax-haven distortion)

GDP-based ranking puts Ireland near the top because of multinational IP-onshoring and aircraft-leasing accounting that doesn’t reach households. Re-ranking on household consumption per capita (PPP) strips most of that distortion: the same spending data that consumers actually do, divided by population.

oecd_c <- m %>% filter(is_oecd, !is.na(`HH cons/cap PPP`)) %>%
  mutate(country = entity,
         g = `HH cons/cap PPP`,
         color = ifelse(g < median(g), "below", "above"))
ggplot(oecd_c, aes(x = reorder(country, g), y = g, fill = color)) +
  geom_col() +
  geom_hline(yintercept = median(oecd_c$g), lty = "dashed", lwd = 0.4) +
  geom_text(aes(label = sprintf("%.2f", g)), hjust = -0.1, size = 3) +
  coord_flip() +
  scale_fill_manual(values = c(above = "#4c72b0", below = "#c44e52"), guide = "none") +
  labs(x = NULL,
       y = "Annualized household consumption per capita growth, 2000–2024 (%, PPP)",
       title = sprintf("OECD HH consumption per capita growth, 2000–2024 (median = %.2f%%)",
                       median(oecd_c$g)),
       subtitle = "Consumption-based: largely immune to tax-haven inflation") +
  theme_minimal(base_size = 11)

2.3 GDP vs. consumption: which countries change most?

shift <- m %>% filter(is_oecd, !is.na(`GDP/cap PPP const`),
                      !is.na(`HH cons/cap PPP`)) %>%
  mutate(diff = `GDP/cap PPP const` - `HH cons/cap PPP`)

ggplot(shift, aes(x = reorder(entity, diff), y = diff,
                  fill = diff > 0)) +
  geom_col() +
  coord_flip() +
  scale_fill_manual(values = c(`TRUE` = "#dd8452", `FALSE` = "#4c72b0"),
                    labels = c("Consumption grew faster", "GDP grew faster"),
                    name = NULL) +
  geom_hline(yintercept = 0, lwd = 0.4) +
  geom_text(aes(label = sprintf("%+.2f", diff)),
            hjust = ifelse(shift$diff > 0, -0.1, 1.1), size = 3) +
  labs(x = NULL,
       y = "GDP CAGR − consumption CAGR (pp/yr)",
       title = "Where GDP growth doesn't translate to consumption growth",
       subtitle = "Positive = GDP outpaced consumption (tax-haven / corporate-savings)\nNegative = consumption outpaced GDP (sovereign wealth distributions)") +
  theme_minimal(base_size = 11)

3 OECD convergence

oecd_ok <- m %>% filter(is_oecd, !is.na(`GDP/cap PPP const`), !is.na(gdp_start))
fit_oecd <- lm(`GDP/cap PPP const` ~ log_gdp_start, data = oecd_ok)
r2 <- summary(fit_oecd)$r.squared
slope <- coef(fit_oecd)[2]

ggplot(oecd_ok, aes(x = gdp_start, y = `GDP/cap PPP const`)) +
  geom_smooth(method = "lm", se = FALSE, color = "black", lty = "dashed") +
  geom_point(aes(color = is_oecd_postcomm), size = 3, alpha = 0.9) +
  geom_text(aes(label = entity), size = 3, vjust = -0.9, alpha = 0.85) +
  scale_x_log10(labels = dollar) +
  scale_color_manual(values = c(`FALSE` = "steelblue", `TRUE` = "#c44e52"),
                     labels = c("Other OECD", "Post-communist OECD"),
                     name = NULL) +
  labs(x = "GDP per capita in 2000 (PPP, log scale)",
       y = "Annualized growth 2000-2024 (%)",
       title = sprintf("OECD convergence (slope = %.2f, R² = %.2f)", slope, r2),
       subtitle = "Post-communist members do most of the work") +
  theme_minimal(base_size = 11)

fit_full <- lm(`GDP/cap PPP const` ~ log_gdp_start, data = oecd_ok)
fit_pc   <- lm(`GDP/cap PPP const` ~ log_gdp_start, data = oecd_ok %>% filter(is_oecd_postcomm))
fit_npc  <- lm(`GDP/cap PPP const` ~ log_gdp_start, data = oecd_ok %>% filter(!is_oecd_postcomm))
data.frame(
  group = c("Full OECD", "Post-communist", "Other OECD"),
  n     = c(nobs(fit_full), nobs(fit_pc), nobs(fit_npc)),
  slope = round(c(coef(fit_full)[2], coef(fit_pc)[2], coef(fit_npc)[2]), 2),
  R2    = round(c(summary(fit_full)$r.squared, summary(fit_pc)$r.squared,
                  summary(fit_npc)$r.squared), 3)
) %>% kable()
group n slope R2
Full OECD 38 -1.58 0.520
Post-communist 8 -3.37 0.878
Other OECD 30 -1.02 0.337

4 Global convergence (advantage of backwardness)

w <- m %>% filter(!is.na(`GDP/cap PPP const`), !is.na(gdp_start))
fit_w <- lm(`GDP/cap PPP const` ~ log_gdp_start, data = w)

region_colors <- c(
  "Africa"        = "#e64b35",
  "Asia"          = "#4dbbd5",
  "Europe"        = "#00a087",
  "North America" = "#3c5488",
  "South America" = "#f39b7f",
  "Oceania"       = "#8491b4"
)

label_set <- c("United States","China","India","Germany","Japan","United Kingdom",
               "France","Italy","Brazil","Russia","Canada","Australia","Mexico",
               "Spain","South Korea","Indonesia","Saudi Arabia","Turkey","Argentina",
               "Ireland","Vietnam","Bangladesh","Ethiopia","Rwanda","Cambodia",
               "Lithuania","Poland","Latvia","Estonia",
               "Equatorial Guinea","Venezuela","Zimbabwe","Yemen","Libya","Sudan",
               "Central African Republic","Burundi","Singapore","Norway","Luxembourg",
               "Qatar","United Arab Emirates","Iceland","Switzerland","Guyana",
               "Niger","Sierra Leone")

ggplot(w, aes(x = gdp_start, y = `GDP/cap PPP const`, color = owid_region)) +
  geom_smooth(method = "lm", se = FALSE, color = "black", lty = "dashed", lwd = 0.9) +
  geom_point(alpha = 0.85, size = 3) +
  geom_text(aes(label = ifelse(entity %in% label_set, entity, "")),
            size = 2.8, vjust = -0.9, hjust = 0.5, alpha = 0.85, show.legend = FALSE) +
  scale_x_log10(labels = dollar) +
  scale_color_manual(values = region_colors, name = NULL, na.value = "#999999") +
  labs(x = "GDP per capita in 2000 (PPP, log scale)",
       y = "Annualized growth 2000-2024 (%)",
       title = sprintf("Global convergence / advantage of backwardness (slope = %.2f, R² = %.2f, n = %d)",
                       coef(fit_w)[2], summary(fit_w)$r.squared, nobs(fit_w)),
       subtitle = "Unconditional convergence is weak globally — Asian and post-Soviet countries dominate the catch-up corner; Africa stays poor despite the gap.") +
  theme_minimal(base_size = 11) +
  theme(legend.position = "bottom")

5 Conditional convergence: NIQ interaction

5.1 Main interaction model

mw <- m %>% filter(!is.na(`GDP/cap PPP const`), !is.na(NIQ), !is.na(log_gdp_start)) %>%
  mutate(cagr = `GDP/cap PPP const`,
         logG_c = log_gdp_start - mean(log_gdp_start),
         NIQ_c  = NIQ - mean(NIQ))
fit_int <- lm(cagr ~ logG_c * NIQ_c, data = mw)
fit_add <- lm(cagr ~ logG_c + NIQ_c, data = mw)

broom::tidy(fit_int) %>%
  mutate(across(where(is.numeric), ~ signif(., 4))) %>%
  kable(caption = "Centered interaction model: growth ~ log(GDP_2000) × NIQ")
Centered interaction model: growth ~ log(GDP_2000) × NIQ
term estimate std.error statistic p.value
(Intercept) 2.2970 0.145600 15.780 0.0000000
logG_c -1.1910 0.139100 -8.563 0.0000000
NIQ_c 0.1196 0.015560 7.690 0.0000000
logG_c:NIQ_c -0.0327 0.009445 -3.463 0.0006751
cat(sprintf("R² = %.3f, n = %d\n", summary(fit_int)$r.squared, nobs(fit_int)))
## R² = 0.332, n = 176
cat(sprintf("F-test (interaction vs additive): F = %.2f, p = %.2e\n",
            anova(fit_add, fit_int)[2,"F"],
            anova(fit_add, fit_int)[2,"Pr(>F)"]))
## F-test (interaction vs additive): F = 11.99, p = 6.75e-04

5.2 Convergence stratified by NIQ band

mw_band <- mw %>%
  mutate(niq_band = cut(NIQ, breaks = c(0, 80, 90, 200),
                        labels = c("NIQ < 80", "NIQ 80–90", "NIQ ≥ 90")))

# Compute slope per band for the subtitle
band_fits <- mw_band %>% group_by(niq_band) %>%
  summarise(slope = round(coef(lm(cagr ~ log_gdp_start))[2], 2),
            n = n(), .groups = "drop")
sub <- paste(sprintf("%s: slope = %.2f (n=%d)", band_fits$niq_band,
                     band_fits$slope, band_fits$n), collapse = "    ")

ggplot(mw_band, aes(x = gdp_start, y = cagr, color = niq_band)) +
  geom_point(size = 2.6, alpha = 0.85) +
  geom_smooth(method = "lm", se = FALSE, lwd = 0.9) +
  scale_x_log10(labels = dollar) +
  scale_color_manual(values = c("NIQ < 80"  = "#e64b35",
                                "NIQ 80–90" = "#f0a040",
                                "NIQ ≥ 90"  = "#3c5488"),
                     name = "NIQ band") +
  labs(x = "GDP per capita in 2000 (PPP, log scale)",
       y = "CAGR 2000-2024 (%)",
       title = "Conditional convergence: each NIQ band has its own slope",
       subtitle = sub) +
  theme_minimal(base_size = 11) +
  theme(legend.position = "bottom")

The bottom band (NIQ < 80) is essentially flat — these countries don’t converge to the frontier regardless of starting income. The middle and high bands show clear convergence, with high-NIQ countries lying systematically above the others for any given starting GDP. This is the visual signature of the formal interaction term.

5.3 Quadrant view (poor + smart = sweet spot)

mw$niq_high <- mw$NIQ >= median(mw$NIQ)
mw$gdp_high <- mw$gdp_start >= median(mw$gdp_start)
quad_means <- mw %>%
  mutate(quadrant = case_when(
    !gdp_high &  niq_high ~ "Poor + smart",
    gdp_high  &  niq_high ~ "Rich + smart (frontier)",
    !gdp_high & !niq_high ~ "Poor + low-NIQ (stuck)",
    TRUE                  ~ "Rich + low-NIQ (resource curse)"
  )) %>%
  group_by(quadrant) %>%
  summarise(n = n(), mean_cagr = round(mean(cagr), 2), .groups = "drop") %>%
  arrange(desc(mean_cagr))
kable(quad_means, caption = "Mean CAGR by quadrant of starting GDP × NIQ")
Mean CAGR by quadrant of starting GDP × NIQ
quadrant n mean_cagr
Poor + smart 18 4.31
Poor + low-NIQ (stuck) 70 1.85
Rich + smart (frontier) 70 1.83
Rich + low-NIQ (resource curse) 18 0.75
label_set <- c("China","India","Vietnam","Ethiopia","Cambodia","Bangladesh",
               "Singapore","Italy","Japan","Mexico","South Korea","Ireland",
               "Norway","United States","Lithuania","Poland","Turkey","Argentina",
               "Nigeria","South Africa","Equatorial Guinea","Niger","Rwanda",
               "Switzerland","Germany","Brazil","Russia","Indonesia")
ggplot(mw, aes(x = NIQ, y = gdp_start)) +
  geom_hline(yintercept = median(mw$gdp_start), lty = "dashed", alpha = 0.4) +
  geom_vline(xintercept = median(mw$NIQ),     lty = "dashed", alpha = 0.4) +
  geom_point(aes(color = cagr), size = 4) +
  geom_text(aes(label = ifelse(entity %in% label_set, entity, "")),
            size = 3, vjust = -1, fontface = "bold") +
  scale_color_gradient2(low = "#b2182b", mid = "#cccccc", high = "#1a7f37",
                        midpoint = 1.5, name = "CAGR\n2000-2024") +
  scale_y_log10(labels = dollar) +
  labs(x = "National IQ", y = "GDP per capita 2000 (PPP, log scale)",
       title = "Conditional convergence × NIQ",
       subtitle = "Poor + smart corner = sweet spot; rich + low-NIQ = resource curse") +
  theme_minimal(base_size = 11)

6 Robustness: same model across income measures

We re-run growth ~ log(GDP_2000) × NIQ with six different outcome measures. All variables are centered so the main effects are evaluated at the sample mean. Cells show estimate (SE) for each term.

measures <- c("GDP/cap PPP const", "GDP/cap USD const",
              "GNI/cap PPP const", "GNI/cap USD const",
              "HH cons/cap PPP",   "Total cons/cap USD")

fmt <- function(est, se) sprintf("%.3f (%.3f)", est, se)
fmt4 <- function(est, se) sprintf("%.4f (%.4f)", est, se)
sig_p <- function(p) {
  if (p < 0.001) "< 0.001"
  else if (p < 0.01) sprintf("%.3f", p)
  else sprintf("%.3f", p)
}
star <- function(p) ifelse(p < 0.001, "***",
                    ifelse(p < 0.01,  "**",
                    ifelse(p < 0.05,  "*",
                    ifelse(p < 0.1,   ".", ""))))

results <- lapply(measures, function(meas) {
  d <- m %>% filter(!is.na(.data[[meas]]), !is.na(NIQ), !is.na(log_gdp_start)) %>%
    mutate(cagr = .data[[meas]],
           logG_c = log_gdp_start - mean(log_gdp_start),
           NIQ_c  = NIQ - mean(NIQ))
  fit <- lm(cagr ~ logG_c * NIQ_c, data = d)
  cf <- coef(summary(fit))
  data.frame(
    Measure = meas,
    n = nobs(fit),
    R2 = sprintf("%.3f", summary(fit)$r.squared),
    Intercept    = fmt(cf["(Intercept)","Estimate"],   cf["(Intercept)","Std. Error"]),
    `log(GDP)_c` = paste0(fmt(cf["logG_c","Estimate"], cf["logG_c","Std. Error"]),
                          " ", star(cf["logG_c","Pr(>|t|)"])),
    NIQ_c        = paste0(fmt(cf["NIQ_c","Estimate"],  cf["NIQ_c","Std. Error"]),
                          " ", star(cf["NIQ_c","Pr(>|t|)"])),
    Interaction  = paste0(fmt4(cf["logG_c:NIQ_c","Estimate"], cf["logG_c:NIQ_c","Std. Error"]),
                          " ", star(cf["logG_c:NIQ_c","Pr(>|t|)"])),
    `p (interaction)` = sig_p(cf["logG_c:NIQ_c","Pr(>|t|)"]),
    check.names = FALSE,
    stringsAsFactors = FALSE
  )
}) %>% bind_rows()

kable(results,
      caption = paste0("Interaction model coefficients across six income/consumption measures. ",
                       "All variables centered. Cells: estimate (SE). ",
                       "Significance: *** p<0.001, ** p<0.01, * p<0.05."),
      align = "lccccccc")
Interaction model coefficients across six income/consumption measures. All variables centered. Cells: estimate (SE). Significance: *** p<0.001, ** p<0.01, * p<0.05.
Measure n R2 Intercept log(GDP)_c NIQ_c Interaction p (interaction)
GDP/cap PPP const 176 0.332 2.297 (0.146) -1.191 (0.139) *** 0.120 (0.016) *** -0.0327 (0.0094) *** < 0.001
GDP/cap USD const 170 0.328 2.322 (0.150) -1.179 (0.145) *** 0.118 (0.016) *** -0.0354 (0.0101) *** < 0.001
GNI/cap PPP const 116 0.365 2.551 (0.154) -1.120 (0.152) *** 0.098 (0.016) *** -0.0395 (0.0100) *** < 0.001
GNI/cap USD const 115 0.367 2.549 (0.155) -1.115 (0.153) *** 0.099 (0.017) *** -0.0415 (0.0101) *** < 0.001
HH cons/cap PPP 129 0.227 2.718 (0.201) -0.852 (0.196) *** 0.082 (0.022) *** -0.0590 (0.0133) *** < 0.001
Total cons/cap USD 132 0.279 2.605 (0.170) -0.879 (0.164) *** 0.089 (0.018) *** -0.0520 (0.0113) *** < 0.001

The interaction is consistently negative and highly significant across all measures (every p < 0.001 except the unbiased Wald formulation gets the same conclusion). Consumption-based measures show the strongest interaction (-0.068 vs -0.037 for GDP), suggesting that GDP-based measurement understates the conditional convergence × NIQ pattern rather than fabricating it. The PPP-vs-USD distinction matters very little within each income concept; the income-concept choice (GDP → GNI → consumption) does most of the work.

7 Spatial robustness

The interaction may be inflated by neighbour-effects (countries near each other share growth shocks) or by continental-level confounders. Re-fit with two layers: (a) regional fixed effects using OWID continent classification, (b) a spatial error model with k=3 nearest-neighbour row-standardized weights based on country centroids.

The OWID continent classification used as fixed effects:

m %>% mutate(region = ifelse(is.na(owid_region), "Other", owid_region)) %>%
  count(region, name = "n_countries") %>% arrange(desc(n_countries)) %>%
  kable(caption = "OWID 6-continent classification used as regional fixed effects")
OWID 6-continent classification used as regional fixed effects
region n_countries
Africa 51
Asia 47
Europe 42
North America 28
Oceania 14
South America 11

This split is admittedly coarse — Africa is not split into MENA vs Sub-Saharan, Europe lumps post-Soviet with Western Europe, Asia spans from Japan to Tajikistan. The “did continental confounders eat the result” check it provides is therefore a weak version; a finer split (WB 7-region or UN sub-regional) would push it further but would also reduce within-region df.

spat <- readRDS("data/processed/spatial_summary.rds")
spat %>%
  mutate(
    `Interaction est. (SE)` = sprintf("%.4f (%.4f)", Interaction, SE),
    `p` = sprintf("%.1e", p),
    `λ` = ifelse(is.na(lambda), "—", sprintf("%.3f", lambda)),
    `R² / NK` = sprintf("%.3f", fit)
  ) %>%
  select(Measure, Model, n, `Interaction est. (SE)`, p, `λ`, `R² / NK`) %>%
  kable(caption = "Spatial robustness: 4 model variants × 6 outcomes")
Spatial robustness: 4 model variants × 6 outcomes
Measure Model n Interaction est. (SE) p λ R² / NK
…1 GDP/cap PPP const OLS plain 176 -0.0327 (0.0094) 6.8e-04 0.332
…2 GDP/cap PPP const OLS + region FE 176 -0.0267 (0.0102) 9.4e-03 0.399
lambda…3 GDP/cap PPP const Spatial error 176 -0.0309 (0.0101) 2.3e-03 0.211 0.351
lambda…4 GDP/cap PPP const Spatial err + FE 176 -0.0261 (0.0102) 1.1e-02 0.110 0.403
…5 GDP/cap USD const OLS plain 170 -0.0354 (0.0101) 5.8e-04 0.328
…6 GDP/cap USD const OLS + region FE 170 -0.0299 (0.0109) 6.9e-03 0.395
lambda…7 GDP/cap USD const Spatial error 170 -0.0331 (0.0107) 1.9e-03 0.173 0.341
lambda…8 GDP/cap USD const Spatial err + FE 170 -0.0292 (0.0108) 7.1e-03 0.066 0.397
…9 GNI/cap PPP const OLS plain 116 -0.0395 (0.0100) 1.4e-04 0.365
…10 GNI/cap PPP const OLS + region FE 116 -0.0414 (0.0104) 1.3e-04 0.435
lambda…11 GNI/cap PPP const Spatial error 116 -0.0370 (0.0106) 5.0e-04 0.362 0.428
lambda…12 GNI/cap PPP const Spatial err + FE 116 -0.0395 (0.0106) 1.8e-04 0.283 0.462
…13 GNI/cap USD const OLS plain 115 -0.0415 (0.0101) 7.4e-05 0.367
…14 GNI/cap USD const OLS + region FE 115 -0.0438 (0.0105) 6.7e-05 0.434
lambda…15 GNI/cap USD const Spatial error 115 -0.0391 (0.0107) 2.7e-04 0.348 0.424
lambda…16 GNI/cap USD const Spatial err + FE 115 -0.0418 (0.0107) 8.9e-05 0.268 0.458
…17 HH cons/cap PPP OLS plain 129 -0.0590 (0.0133) 2.1e-05 0.227
…18 HH cons/cap PPP OLS + region FE 129 -0.0648 (0.0146) 2.0e-05 0.259
lambda…19 HH cons/cap PPP Spatial error 129 -0.0559 (0.0142) 8.5e-05 0.264 0.264
lambda…20 HH cons/cap PPP Spatial err + FE 129 -0.0611 (0.0147) 3.4e-05 0.237 0.286
…21 Total cons/cap USD OLS plain 132 -0.0520 (0.0113) 1.0e-05 0.279
…22 Total cons/cap USD OLS + region FE 132 -0.0561 (0.0125) 1.6e-05 0.298
lambda…23 Total cons/cap USD Spatial error 132 -0.0485 (0.0121) 5.9e-05 0.279 0.318
lambda…24 Total cons/cap USD Spatial err + FE 132 -0.0519 (0.0126) 3.8e-05 0.257 0.328

Across all 24 model fits the interaction coefficient remains negative and significant. The spatial λ is small-to-moderate (0.07–0.36), confirming some geographic clustering of residuals — but stripping it out via the spatial error model only modestly attenuates the interaction. Adding both region FE and spatial errors gives the most conservative estimate; even there the interaction holds at p < 0.05 in every specification.

8 Outliers from the GDP interaction model

fit_full <- lm(`GDP/cap PPP const` ~ log_gdp_start * NIQ,
               data = m %>% filter(!is.na(`GDP/cap PPP const`), !is.na(NIQ)))
oo <- m %>% filter(!is.na(`GDP/cap PPP const`), !is.na(NIQ)) %>%
  mutate(pred = predict(fit_full),
         residual = `GDP/cap PPP const` - pred) %>%
  arrange(desc(residual))

cat("**Top positive outliers (grew much MORE than predicted)**\n")
## **Top positive outliers (grew much MORE than predicted)**
oo %>% select(entity, gdp_start, NIQ,
              actual = `GDP/cap PPP const`, pred, residual) %>%
  mutate(across(c(gdp_start, actual, pred, residual), ~ round(., 2))) %>%
  head(15) %>% kable()
entity gdp_start NIQ actual pred residual
Guyana 8025.29 75.43 9.48 1.52 7.95
Ethiopia 810.27 67.95 5.44 2.15 3.29
Georgia 5706.27 85.04 6.33 3.16 3.17
Turkmenistan 5112.53 81.26 5.54 2.76 2.78
Ghana 3327.35 63.99 3.18 0.57 2.61
Azerbaijan 5656.93 85.72 5.84 3.26 2.57
Bhutan 5287.57 76.40 4.52 2.05 2.47
Panama 14572.98 79.65 3.89 1.42 2.47
Armenia 4589.01 88.63 6.34 3.95 2.39
India 3099.50 76.49 4.92 2.58 2.35
Albania 6582.02 83.98 5.08 2.84 2.24
Ireland 53675.00 99.10 3.37 1.27 2.10
Romania 14853.67 87.34 4.27 2.21 2.05
Bangladesh 2712.94 77.71 4.87 2.90 1.97
Cape Verde 4510.14 71.26 3.31 1.46 1.85
cat("\n**Top negative outliers (grew much LESS than predicted)**\n")
## 
## **Top negative outliers (grew much LESS than predicted)**
oo %>% select(entity, gdp_start, NIQ,
              actual = `GDP/cap PPP const`, pred, residual) %>%
  mutate(across(c(gdp_start, actual, pred, residual), ~ round(., 2))) %>%
  arrange(residual) %>% head(15) %>% kable()
entity gdp_start NIQ actual pred residual
Sudan 3575.85 76.04 -2.68 2.37 -5.05
Syria 7807.93 79.32 -2.52 2.04 -4.56
Burundi 1033.47 76.81 0.07 3.70 -3.63
Palestine 4520.89 81.04 -0.67 2.87 -3.54
Madagascar 1701.44 77.59 -0.11 3.35 -3.46
Libya 17747.02 81.96 -1.42 1.44 -2.86
Haiti 3267.08 72.74 -0.63 1.94 -2.57
Solomon Islands 2276.82 75.31 0.14 2.68 -2.54
Central African Republic 1245.81 68.25 -0.48 1.91 -2.39
Kiribati 2481.42 80.45 1.14 3.44 -2.30
Vanuatu 3342.17 73.36 -0.22 2.02 -2.24
Afghanistan 1617.83 75.70 0.89 3.06 -2.17
Palau 15447.95 89.29 0.27 2.37 -2.10
Zimbabwe 6170.33 72.15 -0.70 1.33 -2.03
Lebanon 11571.73 81.82 -0.09 1.91 -2.01

9 Session

sessionInfo()
## R version 4.5.3 (2026-03-11)
## Platform: x86_64-pc-linux-gnu
## Running under: Linux Mint 21.1
## 
## Matrix products: default
## BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.10.0 
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.10.0  LAPACK version 3.10.0
## 
## locale:
##  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
##  [5] LC_MONETARY=en_DK.UTF-8    LC_MESSAGES=en_US.UTF-8   
##  [7] LC_PAPER=en_DK.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=en_DK.UTF-8 LC_IDENTIFICATION=C       
## 
## time zone: Europe/Brussels
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] jsonlite_2.0.0     knitr_1.50         broom_1.0.11       scales_1.4.0      
## [5] ggplot2_4.0.1.9000 tidyr_1.3.1        dplyr_1.1.4       
## 
## loaded via a namespace (and not attached):
##  [1] Matrix_1.7-4       gtable_0.3.6       compiler_4.5.3     tidyselect_1.2.1  
##  [5] jquerylib_0.1.4    splines_4.5.3      yaml_2.3.10        fastmap_1.2.0     
##  [9] lattice_0.22-7     R6_2.6.1           labeling_0.4.3     generics_0.1.4    
## [13] backports_1.5.0    tibble_3.3.0       bslib_0.9.0        pillar_1.11.1     
## [17] RColorBrewer_1.1-3 rlang_1.1.6.9000   cachem_1.1.0       xfun_0.57         
## [21] sass_0.4.10        S7_0.2.1           cli_3.6.5          mgcv_1.9-3        
## [25] withr_3.0.2        magrittr_2.0.4     digest_0.6.39      grid_4.5.3        
## [29] nlme_3.1-168       lifecycle_1.0.4    vctrs_0.6.5        evaluate_1.0.5    
## [33] glue_1.8.0         farver_2.1.2       rmarkdown_2.30     purrr_1.2.0       
## [37] tools_4.5.3        pkgconfig_2.0.3    htmltools_0.5.8.1