1. Purpose

This RMarkdown reconstructs the empirical workflow of Papageorgiou, Saam and Schulte rather than translating the Stata syntax mechanically.

The replication is organised as:

input/final data → cleaning and sample checks → variable construction → panel structure → CES/NLS estimation → Kmenta approximation → clustered bootstrap → transformations to elasticity of substitution → validation against the published Stata benchmark.

The authors define the elasticity as

\[ \sigma = \frac{1}{1-\psi} \]

The benchmark results are taken from the supplied estimation_results.log.

Important: the current uploaded package contains the two final .dta datasets, the two Stata programs, the paper, appendix, README and Stata log. The README identifies additional raw WIOD/IEA/GGDC files used by generate_data.do, but those raw files are not in the current working directory. Therefore this notebook reproduces the empirical estimation exactly from the authors’ final datasets and documents the raw-data construction from the original do-file. When the raw files are supplied, the raw-data section can be completed without changing the estimation section.

2. What the original workflow does

The supplied README states that generate_data.do uses the raw files to generate the two final datasets, while estimation.do produces the paper’s estimation results. The final electricity dataset covers 26 countries and 1995–2009. The final nonenergy dataset covers 19 countries, 28 industries and 1995–2007.

The original raw-data pipeline is:

  1. WIOD SEA data
  2. GGDC PPP data
  3. WIOD emission-relevant energy data
  4. IEA electricity generation
  5. IEA electricity capacity
  6. IEA electricity fuel use
  7. WIOD supply/use tables
  8. IEA electricity prices
  9. IEA industry prices
  10. Merge all sources
  11. Copy PPP/price information to WIOD subsectors
  12. Deflate and construct real quantities
  13. Construct clean/dirty energy and capital measures
  14. Split into electricity and nonenergy final datasets

This is implemented in the original Stata code in sections (1)–(14).

3. Raw-data construction map

3.1 Raw files required

The README lists:

  • flatfile_SEA.txt
  • flatfile_EM_may12.txt
  • PPP_*.xml
  • *_SUT_*.xls
  • IEA_elec_gen.xml
  • IEA_elec_cap.xml
  • IEA_elec_fuel.xml
  • IEA_elec_prices_NCV.xml
  • IEA_ind_prices_NCV.xml

The original Stata code also creates intermediate files such as SEA.dta, PPP97.dta, EM.dta, SUTS.dta, and the IEA .dta files.

The current notebook deliberately does not invent raw files that have not been supplied.

4. Load the authors’ final datasets

electricity <- read_dta("~/Desktop/FAU /JOBS/LEibneiz/replication/repli_cleandirty_v1/electricity_sector.dta") |>
  mutate(country = as.character(country), industry = as.character(industry))

nonenergy <- read_dta("~/Desktop/FAU /JOBS/LEibneiz/replication/repli_cleandirty_v1/nonenergy_industries.dta") |>
  mutate(country = as.character(country), industry = as.character(industry))

bind_rows(
  electricity |> summarise(dataset = "electricity", n = n(),
                            countries = n_distinct(country),
                            min_year = min(year), max_year = max(year)),
  nonenergy |> summarise(dataset = "nonenergy", n = n(),
                          countries = n_distinct(country),
                          min_year = min(year), max_year = max(year))
) |> kable()
dataset n countries min_year max_year
electricity 390 26 1995 2009
nonenergy 6914 19 1995 2007

Benchmark: 390 electricity observations; 6,914 non-energy observations.

5. Electricity-sector variable construction

The original final dataset already contains the variables generated in generate_data.do.

The clean capacity measure \(EC_c\) (nuclear, hydro, geothermal, solar, tide/wave/ocean, wind) is: \[EC_c = EC_{nuclear}+EC_{hydro}+EC_{geotherm}+EC_{solar}+EC_{tidewaveocean}+EC_{wind}\] Dirty capacity \(EC_d\) (combustion + other) is:

\[EC_d = EC_{totcombust}+EC_{other}\] The authors also construct clean and dirty fuel use, and an alternative capital proxy based on technology-specific EIA investment costs.The estimation variables are: \[\ln EG,\quad \ln EC_c,\quad \ln EC_d,\] and

\[\ln EG-\ln EC_d,\qquad\ln EC_c-\ln EC_d,\qquad\frac12(\ln EC_c-\ln EC_d)^2.\]

elec_audit <- electricity |> #The |> is the base R pipe.It means:Take electricity and pass it into the next operation.
  summarise(
    n = n(),
    countries = n_distinct(country),
    years = n_distinct(year),
    min_year = min(year),
    max_year = max(year),
    zero_clean_capacity = sum(EC_c <= 0, na.rm = TRUE),
    zero_dirty_capacity = sum(EC_d <= 0, na.rm = TRUE),
    missing_output = sum(is.na(ln_eg)),
    missing_clean = sum(is.na(ln_ecc)),
    missing_dirty = sum(is.na(ln_ecd))
  )

knitr::kable(elec_audit)
n countries years min_year max_year zero_clean_capacity zero_dirty_capacity missing_output missing_clean missing_dirty
390 26 15 1995 2009 0 0 0 0 0

Reconstructing the transformations independently as a validation check:

#The purpose here is: Independently reconstruct the important variables and compare our reconstruction with the variables supplied in the .dta file.This is extremely valuable for replication.
# converting the values into natural logarithms 

electricity <- electricity |>
  arrange(country, year) |>
  mutate(
    ln_eg_R  = log(EG_total), ln_ecc_R = log(EC_c), ln_ecd_R = log(EC_d),
    ln_egecd_R = ln_eg_R - ln_ecd_R,
    ln_eccd_R  = ln_ecc_R - ln_ecd_R,
    ln_eccd_2_R = 0.5 * ln_eccd_R^2,
    ln_ecc_alt_R = log(EC_c_alt), ln_ecd_alt_R = log(EC_d_alt)
  )

# Independent recomputation vs. supplied values -- should all be ~0
vars <- c("ln_eg", "ln_ecc", "ln_ecd", "ln_egecd", "ln_eccd", "ln_eccd_2") #This simply creates a list of variables that we want to check.

#tibble() creates a small data frame We are telling R: Create a table where the first column contains the variable names.
tibble( 
  variable = vars,
  max_abs_difference = map_dbl(vars, \(v)
    max(abs(electricity[[v]] - electricity[[paste0(v, "_R")]]), na.rm = TRUE))
) |> kable(digits = 10)
variable max_abs_difference
ln_eg 4.7580e-07
ln_ecc 4.7670e-07
ln_ecd 4.7670e-07
ln_egecd 8.8500e-07
ln_eccd 9.3660e-07
ln_eccd_2 1.7015e-06

6. Nonenergy-sector variable construction

The authors aggregate emission-relevant energy by fuel type.

Clean energy includes:

Dirty energy contains the remaining emission-relevant energy sources.

The paper’s appendix describes three main construction stages:

  1. aggregate fuel-specific energy use into clean and dirty energy
  2. derive intermediate energy, service and material inputs from WIOD use tables
  3. convert nominal local-currency values to real 1997 US dollars using GGDC PPPs and WIOD price indices

Nonenergy: clean energy = biogasoline, biodiesel, biogas, other renewables, electricity, heat, hydro, geothermal, solar, wind, other, nuclear, waste; dirty = remaining emission-relevant sources. Final sample: 6,914 obs.

nonenergy |>
  summarise(n = n(), countries = n_distinct(country),
            industries = n_distinct(industry),
            cells = n_distinct(paste(country, industry)),
            zero_clean = sum(xc <= 0, na.rm = TRUE),
            zero_dirty = sum(xd <= 0, na.rm = TRUE)) |>
  kable()
n countries industries cells zero_clean zero_dirty
6914 19 28 532 0 0

The final sample should contain 6,914 observations.

7. Panel structure

Electricity & Nonenergy

#This chunk prepares the data as panel data, meaning observations are organized by country and year (and, for nonenergy, by industry).
#arrange(country, year) → puts each country's observations in chronological order.
#group_by(country) → tells R to calculate lags within each country, not across countries.
#lag(ln_eg) → gives the previous year's \(\ln EG\).
#Therefore:
#$$ dln\_eg_{it}=\ln EG_{it}-\ln EG_{i,t-1} $$ which is the first difference used in the FD CES model.
#l1EC_c and l1EC_d similarly store the previous year's clean and dirty capacity.
#ungroup() → removes the grouping after the calculations.

elec_panel <- electricity |>
  arrange(country, year) |>
  group_by(country) |>
  mutate(dln_eg = ln_eg - lag(ln_eg), l1EC_c = lag(EC_c), l1EC_d = lag(EC_d)) |>
  ungroup()
table(table(elec_panel$country))   # expect balanced: 26 x 15
## 
## 15 
## 26
#Here the panel is country × industry × year.
#arrange() → puts each country-industry cell in time order.
#group_by(country, industry) → treats each country-industry combination as one panel.
#cur_group_id() → assigns a unique numerical ID to every country-industry cell.

nonenergy <- nonenergy |>
  arrange(country, industry, year) |>
  group_by(country, industry) |>
  mutate(id = cur_group_id()) |>
  ungroup()
table(table(nonenergy$id))          # expect 532 cells
## 
##  11  13 
##   1 531

The original Stata code reports a strongly balanced 26-country panel from 1995 to 2009 and therefore 15 years per country.

The paper reports 532 country-industry cells and 6,914 observations.

8. Cluster-robust covariance helper

The coefficient estimates are obtained by least squares. Clustering changes the covariance matrix, not the point estimates.

# We use clustered standard errors because observations within the same
# country may be correlated over time. Clustering changes the estimated
# standard errors and p-values, but NOT the regression coefficients.

# Calculate a country-clustered covariance matrix.
# This is used to obtain the cluster-robust standard errors.
cluster_vcov <- function(model, cluster) {
  sandwich::vcovCL(model, cluster = cluster, type = "HC1")
}
# Create a regression table containing coefficients, clustered SEs,
# test statistics, and p-values.
coef_table_cluster <- function(model, cluster) {
  V <- cluster_vcov(model, cluster)  # Obtain the clustered covariance matrix.
  b <- coef(model) # Extract the original regression coefficients. Clustering does NOT change these point estimates.
  se <- sqrt(diag(V)) # Standard errors are the square roots of the diagonal elements of the clustered covariance matrix.
  tibble(  # Combine everything into one table.
    term = names(b), 
    estimate = unname(b), # Estimated regression coefficient
    std.error = se,   # Country-clustered standard error
    statistic = b / se,  # t/z-style statistic = coefficient / clustered SE
    p.value = 2 * pt(abs(b / se), df = length(unique(cluster)) - 1,
                     lower.tail = FALSE) # Two-sided p-value using number of countries - 1 as the degrees of freedom, following the Stata benchmark.
  )
}

