Getting started: clustered IV with weak-instrument-robust inference (the queens data)

This vignette walks through the package’s workflow on a real, named dataset: Dube and Harish (2020) ask whether states ruled by queens fought more wars than states ruled by kings, instrumenting queenly rule with the gender composition of the previous ruler’s family. The design has everything this package is for: a binary endogenous regressor, multiple excluded instruments that are not strong, a moderate number of clusters (176 reign spells), and a covariate block that mixes dense controls with several fixed-effect dimensions.

The vignette is precomputed: the panel cannot be redistributed inside an R package, so the code was executed by the maintainer against a local copy and the outputs are stored. Every displayed result comes from the displayed code.

Data

The analysis file is the main panel from the replication materials of Dube and Harish (2020, Journal of Political Economy): one row per polity-year, 3,586 rows and 778 columns. Set data_path to your local copy; the hashes let you confirm you hold the same file:

data_path <- "queens_main_panel.dta"
library(clusterIV)

d <- as.data.frame(haven::read_dta(data_path))
dim(d)
#> [1] 3586  778
unname(tools::md5sum(data_path))
#> [1] "29d55f9aeac21a33d09c92bc279e829e"

The roles in the baseline specification (Dube and Harish, Table A.5, column 1):

It is an unweighted linear-probability IV model. The fixed-effect factors are built once:

d$sibling_fe <- factor(d$totsib2)
d$decade_fe  <- factor(d$decade_indicator)
d$polity_fe  <- factor(d$kingdom_id)
c(G = length(unique(d$inv1_clust_broadgreign_id)),
  fe_levels = nlevels(d$sibling_fe) + nlevels(d$decade_fe) +
    nlevels(d$polity_fe))
#>         G fe_levels 
#>       176        79

One call: iv_infer()

iv_infer() is the recommended entry point. It returns the CJIVE point estimate, the cluster-jackknife Anderson–Rubin (CJAR) confidence set, and the cluster-jackknife score (CJS) test in one panel, sharing all the expensive preprocessing:

panel <- iv_infer(
  anypartBDICTC ~ queen2 | fb + femsib2 |          # y ~ x | z | fe
    sibling_fe + decade_fe + polity_fe,
  data        = d,
  cluster     = ~ inv1_clust_broadgreign_id,       # 176 reign clusters
  controls    = ~ fbmg + mgsib2 + inv1_any_NMBlc_hw +
    inv1_any_MBlc_hw + inv1_gunrel,                # dense controls
  beta0       = 0,                                 # tested point null
  level       = 0.95,                              # set level
  calibration = "chisq",                           # CJAR critical value (default)
  variance    = "plain"                            # jackknife variance (default)
)
panel
#> Cluster IV inference panel (CJIVE + CJAR + CJS)
#> Call: iv_infer.formula(formula = anypartBDICTC ~ queen2 | fb + femsib2 |      sibling_fe + decade_fe + polity_fe, data = d, cluster = ~inv1_clust_broadgreign_id,      controls = ~fbmg + mgsib2 + inv1_any_NMBlc_hw + inv1_any_MBlc_hw +          inv1_gunrel, level = 0.95, beta0 = 0, calibration = "chisq",      variance = "plain")
#> 
#>   CJIVE/Wald (H0: beta = 0): coefficient = 0.4134   cluster-robust SE = 0.1623   z = 2.546   p = 0.01089
#>     95% Wald interval = [0.09517, 0.7316]
#>   CJAR (H0: beta = 0): T = 3.737   one-sided p = 0.008761
#>     95% confidence set = [0.135, 0.8381]
#>   CJS (H0: beta = 0): LM = 5.778   p = 0.01623
#>     95% confidence set = [0.09477, 0.8839]
#>   F_CJ = 7.756   critical value = 1.996
#>   F_CJS^2 = 12.08   critical value = 3.841
#>   effective F (Montiel Olea-Pflueger) = 10.68   critical value (tau = 10%, alpha = 5%) = 19.53   K_eff = 1.899
#>   n = 3586   G = 176 clusters   k = 2 instruments
#>   max within-cluster leverage = 0.0774
#>   absorbed fixed effects: 3 dimension(s), 79 levels   raw nuisance count/n = 0.02287

How to read this panel:

The components are ordinary fitted objects:

