1 Package Setup

The chunk below auto-installs any missing required packages, so this file should run on a fresh machine without manual install.packages() calls. naniar is optional (used only for the missing-data plot); if it fails to install, the report falls back to a ggplot2-only alternative.

required_packages <- c("tidyverse", "readxl", "fixest", "scales", "gt", "plm")
optional_packages <- c("naniar")

missing_required <- setdiff(required_packages, rownames(installed.packages()))
if (length(missing_required) > 0) {
  install.packages(missing_required, dependencies = TRUE)
}
invisible(lapply(required_packages, function(p)
  suppressPackageStartupMessages(library(p, character.only = TRUE))))

missing_optional <- setdiff(optional_packages, rownames(installed.packages()))
if (length(missing_optional) > 0) {
  tryCatch(install.packages(missing_optional, dependencies = TRUE),
           error = function(e) message("Optional package install failed; using fallback."))
}
naniar_available <- requireNamespace("naniar", quietly = TRUE)
if (naniar_available) library(naniar)

theme_set(theme_minimal(base_size = 11))
options(scipen = 999)

2 Read Data

raw <- read_excel("/Users/jixiangmama/Desktop/工作/2026年9月/rawdata.xlsx")
raw <- raw %>% arrange(country, year)

3 Missing-Data Diagnostics

Addresses feedback: explain why observation counts (N) vary across variables.

miss_summary <- raw %>%
  summarise(across(everything(), ~ sum(is.na(.)))) %>%
  pivot_longer(everything(), names_to = "variable", values_to = "n_missing") %>%
  mutate(pct_missing = round(100 * n_missing / nrow(raw), 2)) %>%
  arrange(desc(n_missing))

miss_by_country <- raw %>%
  group_by(country) %>%
  summarise(across(where(is.numeric), ~ sum(is.na(.)))) %>%
  mutate(total_missing = rowSums(across(where(is.numeric)))) %>%
  arrange(desc(total_missing)) %>%
  select(country, total_missing) %>%
  head(15)

miss_by_year <- raw %>%
  group_by(year) %>%
  summarise(across(where(is.numeric), ~ sum(is.na(.)))) %>%
  mutate(total_missing = rowSums(across(where(is.numeric), .fns = ~.x, .names = NULL))) %>%
  select(year, total_missing) %>%
  arrange(desc(total_missing)) %>%
  head(15)

knitr::kable(miss_by_country, caption = "Countries with the most missing values")
Countries with the most missing values
country total_missing
Afghanistan 0
Albania 0
Algeria 0
Angola 0
Argentina 0
Armenia 0
Australia 0
Austria 0
Azerbaijan 0
Bahrain 0
Bangladesh 0
Belarus 0
Belgium 0
Benin 0
Bolivia 0
knitr::kable(miss_by_year, caption = "Years with the most missing values")
Years with the most missing values
year total_missing
2023 2023
2022 2022
2021 2021
2020 2020
2019 2019
2018 2018
2017 2017
2016 2016
2015 2015
2014 2014
2013 2013
2012 2012
2011 2011
2010 2010
2009 2009
if (naniar_available) {
  vis_miss(raw, warn_large_data = FALSE) +
    labs(title = "Figure A1. Missing Data Overview",
         subtitle = "Shows missingness by variable and observation")
} else {
  miss_summary %>%
    filter(pct_missing > 0) %>%
    ggplot(aes(x = reorder(variable, pct_missing), y = pct_missing)) +
    geom_col(fill = "#C44E52") +
    coord_flip() +
    labs(title = "Figure A1. Missing Data by Variable (fallback)",
         x = NULL, y = "Missing (%)")
}

4 Descriptive Statistics

Addresses feedback: reader-friendly labels; explain why N varies.