10. Electricity Table 3

This section reproduces Table 3 — Nonlinear Estimation and Kmenta Approximation of CES: Electricity Sector.

The four specifications are:

The authors define the elasticity of substitution as

\[\sigma=\frac{1}{1-\psi}.\] The CES specification is estimated using the same starting values as the original Stata program: a = 20 d = 0.01 = -0.5 = 0.5

and a maximum of 100 iterations.

The electricity dataset contains 390 observations covering 26 countries over 1995–2009. The first-difference specifications therefore contain 364 observations. These dimensions are also reported in the supplied replication materials.

10.1 Nonlinear CES estimator

For a two-input CES:

\[CES(X_c,X_d)=\left[\omega X_c^\psi+(1-\omega)X_d^\psi\right]^{1/\psi}.\]

The elasticity is

\[\sigma=\frac{1}{1-\psi}.\]

The electricity levels specification is:

\[\ln EG_{it}=a+d\,year_t+\alpha_i+\frac{1}{\psi}\ln\left[\omega EC_{c,it}^{\psi}+(1-\omega)EC_{d,it}^{\psi}\right]+\varepsilon_{it}.\]

Starting values follow the original Stata program: a=20, d=0.01, psi=-0.5, omega=0.5, 100 iterations for NLS specs.

10.2 CES in levels — Column 1

The country fixed effects are profiled out of the nonlinear least-squares objective. This is equivalent to including country dummy variables while avoiding an unnecessarily large nonlinear optimization problem.

# CES production function written in logarithmic form.
# It combines clean and dirty energy inputs using the CES specification.
ces_log <- function(xc, xd, psi, omega) (1 / psi) * log(omega * xc^psi + (1 - omega) * xd^psi)
# xc = clean energy/capital input, xd = dirty energy/capital input ,psi = CES curvature parameter, omega = distribution/weight parameter. The function returns the CES contribution to log output.

# Column 1: CES NLS in levels, country FE profiled out via QR (computed ONCE
# per fit call, not inside the objective -- this was the main source of slow
# knitting, since the objective is evaluated hundreds/thousands of times).


coef.optim_fit <- function(object, ...) object$par # Extracting the estimated parameters from an optim() object. This makes coef(fit1) return the estimated parameter vector.

# Define the function that estimates the electricity CES model using NLS.
fit_elec_nls_explicit <- function(dat, # Dataset containing electricity observations
                                 clean = "EC_c", # Variable for clean energy/capacity
                                 dirty = "EC_d", # Variable for dirty energy/capacity
                                 fe_var = "country", # Country fixed effects
                                 # Starting values for numerical optimization
                                 start = c(a = 20, d = 0.01, psi = -0.5, omega = 0.5)) {
  cc <- dat[[clean]]; dd <- dat[[dirty]]   # Extract clean-energy & dirty-enrgy observations from the 
  
  # Creating a country dummy-variable matrix.
  # This represents the country fixed effects in the model.
  # intercept = FALSE avoids creating an additional common intercept.
  X   <- model.matrix(reformulate(fe_var, intercept = FALSE), data = dat)
  # Calculate X'X once.
  # We need this matrix repeatedly when solving for the country fixed effects.
  # Computing it here, rather than inside the objective function,
  # makes the optimization much faster.
  XtX <- crossprod(X)

  
  # Define the objective function that optim() will minimize.
  objective <- function(par) {
    # Construct the model's predicted log output before country fixed effects.
    #
    # a                  = common intercept
    # d * year           = time trend
    # ces_log(...)       = CES contribution of clean and dirty inputs
    core <- par["a"] + par["d"] * dat$year + ces_log(cc, dd, par["psi"], par["omega"])
    # Calculate the part of log output that remains after
    # removing the predicted common component.
    y  <- dat$ln_eg - core
    # Estimate the country fixed effects for the current values
    # of a, d, psi and omega.
    #
    # qr.solve() solves:
    #        (X'X) FE = X'y
    #
    # so that FE = (X'X)^(-1) X'y.
    fe <- qr.solve(XtX, crossprod(X, y))
    # Calculate the residual sum of squares (SSR).
    # This is what optim() tries to minimize.
    #
    # Smaller SSR = model fits the observed log electricity output better.
    sum((y - drop(X %*% fe))^2)
  }
  # Numerically minimize the SSR defined above.
  # Nelder-Mead searches for the values of a, d, psi and omega
  # that give the smallest residual sum of squares.
  fit <- optim(start, objective, method = "Nelder-Mead",
               control = list(maxit = 10000, reltol = 1e-8))  # Allow up to 10,000 optimization iterations. Require a very small relative change before convergence
  
  # Calculate total sum of squares around the mean of log output.
  # This provides the denominator for the pseudo R-squared.
  tss    <- sum((dat$ln_eg - mean(dat$ln_eg))^2)
  # Calculate pseudo R-squared:
  # 1 - unexplained variation / total variation.
  #
  # fit$value = minimized residual sum of squares.
  fit$r2 <- 1 - fit$value / tss   # pseudo R^2 from the profiled SSR
  structure(fit, class = c("optim_fit", class(fit)))
}
# Give the optimization result a special class.
  # This allows coef.optim_fit() above to extract fit$par
  # when coef(fit1) is called.
# Estimate the electricity CES model using the electricity dataset.
# The default variables are EC_c (clean) and EC_d (dirty).
fit1     <- fit_elec_nls_explicit(electricity)
elec_nls <- coef(fit1)
sigma_elec_nls <- 1 / (1 - elec_nls["psi"])
adjr2_1  <- fit1$r2
elec_nls; sigma_elec_nls
##            a            d          psi        omega 
## 18.408033256 -0.001263206  0.456570304  0.219438245
##      psi 
## 1.840164

The Stata benchmark gives:

  • \(\psi \approx 0.4566\)
  • \(\omega \approx 0.2195\)
  • \(\sigma \approx 1.8402\)
  • \(N=390\)

The supplied Stata log reports these estimates and clustered standard errors. The paper rounds the headline estimates to about \(\psi=0.457\) and \(\sigma=1.840\).

The logic in simple terms

Your model is essentially trying to explain:

\[ \text{log electricity output} = \text{country effect} + \text{time trend} + \text{CES combination of clean and dirty inputs} + \text{error} \]

The important part is that optim() does not directly estimate the country fixed effects together with everything else.

Instead, for every trial value of:

a d psi omega

the code:

Calculates the CES component. Calculates the remaining part of output. Solves for the country fixed effects using qr.solve(). Calculates the residual sum of squares. optim() searches for the parameter values that minimize that residual sum of squares.

So the computational logic is:

Try parameters → calculate CES → estimate country FE → calculate errors → calculate SSR → try better parameters → repeat until convergence.

10.3 CES first-difference specification — Column 2

The original specification is:

\[\Delta\ln EG_{it}=d+\frac{1}{\psi}\ln\left(\frac{\omega EC_{c,it}^{\psi}+(1-\omega)EC_{d,it}^{\psi}}{\omega EC_{c,i,t-1}^{\psi}+(1-\omega)EC_{d,i,t-1}^{\psi}}\right)+\varepsilon_{it}.\]

The first differences remove the country fixed effects.

# Column 2: CES NLS in first differences.
# First differencing removes the country fixed effects because they do not change over time.
# Prepare the electricity panel for first-difference estimation.
elec_fd <- electricity |> # Sort observations within each country chronologically. This is essential because lag() must know which observation is the previous year.
  arrange(country, year) |> # Treat each country as a separate time series.
  # The lag and difference calculations will therefore be done within each country.
  group_by(country) |>
# Create the variables needed for the first-difference CES model.
  mutate(
    # Change in log electricity output: current year's log output minus previous year's log output. This is Δln(EG).
    dln_eg = ln_eg - lag(ln_eg), # Previous year's clean-energy capacity. Needed because the CES function is evaluated in both periods.
    l1EC_c = lag(EC_c), # Previous year's dirty-energy capacity. Also needed to compare the CES input combination between two periods.
    l1EC_d = lag(EC_d)
  ) |># Remove the country grouping after calculating the lags. This prevents later operations from accidentally remaining country-specific.
  ungroup() |># Keep only observations for which the variables needed for estimation are finite numbers. The first year of every country has no lag, so it produces NA. Those observations cannot be used in the first-difference regression.
  filter(is.finite(dln_eg),is.finite(l1EC_c),is.finite(l1EC_d) ) # Estimate the nonlinear CES model in first differences.
fit_elec_fd <- nlsLM( # Dependent variable: # Δln(EG), the change in log electricity output.
  dln_eg ~ # d represents the time-trend coefficient after first differencing. The country fixed effect has disappeared through differencing.
    d +
 #Change in the CES log production component. The numerator is the CES energy combination in the current year. The denominator is the CES energy combination in the previous year.
   
    # Taking the log of their ratio gives the first difference of the CES component.
    (1 / psi) * log((omega * EC_c^psi +(1 - omega) * EC_d^psi) /(omega * l1EC_c^psi + (1 - omega) * l1EC_d^psi)),
# Use the first-difference dataset created above.
  data = elec_fd,
# Starting values for the nonlinear optimization.
  # nlsLM needs reasonable initial guesses before searching
  # for the parameters that minimize the residuals.
  start = list(d = 0.01, psi = -0.5,omega = 0.5),
# Allow up to 100 iterations for the nonlinear optimization.
  control = nls.lm.control(maxiter = 100)
)


# Extract the estimated coefficients from the nonlinear model. These are the estimates of d, psi and omega.
fd_coef <- coef(fit_elec_fd)


# Convert the estimated CES parameter psi into the elasticity of substitution sigma. sigma = 1 / (1 - psi)
sigma_elec_fd <- 1 / (1 - fd_coef["psi"])


# Calculate a pseudo/adjusted fit measure for the first-difference model.
#
# deviance(fit_elec_fd) = residual sum of squares.
# The denominator measures the total variation in Δln(EG).
adjr2_2 <- 1 - deviance(fit_elec_fd) /sum((elec_fd$dln_eg - mean(elec_fd$dln_eg))^2)


# Display the estimated first-difference CES coefficients:
# d, psi and omega.
fd_coef
##           d         psi       omega 
## -0.00318516  0.48655838  0.44177154
# Display the estimated elasticity of substitution sigma.
sigma_elec_fd
##      psi 
## 1.947641

The supplied benchmark is approximately:

  • \(d=-0.00319\)
  • \(\psi=0.48655\)
  • \(\omega=0.44177\)
  • \(\sigma\approx1.948\)
  • \(N=364\)

The specification contains 364 observations.

The logic of this chunk

