Skip to contents

This vignette illustrates, on a stylized constructed example, the figures of Masten and Poirier (2026), The Effect of Omitted Variables on the Sign of Regression Coefficients. The paper’s Figures 1, 2, and S1 are built on its empirical application to Satyanath, Voigtländer and Voth (2017), whose microdata is distributed separately (Harvard Dataverse, doi:10.7910/DVN/EE6I7N) and is not bundled here; the paper itself does not print the moments needed to regenerate those curves. Its survey tables aggregate across many published studies. So this vignette exercises the machinery in regsensitivity end-to-end on a data-generating process we control, and the structural features of the published figures – at most three solutions at any δ\delta, the vertical asymptote at δ=1\delta = 1 – are what should carry over. The numerical values should not be compared.

A stylized data-generating process

We construct a small example where the medium-regression coefficient is positive but Oster’s identified set ℬI(δ,Rlong)\mathcal{B}_I(\delta, R_{long}) crosses zero at moderate δ\delta.

library(regsensitivity)
library(ggplot2)

set.seed(2208)
n <- 5000
w1 <- rnorm(n)
w2 <- rnorm(n)
w3 <- rnorm(n)
x  <- 0.3 * w1 + 0.3 * w2 + 0.5 * w3 + rnorm(n)
y  <- 0.8 * x + 0.4 * w1 - 0.3 * w2 + 0.6 * w3 + rnorm(n)
dat <- data.frame(y = y, x = x, w1 = w1, w2 = w2)   # w3 is unobserved!

form <- y ~ x + w1 + w2

The “true” long-regression coefficient is 0.8; the medium regression sees an upward-biased estimate because w3 is correlated with x.

coef(lm(form, data = dat))["x"]
#>        x 
#> 1.060427

Two observed controls rather than one is not cosmetic. With a single control, γmed\gamma_{med} and πmed\pi_{med} are scalars, the numerator and denominator of δ(β)\delta(\beta) share a root, and the asymptote at that β\beta cancels into a removable hole – a degenerate case with none of the structure the paper’s figures show.

Figure 1: Oster’s identified set ℬI(δ,Rlong)\mathcal{B}_I(\delta, R_{long})

res <- regsen_bounds(form, dat,
                      analysis = "oster",
                      delta = seq(-2, 2, 0.02),
                      r2long = 1)
plot(res, ylim = c(-2, 4),
     title = "Figure 1 (stylized): Oster identified set vs delta")

The branches of the cubic in β\beta that solve Oster’s equation give the identified set. The cubic’s leading coefficient is proportional to δ−1\delta - 1, so a root escapes to infinity exactly at δ=1\delta = 1: below it there is one solution near βmed\beta_{med}, and at δ=1\delta = 1 a second branch enters from −∞-\infty and sweeps up across zero. The curves are drawn as branches of δ(β)\delta(\beta) rather than as functions of δ\delta, which is why a branch can fold back – one δ\delta carrying two solutions on the same branch – as this one does just past δ=1\delta = 1.

Figure 2: cumulative identified set δ≤d‾\delta \le \bar d

res2 <- regsen_bounds(form, dat,
                       analysis = "oster",
                       delta = seq(0, 1, 0.005),
                       delta_type = "bound",
                       r2long = 1)
plot(res2, ylim = c(0, 2),
     title = "Figure 2 (stylized): cumulative identified set")

The cumulative set is a narrow cone around βmed\beta_{med} for every δ\delta short of one, and at δ=1\delta = 1 it is the whole real line: both bounds leave the panel rather than flattening along its edge. The sign of the effect is therefore not in doubt anywhere below the threshold and completely undetermined at it – the discontinuity, not a gradual widening, is what does the work.

Table 1: breakdown points

Quantity Value
oster_breakdown_eq, β=0\beta = 0, Rlong=1R_{long}=1 1.2342
oster_breakdown_bound, β>0\beta > 0, Rlong=1R_{long}=1 0.9937
DMP r‾Xbp\bar r_X^{bp} at c‾=1\bar c = 1 0.9013

Effect of a maxovb constraint

Masten-Poirier (2026) extend Oster by adding a maximum omitted variable bias constraint. A sign change requires the bias to carry β\beta all the way from βmed\beta_{med} to zero, so any cap below βmed\beta_{med} rules one out and the breakdown point is +∞+\infty:

bmed <- regsen_bounds(form, dat, analysis = "oster",
                       delta = 0, r2long = 1)$dgp$beta_med
sapply(c(0.5, 1, round(bmed, 3) + 0.001), function(M) {
    regsen_breakdown(form, dat, analysis = "oster",
                      r2long = 1, maxovb = M)$results$breakdown[1]
})
#> [1]      Inf      Inf 1.233942

Above that threshold the cap binds without forbidding the sign change, and the breakdown point falls back towards its uncapped value as the cap loosens:

ovbs <- seq(1.1, 3, 0.1)
out <- lapply(ovbs, function(M) {
    r <- regsen_breakdown(form, dat,
                           analysis = "oster",
                           r2long = 1, maxovb = M)
    data.frame(maxovb = M, breakdown = r$results$breakdown[1])
})
out <- do.call(rbind, out)
uncapped <- regsen_breakdown(form, dat, analysis = "oster",
                              r2long = 1)$results$breakdown[1]

ggplot(out, aes(maxovb, breakdown)) +
    geom_hline(yintercept = 1, linetype = "dotted", colour = "grey40") +
    geom_line() + geom_point() +
    labs(x = "maxovb", y = "Delta (breakdown)",
         title = "Oster sign-change breakdown vs max OVB") +
    theme_regsen()

A tighter cap on the OVB magnitude lifts the breakdown delta, and every value drawn is above one – the dotted line. That is the point of the constraint: without it, Masten and Poirier’s Theorem 2 caps the sign-change breakdown at one (here 0.9937), so no sign conclusion can be called robust by the |δ|>1|\delta| > 1 convention. The cap is the extra assumption that lifts the ceiling, and it has to be argued for rather than chosen for the answer it gives.

Meta-analysis tables (paper Tables 3-4)

The paper’s Tables 3 and 4 aggregate breakdown-point statistics across many published studies. (Table 2 belongs to the Satyanath et al. application, not the survey.) The package contains the primitives needed to reproduce those tables (regsen_breakdown(... maxovb = ...)); plug your own meta-analysis data in and call:

results <- lapply(study_list, function(study) {
    out <- regsen_breakdown(study$formula, study$data,
                             analysis = "oster",
                             r2long = 1, maxovb = study$maxovb,
                             beta = bnd_eq(0))
    out$results$breakdown[1]
})