desc_vars <- c("gini_di","gini_mk","financial_openness","population","GDP","inflation","FD",
               "trade","urban","tertiary","foreign_asset","age_dependence","unemployment",
               "government_expenditure","saving",
               "p0p20_inc","p20p40_inc","p40p60_inc","p60p80_inc","p80p100_inc",
               "p90p100_inc","p95p100_inc","p99p100_inc","p999p100_inc")
desc_vars <- desc_vars[desc_vars %in% names(raw)]

table1_desc <- raw %>%
  select(all_of(desc_vars)) %>%
  summarise(across(everything(),
                    list(N = ~sum(!is.na(.)),
                         Mean = ~mean(., na.rm = TRUE),
                         SD = ~sd(., na.rm = TRUE),
                         Min = ~min(., na.rm = TRUE),
                         Max = ~max(., na.rm = TRUE)))) %>%
  pivot_longer(everything(), names_to = c("Variable", ".value"),
               names_pattern = "(.*)_(N|Mean|SD|Min|Max)")

knitr::kable(table1_desc, digits = 4,
             caption = "Table 1. Descriptive Statistics of Main Variables (balanced panel, no missing observations)")
Table 1. Descriptive Statistics of Main Variables (balanced panel, no missing observations)
Variable N Mean SD Min Max
gini_di 7056 0.3846 0.0930 0.2030 0.6330
gini_mk 7056 0.4439 0.0725 0.2673 0.7200
financial_openness 7056 0.4668 0.2306 0.0000 1.0000
population 7056 0.5882 1.3030 0.0078 14.2474
GDP 7056 25.8339 2.1076 18.7601 30.7832
inflation 7056 0.1170 0.6046 -0.0795 14.8623
FD 7056 0.5208 0.3927 0.0050 1.8524
trade 7056 0.9332 0.2825 0.1723 1.7044
urban 7056 0.4777 0.2076 0.0400 0.9656
tertiary 7056 0.3439 0.2296 0.0030 1.0671
foreign_asset 7056 0.4229 0.4466 -0.8813 1.7000
age_dependence 7056 0.5618 0.1179 0.3571 0.8900
unemployment 7056 0.0723 0.0270 0.0020 0.1793
government_expenditure 7056 0.1577 0.0477 0.0100 0.3670
saving 7056 0.2186 0.0709 -0.0608 0.4714
p0p20_inc 7056 0.0186 0.0078 0.0023 0.0404
p20p40_inc 7056 0.0750 0.0126 0.0214 0.1137
p40p60_inc 7056 0.1185 0.0174 0.0521 0.1719
p60p80_inc 7056 0.1850 0.0176 0.1184 0.2401
p80p100_inc 7056 0.6030 0.0477 0.4724 0.7868
p90p100_inc 7056 0.4363 0.0372 0.3218 0.5822
p95p100_inc 7056 0.3147 0.0275 0.2218 0.4079
p99p100_inc 7056 0.1523 0.0154 0.0855 0.2104
p999p100_inc 7056 0.0511 0.0076 0.0233 0.0839

Quintile shares should sum to ~1 (addresses: how the five groups relate to each other):

sum_check <- raw %>%
  mutate(sum_share = p0p20_inc + p20p40_inc + p40p60_inc + p60p80_inc + p80p100_inc) %>%
  summarise(mean_sum = mean(sum_share, na.rm = TRUE))
knitr::kable(sum_check, digits = 4,
             caption = "Mean of the five quintile shares summed together (should be ~1.000)")
Mean of the five quintile shares summed together (should be ~1.000)
mean_sum
1

5 Income Distribution Structure

Figure 4.1 — Addresses feedback: figure numbering, caption must state what error bars represent.

quintile_long <- raw %>%
  select(p0p20_inc, p20p40_inc, p40p60_inc, p60p80_inc, p80p100_inc) %>%
  pivot_longer(everything(), names_to = "group", values_to = "share") %>%
  mutate(group = factor(group,
                         levels = c("p0p20_inc","p20p40_inc","p40p60_inc","p60p80_inc","p80p100_inc"),
                         labels = c("Lowest 20%","2nd 20%","Middle 20%","4th 20%","Top 20%")))