Think of it as:

Original panel data

\[\ln(EG_{it}) =FE_i + dt + CES(EC_{cit},EC_{dit})+\epsilon_{it}\]

Then we take the difference between year \(t\) and \(t-1\):

\[\Delta\ln(EG_{it})=d+\Delta CES_{it}+\Delta\epsilon_{it}\]

The important thing is:

\[FE_i-FE_i=0\]

So country fixed effects disappear automatically.

Why do we need lag()?

For example, suppose Germany has:

Year ln_eg EC_c EC_d
1995 5.0 10 20
1996 5.2 12 21
1997 5.3 14 22

10.4. Kmenta approximation (Columns 3 & 4)

The paper uses the first-order Kmenta approximation:

\[\ln Y_{it}=a_i+d_t+\omega\ln K_{Cit} +(1-\omega)\ln K_{Dit} + \frac{(1-\omega)\psi}{2}(\ln K_C-\ln K_D)^2.\] After subtracting \(\ln K_D\):

\[\ln y_{it}=a_i+d_t+\omega\ln k_{it}-\beta_2(\ln k_{it})^2+\epsilon_{it}.\]

The authors recover the CES parameters from the estimated translog coefficients. The exact transformation used in Stata is preserved below.

# Estimate the Kmenta model and recover the CES parameters (omega, psi and sigma) from the OLS coefficients.
# Regression formula to be estimated
# Dataset used for estimation
# Name of the first Kmenta coefficient
# Name of the quadratic Kmenta coefficient
# Default: cluster standard errors by country

estimate_kmenta <- function(formula, dat, x_var, x2_var, cluster = ~country) {
  environment(formula) <- environment()
  m <- lm(formula, data = dat)
  V <- vcovCL(m, cluster = cluster, type = "HC1")
  b <- coef(m)
  beta1 <- unname(b[x_var]); beta2 <- unname(b[x2_var])
  sigma <- beta1 * (1 - beta1) / (beta1 * (1 - beta1) - beta2)
  psi   <- 1 - 1 / sigma

  grad_psi <- grad(function(z) {
    s <- z[1] * (1 - z[1]) / (z[1] * (1 - z[1]) - z[2])
    1 - 1 / s
  }, c(beta1, beta2))
  Vsub   <- V[c(x_var, x2_var), c(x_var, x2_var)]
  se_psi <- sqrt(drop(grad_psi %*% Vsub %*% grad_psi))

  list(model = m, coefs = b, se = sqrt(diag(V)),
       omega = beta1, psi = psi, sigma = sigma, se_psi = se_psi,
       adjr2 = summary(m)$adj.r.squared, N = nobs(m))
}
elec_k <- electricity |> mutate(y_k = ln_egecd, x_k = ln_eccd, x_k2 = ln_eccd_2)
kmenta_levels <- estimate_kmenta(y_k ~ country + year + x_k + x_k2, elec_k, "x_k", "x_k2")

elec_kfd <- electricity |>
  arrange(country, year) |> group_by(country) |>
  mutate(dy = ln_egecd - lag(ln_egecd), dx = ln_eccd - lag(ln_eccd),
         dx2 = ln_eccd_2 - lag(ln_eccd_2)) |>
  ungroup() |> filter(is.finite(dy), is.finite(dx), is.finite(dx2))
kmenta_fd <- estimate_kmenta(dy ~ dx + dx2, elec_kfd, "dx", "dx2")

list(levels = kmenta_levels[c("omega", "psi", "sigma", "adjr2", "N")],
     fd     = kmenta_fd[c("omega", "psi", "sigma", "adjr2", "N")])
## $levels
## $levels$omega
## [1] 0.2453325
## 
## $levels$psi
## [1] 0.4463543
## 
## $levels$sigma
## [1] 1.806209
## 
## $levels$adjr2
## [1] 0.9679572
## 
## $levels$N
## [1] 390
## 
## 
## $fd
## $fd$omega
## [1] 0.4510513
## 
## $fd$psi
## [1] 0.4544551
## 
## $fd$sigma
## [1] 1.83303
## 
## $fd$adjr2
## [1] 0.5461071
## 
## $fd$N
## [1] 364

Benchmarks – levels: \(\omega=0.2453,\ \psi=0.4464,\ \sigma=1.8062,\ R^2_{adj}=0.9680,\ N=390\). FD: \(\omega=0.4511,\ \psi=0.4545,\ \sigma=1.8330,\ R^2_{adj}=0.5461,\ N=364\).

10.5 400-replication country-cluster bootstrap

Only Columns 1 & 2 need bootstrapped SEs (3 & 4 already have clustered OLS SEs). Cluster indices are precomputed once (split()) and each bootstrap sample is built with a single vectorized subset instead of a per-cluster loop + bind_rows() – this was the other main slow part of the original.

cluster_bootstrap <- function(dat, cluster, fit_fun, B = 400L, seed = 123L,
                               idcluster = FALSE, reject_fun = \(p) FALSE,
                               max_attempts = 10000L) {
  set.seed(seed)
  cluster_values <- as.character(dat[[cluster]])
  dat <- dat[!is.na(cluster_values), , drop = FALSE]
  cluster_values <- cluster_values[!is.na(cluster_values)]
  clusters  <- unique(cluster_values)
  row_index <- split(seq_len(nrow(dat)), cluster_values)  # computed once, O(1) lookup below

  results <- vector("list", B); accepted <- 0L; attempts <- 0L
  while (accepted < B && attempts < max_attempts) {
    attempts <- attempts + 1L
    sampled  <- sample(clusters, length(clusters), replace = TRUE)
    idx      <- row_index[sampled]
    n_per    <- lengths(idx)
    boot_dat <- dat[unlist(idx, use.names = FALSE), , drop = FALSE]
    if (idcluster) boot_dat$.boot_cluster <- paste0("boot_", rep(seq_along(sampled), n_per))

    fit <- tryCatch(suppressWarnings(fit_fun(boot_dat)), error = \(e) NULL)
    if (is.null(fit)) next
    p <- tryCatch(as.numeric(coef(fit)), error = \(e) NULL)
    if (is.null(p) || !all(is.finite(p))) next
    names(p) <- names(coef(fit))
    if (isTRUE(tryCatch(reject_fun(p), error = \(e) TRUE))) next

    accepted <- accepted + 1L
    results[[accepted]] <- p
  }
  if (accepted < B) stop("Only ", accepted, " of ", B, " replications succeeded after ", attempts, " attempts.")
  as.data.frame(do.call(rbind, results), check.names = FALSE)
}

reject_psi <- \(p) isTRUE(p["psi"] > 1) || isTRUE(is.na(p["psi"]))

boot_levels <- cluster_bootstrap(
  electricity, "country",
  fit_fun = \(x) fit_elec_nls_explicit(x, fe_var = ".boot_cluster"),
  idcluster = TRUE, reject_fun = reject_psi)

boot_fd <- cluster_bootstrap(
  elec_fd, "country",
  fit_fun = \(x) nlsLM(
    dln_eg ~ d + (1 / psi) * log(
      (omega * EC_c^psi + (1 - omega) * EC_d^psi) /
      (omega * l1EC_c^psi + (1 - omega) * l1EC_d^psi)),
    data = x, start = as.list(fd_coef), control = nls.lm.control(maxiter = 100)),
  reject_fun = reject_psi)

se_nls <- sapply(boot_levels[c("d", "psi", "omega")], sd, na.rm = TRUE)
se_fd  <- sapply(boot_fd[c("d", "psi", "omega")], sd, na.rm = TRUE)
tibble(spec = c("CES NLS levels", "CES NLS first differences"),
       requested = 400, successful = c(nrow(boot_levels), nrow(boot_fd)))
## # A tibble: 2 × 3
##   spec                      requested successful
##   <chr>                         <dbl>      <int>
## 1 CES NLS levels                  400        400
## 2 CES NLS first differences       400        400

10.6 Table 3

One column-builder function replaces the four nearly-identical blocks in the original (NLS uses bootstrap/normal inference – df = Inf in pt() is just pnorm(); OLS uses df = n_clusters - 1).

stars    <- \(p) ifelse(p < .01, "***", ifelse(p < .05, "**", ifelse(p < .10, "*", "")))
fmt      <- \(x) formatC(as.numeric(x), format = "f", digits = 3)
stat_row <- \(est, se) paste0("(", formatC(as.numeric(est / se), format = "f", digits = 2), ")")

col_stats <- function(d, se_d, omega, se_omega, psi, se_psi, sigma, adjr2, N, hasFE, df = Inf) {
  p <- \(z) 2 * pt(-abs(z), df = df)
  c(paste0(fmt(d), stars(p(d / se_d))),            stat_row(d, se_d),
    paste0(fmt(omega), stars(p(omega / se_omega))), stat_row(omega, se_omega),
    paste0(fmt(psi), stars(p(psi / se_psi))),        stat_row(psi, se_psi),
    if (hasFE) "Yes" else "No",
    fmt(adjr2), fmt(1 - pchisq((psi / se_psi)^2, df = 1)), fmt(sigma), as.character(N))
}
df_cluster <- n_distinct(electricity$country) - 1

cols <- list(
  NLS      = col_stats(elec_nls["d"], se_nls["d"], elec_nls["omega"], se_nls["omega"],
                        elec_nls["psi"], se_nls["psi"], sigma_elec_nls, adjr2_1,
                        nrow(electricity), hasFE = TRUE),
  `FD NLS` = col_stats(fd_coef["d"], se_fd["d"], fd_coef["omega"], se_fd["omega"],
                        fd_coef["psi"], se_fd["psi"], sigma_elec_fd, adjr2_2,
                        nrow(elec_fd), hasFE = FALSE),
  OLS      = col_stats(kmenta_levels$coefs["year"], kmenta_levels$se["year"],
                        kmenta_levels$omega, kmenta_levels$se["x_k"],
                        kmenta_levels$psi, kmenta_levels$se_psi, kmenta_levels$sigma,
                        kmenta_levels$adjr2, kmenta_levels$N, hasFE = TRUE, df = df_cluster),
  `FD OLS` = col_stats(kmenta_fd$coefs["(Intercept)"], kmenta_fd$se["(Intercept)"],
                        kmenta_fd$omega, kmenta_fd$se["dx"],
                        kmenta_fd$psi, kmenta_fd$se_psi, kmenta_fd$sigma,
                        kmenta_fd$adjr2, kmenta_fd$N, hasFE = FALSE, df = df_cluster)
)

table3 <- as_tibble(cols) |>
  mutate(term = c("d", "", "omega", "", "psi", "", "Country DV",
                   "Adjusted R2", "psi = 0 (Wald p)", "sigma", "N"), .before = 1)