coef(panel$cjive)
#>    queen2 
#> 0.4133645
confint(panel$cjar)
#>         lower     upper
#> [1,] 0.135045 0.8381354
plot(panel)
plot of chunk pcurve
plot of chunk pcurve

Comparing estimators

iv_compare() reports OLS, 2SLS, the improved jackknife IV (IJIVE row, historically labelled JIVE), and CJIVE on the identical design. The published 2SLS coefficient for this specification is 0.388:

W <- model.matrix(~ fbmg + mgsib2 + inv1_any_NMBlc_hw + inv1_any_MBlc_hw +
                    inv1_gunrel + sibling_fe + decade_fe + polity_fe,
                  data = d)[, -1L, drop = FALSE]
cmp <- iv_compare(d$anypartBDICTC, d$queen2,
                  z = data.matrix(d[, c("fb", "femsib2")]),
                  cluster  = d$inv1_clust_broadgreign_id,
                  controls = W)
print(cmp, digits = 3)
#>   estimator coefficient     se statistic  p.value conf.low conf.high
#> 1       OLS       0.130 0.0365      3.57 0.000362   0.0587     0.202
#> 2      2SLS       0.388 0.1449      2.68 0.007405   0.1040     0.672
#> 3      JIVE       0.389 0.1458      2.67 0.007556   0.1037     0.675
#> 4     CJIVE       0.413 0.1623      2.55 0.010891   0.0952     0.732

Two remarks. First, the OLS coefficient (0.13) is far below every IV estimate — the published paper’s point. Second, passing the fixed effects as dense model.matrix columns, as here, is numerically identical to absorbing them via fixed_effects =; the absorption route never forms the dummy matrix and is the one that scales.

A note on cross-software comparison: Stata’s weakivtest multiplies the effective F by a finite-sample factor G/(G-1) * (n-1)/(n-L). The package reports the unfactored statistic, so the panel’s F_eff of 10.678 corresponds to the published 10.372.

The published specification set

Table A.5 of Dube and Harish varies the instrument set (columns 1, 2, 4 and 5), and Ligtenberg (2025) adds a pooled specification using all five instruments. The loop below runs all five with the package, collecting the 2SLS anchor, the CJAR and CJS confidence sets, and the diagnostics:

specs <- list(
  `A.5 col 1 (FBM, Sis)` = list(
    z = c("fb", "femsib2"),
    ctrl = c("fbmg", "mgsib2", "inv1_any_NMBlc_hw", "inv1_any_MBlc_hw",
             "inv1_gunrel")),
  `A.5 col 2 (+ Sis x No children)` = list(
    z = c("fb", "femsib2", "femsib2xnolc"),
    ctrl = c("fbmg", "mgsib2", "mgsib2xnolc", "inv1_any_NO_lc_hw",
             "inv1_gunrel")),
  `A.5 col 4 (+ Sis x FBM)` = list(
    z = c("fb", "femsib2", "femsib2xfb"),
    ctrl = c("fbmg", "mgsib2", "inv1_any_NMBlc_hw", "inv1_any_MBlc_hw",
             "inv1_gunrel")),
  `A.5 col 5 (+ FBM x Two children)` = list(
    z = c("fb", "fbxtwolc", "femsib2"),
    ctrl = c("fbmg", "fbmgxtwolc", "mgsib2", "inv1_two_lc_hw",
             "inv1_any_NMBlc_hw", "inv1_any_MBlc_hw", "inv1_gunrel")),
  `Pooled (all five instruments)` = list(
    z = c("fb", "femsib2", "femsib2xnolc", "femsib2xfb", "fbxtwolc"),
    ctrl = c("fbmg", "mgsib2", "mgsib2xnolc", "fbmgxtwolc",
             "inv1_any_NO_lc_hw", "inv1_two_lc_hw", "inv1_any_NMBlc_hw",
             "inv1_any_MBlc_hw", "inv1_gunrel"))
)

fmt_set <- function(cs) {
  if (nrow(cs) == 0L) return("(empty)")
  paste(apply(cs, 1L, function(z) sprintf("[%.3f, %.3f]", z[1L], z[2L])),
        collapse = " U ")
}