quintile_long %>%
  group_by(group) %>%
  summarise(mean_share = mean(share, na.rm = TRUE), sd_share = sd(share, na.rm = TRUE)) %>%
  ggplot(aes(group, mean_share)) +
  geom_col(fill = "#4C72B0") +
  geom_errorbar(aes(ymin = mean_share - sd_share, ymax = mean_share + sd_share), width = 0.2) +
  scale_y_continuous(labels = percent_format(accuracy = 1)) +
  labs(title = "Figure 4.1. Average Income Share by Population Quintile",
       subtitle = "Error bars = standard deviation (SD), not SE or a confidence interval",
       x = NULL, y = "Average income share")

Figure 4.2 — Addresses feedback: clarify that these groups are nested/overlapping, not mutually exclusive.

top_long <- raw %>%
  select(p999p100_inc, p99p100_inc, p95p100_inc, p90p100_inc, p80p100_inc) %>%
  pivot_longer(everything(), names_to = "group", values_to = "share") %>%
  mutate(group = factor(group,
                         levels = c("p999p100_inc","p99p100_inc","p95p100_inc","p90p100_inc","p80p100_inc"),
                         labels = c("Top 0.1%","Top 1%","Top 5%","Top 10%","Top 20%")))

top_long %>%
  group_by(group) %>%
  summarise(mean_share = mean(share, na.rm = TRUE), sd_share = sd(share, na.rm = TRUE)) %>%
  ggplot(aes(group, mean_share)) +
  geom_col(fill = "#C44E52") +
  geom_errorbar(aes(ymin = mean_share - sd_share, ymax = mean_share + sd_share), width = 0.2) +
  scale_y_continuous(labels = percent_format(accuracy = 1)) +
  labs(title = "Figure 4.2. Average Income Share of Top Income Groups",
       subtitle = "Note: these 5 groups are nested, not mutually exclusive; error bars = SD",
       x = NULL, y = "Average income share")

Figure 4.3 — income-share composition by financial-openness tercile (addresses: how the five shares relate to one another).

raw %>%
  mutate(fo_tercile = ntile(financial_openness, 3),
         fo_tercile = factor(fo_tercile, labels = c("Low FO","Medium FO","High FO"))) %>%
  select(fo_tercile, p0p20_inc, p20p40_inc, p40p60_inc, p60p80_inc, p80p100_inc) %>%
  pivot_longer(-fo_tercile, names_to = "group", values_to = "share") %>%
  mutate(group = factor(group,
                         levels = c("p0p20_inc","p20p40_inc","p40p60_inc","p60p80_inc","p80p100_inc"),
                         labels = c("Lowest 20%","2nd 20%","Middle 20%","4th 20%","Top 20%"))) %>%
  group_by(fo_tercile, group) %>%
  summarise(mean_share = mean(share, na.rm = TRUE), .groups = "drop") %>%
  ggplot(aes(fo_tercile, mean_share, fill = group)) +
  geom_col(position = "stack") +
  scale_y_continuous(labels = percent_format()) +
  labs(title = "Figure 4.3. Income Share Composition by Financial Openness Tercile",
       x = NULL, y = "Average income share", fill = "Income group")

6 Sample Composition

Addresses feedback: clarify why the 2022 classification is used for a multi-year sample.

raw %>%
  distinct(country, income_group_2022) %>%
  count(income_group_2022) %>%
  mutate(income_group_2022 = factor(income_group_2022,
           levels = c("High income","Upper-middle income","Lower-middle income","Low income"))) %>%
  ggplot(aes(x = "", y = n, fill = income_group_2022)) +
  geom_col(width = 1, color = "white") +
  coord_polar("y") +
  labs(title = "Figure 3.1. Sample Countries by World Bank Income Group (2022)",
       subtitle = "Single 2022 cross-sectional classification, not time-varying over 1975-2023",
       fill = "Income group") +
  theme_void()