table3 |>
  kable(format = "html", escape = FALSE,
        caption = "Table 3.-Nonlinear Estimation and Kmenta Approximation of CES: Electricity Sector") |>
  add_header_above(c(" " = 1, "CES" = 2, "Kmenta" = 2)) |>
  kable_styling(full_width = FALSE, bootstrap_options = "condensed") |>
  row_spec(c(2, 4, 6), italic = TRUE)
Table 3.-Nonlinear Estimation and Kmenta Approximation of CES: Electricity Sector
CES
Kmenta
term NLS FD NLS OLS FD OLS
d -0.001 -0.003 -0.001 -0.003
(-0.67) (-1.60) (-0.50) (-1.18)
omega 0.219*** 0.442*** 0.245*** 0.451***
(3.01) (6.71) (6.02) (7.87)
psi 0.457** 0.487*** 0.446*** 0.454***
(2.39) (4.43) (3.39) (9.87)
Country DV Yes No Yes No
Adjusted R2 0.998 0.192 0.968 0.546
psi = 0 (Wald p) 0.017 0.000 0.001 0.000
sigma 1.840 1.948 1.806 1.833
N 390 364 390 364

z-statistics in parentheses. Significantly different from 0 at 1%, 5%, 10%. Columns 1 and 2 provide bootstrapped standard errors based on 400 replications with country as cluster variable. Specification 1 applies the nonlinear least squares (NLS) estimator and includes country dummies. Specification 2 applies the NLS estimator to a first-differenced version of the model. Specification 3 applies the OLS estimator and includes country dummies. Specification 4 applies the OLS estimator to a first-differenced version of the model. \(\psi=0\) reports the significance level of a Wald test with \(H_0:\psi=0\).

Interpretation of Table 3

Table 3 shows that clean and dirty electricity capacities are substitutes, with the estimated elasticity of substitution significantly above 1.

  • The CES-NLS estimates give \(\psi \approx 0.46\), implying \(\sigma \approx 1.84\) in levels and \(\sigma \approx 1.95\) in first differences.
  • The Kmenta-OLS estimates are very similar, with \(\sigma \approx 1.81\)\(1.83\), supporting the robustness of the CES results across estimation methods.
  • The hypothesis \(\psi = 0\) is rejected in all four specifications, indicating that the elasticity of substitution is significantly different from 1.
  • The first-difference specifications also produce elasticities well above 1, showing that the result is not driven solely by country fixed effects.
  • Overall, the authors find an elasticity of substitution of around 2 in the electricity-generating sector. This is important because an elasticity above 1 is a necessary condition for long-run green growth in their theoretical framework.
  • The results therefore suggest that clean electricity capacity can substitute for dirty capacity relatively strongly, which is favorable for the transition toward cleaner energy and long-run green growth.

Overall interpretation: The evidence indicates substantial substitution possibilities between clean and dirty electricity capacity, with an elasticity of about 2. This supports the view that replacing dirty energy with clean energy is technologically feasible without a proportionally large loss in productive capacity.

The authors caution, however, that the estimates should be interpreted as associations rather than causal effects, because potential instruments for addressing endogeneity were not found to be sufficiently exogenous.

11. Table 4 — Alternative Capital Proxy

# The alternative proxy uses EIA-based capital stocks instead of installed capacity.
# The available alternative-capital sample ends in 2007: 338 observations in levels
# and 312 observations after first differencing.

elec_alt <- electricity |>
  filter(is.finite(ln_ecc_alt), is.finite(ln_ecd_alt)) |>
  arrange(country, year) |>
  mutate(
    ln_egecd_alt   = ln_eg - ln_ecd_alt,
    ln_eccd_alt    = ln_ecc_alt - ln_ecd_alt,
    ln_eccd_2_alt  = 0.5 * ln_eccd_alt^2
  )
table(table(elec_alt$country))
## 
## 13 
## 26

11.1 Table 4: Column 1

# Column 1: CES NLS in levels with country fixed effects.
fit_alt_nls <- fit_elec_nls_explicit(elec_alt, clean = "EC_c_alt", dirty = "EC_d_alt")

alt_nls <- coef(fit_alt_nls)
sigma_alt_nls <- 1 / (1 - alt_nls["psi"])

alt_nls; sigma_alt_nls
##           a           d         psi       omega 
## 21.90192211 -0.00968515  0.42339436  0.19330930
##      psi 
## 1.734288

The supplied benchmark is approximately:

  • \(d=-0.0010\)
  • \(\psi=0.4233\)
  • \(\omega=0.0.1933\)
  • \(\sigma\approx1.734\)
  • \(N=366\)

11.1 Table 4: Column 2

# Column 2: CES NLS in first differences.
elec_alt_fd <- elec_alt |> group_by(country) |>
  mutate(
    dln_eg = ln_eg - lag(ln_eg),
    l1EC_c_alt = lag(EC_c_alt),
    l1EC_d_alt = lag(EC_d_alt)
  ) |>
  ungroup() |>
  filter(is.finite(dln_eg), is.finite(l1EC_c_alt), is.finite(l1EC_d_alt))

fit_alt_fd <- nlsLM(dln_eg ~ d + (1 / psi) * log((omega * EC_c_alt^psi + (1 - omega) * EC_d_alt^psi) /(omega * l1EC_c_alt^psi + (1 - omega) * l1EC_d_alt^psi)),
  data = elec_alt_fd, start = list(d = 0.01, psi = -0.5, omega = 0.5), control = nls.lm.control(maxiter = 100))

alt_fd <- coef(fit_alt_fd)
sigma_alt_fd <- 1 / (1 - alt_fd["psi"])

alt_fd; sigma_alt_fd
##            d          psi        omega 
## -0.009135365  0.459919244  0.388119217
##      psi 
## 1.851575

The supplied benchmark is approximately:

  • \(d=-0.009135365\)
  • \(\psi=0.459919244\)
  • \(\omega=0.388119217\)
  • \(\sigma\approx1.851575\)
  • \(N=312\)

11.3 400-replication country-cluster bootstrap, matching the Stata specification

boot_alt_nls <- cluster_bootstrap(
  elec_alt, "country",
  fit_fun = \(x) fit_elec_nls_explicit(
    x, clean = "EC_c_alt", dirty = "EC_d_alt",
    fe_var = ".boot_cluster", start = alt_nls
  ),
  idcluster = TRUE, reject_fun = reject_psi
)

boot_alt_fd <- cluster_bootstrap(
  elec_alt_fd, "country",
  fit_fun = \(x) nlsLM(
    dln_eg ~ d + (1 / psi) * log(
      (omega * EC_c_alt^psi + (1 - omega) * EC_d_alt^psi) /
      (omega * l1EC_c_alt^psi + (1 - omega) * l1EC_d_alt^psi)
    ),
    data = x, start = as.list(alt_fd),
    control = nls.lm.control(maxiter = 100)
  ),
  reject_fun = reject_psi
)

se_alt_nls <- sapply(boot_alt_nls[c("d", "psi", "omega")], sd)
se_alt_fd  <- sapply(boot_alt_fd[c("d", "psi", "omega")], sd)

tibble(spec = c("CES NLS levels", "CES NLS first differences"),
       requested = 400, successful = c(nrow(boot_alt_nls), nrow(boot_alt_fd)))
## # A tibble: 2 × 3
##   spec                      requested successful
##   <chr>                         <dbl>      <int>
## 1 CES NLS levels                  400        400
## 2 CES NLS first differences       400        400

11.4 Table 4:Column 3&4

# Columns 3 and 4: Kmenta approximation.
k_alt <- elec_alt

k_alt_levels <- estimate_kmenta(
  ln_egecd_alt ~ country + year + ln_eccd_alt + ln_eccd_2_alt,
  k_alt, "ln_eccd_alt", "ln_eccd_2_alt"
)

k_alt_fd <- k_alt |>
  group_by(country) |>
  mutate(
    dy = ln_egecd_alt - lag(ln_egecd_alt),
    dx = ln_eccd_alt - lag(ln_eccd_alt),
    dx2 = ln_eccd_2_alt - lag(ln_eccd_2_alt)
  ) |>
  ungroup() |>
  filter(is.finite(dy), is.finite(dx), is.finite(dx2))

k_alt_fd <- estimate_kmenta(
  dy ~ dx + dx2, k_alt_fd, "dx", "dx2"
)

# Adjusted R2 for the NLS specifications.
adj_alt_nls <- 1 - (1 - fit_alt_nls$r2) *
  (nrow(elec_alt) - 1) / (nrow(elec_alt) - 30)

adj_alt_fd <- 1 - (1 - (1 - deviance(fit_alt_fd) /
  sum((elec_alt_fd$dln_eg - mean(elec_alt_fd$dln_eg))^2))) *
  (nrow(elec_alt_fd) - 1) / (nrow(elec_alt_fd) - 3)

# Published Table 4 structure.
table4 <- tibble(
  term = c("d", "", "omega", "", "psi", "", "Country DV",
           "Adjusted R2", "psi = 0 (Wald p)", "sigma", "N"),

  `NLS` = col_stats(
    alt_nls["d"], se_alt_nls["d"],
    alt_nls["omega"], se_alt_nls["omega"],
    alt_nls["psi"], se_alt_nls["psi"],
    sigma_alt_nls, adj_alt_nls, nrow(elec_alt),
    hasFE = TRUE
  ),

  `FD NLS` = col_stats(
    alt_fd["d"], se_alt_fd["d"],
    alt_fd["omega"], se_alt_fd["omega"],
    alt_fd["psi"], se_alt_fd["psi"],
    sigma_alt_fd, adj_alt_fd, nrow(elec_alt_fd),
    hasFE = FALSE
  ),

  `OLS` = col_stats(
    k_alt_levels$coefs["year"], k_alt_levels$se["year"],
    k_alt_levels$omega, k_alt_levels$se["ln_eccd_alt"],
    k_alt_levels$psi, k_alt_levels$se_psi,
    k_alt_levels$sigma, k_alt_levels$adjr2, k_alt_levels$N,
    hasFE = TRUE, df = df_cluster
  ),

  `FD OLS` = col_stats(
    k_alt_fd$coefs["(Intercept)"], k_alt_fd$se["(Intercept)"],
    k_alt_fd$omega, k_alt_fd$se["dx"],
    k_alt_fd$psi, k_alt_fd$se_psi,
    k_alt_fd$sigma, k_alt_fd$adjr2, k_alt_fd$N,
    hasFE = FALSE, df = df_cluster
  )
)