rows <- lapply(names(specs), function(nm) {
  s <- specs[[nm]]
  Ws <- model.matrix(
    stats::reformulate(c(s$ctrl, "sibling_fe", "decade_fe", "polity_fe")),
    data = d)[, -1L, drop = FALSE]
  z  <- data.matrix(d[, s$z])
  cmp <- iv_compare(d$anypartBDICTC, d$queen2, z,
                    cluster = d$inv1_clust_broadgreign_id, controls = Ws)
  ar <- cjar(d$anypartBDICTC, d$queen2, z,
             cluster = d$inv1_clust_broadgreign_id, controls = Ws)
  sc <- cjscore(d$anypartBDICTC, d$queen2, z,
                cluster = d$inv1_clust_broadgreign_id, controls = Ws)
  data.frame(spec = nm, k = ar$k,
             `2SLS` = cmp$coefficient[cmp$estimator == "2SLS"],
             CJIVE = cmp$coefficient[cmp$estimator == "CJIVE"],
             `CJAR 95% set` = fmt_set(ar$conf_set),
             `CJS 95% set` = fmt_set(sc$conf_set),
             F_CJ = round(ar$F_CJ, 2),
             check.names = FALSE)
})
tab <- do.call(rbind, rows)
print(tab, row.names = FALSE, digits = 3)
#>                              spec k  2SLS CJIVE   CJAR 95% set    CJS 95% set F_CJ
#>              A.5 col 1 (FBM, Sis) 2 0.388 0.413 [0.135, 0.838] [0.095, 0.884] 7.76
#>   A.5 col 2 (+ Sis x No children) 3 0.313 0.335 [0.241, 0.537] [0.087, 0.701] 8.33
#>           A.5 col 4 (+ Sis x FBM) 3 0.288 0.311 [0.133, 0.592] [0.008, 0.680] 7.29
#>  A.5 col 5 (+ FBM x Two children) 3 0.313 0.338 [0.086, 0.797] [0.092, 0.865] 5.66
#>     Pooled (all five instruments) 5 0.226 0.241 [0.037, 0.518] [0.032, 0.535] 7.08

The four A.5 rows reproduce the published 2SLS coefficients (0.388, 0.313, 0.288, 0.313) to three decimals; the CJIVE estimates and the CJAR/CJS confidence sets are the package’s own output, computed by exact polynomial inversion rather than a parameter grid.

Exporting results

tidy() and glance() methods (registered through the optional generics package) return plain data frames, one row per inferential procedure, so results flow into knitr::kable(), modelsummary, or any LaTeX pipeline:

library(generics)   # provides the tidy() / glance() generics
#> 
#> Attaching package: 'generics'
#> The following objects are masked from 'package:base':
#> 
#>     as.difftime, as.factor, as.ordered, intersect, is.element, setdiff,
#>     setequal, union
td <- tidy(panel)
td[, c("procedure", "estimate", "std.error", "statistic", "p.value",
       "conf.low", "conf.high", "shape")]
#>                          procedure  estimate std.error statistic     p.value   conf.low
#> 1                       CJIVE/Wald 0.4133645  0.162348  2.546163 0.010891440 0.09516822
#> 2 cluster jackknife Anderson-Rubin        NA        NA  3.737397 0.008761418 0.13504496
#> 3          cluster jackknife score        NA        NA  5.777926 0.016228678 0.09477295
#>   conf.high   shape
#> 1 0.7315609 bounded
#> 2 0.8381354 bounded
#> 3 0.8839396 bounded

A confidence set that is disjoint or unbounded cannot be flattened into two numbers; the full endpoint matrix always sits in the conf.set list-column, and shape says what kind of set each row carries. For a LaTeX table:

knitr::kable(td[, c("procedure", "estimate", "std.error", "conf.low",
                    "conf.high")],
             format = "latex", digits = 3, booktabs = TRUE)

Caveats worth knowing here

References

Dube, O. and Harish, S. P. (2020). Queens. Journal of Political Economy, 128(7), 2579–2652. The data and the published 2SLS specifications (Table A.5).

Ligtenberg, J. W. (2025). Inference in clustered IV models with many and weak instruments. arXiv:2306.08559v3. The CJAR and CJS tests and the pooled specification.

Frandsen, B., Leslie, E. and McIntyre, S. (2025). Cluster jackknife instrumental variables estimation. Review of Economics and Statistics. doi:10.1162/rest.a.263. The CJIVE estimator.

Montiel Olea, J. L. and Pflueger, C. (2013). A robust test for weak instruments. Journal of Business & Economic Statistics, 31(3), 358–369. The effective first-stage F reported as F_eff.