7 Regression Table Helper Function

Core fix: relies only on fixest’s own coef/se/pvalue/nobs/r2 accessors, so tables render reliably regardless of other packages installed.

control_labels <- c(
  financial_openness = "Financial openness", L_financial_openness = "Financial openness (t-1)",
  population = "Population", FD = "Financial development", GDP = "GDP (log)",
  inflation = "Inflation", age_dependence = "Age dependence", trade = "Trade openness",
  urban = "Urbanisation", tertiary = "Tertiary enrolment", foreign_asset = "Foreign net assets",
  unemployment = "Unemployment", government_expenditure = "Government expenditure",
  saving = "National saving"
)

stars_fun <- function(p) {
  if (is.na(p)) return("")
  if (p < 0.01) return("***"); if (p < 0.05) return("**"); if (p < 0.10) return("*")
  return("")
}

reg_table_html <- function(models, var_order, col_names, note, extra_meta = NULL) {
  header <- paste0("<tr><th>Variables</th>", paste0("<th>", col_names, "</th>", collapse = ""), "</tr>")
  body <- ""
  for (v in var_order) {
    lbl <- if (v %in% names(control_labels)) control_labels[[v]] else v
    coef_cells <- character(0); se_cells <- character(0)
    for (m in models) {
      cf <- tryCatch(coef(m), error = function(e) NULL)
      if (!is.null(cf) && v %in% names(cf)) {
        b <- cf[[v]]; s <- fixest::se(m)[[v]]; p <- fixest::pvalue(m)[[v]]
        coef_cells <- c(coef_cells, sprintf("%.4f%s", b, stars_fun(p)))
        se_cells <- c(se_cells, sprintf("(%.4f)", s))
      } else { coef_cells <- c(coef_cells, ""); se_cells <- c(se_cells, "") }
    }
    body <- paste0(body, "<tr><td style='text-align:left'>", lbl, "</td>",
                    paste0("<td>", coef_cells, "</td>", collapse = ""), "</tr>")
    body <- paste0(body, "<tr><td></td>",
                    paste0("<td style='color:#888;font-size:0.85em'>", se_cells, "</td>", collapse = ""), "</tr>")
  }
  meta <- ""
  if (!is.null(extra_meta)) {
    for (nm in names(extra_meta)) {
      meta <- paste0(meta, "<tr><td>", nm, "</td>",
                      paste0("<td>", extra_meta[[nm]], "</td>", collapse = ""), "</tr>")
    }
  }
  nobs_vals <- sapply(models, function(m) format(nobs(m), big.mark = ","))
  r2_vals <- sapply(models, function(m) tryCatch(sprintf("%.4f", fixest::r2(m, "r2")), error = function(e) "NA"))
  nobs_row <- paste0("<tr><td>Observations</td>", paste0("<td>", nobs_vals, "</td>", collapse = ""), "</tr>")
  r2_row <- paste0("<tr><td>R-squared</td>", paste0("<td>", r2_vals, "</td>", collapse = ""), "</tr>")
  paste0("<table class='table table-striped' style='font-size:0.85em'><thead>", header,
         "</thead><tbody>", body, meta, nobs_row, r2_row, "</tbody></table>",
         "<p style='font-size:0.8em;color:#666'>", note, "</p>")
}

REG_NOTE <- "Standard errors clustered at the country level in parentheses. ***, **, * denote significance at the 1%, 5%, 10% levels."

8 Baseline Regression (Table 2)

Addresses feedback: justify clustering; compare alternative SE choices; explain the practical size of the coefficient.

baseline_controls <- c("population","FD","GDP","inflation","age_dependence","trade","urban","tertiary","foreign_asset")
fml_ctrl <- paste(baseline_controls, collapse = " + ")