table4 |>
  kable(
    format = "html", escape = FALSE,
    caption = "Table 4.-Nonlinear Estimation and Kmenta Approximation of CES with an Alternative Capital Proxy: Electricity Sector"
  ) |>
  add_header_above(c(" " = 1, "CES" = 2, "Kmenta" = 2)) |>
  kable_styling(full_width = FALSE, bootstrap_options = "condensed") |>
  row_spec(c(2, 4, 6), italic = TRUE)
Table 4.-Nonlinear Estimation and Kmenta Approximation of CES with an Alternative Capital Proxy: Electricity Sector
CES
Kmenta
term NLS FD NLS OLS FD OLS
d -0.010*** -0.009*** -0.009*** -0.009***
(-3.63) (-3.89) (-3.97) (-3.35)
omega 0.193* 0.388*** 0.203*** 0.401***
(1.88) (3.75) (4.01) (6.39)
psi 0.423* 0.460** 0.535** 0.441***
(1.89) (2.51) (2.74) (5.17)
Country DV Yes No Yes No
Adjusted R2 0.997 0.053 0.965 0.555
psi = 0 (Wald p) 0.059 0.012 0.006 0.000
sigma 1.734 1.852 2.152 1.789
N 338 312 338 312

z-statistics in parentheses. Significantly different from 0 at 1%, 5%, 10%. Columns 1 and 2 provide bootstrapped standard errors based on 400 replications with country as cluster variable. Specification 1 applies the nonlinear least squares (NLS) estimator and includes country dummies. Specification 2 applies the NLS estimator to a first-differenced version of the model. Specification 3 applies the OLS estimator and includes country dummies. Specification 4 applies the OLS estimator to a first-differenced version of the model. \(\psi = 0\) reports the significance level of a Wald test with \(H_0:\psi = 0\).

Interpretation of Table 4

Table 4 examines whether the estimated elasticity of substitution between clean and dirty electricity capacity is robust when the authors replace the main capital measure with an alternative EIA-based capital proxy. The alternative proxy is available for fewer observations, so the sample falls from 390 to 338 observations in levels and from 364 to 312 observations in first differences. :contentReferenceoaicite:0

The results continue to show an elasticity of substitution above one in all four specifications:

  • The CES-NLS estimate gives \(\sigma = 1.734\) in levels and \(\sigma = 1.852\) in first differences.
  • The Kmenta estimates are \(\sigma = 2.152\) in levels and \(\sigma = 1.789\) in first differences.
  • The corresponding estimates of \(\psi\) are positive in every specification, ranging from 0.423 to 0.535.
  • The hypothesis \(\psi = 0\), which corresponds to an elasticity of substitution of one, is rejected at conventional significance levels in all four specifications. :contentReferenceoaicite:1

The similarity between the levels and first-difference estimates is important. The levels specifications include country fixed effects, while the first-difference specifications remove these country-specific effects. The elasticity remains well above one after differencing, suggesting that the main result is not simply driven by permanent differences between countries.

The Kmenta estimates are somewhat higher than the nonlinear CES estimates, but the overall conclusion is unchanged. The authors use the Kmenta model as a robustness check because it provides a linear approximation to the CES production function. :contentReferenceoaicite:2

The alternative capital proxy therefore supports the main finding of the paper: clean and dirty electricity-generating capacity can substitute for one another with an elasticity greater than one. Since \(\sigma > 1\) is the necessary condition identified by the authors for long-run green growth without technical change, the robustness results remain consistent with the possibility of a transition from dirty toward clean electricity without requiring continuous technological progress.

However, the authors note that this sensitivity analysis is limited by the lack of plant cost data across fuels, so the alternative proxy should be interpreted as a robustness check rather than a completely independent measure of capital. :contentReferenceoaicite:3

Overall interpretation: Table 4 confirms that the paper’s central result is robust to using a different measure of electricity-generation capital. Across NLS, first differences, and Kmenta specifications, the estimated elasticity remains above one and is statistically different from the unit-elasticity case. The estimated elasticity is roughly 1.7–2.2, indicating substantial substitution possibilities between clean and dirty electricity capacity.

Interpretation of the parameters

The parameter \(\omega\) measures the distribution or relative importance of the clean input in the CES aggregator. Its estimates are positive and statistically significant in all four specifications, although the magnitude changes from 0.193 to 0.401. This variation is expected when a different capital measure is used, but the positive estimates remain consistent with clean capital entering the production function. :contentReferenceoaicite:4

The parameter \(d\) captures the trend component associated with neutral technical change. It is negative and statistically significant in all four specifications, with estimates between approximately \(-0.009\) and \(-0.010\). Thus, the estimated trend component is negative in this alternative-capital specification.

The adjusted \(R^2\) is very high for the levels specifications (0.997 and 0.965), while it is considerably lower for the first-difference specifications (0.053 and 0.555). This is not surprising because differencing removes the country-specific effects that explain a substantial part of the variation in the levels data.

Main conclusion

The most important result is not the exact value of \(\omega\) or \(d\), but the estimate of \(\sigma\). The four estimates are:

\[ \sigma = 1.734,\quad 1.852,\quad 2.152,\quad 1.789. \]

All are greater than one. Therefore, changing the capital proxy does not overturn the paper’s main conclusion that there is substantial substitutability between clean and dirty electricity inputs.

The result is particularly important because the paper’s theoretical framework requires \(\sigma > 1\) for long-run green growth in the absence of technical change. Hence, Table 4 provides evidence that this conclusion is robust to the choice of capital proxy. :contentReferenceoaicite:5

The OLS transformation is also reproduced exactly. The alternative-capital Kmenta regression uses:

electricity <- electricity |>
  mutate(
    ln_egecd_alt = ln_eg - ln_ecd_alt,
    ln_eccd_alt = ln_ecc_alt - ln_ecd_alt,
    ln_eccd_2_alt = 0.5 * ln_eccd_alt^2
  )

The resulting Stata coefficients are:

  • Levels: \(\omega = 0.2025437\), \(\psi = 0.5352242\), \(\sigma = 2.1515751\)
  • First differences: \(\omega = 0.4012334\), \(\psi = 0.4408761\), \(\sigma = 1.7885124\)

Table 4 specification

Table 4 changes only the capital measure relative to Table 3. Instead of installed clean and dirty generation capacity (EC_c, EC_d), the robustness specification uses the EIA-based alternative capital stocks EC_c_alt and EC_d_alt.

The four specifications are:

  1. NLS levels + country fixed effects
  2. NLS first differences
  3. Kmenta OLS levels + country fixed effects
  4. Kmenta OLS first differences

The alternative capital measure is available for 1995–2007, giving 338 observations in levels. First differencing removes the first observation for each country, giving 312 observations.

Expected point estimates

The R implementation should reproduce the following Stata estimates approximately:

Statistic NLS FD NLS Kmenta OLS FD Kmenta OLS
\(\omega\) 0.1933 0.3881 0.2025 0.4012
\(\psi\) 0.4233 0.4599 0.5352 0.4409
\(\sigma\) 1.7340 1.8516 2.1516 1.7885
\(N\) 338 312 338 312

The Kmenta specifications should reproduce the published Stata coefficients particularly closely because they use ordinary least squares followed by the same nonlinear Kmenta transformation.

16. Table 5 — Cobb-Douglas nested in CES: Electricity Sector

The key difference from Tables 3 and 4 is that Table 5 incorporates fuel use into the dirty-energy input through a Cobb-Douglas aggregate:

\[X_d = EC_d^\alpha FU_d^{1-\alpha}. \]

The Stata specification is explicitly:

\[\ln EG =a + d \cdot year+ \text{country FE}+ \frac{1}{\psi}\ln\left[\omega EC_c^\psi+(1-\omega)\left(EC_d^\alpha FU_d^{1-\alpha}\right)^\psi\right].\]

The corresponding R specification is:

# Dirty input becomes a Cobb-Douglas aggregate of capacity and fuel: EC_d^alpha * FU_d^(1-alpha)
fit_cdces <- function(dat, clean="EC_c", dirty="EC_d", fe_var="country",
                       start=c(a=0, d=.01, omega=.5, alpha=.7, psi=-.2)) {
  C <- dat[[clean]]; D <- dat[[dirty]]; Fu <- dat$FU_d
  X <- model.matrix(reformulate(fe_var, intercept=FALSE), dat); XtX <- crossprod(X)
  obj <- function(p) {
    y <- dat$ln_eg - p["a"] - p["d"]*dat$year - ces_log(C, D^p["alpha"]*Fu^(1-p["alpha"]), p["psi"], p["omega"])
    fe <- qr.solve(XtX, crossprod(X, y)); sum((y - X %*% fe)^2)
  }
  fit <- optim(start, obj, method="Nelder-Mead", control=list(maxit=10000, reltol=1e-10))
  fit$r2 <- 1 - fit$value/sum((dat$ln_eg-mean(dat$ln_eg))^2)
  structure(fit, class=c("optim_fit", class(fit)))
}
# FD version: formula built from column names so one function serves both capital proxies
fit_cdces_fd <- function(dat, clean, dirty, clag, dlag, start=list(d=.01,omega=.5,alpha=.5,psi=-.5)) {
  f <- as.formula(sprintf(
    "dln_eg ~ d + (1/psi)*log((omega*%s^psi+(1-omega)*(%s^alpha*FU_d^(1-alpha))^psi)/(omega*%s^psi+(1-omega)*(%s^alpha*l1FU_d^(1-alpha))^psi))",
    clean, dirty, clag, dlag))
  nlsLM(f, data=dat, start=start, control=nls.lm.control(maxiter=100))
}

cd_main    <- fit_cdces(electricity)
cd_main_fd <- electricity |> arrange(country,year) |> group_by(country) |>
  mutate(dln_eg=ln_eg-lag(ln_eg), l1EC_c=lag(EC_c), l1EC_d=lag(EC_d), l1FU_d=lag(FU_d)) |>
  ungroup() |> filter(if_all(c(dln_eg,l1EC_c,l1EC_d,l1FU_d), is.finite))
cd_main_fd_fit <- fit_cdces_fd(cd_main_fd, "EC_c","EC_d","l1EC_c","l1EC_d")

cd_alt    <- fit_cdces(elec_alt, clean="EC_c_alt", dirty="EC_d_alt")
cd_alt_fd <- elec_alt |> arrange(country,year) |> group_by(country) |>
  mutate(dln_eg=ln_eg-lag(ln_eg), l1EC_c_alt=lag(EC_c_alt), l1EC_d_alt=lag(EC_d_alt), l1FU_d=lag(FU_d)) |>
  ungroup() |> filter(if_all(c(dln_eg,l1EC_c_alt,l1EC_d_alt,l1FU_d), is.finite))
cd_alt_fd_fit <- fit_cdces_fd(cd_alt_fd, "EC_c_alt","EC_d_alt","l1EC_c_alt","l1EC_d_alt")

