Getting a sensitivity analysis into a paper means two things: a figure that survives a journal’s production process, and a table in whatever markup the manuscript is written in. This vignette covers both.
data(bfg2020)
bfg2020$statea <- factor(bfg2020$statea)
w1 <- c("log_area_2010", "lat", "lon", "temp_mean", "rain_mean",
"elev_mean", "d_coa", "d_riv", "d_lak", "ave_gyi")
form <- avgrep2000to2016 ~ tye_tfe890_500kNI_100_l6 +
log_area_2010 + lat + lon + temp_mean + rain_mean + elev_mean +
d_coa + d_riv + d_lak + ave_gyi + statea
res <- regsen_bounds(form, bfg2020, compare = w1,
cbar = c(0.1, 0.5, 1))Figures
plot() returns an ordinary ggplot, so
nothing here is a dead end: anything the arguments do not cover, you add
yourself.
plot(res)
The defaults follow the economics journals: an L of axis lines rather than a box, no grid, ticks outside the panel, and no title, because the caption carries it.
Reading an unbounded identified set
Where the identified set is unbounded, the bounds leave the panel rather than tracing along its edge. That distinction matters: a line running flat along the axis reads as “the bound levels off here”, which is the opposite of what an infinite bound means.

The green and orange curves exit the top and bottom of the panel. They do not plateau.
Reference lines
xline is the equivalent of Stata’s xline(),
for marking a threshold under discussion in the text.

Sizing for a journal
base_size scales the type. For a two-column figure,
render at about 3.3 inches wide and pass base_size = 9,
which puts the type near 8pt on the page.
plot(res, base_size = 9, show_legend = FALSE)
Matching your manuscript’s font
Fonts are left to you, because what is installed varies by machine and a missing family would make the figure unreproducible. To match a LaTeX body type:
plot(res) + theme_regsen(base_family = "Times New Roman")Colour, greyscale and accessibility
The palette is Okabe-Ito, which stays distinguishable under the common forms of colour vision deficiency. Series are also encoded by line type, so the figure still reads when a journal prints it in greyscale – colour alone would not survive that.
plot(res) + scale_colour_grey(start = 0, end = 0.6)
#> Scale for colour is already present.
#> Adding another scale for colour, which will replace the existing scale.
Overriding anything
plot(res, subtitle = NA) +
theme_regsen(grid = "y", base_size = 10) +
labs(caption = "BFG (2020); bounds under DMP (2026).")
Tables
The tidy route
as.data.frame() gives the results as a plain data frame.
This is the recommended entry point: it hands the numbers to whichever
table package you already use, rather than tying the package to one of
them.
head(as.data.frame(res, digits = 3))
#> rxbar rybar cbar bmin bmax
#> 1 0.000 Inf 0.1 2.0500 2.05
#> 2 0.408 Inf 0.1 1.4200 2.69
#> 3 0.816 Inf 0.1 0.7240 3.39
#> 4 1.220 Inf 0.1 -0.0602 4.17
#> 5 1.630 Inf 0.1 -0.9660 5.08
#> 6 2.040 Inf 0.1 -2.0500 6.16From here kableExtra, gt,
modelsummary, huxtable, tinytable
and xtable all work as usual.
The convenience route
regsen_table() covers the common case in one call. The
format argument follows the document you are knitting to,
so the same chunk works whether the target is PDF, HTML or Markdown.
regsen_table(res, format = "markdown", digits = 3, max_rows = 6)| rxbar | rybar | cbar | Lower | Upper |
|---|---|---|---|---|
| 0 | +Inf | 0.100 | 2.05 | 2.05 |
| 2.45 | +Inf | 0.100 | -3.41 | 7.52 |
| 0.816 | +Inf | 0.500 | 0.245 | 3.86 |
| 3.26 | +Inf | 0.500 | -Inf | +Inf |
| 1.63 | +Inf | 1.00 | -Inf | +Inf |
| 4.08 | +Inf | 1.00 | -Inf | +Inf |
LaTeX for a journal
The LaTeX flavour follows the economics convention:
booktabs rules and notes inside a
threeparttable, so the note block sets to the width of the
table rather than the width of the page.
cat(regsen_table(res, format = "latex", digits = 3, max_rows = 4,
caption = "Identified sets for the long-regression coefficient",
label = "idset", notes = TRUE, escape = FALSE),
sep = "\n")notes = TRUE writes the note a referee looks for – which
analysis, which hypothesis, how many observations. Supply a character
vector instead to write your own.
Using it in a manuscript needs one line in the preamble:
Set threeparttable = FALSE if you would rather not, and
the notes are appended as plain \footnotesize lines.
Infinite bounds
An unbounded identified set is a result, not missing data, so it is
rendered rather than blanked: $+\infty$ in LaTeX, the
infinity character in HTML, +Inf in Markdown.
regsen_table(res, format = "markdown", digits = 3, max_rows = 8)| rxbar | rybar | cbar | Lower | Upper |
|---|---|---|---|---|
| 0 | +Inf | 0.100 | 2.05 | 2.05 |
| 2.04 | +Inf | 0.100 | -2.05 | 6.16 |
| 3.67 | +Inf | 0.100 | -14.2 | 18.3 |
| 1.22 | +Inf | 0.500 | -2.40 | 6.51 |
| 2.86 | +Inf | 0.500 | -Inf | +Inf |
| 0.408 | +Inf | 1.00 | 1.39 | 2.72 |
| 2.04 | +Inf | 1.00 | -Inf | +Inf |
| 4.08 | +Inf | 1.00 | -Inf | +Inf |
Feeding modelsummary
regsen_tidy() and regsen_glance() speak
broom’s vocabulary, so results flow into modelsummary and
anything else built on it. When generics is installed these
are also reachable as tidy() and glance().
head(regsen_tidy(res), 4)
#> term estimate conf.low conf.high rxbar rybar cbar
#> 1 tye_tfe890_500kNI_100_l6 2.054759 2.05475946 2.054759 0.0000000 Inf 0.1
#> 2 tye_tfe890_500kNI_100_l6 2.054759 1.42206743 2.687451 0.4080674 Inf 0.1
#> 3 tye_tfe890_500kNI_100_l6 2.054759 0.72444299 3.385076 0.8161347 Inf 0.1
#> 4 tye_tfe890_500kNI_100_l6 2.054759 -0.06022714 4.169746 1.2242021 Inf 0.1
regsen_glance(res)
#> analysis subcommand outcome treatment nobs
#> 1 DMP (2026) bounds avgrep2000to2016 tye_tfe890_500kNI_100_l6 2036
#> beta_medium breakdown hypothesis n_gridpoints
#> 1 2.054759 NA Beta > 0 33estimate is the medium-regression coefficient, never a
function of the bounds. Under DMP with rybar = Inf the
identified set is symmetric about that coefficient, so a midpoint would
agree here and then diverge for Oster or finite rybar – and
it is undefined wherever the set is unbounded.