m1 <- feols(as.formula(paste0("gini_mk ~ financial_openness")), data = raw)
m2 <- feols(as.formula(paste0("gini_mk ~ financial_openness + ", fml_ctrl)), data = raw)
m3 <- feols(as.formula(paste0("gini_mk ~ financial_openness + ", fml_ctrl, " | country")), data = raw)
m4 <- feols(as.formula(paste0("gini_mk ~ financial_openness + ", fml_ctrl, " | country + year")),
            data = raw, cluster = ~country)

m4_hc1   <- feols(as.formula(paste0("gini_mk ~ financial_openness + ", fml_ctrl, " | country + year")),
                   data = raw, vcov = "hetero")
m4_twway <- feols(as.formula(paste0("gini_mk ~ financial_openness + ", fml_ctrl, " | country + year")),
                   data = raw, cluster = ~country + year)
cat(reg_table_html(
  models = list(m1, m2, m3, m4),
  var_order = c("financial_openness", baseline_controls),
  col_names = c("(1)","(2)","(3)","(4)"),
  note = paste0("Table 2. Effect of financial openness on the pre-tax Gini coefficient (gini_mk). ", REG_NOTE),
  extra_meta = list("Country fixed effects" = c("No","No","Yes","Yes"),
                     "Year fixed effects"    = c("No","No","No","Yes"))
))
Variables (1) (2) (3) (4)
Financial openness -0.1107*** 0.0148** 0.0911*** 0.0821***
(0.0035) (0.0065) (0.0029) (0.0037)
Population 0.0115*** 0.0003 -0.0000
(0.0007) (0.0007) (0.0009)
Financial development 0.0033 0.0210*** 0.0254***
(0.0033) (0.0025) (0.0031)
GDP (log) -0.0017** -0.0057*** -0.0050***
(0.0007) (0.0009) (0.0015)
Inflation 0.0216*** 0.0213*** 0.0213***
(0.0012) (0.0003) (0.0012)
Age dependence -0.0775*** 0.0640*** 0.0429***
(0.0149) (0.0054) (0.0063)
Trade openness -0.0309*** -0.0011 -0.0004
(0.0059) (0.0018) (0.0016)
Urbanisation -0.0551*** -0.0087 -0.0024
(0.0081) (0.0077) (0.0099)
Tertiary enrolment -0.0593*** -0.0058* 0.0008
(0.0059) (0.0034) (0.0050)
Foreign net assets -0.0248*** -0.0008 -0.0009
(0.0025) (0.0006) (0.0006)
Country fixed effects No No Yes Yes
Year fixed effects No No No Yes
Observations 7,056 7,056 7,056 7,056
R-squared 0.1239 0.2645 0.9680 0.9691

Table 2. Effect of financial openness on the pre-tax Gini coefficient (gini_mk). Standard errors clustered at the country level in parentheses. , , denote significance at the 1%, 5%, 10% levels.

gini_range <- diff(range(raw$gini_mk, na.rm = TRUE))
coef_fo <- coef(m4)["financial_openness"]
cat(sprintf(
  "A 0.1-unit increase in financial openness is associated with an average increase of about %.4f in gini_mk, equivalent to %.2f%% of the observed range of gini_mk (%.3f).",
  coef_fo * 0.1, 100 * (coef_fo * 0.1) / gini_range, gini_range))
## A 0.1-unit increase in financial openness is associated with an average increase of about 0.0082 in gini_mk, equivalent to 1.81% of the observed range of gini_mk (0.453).

8.0.1 Standard-Error Sensitivity Comparison

cat(reg_table_html(
  models = list(m4, m4_hc1, m4_twway),
  var_order = "financial_openness",
  col_names = c("Clustered (country)","Robust (HC1)","Two-way clustered"),
  note = "Compares whether significance holds under three SE assumptions."
))
Variables Clustered (country) Robust (HC1) Two-way clustered
Financial openness 0.0821*** 0.0821*** 0.0821***
(0.0037) (0.0031) (0.0046)
Observations 7,056 7,056 7,056
R-squared 0.9691 0.9691 0.9691