b_main    <- cluster_bootstrap(electricity, "country", \(x) fit_cdces(x, fe_var=".boot_cluster"), idcluster=TRUE, reject_fun=reject_psi)
b_main_fd <- cluster_bootstrap(cd_main_fd, "country", \(x) fit_cdces_fd(x,"EC_c","EC_d","l1EC_c","l1EC_d"), reject_fun=reject_psi)
b_alt     <- cluster_bootstrap(elec_alt, "country", \(x) fit_cdces(x, clean="EC_c_alt", dirty="EC_d_alt", fe_var=".boot_cluster"), idcluster=TRUE, reject_fun=reject_psi)
b_alt_fd  <- cluster_bootstrap(cd_alt_fd, "country", \(x) fit_cdces_fd(x,"EC_c_alt","EC_d_alt","l1EC_c_alt","l1EC_d_alt"), reject_fun=reject_psi)

se5 <- lapply(list(main=b_main, main_fd=b_main_fd, alt=b_alt, alt_fd=b_alt_fd),
              \(b) sapply(b[c("d","alpha","omega","psi")], sd, na.rm=TRUE))

adjR2 <- function(fit, dat, yvar, k) {
  r2 <- if (inherits(fit,"optim_fit")) fit$r2 else 1 - deviance(fit)/sum((dat[[yvar]]-mean(dat[[yvar]]))^2)
  n <- nrow(dat); 1 - (1-r2)*(n-1)/(n-k-1)
}
col5 <- function(fit, se, dat, yvar, k, hasFE) {
  b <- coef(fit)
  rows <- unlist(lapply(c("d","alpha","omega","psi"), \(p)
    c(paste0(fmt(b[p]), stars(2*pnorm(-abs(b[p]/se[p])))), stat_row(b[p], se[p]))))
  c(rows, if (hasFE) "Yes" else "No", fmt(adjR2(fit,dat,yvar,k)),
    fmt(2*pnorm(-abs(b["psi"]/se["psi"]))), fmt(1/(1-b["psi"])), as.character(nrow(dat)))
}

table5 <- tibble(
  term = c("d","","alpha","","omega","","psi","","Country DV","Adjusted R2","psi = 0 (Wald p)","sigma","N"),
  `NLS`          = col5(cd_main,        se5$main,    electricity, "ln_eg",  26+4, TRUE),
  `FD NLS`       = col5(cd_main_fd_fit, se5$main_fd, cd_main_fd,  "dln_eg", 4,    FALSE),
  `NLS (Alt)`    = col5(cd_alt,         se5$alt,     elec_alt,    "ln_eg",  n_distinct(elec_alt$country)+4, TRUE),
  `FD NLS (Alt)` = col5(cd_alt_fd_fit,  se5$alt_fd,  cd_alt_fd,   "dln_eg", 4,    FALSE)
)

table5 |> kable(format="html", escape=FALSE,
                 caption="Table 5.-Nonlinear Estimation of Cobb-Douglas in CES: Electricity Sector") |>
  add_header_above(c(" "=1, "Main Capital Proxy"=2, "Alternative Capital Proxy"=2)) |>
  kable_styling(full_width=FALSE, bootstrap_options="condensed") |> row_spec(c(2,4,6,8), italic=TRUE)
Table 5.-Nonlinear Estimation of Cobb-Douglas in CES: Electricity Sector
Main Capital Proxy
Alternative Capital Proxy
term NLS FD NLS NLS (Alt) FD NLS (Alt)
d 0.003 0.002 -0.000 -0.000
(1.54) (1.38) (-0.19) (-0.10)
alpha 0.437*** 0.379*** 0.347*** 0.311***
(6.38) (3.93) (5.09) (4.07)
omega 0.487*** 0.707*** 0.010 0.005
(4.71) (9.15) (0.26) (0.79)
psi 0.508*** 0.651*** 0.508*** 0.644***
(3.28) (4.89) (3.38) (5.57)
Country DV Yes No Yes No
Adjusted R2 0.999 0.524 0.999 0.498
psi = 0 (Wald p) 0.001 0.000 0.001 0.000
sigma 2.031 2.867 2.032 2.810
N 390 364 338 312

z-statistics are reported in parentheses. Significance levels are denoted by 1%, 5%, and 10%. All columns report bootstrapped standard errors based on 400 replications, with country as the clustering variable.Specifications 1 and 3 use the nonlinear least squares (NLS) estimator and include country dummies. Specifications 2 and 4 apply the NLS estimator to the first-differenced version of the model.The column \(\psi = 0\) reports the significance level of a Wald test of the null hypothesis:\(H_0: \psi = 0.\)

17. Table 6 — Nonlinear Estimation and Kmenta Approximation of CES in Cobb-Douglas: Nonenergy Industries

Unlike the electricity sector, the nonenergy specification nests the CES energy aggregate inside a Cobb-Douglas function of capital, labor and (for gross output) intermediate materials/services. Following equation (8)/(9) in the paper, the energy aggregate does not carry a distribution weight \(\omega\) — clean and dirty energy enter symmetrically:

\[ \ln Y_{ijt}=a_i+a_j+d_t+(1-\alpha-\gamma-\theta)\ln L_{ijt}+\alpha\ln K_{ijt} +\theta\ln MS_{ijt}+\gamma\left[\frac{1}{\psi}\ln\left(E_{C,ijt}^{\psi}+E_{D,ijt}^{\psi}\right)\right]+\varepsilon_{ijt}. \]

\(\theta\) (materials/services) only enters the gross-output specification (Column 2); the value-added-plus-energy specification (Column 1) drops it. Fixed effects are two-way (country and industry), profiled out exactly as in fit_cdces() above but with a two-factor design matrix.

The supplied Stata benchmark is approximately:

The Stata bootstrap for Table 6 clusters at the country-industry cell (cluster(country industry)) but — unlike the electricity tables — does not relabel the fixed effects after resampling (idcluster is not used here; the original ${CDCES}/${INDCES} dummies are reused as-is). cluster_bootstrap() already supports this via idcluster = FALSE.

The Stata cnsreg constrains the coefficients on ln_xdl and ln_xcl to be equal. Constraining two coefficients to be equal is algebraically identical to replacing the two regressors with their sum and estimating one shared coefficient — so we fold that constraint directly into the design matrix rather than reimplementing cnsreg.

fit_ne <- function(dat, yvar, theta=FALSE, start) {
  X <- model.matrix(~0+factor(country)+factor(industry), dat); XtX <- crossprod(X)
  y <- dat[[yvar]]; xl<-dat$ln_xl; xk<-dat$ln_xk; xc<-dat$xc; xd<-dat$xd
  if (theta) xm <- dat$ln_xiims
  obj <- function(p) {
    ces <- (1/p["psi"])*log(xd^p["psi"]+xc^p["psi"])
    core <- if (theta) p["d"]*dat$year + (1-p["alpha"]-p["gamma"]-p["theta"])*xl + p["alpha"]*xk + p["theta"]*xm + p["gamma"]*ces
            else       p["d"]*dat$year + (1-p["alpha"]-p["gamma"])*xl + p["alpha"]*xk + p["gamma"]*ces
    r <- y-core; fe <- qr.solve(XtX, crossprod(X,r)); sum((r - X%*%fe)^2)
  }
  fit <- optim(start, obj, method="Nelder-Mead", control=list(maxit=20000, reltol=1e-10))
  fit$r2 <- 1 - fit$value/sum((y-mean(y))^2); structure(fit, class=c("optim_fit", class(fit)))
}
ne_va <- fit_ne(nonenergy, "ln_vaxiie", FALSE, c(d=.01,alpha=.3,gamma=.1,psi=.2))
ne_go <- fit_ne(nonenergy, "ln_go",     TRUE,  c(d=.01,alpha=.3,gamma=.1,theta=.1,psi=-.2))

b_va <- cluster_bootstrap(nonenergy, "id", \(x) fit_ne(x,"ln_vaxiie",FALSE,coef(ne_va)), reject_fun=reject_psi)
b_go <- cluster_bootstrap(nonenergy, "id", \(x) fit_ne(x,"ln_go",TRUE,coef(ne_go)),       reject_fun=reject_psi)
se_va <- sapply(b_va[c("d","alpha","gamma","psi")], sd, na.rm=TRUE)
se_go <- sapply(b_go[c("d","alpha","gamma","theta","psi")], sd, na.rm=TRUE)

# cnsreg's equal-coefficient constraint on ln_xdl/ln_xcl <=> summing the two regressors
nek <- nonenergy |> mutate(xc2 = ln_xdl + ln_xcl)

kmenta_ne <- function(f, comb, x2, theta_var=NULL) {
  m <- lm(f, data=nek); V <- vcovCL(m, cluster=~id, type="HC1"); b <- coef(m)
  bc <- b[comb]; b2 <- b[x2]; gamma <- 2*bc; psi <- b2/(0.25*gamma); sigma <- 1/(1-psi)
  g <- grad(\(z) z[2]/(0.25*(2*z[1])), c(bc,b2)); Vs <- V[c(comb,x2),c(comb,x2)]
  list(d=b["year"], se_d=sqrt(V["year","year"]),
       alpha=b["ln_xkl"], se_alpha=sqrt(V["ln_xkl","ln_xkl"]),
       gamma=gamma, se_gamma=2*sqrt(V[comb,comb]),
       theta=if(!is.null(theta_var)) b[theta_var] else NA, se_theta=if(!is.null(theta_var)) sqrt(V[theta_var,theta_var]) else NA,
       psi=psi, se_psi=sqrt(drop(g %*% Vs %*% g)), sigma=sigma, adjr2=summary(m)$adj.r.squared, N=nobs(m))
}
k_va <- kmenta_ne(ln_vaxiiel ~ factor(country)+factor(industry)+year+ln_xkl+xc2+ln_xdc_2, "xc2","ln_xdc_2")
k_go <- kmenta_ne(ln_gol ~ factor(country)+factor(industry)+year+ln_xkl+xc2+ln_xdc_2+ln_xiimsl, "xc2","ln_xdc_2","ln_xiimsl")

row6 <- \(est,se) c(paste0(fmt(est), stars(2*pnorm(-abs(est/se)))), sprintf("(%.2f)", est/se))
adjR2ne <- \(fit,k) 1 - (1-fit$r2)*(nrow(nonenergy)-1)/(nrow(nonenergy)-k-1)

table6 <- tibble(
  term = c("d","","alpha","","gamma","","theta","","psi","","Country DV","Industry DV","Adjusted R2","psi = 0","sigma","N"),

  `NLS VA+IIE` = c(row6(coef(ne_va)["d"],se_va["d"]), row6(coef(ne_va)["alpha"],se_va["alpha"]),
                   row6(coef(ne_va)["gamma"],se_va["gamma"]), "","",
                   row6(coef(ne_va)["psi"],se_va["psi"]), "Yes","Yes",
                   fmt(adjR2ne(ne_va,18+27+4)), fmt(2*pnorm(-abs(coef(ne_va)["psi"]/se_va["psi"]))),
                   fmt(1/(1-coef(ne_va)["psi"])), nrow(nonenergy)),

  `NLS GO` = c(row6(coef(ne_go)["d"],se_go["d"]), row6(coef(ne_go)["alpha"],se_go["alpha"]),
               row6(coef(ne_go)["gamma"],se_go["gamma"]), row6(coef(ne_go)["theta"],se_go["theta"]),
               row6(coef(ne_go)["psi"],se_go["psi"]), "Yes","Yes",
               fmt(adjR2ne(ne_go,18+27+5)), fmt(2*pnorm(-abs(coef(ne_go)["psi"]/se_go["psi"]))),
               fmt(1/(1-coef(ne_go)["psi"])), nrow(nonenergy)),

  `Kmenta VA+IIE` = c(row6(k_va$d,k_va$se_d), row6(k_va$alpha,k_va$se_alpha), row6(k_va$gamma,k_va$se_gamma), "","",
                      row6(k_va$psi,k_va$se_psi), "Yes","Yes",
                      fmt(k_va$adjr2), fmt(2*pnorm(-abs(k_va$psi/k_va$se_psi))), fmt(k_va$sigma), k_va$N),

  `Kmenta GO` = c(row6(k_go$d,k_go$se_d), row6(k_go$alpha,k_go$se_alpha), row6(k_go$gamma,k_go$se_gamma),
                  row6(k_go$theta,k_go$se_theta), row6(k_go$psi,k_go$se_psi), "Yes","Yes",
                  fmt(k_go$adjr2), fmt(2*pnorm(-abs(k_go$psi/k_go$se_psi))), fmt(k_go$sigma), k_go$N)
)

table6 |> kable(format="html", escape=FALSE,
                 caption="Table 6.-Nonlinear Estimation and Kmenta Approximation of CES in Cobb-Douglas: Nonenergy Industries") |>
  add_header_above(c(" "=1, "CES in Cobb-Douglas"=2, "Kmenta"=2)) |>
  kable_styling(full_width=FALSE, bootstrap_options="condensed") |> row_spec(c(2,4,6,8,10), italic=TRUE)
Table 6.-Nonlinear Estimation and Kmenta Approximation of CES in Cobb-Douglas: Nonenergy Industries
CES in Cobb-Douglas
Kmenta
term NLS VA+IIE NLS GO Kmenta VA+IIE Kmenta GO
d 0.010*** 0.003* 0.010*** 0.002*
(4.83) (1.72) (4.79) (1.73)
alpha 0.359*** 0.186*** 0.360*** 0.187***
(7.33) (6.58) (7.78) (7.04)
gamma 0.260*** 0.121*** 0.258*** 0.120***
(6.13) (4.90) (6.15) (4.88)
theta 0.565*** 0.566***
(15.07) (16.54)
psi 0.651** 0.654 0.394*** 0.276
(2.05) (1.27) (2.97) (1.41)
Country DV Yes Yes Yes Yes
Industry DV Yes Yes Yes Yes
Adjusted R2 0.948 0.982 0.738 0.906
psi = 0 0.040 0.206 0.003 0.159
sigma 2.868 2.888 1.651 1.382
N 6914 6914 6914 6914

z-statistics in parentheses. Significance denoted 1%, 5%, 10%. Columns 1 and 2 report bootstrapped standard errors from 400 replications (here 100, for speed) clustered at the country-industry cell; unlike the electricity tables, fixed effects are not relabeled after resampling. Columns 3 and 4 apply OLS to the linear Kmenta (translog) approximation with country and industry fixed effects and cluster-robust SEs; \(\gamma\), \(\psi\) and \(\sigma\) are recovered from the constrained coefficients via the delta method. \(\psi=0\) reports the significance level of a Wald test with \(H_0:\psi=0\).

Interpretation of Table 6

  • The CES-in-Cobb-Douglas estimates give \(\psi \approx 0.65\) for both dependent variables, implying an elasticity of substitution close to 2.9 between clean and dirty energy inputs in the nonenergy sector — noticeably higher than the electricity sector’s \(\sigma\approx1.8\)\(2.0\).
  • The hypothesis \(\psi=0\) (unit elasticity) is rejected in both NLS specifications.
  • The Kmenta approximation again pulls \(\sigma\) toward 1 (to about 1.4–1.7), consistent with the paper’s warning that the translog linearization is biased toward unitary substitution for large input ratios — the NLS estimates should be treated as the primary evidence.
  • \(\alpha\) (capital share) and \(\theta\) (materials/services share) are economically sensible and stable across specifications, supporting the overall CES-in-Cobb-Douglas functional form.

Overall interpretation: Combined with Tables 3–5, the nonenergy results reinforce the paper’s central claim — clean and dirty energy inputs are substitutes with an elasticity significantly above 1 both at the electricity-generation level and at the level of the broader nonenergy economy, a necessary condition (in the absence of technical change) for long-run green growth in the AABH-style framework.

Stata vs R validation: Tables 3–6

# ============================================================
# STATA vs R VALIDATION: POINT ESTIMATES
# Uses the exact objects already created above.
# ============================================================

validation <- bind_rows(

  # ---- Table 3: Electricity sector ----
  tibble(
    table = "Table 3", specification = c("NLS", "FD NLS", "Kmenta", "FD Kmenta"),
    parameter = "psi",
    stata = c(0.457, 0.487, 0.446, 0.454),
    r = c(elec_nls["psi"], fd_coef["psi"],
          kmenta_levels$psi, kmenta_fd$psi)
  ),

  # ---- Table 4: Alternative capital proxy ----
  tibble(
    table = "Table 4", specification = c("NLS", "FD NLS", "Kmenta", "FD Kmenta"),
    parameter = "psi",
    stata = c(0.4233, 0.4599, 0.5352, 0.4409),
    r = c(alt_nls["psi"], alt_fd["psi"],
          k_alt_levels$psi, k_alt_fd$psi)
  ),

  # ---- Table 5: Cobb-Douglas nested in CES ----
  tibble(
    table = "Table 5",
    specification = c("NLS", "FD NLS", "NLS Alt", "FD NLS Alt"),
    parameter = "psi",
    stata = c(0.5077, 0.6512, 0.5079, 0.6441),
    r = c(coef(cd_main)["psi"], coef(cd_main_fd_fit)["psi"],
          coef(cd_alt)["psi"], coef(cd_alt_fd_fit)["psi"])
  ),

  # ---- Table 6: Nonenergy industries ----
  tibble(
    table = "Table 6",
    specification = c("NLS VA+IIE", "NLS GO", "Kmenta VA+IIE", "Kmenta GO"),
    parameter = "psi",
    stata = c(0.651, 0.654, 0.394, 0.276),
    r = c(coef(ne_va)["psi"], coef(ne_go)["psi"],
          k_va$psi, k_go$psi)
  )
) |>
  mutate(
    difference = r - stata,
    abs_difference = abs(difference),
    status = case_when(
      abs_difference < .001 ~ "Exact",
      abs_difference < .005 ~ "Very close",
      abs_difference < .01  ~ "Close",
      TRUE ~ "Investigate"
    )
  )

validation |>
  mutate(
    stata = round(stata, 4),
    r = round(r, 4),
    difference = round(difference, 5),
    abs_difference = round(abs_difference, 5)
  ) |>
  kable(format = "html",
        caption = "Stata vs R validation of CES elasticity parameter psi") |>
  kable_styling(full_width = FALSE, bootstrap_options = "condensed")
Stata vs R validation of CES elasticity parameter psi
table specification parameter stata r difference abs_difference status
Table 3 NLS psi 0.4570 0.4566 -0.00043 0.00043 Exact
Table 3 FD NLS psi 0.4870 0.4866 -0.00044 0.00044 Exact
Table 3 Kmenta psi 0.4460 0.4464 0.00035 0.00035 Exact
Table 3 FD Kmenta psi 0.4540 0.4545 0.00046 0.00046 Exact
Table 4 NLS psi 0.4233 0.4234 0.00009 0.00009 Exact
Table 4 FD NLS psi 0.4599 0.4599 0.00002 0.00002 Exact
Table 4 Kmenta psi 0.5352 0.5352 0.00002 0.00002 Exact
Table 4 FD Kmenta psi 0.4409 0.4409 -0.00002 0.00002 Exact
Table 5 NLS psi 0.5077 0.5077 -0.00005 0.00005 Exact
Table 5 FD NLS psi 0.6512 0.6513 0.00006 0.00006 Exact
Table 5 NLS Alt psi 0.5079 0.5079 0.00003 0.00003 Exact
Table 5 FD NLS Alt psi 0.6441 0.6441 0.00004 0.00004 Exact
Table 6 NLS VA+IIE psi 0.6510 0.6514 0.00037 0.00037 Exact
Table 6 NLS GO psi 0.6540 0.6538 -0.00024 0.00024 Exact
Table 6 Kmenta VA+IIE psi 0.3940 0.3943 0.00031 0.00031 Exact
Table 6 Kmenta GO psi 0.2760 0.2764 0.00044 0.00044 Exact

FULL PARAMETER VALIDATION