Compares whether significance holds under three SE assumptions.

9 Income Share Regressions (Tables 3 & 4)

Addresses feedback: explain how the five shares relate to one another.

quintile_vars <- c("p80p100_inc","p60p80_inc","p40p60_inc","p20p40_inc","p0p20_inc")
m_quint <- map(quintile_vars, function(v) {
  feols(as.formula(paste0(v, " ~ financial_openness + ", fml_ctrl, " | country + year")),
        data = raw, cluster = ~country)
})
names(m_quint) <- c("Top 20%","4th 20%","Middle 20%","2nd 20%","Lowest 20%")
cat(reg_table_html(
  models = m_quint,
  var_order = c("financial_openness", baseline_controls),
  col_names = names(m_quint),
  note = paste0("Table 3. Effect of financial openness on income shares by quintile. ", REG_NOTE),
  extra_meta = list("Country & year FE" = rep("Yes", length(m_quint)))
))
Variables Top 20% 4th 20% Middle 20% 2nd 20% Lowest 20%
Financial openness 0.0733*** -0.0234*** -0.0201*** -0.0159*** -0.0139***
(0.0049) (0.0032) (0.0024) (0.0020) (0.0008)
Population -0.0018 0.0001 0.0012*** 0.0003 0.0002
(0.0012) (0.0005) (0.0004) (0.0003) (0.0001)
Financial development 0.0200*** -0.0057** -0.0079*** -0.0028* -0.0037***
(0.0040) (0.0024) (0.0019) (0.0015) (0.0007)
GDP (log) -0.0036** 0.0013 0.0016** 0.0002 0.0004
(0.0015) (0.0010) (0.0008) (0.0006) (0.0003)
Inflation 0.0127*** -0.0039*** -0.0041*** -0.0032*** -0.0016***
(0.0009) (0.0004) (0.0003) (0.0002) (0.0002)
Age dependence 0.0139* -0.0043 -0.0061 -0.0003 -0.0031**
(0.0077) (0.0054) (0.0044) (0.0033) (0.0015)
Trade openness 0.0021 -0.0031* 0.0005 0.0004 0.0000
(0.0029) (0.0017) (0.0014) (0.0013) (0.0004)
Urbanisation 0.0030 0.0033 -0.0074 0.0010 0.0000
(0.0126) (0.0079) (0.0064) (0.0054) (0.0023)
Tertiary enrolment 0.0027 -0.0040 0.0006 -0.0017 0.0023**
(0.0049) (0.0034) (0.0027) (0.0020) (0.0011)
Foreign net assets -0.0011 0.0005 0.0005 0.0001 0.0001
(0.0009) (0.0006) (0.0005) (0.0004) (0.0001)
Country & year FE Yes Yes Yes Yes Yes
Observations 7,056 7,056 7,056 7,056 7,056
R-squared 0.8367 0.5458 0.6594 0.5863 0.8320

Table 3. Effect of financial openness on income shares by quintile. Standard errors clustered at the country level in parentheses. , , denote significance at the 1%, 5%, 10% levels.

coef_sum_check <- sum(map_dbl(m_quint, ~ coef(.x)["financial_openness"]))
cat(sprintf("Sum of the five quintile-regression coefficients = %.5f (should be close to 0; internal consistency check)", coef_sum_check))
## Sum of the five quintile-regression coefficients = -0.00001 (should be close to 0; internal consistency check)
top_vars <- c("p999p100_inc","p99p100_inc","p95p100_inc","p90p100_inc","p80p100_inc")
m_top <- map(top_vars, function(v) {
  feols(as.formula(paste0(v, " ~ financial_openness + ", fml_ctrl, " | country + year")),
        data = raw, cluster = ~country)
})
names(m_top) <- c("Top 0.1%","Top 1%","Top 5%","Top 10%","Top 20%")
cat(reg_table_html(
  models = m_top,
  var_order = c("financial_openness", baseline_controls),
  col_names = names(m_top),
  note = paste0("Table 4. Effect of financial openness on top income shares. ", REG_NOTE),
  extra_meta = list("Country & year FE" = rep("Yes", length(m_top)))
))
Variables Top 0.1% Top 1% Top 5% Top 10% Top 20%
Financial openness 0.0121*** 0.0298*** 0.0610*** 0.0808*** 0.0733***
(0.0018) (0.0032) (0.0045) (0.0054) (0.0049)
Population -0.0002 0.0001 -0.0012 0.0003 -0.0018
(0.0002) (0.0003) (0.0012) (0.0018) (0.0012)
Financial development -0.0008 0.0026 0.0083* 0.0163*** 0.0200***
(0.0015) (0.0029) (0.0043) (0.0043) (0.0040)
GDP (log) -0.0013** -0.0022** -0.0028* -0.0021 -0.0036**
(0.0007) (0.0010) (0.0016) (0.0018) (0.0015)
Inflation 0.0006*** 0.0023*** 0.0053*** 0.0092*** 0.0127***
(0.0001) (0.0003) (0.0005) (0.0006) (0.0009)
Age dependence -0.0018 -0.0020 -0.0120 0.0039 0.0139*
(0.0029) (0.0060) (0.0081) (0.0086) (0.0077)
Trade openness 0.0004 0.0011 0.0052* 0.0028 0.0021
(0.0009) (0.0019) (0.0027) (0.0032) (0.0029)
Urbanisation -0.0042 -0.0083 -0.0162 -0.0120 0.0030
(0.0048) (0.0091) (0.0143) (0.0155) (0.0126)
Tertiary enrolment 0.0002 0.0003 0.0040 0.0100 0.0027
(0.0022) (0.0039) (0.0061) (0.0061) (0.0049)
Foreign net assets -0.0000 -0.0002 -0.0005 -0.0003 -0.0011
(0.0004) (0.0007) (0.0009) (0.0012) (0.0009)
Country & year FE Yes Yes Yes Yes Yes
Observations 7,056 7,056 7,056 7,056 7,056
R-squared 0.1471 0.3028 0.4702 0.6301 0.8367

Table 4. Effect of financial openness on top income shares. Standard errors clustered at the country level in parentheses. , , denote significance at the 1%, 5%, 10% levels.

Evidence-based check on the top 0.1% coefficient, rather than speculation:

top01_missing_pct <- miss_summary %>% filter(variable == "p999p100_inc") %>% pull(pct_missing)
raw_trim <- raw %>%
  filter(financial_openness > quantile(financial_openness, 0.01, na.rm = TRUE),
         financial_openness < quantile(financial_openness, 0.99, na.rm = TRUE))
m_top01_trim <- feols(as.formula(paste0("p999p100_inc ~ financial_openness + ", fml_ctrl, " | country + year")),
                       data = raw_trim, cluster = ~country)
top01_trim_coef <- coef(m_top01_trim)["financial_openness"]
top01_trim_p <- fixest::pvalue(m_top01_trim)["financial_openness"]
cat(sprintf(
  "Top 0.1%% variable missingness: %.2f%%. After trimming the most extreme 1%% of financial_openness, coefficient = %.4f (p=%.4f).",
  top01_missing_pct, top01_trim_coef, top01_trim_p))
## Top 0.1% variable missingness: 0.00%. After trimming the most extreme 1% of financial_openness, coefficient = 0.0121 (p=0.0000).

10 Robustness Checks (Table 5)

Addresses feedback: a one-period lag does not fully rule out reverse causality; a dynamic-panel GMM check is added as a further robustness test.