validation_full <- bind_rows(

  # Table 3
  tibble(
    table = "Table 3",
    specification = rep(c("NLS", "FD NLS", "Kmenta", "FD Kmenta"),
                        each = 4),
    parameter = rep(c("d", "omega", "psi", "sigma"), 4),
    stata = c(
      -0.001, 0.219, 0.457, 1.840,
      -0.003, 0.442, 0.487, 1.948,
      -0.001, 0.245, 0.446, 1.806,
      -0.003, 0.451, 0.454, 1.833
    ),
    r = c(
      elec_nls["d"], elec_nls["omega"], elec_nls["psi"], sigma_elec_nls,
      fd_coef["d"], fd_coef["omega"], fd_coef["psi"], sigma_elec_fd,
      kmenta_levels$coefs["year"], kmenta_levels$omega,
      kmenta_levels$psi, kmenta_levels$sigma,
      kmenta_fd$coefs["(Intercept)"], kmenta_fd$omega,
      kmenta_fd$psi, kmenta_fd$sigma
    )
  ),

  # Table 4
  tibble(
    table = "Table 4",
    specification = rep(c("NLS", "FD NLS", "Kmenta", "FD Kmenta"),
                        each = 4),
    parameter = rep(c("d", "omega", "psi", "sigma"), 4),
    stata = c(
      -0.009685, 0.1933, 0.4233, 1.7340,
      -0.009135, 0.3881, 0.4599, 1.8516,
      -0.009, 0.2025, 0.5352, 2.1516,
      -0.009, 0.4012, 0.4409, 1.7885
    ),
    r = c(
      alt_nls["d"], alt_nls["omega"], alt_nls["psi"], sigma_alt_nls,
      alt_fd["d"], alt_fd["omega"], alt_fd["psi"], sigma_alt_fd,
      k_alt_levels$coefs["year"], k_alt_levels$omega,
      k_alt_levels$psi, k_alt_levels$sigma,
      k_alt_fd$coefs["(Intercept)"], k_alt_fd$omega,
      k_alt_fd$psi, k_alt_fd$sigma
    )
  ),

  # Table 5
  tibble(
    table = "Table 5",
    specification = rep(c("NLS", "FD NLS", "NLS Alt", "FD NLS Alt"),
                        each = 5),
    parameter = rep(c("d", "alpha", "omega", "psi", "sigma"), 4),
    stata = c(
       0.003, 0.4374, 0.4875, 0.5077, 2.031,
       0.003, 0.3791, 0.7071, 0.6512, 2.867,
       0.000, 0.3467, 0.0101, 0.5079, 2.032,
       0.000, 0.3107, 0.0048, 0.6441, 2.810
    ),
    r = c(
      coef(cd_main)["d"], coef(cd_main)["alpha"], coef(cd_main)["omega"],
      coef(cd_main)["psi"], 1/(1-coef(cd_main)["psi"]),

      coef(cd_main_fd_fit)["d"], coef(cd_main_fd_fit)["alpha"],
      coef(cd_main_fd_fit)["omega"], coef(cd_main_fd_fit)["psi"],
      1/(1-coef(cd_main_fd_fit)["psi"]),

      coef(cd_alt)["d"], coef(cd_alt)["alpha"], coef(cd_alt)["omega"],
      coef(cd_alt)["psi"], 1/(1-coef(cd_alt)["psi"]),

      coef(cd_alt_fd_fit)["d"], coef(cd_alt_fd_fit)["alpha"],
      coef(cd_alt_fd_fit)["omega"], coef(cd_alt_fd_fit)["psi"],
      1/(1-coef(cd_alt_fd_fit)["psi"])
    )
  ),

  # Table 6
  tibble(
    table = "Table 6",
    specification = rep(c("NLS VA+IIE", "NLS GO",
                          "Kmenta VA+IIE", "Kmenta GO"), each = 4),
    parameter = rep(c("alpha", "gamma", "psi", "sigma"), 4),
    stata = c(
      0.359, 0.260, 0.651, 2.868,
      0.186, 0.121, 0.654, 2.888,
      0.360, 0.258, 0.394, 1.651,
      0.187, 0.120, 0.276, 1.382
    ),
    r = c(
      coef(ne_va)["alpha"], coef(ne_va)["gamma"], coef(ne_va)["psi"],
      1/(1-coef(ne_va)["psi"]),

      coef(ne_go)["alpha"], coef(ne_go)["gamma"], coef(ne_go)["psi"],
      1/(1-coef(ne_go)["psi"]),

      k_va$alpha, k_va$gamma, k_va$psi, k_va$sigma,

      k_go$alpha, k_go$gamma, k_go$psi, k_go$sigma
    )
  )
) |>
  mutate(
    difference = r - stata,
    abs_difference = abs(difference),
    status = case_when(
      abs_difference < .001 ~ "Exact",
      abs_difference < .005 ~ "Very close",
      abs_difference < .01  ~ "Close",
      TRUE ~ "Investigate"
    )
  )

validation_full |>
  mutate(
    stata = round(stata, 4),
    r = round(r, 4),
    difference = round(difference, 5)
  ) |>
  kable(format = "html",
        caption = "Stata vs R validation of main parameter estimates") |>
  kable_styling(full_width = FALSE, bootstrap_options = "condensed")
Stata vs R validation of main parameter estimates
table specification parameter stata r difference abs_difference status
Table 3 NLS d -0.0010 -0.0013 -0.00026 0.0002632 Exact
Table 3 NLS omega 0.2190 0.2194 0.00044 0.0004382 Exact
Table 3 NLS psi 0.4570 0.4566 -0.00043 0.0004297 Exact
Table 3 NLS sigma 1.8400 1.8402 0.00016 0.0001644 Exact
Table 3 FD NLS d -0.0030 -0.0032 -0.00019 0.0001852 Exact
Table 3 FD NLS omega 0.4420 0.4418 -0.00023 0.0002285 Exact
Table 3 FD NLS psi 0.4870 0.4866 -0.00044 0.0004416 Exact
Table 3 FD NLS sigma 1.9480 1.9476 -0.00036 0.0003589 Exact
Table 3 Kmenta d -0.0010 -0.0010 0.00002 0.0000217 Exact
Table 3 Kmenta omega 0.2450 0.2453 0.00033 0.0003325 Exact
Table 3 Kmenta psi 0.4460 0.4464 0.00035 0.0003543 Exact
Table 3 Kmenta sigma 1.8060 1.8062 0.00021 0.0002094 Exact
Table 3 FD Kmenta d -0.0030 -0.0031 -0.00010 0.0000952 Exact
Table 3 FD Kmenta omega 0.4510 0.4511 0.00005 0.0000513 Exact
Table 3 FD Kmenta psi 0.4540 0.4545 0.00046 0.0004551 Exact
Table 3 FD Kmenta sigma 1.8330 1.8330 0.00003 0.0000298 Exact
Table 4 NLS d -0.0097 -0.0097 0.00000 0.0000001 Exact
Table 4 NLS omega 0.1933 0.1933 0.00001 0.0000093 Exact
Table 4 NLS psi 0.4233 0.4234 0.00009 0.0000944 Exact
Table 4 NLS sigma 1.7340 1.7343 0.00029 0.0002876 Exact
Table 4 FD NLS d -0.0091 -0.0091 0.00000 0.0000004 Exact
Table 4 FD NLS omega 0.3881 0.3881 0.00002 0.0000192 Exact
Table 4 FD NLS psi 0.4599 0.4599 0.00002 0.0000192 Exact
Table 4 FD NLS sigma 1.8516 1.8516 -0.00003 0.0000250 Exact
Table 4 Kmenta d -0.0090 -0.0091 -0.00010 0.0000972 Exact
Table 4 Kmenta omega 0.2025 0.2025 0.00004 0.0000437 Exact
Table 4 Kmenta psi 0.5352 0.5352 0.00002 0.0000240 Exact
Table 4 Kmenta sigma 2.1516 2.1516 -0.00003 0.0000258 Exact
Table 4 FD Kmenta d -0.0090 -0.0090 0.00001 0.0000091 Exact
Table 4 FD Kmenta omega 0.4012 0.4012 0.00003 0.0000334 Exact
Table 4 FD Kmenta psi 0.4409 0.4409 -0.00002 0.0000242 Exact
Table 4 FD Kmenta sigma 1.7885 1.7885 0.00001 0.0000114 Exact
Table 5 NLS d 0.0030 0.0025 -0.00049 0.0004899 Exact
Table 5 NLS alpha 0.4374 0.4374 0.00003 0.0000340 Exact
Table 5 NLS omega 0.4875 0.4875 -0.00003 0.0000337 Exact
Table 5 NLS psi 0.5077 0.5077 -0.00005 0.0000469 Exact
Table 5 NLS sigma 2.0310 2.0311 0.00009 0.0000884 Exact
Table 5 FD NLS d 0.0030 0.0025 -0.00051 0.0005112 Exact
Table 5 FD NLS alpha 0.3791 0.3791 -0.00002 0.0000206 Exact
Table 5 FD NLS omega 0.7071 0.7071 -0.00004 0.0000411 Exact
Table 5 FD NLS psi 0.6512 0.6513 0.00006 0.0000557 Exact
Table 5 FD NLS sigma 2.8670 2.8674 0.00043 0.0004308 Exact
Table 5 NLS Alt d 0.0000 -0.0004 -0.00040 0.0004016 Exact
Table 5 NLS Alt alpha 0.3467 0.3467 0.00002 0.0000188 Exact
Table 5 NLS Alt omega 0.0101 0.0101 0.00005 0.0000469 Exact
Table 5 NLS Alt psi 0.5079 0.5079 0.00003 0.0000322 Exact
Table 5 NLS Alt sigma 2.0320 2.0322 0.00024 0.0002402 Exact
Table 5 FD NLS Alt d 0.0000 -0.0002 -0.00020 0.0001987 Exact
Table 5 FD NLS Alt alpha 0.3107 0.3108 0.00006 0.0000557 Exact
Table 5 FD NLS Alt omega 0.0048 0.0048 0.00000 0.0000018 Exact
Table 5 FD NLS Alt psi 0.6441 0.6441 0.00004 0.0000357 Exact
Table 5 FD NLS Alt sigma 2.8100 2.8101 0.00006 0.0000599 Exact
Table 6 NLS VA+IIE alpha 0.3590 0.3589 -0.00005 0.0000523 Exact
Table 6 NLS VA+IIE gamma 0.2600 0.2597 -0.00026 0.0002604 Exact
Table 6 NLS VA+IIE psi 0.6510 0.6514 0.00037 0.0003725 Exact
Table 6 NLS VA+IIE sigma 2.8680 2.8684 0.00039 0.0003907 Exact
Table 6 NLS GO alpha 0.1860 0.1861 0.00006 0.0000631 Exact
Table 6 NLS GO gamma 0.1210 0.1208 -0.00021 0.0002116 Exact
Table 6 NLS GO psi 0.6540 0.6538 -0.00024 0.0002402 Exact
Table 6 NLS GO sigma 2.8880 2.8882 0.00017 0.0001683 Exact
Table 6 Kmenta VA+IIE alpha 0.3600 0.3604 0.00038 0.0003767 Exact
Table 6 Kmenta VA+IIE gamma 0.2580 0.2581 0.00015 0.0001455 Exact
Table 6 Kmenta VA+IIE psi 0.3940 0.3943 0.00031 0.0003090 Exact
Table 6 Kmenta VA+IIE sigma 1.6510 1.6510 0.00001 0.0000069 Exact
Table 6 Kmenta GO alpha 0.1870 0.1868 -0.00021 0.0002050 Exact
Table 6 Kmenta GO gamma 0.1200 0.1205 0.00047 0.0004651 Exact
Table 6 Kmenta GO psi 0.2760 0.2764 0.00044 0.0004394 Exact
Table 6 Kmenta GO sigma 1.3820 1.3821 0.00005 0.0000542 Exact