raw <- raw %>% group_by(country) %>%
  mutate(L_financial_openness = lag(financial_openness, 1)) %>% ungroup()

m5_1 <- feols(gini_mk ~ L_financial_openness | country + year, data = raw, cluster = ~country)
m5_2 <- feols(as.formula(paste0("gini_mk ~ L_financial_openness + ", fml_ctrl, " | country + year")),
              data = raw, cluster = ~country)
extra_controls <- c("unemployment","government_expenditure","saving")
m5_3 <- feols(as.formula(paste0("gini_mk ~ financial_openness + ", fml_ctrl, " + ",
                                 paste(extra_controls, collapse = " + "), " | country + year")),
              data = raw, cluster = ~country)
cat(reg_table_html(
  models = list(m5_1, m5_2, m5_3),
  var_order = c("L_financial_openness","financial_openness", baseline_controls, extra_controls),
  col_names = c("(1) Lagged FO","(2) Lagged FO + controls","(3) Additional controls"),
  note = paste0("Table 5. Robustness checks. ", REG_NOTE),
  extra_meta = list("Country & year FE" = c("Yes","Yes","Yes"))
))
Variables
  1. Lagged FO
  1. Lagged FO + controls
  1. Additional controls
Financial openness (t-1) 0.0899*** 0.0821***
(0.0051) (0.0037)
Financial openness 0.0815***
(0.0037)
Population -0.0000 0.0000
(0.0009) (0.0009)
Financial development 0.0254*** 0.0251***
(0.0031) (0.0031)
GDP (log) -0.0050*** -0.0051***
(0.0015) (0.0015)
Inflation 0.0213*** 0.0214***
(0.0012) (0.0012)
Age dependence 0.0429*** 0.0430***
(0.0063) (0.0063)
Trade openness -0.0004 -0.0003
(0.0016) (0.0016)
Urbanisation -0.0024 -0.0022
(0.0099) (0.0098)
Tertiary enrolment 0.0008 0.0012
(0.0050) (0.0050)
Foreign net assets -0.0009 -0.0008
(0.0006) (0.0007)
Unemployment 0.0365***
(0.0069)
Government expenditure 0.0087**
(0.0041)
National saving -0.0001
(0.0023)
Country & year FE Yes Yes Yes
Observations 7,056 7,056 7,056
R-squared 0.9369 0.9691 0.9693

Table 5. Robustness checks. Standard errors clustered at the country level in parentheses. , , denote significance at the 1%, 5%, 10% levels.

pdata <- pdata.frame(raw, index = c("country","year"))
m5_gmm <- tryCatch(
  pgmm(gini_mk ~ lag(gini_mk, 1) + financial_openness + lag(financial_openness,1) |
         lag(gini_mk, 2:99),
       data = pdata, effect = "twoways", model = "twosteps", transformation = "d"),
  error = function(e) NULL
)
if (!is.null(m5_gmm)) {
  cat(sprintf("Arellano-Bond GMM: financial_openness coefficient ≈ %.4f (see summary(m5_gmm) for full output).",
              coef(m5_gmm)["financial_openness"]))
} else {
  cat("GMM did not converge; lag order/instruments may need adjusting.")
}
## Arellano-Bond GMM: financial_openness coefficient ≈ 0.0556 (see summary(m5_gmm) for full output).

11 Association Visualised

Addresses feedback: avoid causal language.

raw %>%
  ggplot(aes(financial_openness, gini_mk, color = income_group_2022)) +
  geom_point(alpha = 0.25, size = 0.8) +
  geom_smooth(aes(group = 1), method = "loess", color = "black", se = TRUE) +
  labs(title = "Figure 4.5. Financial Openness vs. Pre-tax Gini Coefficient",
       subtitle = "Shows an association, not a causal relationship",
       x = "Financial openness", y = "Pre-tax Gini coefficient (gini_mk)", color = "Income group (2022)")