regsensitivity 0.2.0
The Oster equality identified-set plot draws the actual branches
-
plot()on ananalysis = "oster",delta_type = "eq"sweep now groups the roots by branch rather than by sort order. Up to three roots come back perdelta, sorted within eachdelta, so the root continuing a given branch moves between thebeta1/beta2/beta3columns wherever a fold adds two roots or one escapes to infinity. Drawing a column as a line cut continuous branches into pieces and joined unrelated ones; a heuristic that broke the line at an asymptote and atdelta = 1suppressed the false connectors but left visible gaps in curves that are not broken.Branch identity does not need a heuristic.
delta(beta)is a ratio of cubics; between two consecutive poles of it the map is continuous and single valued, so the roots in one pole interval are one branch, and walking that interval in increasingbetatraverses it end to end. A branch that folds back is not a function ofdelta, so the branches are drawn withgeom_path(). A zero of the denominator of
delta(beta)that is also a zero of its numerator is a hole, not an asymptote, and no longer splits a branch. With a single control covariate it is the only real root of the denominator, which made that case a single continuous curve reported as two.The spurious root – the one solving the cubic at every
delta, since it is that common root – no longer survives at scattereddeltaand draws a stray spur along the asymptote. Its residual is set by how accuratelypolyroot()locates it, which is well above the few digits of machine epsilon the filter allowed.plot()warns when adeltagrid is too coarse to draw anything, and errors when Oster’s cubic has no real solution anywhere on it. Passingdelta = c(-3, 3)– the endpoints, as Stata’sdelta()takes them, rather than the grid – silently produced an empty panel.
The identified set is now computed everywhere
-
The region
rxbar > rmax(cbar) > rybarno longer raises an error.regsen_bounds()andregsen_breakdown()compute it like any other, and the numbers agree with an independent search over the constraint set of DMP (2026) Theorem 5 to within 0.01, continuously acrossrmax.What stood in the way was the solver for the constraint Assumption A3 places on
z. That constraint is a quadratic inequality whose leading coefficient turns negative exactly whenrxbar * ||c|| > 1; its solution set is then the complement of an interval, not an interval, and the solver returned the hull of the two pieces. Sampling that hull hands the optimizer selection equations that violate A3, so the region was refused rather than reported. The feasible set is now represented as the list of intervals it is, and sampled in proportion to their lengths. The breakdown point in
rxbaris no longer capped atrmax(cbar). That ceiling binds only whenrybaris unrestricted; withrybarfinite the breakdown point can lie well past it. Where norxbaroverturns the conclusion – the horizontal arm of the breakdown frontier – the reported value is now+Infinstead of the ceiling.regsen_breakdown()gainsdirection = "rybar", giving the frontierrybar_bf(rxbar)of DMP (2026) Theorem 4 over a grid ofrxbar. This is the form the paper’s Figure 1 plots, and the only one that can describe the frontier’s horizontal arm.
Assumption A6 in its general form
-
regsen_bounds()andregsen_breakdown()gainclow, the lower end of DMP Assumption A6,R(W2 ~ W1 . W0)in[clow, cbar]. Asserting that the controls are at least somewhat endogenous tightens the identified set and raises the breakdown point.clow = 0, the default, is the previous behaviour.
Parallel computation
-
regsen_bounds(),regsen_breakdown()andregsen_multi()gain anncoresargument, joiningregsen_boot(), and all four default to the session setting of the newregsen_cores(). The grid points of an identified set, the values of a breakdown frontier and the treatments of a multi-treatment sweep are independent and are spread across the cores; on the bundled data a finite-rybarfrontier runs about three times faster on six cores. Results are identical for any number of cores: nothing but the bootstrap draws random numbers, and it seeds each replicate itself. - The default stays serial, as CRAN policy requires of a package that has not been asked.
regsen_cores("auto")sets the session to all but two of the machine’s cores;regsen_cores(n)sets a number. Work inside a bootstrap replicate or a treatment always runs serially, so cores are not oversubscribed by nesting. - The socket backend that Windows uses can be selected on any machine with
options(regsensitivity.backend = "psock"), and the test suite runs it everywhere, so a change that works under fork but not on Windows is caught before it ships. Arguments a worker closure touches are forced before the closure is built: a socket worker is a fresh session and would otherwise receive an unevaluated promise into a frame it does not have. - A replicate or grid point that errors is now confined to its own slot.
mclapply()preschedules jobs onto cores and marks every job on a core as failed when one errors, so a single bad bootstrap resample used to take a whole core’s worth of replicates down with it.
Inference
-
regsen_boot()now reports a bias-corrected and accelerated (BCa) interval alongside the percentile interval, and returns it by default;type = "perc"restores the old choice. The acceleration comes from a delete-one jackknife over the units the bootstrap resamples – rows, or clusters under a cluster bootstrap. In the simulation design of the package’s paper atn = 500, coverage improves from 92.0% to 94.0% against a nominal 95% at the same interval width; the two agree byn = 1000.z0andaccelerationare returned so the correction can be inspected, and an endpoint that falls on an extreme replicate is reported as such. - Bootstrap replicates are about three times faster on the bundled data, 5 milliseconds against 14. A replicate is now a row subset of model matrices built once, rather than a fresh
model.frame()call per draw, which is what makes the BCa jackknife – one computation per row – affordable.
Bug fixes
-
rmax, therxbarat which the identified set becomes unbounded, was wrong forcbar < 1. It solved thecbarbranch of DMP appendix equation (S18) unconditionally, when the maximising||c||isrxbaritself wherever A6 allows it. Withcbar = 0.5on the bundled data, calibrating against the ten geographic and climate covariates, it reported 1.48 where the set is in fact unbounded from 1.19, soregsen_bounds()printed a large finite number in place of an infinite one across that range, and the defaultrxbargrid ran past the point where the analysis says anything. - A breakdown point for an upper-bound hypothesis (
beta = bnd_ub(v)) was computed from the lower end of the identified set. Withrybarfinite the set is not symmetric aroundbeta_med, so the reported value was wrong; the tested end is now chosen by the direction of the hypothesis. - An equality hypothesis (
beta = bnd_eq(v)) now breaks down at whichever end of the identified set can reach the hypothesised value, rather than always at the upper one. - A name in
compareornocomparethat is not a control on the right-hand side of the formula – a typo, or the treatment itself – is now an error. It used to be dropped silently, so a misspelt covariate quietly changed which variables calibrated the analysis. - The sensitivity parameters are validated:
cbarmust lie in[0, 1],rxbarandrybarmust be non-negative,r2longnon-negative andmaxovbnon-negative orNA, and none may be missing. Out-of-range values used to reach the closed forms and the optimizer and come back asNAwithout explanation. - Under
analysis = "oster"withdelta_type = "bound", amaxovbcap is now applied atdelta >= 1too. The raw set is the whole real line there, and the cap was skipped, so the reported bounds were-Infand+Infwhere the constraint saysbeta_med - maxovbandbeta_med + maxovb. - Comparison covariates that are collinear with one another are dropped, as the documentation already said and as Stata does; only those collinear with
W0were.Var(W1)is singular otherwise, and the analysis went through a regularised inverse of it. The test for collinearity withW0is now relative to the covariate’s own variance, so a covariate measured in small units is no longer mistaken for a constant. -
regsen_boot()keeps replicates on which the breakdown point is+Inf– the hypothesis survived every value of the sensitivity parameter – in both intervals, where they count as+Inf. They were dropped along with failed replicates, so an interval whose upper tail is unbounded was reported with a finite upper endpoint. The count is returned as$infiniteand printed. -
print()of aregsen_boot()result reports the interval for the magnitude of a signed (Oster) breakdown point correctly. Takingabs()of each endpoint reversed a negative interval and, for one straddling zero, hid that the magnitude may be as small as zero. -
regsen_table(label = ...)warns whencaptionis not given, sinceknitr::kable()emits a float only with a caption and the label had nowhere to attach; it was dropped silently. Ther2longcolumn head uses\mathrmrather than\text, so it no longer needsamsmath. -
scale_colour_regsen()andscale_fill_regsen()recycle the palette (with a warning) when a sweep has more than eight groups, instead of erroring insideplot(). - The default y-range of an identified-set plot is read off both bounds. It was read off the lower bound alone, which cropped the upper one whenever the set is not symmetric about
beta_med(finiterybar, or Oster). -
plot()of a result whose sensitivity parameters are all single values says so, rather than failing with a subscript error. - A logical
subsetcontainingNAtreatsNAasFALSE, assubset()does. TheNAused to select a row of missing values and leave anNAin the row bookkeeping thatregsen_boot()uses to line up a cluster column. - The
ngridargument ofregsen_bounds()andregsen_breakdown()is removed: it was documented but never used. - The explorer app no longer errors when the
rybarbox is left at its default. A numeric input cannot carryInf, so blank now means unrestricted, and a cleared box greys the panel instead of raising. - Masten and Poirier (2026) is cited as published, in the American Economic Review 116(7), rather than as an arXiv preprint.
Documentation
New vignette, Reading the output: interpretation, edge cases and decisions (
vignette("interpreting-results")). It says what each number means, gives a decision path from breakdown point andrho_kto a verdict, and walks through the cases that confuse readers – a robust and a fragile conclusion on worlds where the omitted variable exists, a breakdown point of zero, a set that is unbounded early because the calibration set is weak, what goes incompare, whencbarmatters, an infinite breakdown point under finiterybar, the two Oster breakdown points, and how to read the bootstrap print – each with the sentence to write in the paper. It ends with a list of common mistakes and a reporting checklist.-
The two Oster breakdown points now carry the names Masten and Poirier
- give them, in
?regsen_breakdown, the interpretation vignette and the paper: the explain away point (beta = bnd_eq(0); thepsacalcnumber) and the sign change point (the default). The documentation also states that the sign change value is capped at one by their Theorem 2, not by convention, and that onlymaxovblifts the cap. On the frontier data the two are -23.3 and 0.974, and a reader who took one for the other reached the opposite verdict.
- give them, in
Verification
- The identified set is now checked against worlds in which the omitted variable exists. Each test world simulates
W2, reads the truebeta_long,r_X,r_Yandcoff it by the projections DMP (2026) define them through, and requires the package’s set at exactly those parameters to containbeta_long– withrybarunrestricted, withrybarfinite (the optimizer), and with two-sided A6. A search over constructed omitted variables on fixed observed data stays inside therybar = Infset and reaches within 15% of its ends, so the set is neither invalid nor loose. - Property tests pin what must hold on any data: sets nest as each parameter is relaxed and collapse to
beta_medatrxbar = 0; the breakdown point is where the tested bound crosses the hypothesis;rmaxis where the set becomes unbounded; rescalingYorXscales the bounds as the model says, and rescaling, shifting or reordering the covariates leaves bounds and breakdown points unchanged, for both DMP and Oster.
Accuracy and speed
- The global optimizer behind the finite-
rybaridentified set now polishes its result with a local search. Bounds agree with a run twenty times as long to within 3e-4, where the previous setting could be off by 0.02. - A breakdown search evaluates only the end of the identified set its hypothesis tests, and finds the crossing by root-finding rather than fixed-iteration bisection. Together these make a breakdown point at finite
rybarseveral times faster despite the more accurate optimizer.
regsensitivity 0.1.2
Plots
The Oster equality identified-set plot no longer draws a false vertical connector where the numerically ordered solution branches swap arms across an asymptote. Branches are now split exactly where a step would cross a vertical asymptote of
delta(beta), computed from the model rather than guessed from the drawing. With severalr2longvalues the curves are now drawn (and coloured) perr2longinstead of being interleaved into one line.plot()gainsxline, drawing vertical reference lines. This closes a gap against the Stata package, whose vignette usesxline()to mark values such asrmaxor a breakdown point; those figures could not be reproduced in R before.New
theme_regsen()andscale_colour_regsen()(withscale_color_regsen()andscale_fill_regsen()aliases), exported so a plot can be rebuilt with the same look. Defaults are aimed at print: a faint y-grid, a thin panel border, legend on top, and the colourblind-safe Okabe-Ito palette, which also survives greyscale printing.Axis and legend titles now use plotmath, so they read as the symbols in the papers rather than as parameter names.
plot()gainsbase_size; use9for a two-column journal figure.Passing
NAtotitle,subtitle,xtitleorytitledrops the annotation. The auto-generated subtitle is no longer applied by default, since a journal figure carries its description in the caption.
Calibration
- New
calibrate_rho()andcalibrate_partial_r2(), computing the point-identified reference values of DMP (2026) Section 3.4 (Table 4) and Table 3, so the sensitivity parameters can be read against the observed covariates they are calibrated to.
Verification
- The Oster (2019) implementation is now checked against the paper rather than against its own past output. A test round-trips Proposition 3 into Proposition 2: the delta reported as the breakdown for a target beta, fed back through the identified set, must return that beta. It does, to machine precision, for several targets.
-
results$breakdownis documented as signed. The sign carries the direction of selection; passingabs()of it back intoregsen_bounds()lands on a different branch of the Oster cubic. The scalar$breakdownreports the magnitude and exists onregsen_bounds()output only – aregsen_breakdown()result carries the value inresults$breakdownalone.
Replication fidelity
- The DMP (2026) Figure 1 reproduction now matches the published figure. The left panel gains the dashed bounds under the common-maximal-impact restriction
rybar = rxbarand the Table 4 calibration tick marks; both zero crossings are reported and agree with the published 0.804 and 0.959. The right panel previously plotted the breakdown point againstcbar, a different object from the paper’s, which is the breakdown frontier in (rxbar, rybar) space; it is now that frontier, one curve percbar. -
plot()no longer paints a line along the panel edge when a stretch of the sweep is unbounded. Only the first point of each unbounded run is routed off-panel and the rest are blank, so the curve leaves the plot once instead of running along its boundary. Runs are detected per series, so one beginning at a group boundary still gets its own exit.
Bug fixes
-
regsen_bounds(analysis = "oster", delta_type = "bound")reported a bounded identified set where the set is in fact unbounded. The solver correctly returns-Inf/Inffor|delta| <= dbarwithdbar >= 1, but the routine accumulating bounds across the grid discarded infinite values alongsideNAand carried the last finite bound forward. On the bundled data,delta = c(0.5, 1, 2)reported[1.31, 2.98]at every point instead of(-Inf, Inf)fromdbar = 1on.NAstill carries over, since it means the solver failed and carries no information; an infinity is a bound and now propagates. This is the one direction a sensitivity analysis must not err in, since it made results look more robust than they are. Previously validated numbers are unchanged – the snapshot tests pass untouched.
Interactive
- New
regsen_explore(), a shiny application for moving the sensitivity parameters by hand and watching the identified set respond. is a Suggests, so the rest of the package works without it. Inputs are validated and the model is fitted once before the app opens, so a bad call reports at the console rather than as a banner in a browser tab.
Several treatments
- New
regsen_multi(), running the analysis once per candidate treatment and collecting the breakdown points into one table, with aplot()method ordering treatments by fragility. Every treatment is analysed in the same specification – the others stay as controls – so the rows are comparable. A treatment whose analysis fails yields anNArow with the reason rather than aborting the sweep.
Inference
-
regsen_boot()gainsncores, running replications in parallel: forking on macOS and Linux, a PSOCK cluster on Windows. A progress bar is not shown in parallel, since it cannot report meaningfully from several workers. - Results for a given
seedare now identical regardless ofncores. Each replicate seeds itself from a vector drawn once up front, rather than relying on parallel RNG substreams, which would have made the numbers depend on how the work happened to be divided.
Documentation
- New vignette “Publication-ready plots and tables”, covering figure sizing for journals, greyscale and colour-vision safety, and the LaTeX / HTML / Markdown table output.
- New vignette “Coming from the Stata package”, mapping every command and option in the Stata package’s own vignette onto its R equivalent. It doubles as a coverage check: a gap would show as a missing row rather than going unnoticed.
Interoperability
- New
regsen_tidy()andregsen_glance(), broom-style tidiers. They are registered ongenerics::tidy()/glance()when is installed, so results flow into and anything else speaking that vocabulary;genericsstays a Suggests. - New
autoplot()method, forwarding toplot().
Replication
- The DMP (2026) vignette now checks its numbers against the published values in a table, and
stopifnot()s the comparison, so a regression stops the build instead of quietly shipping a wrong replication. - Calibration labels in the Figure 1 overlay are staggered across three x positions; at a single x they collided into an unreadable stack.
Tables
- New
as.data.frame()method returning the results as a plain data frame, so they can be passed to any table package (kableExtra, gt, modelsummary, huxtable, tinytable, xtable). - New
regsen_table()rendering results as LaTeX, HTML or Markdown, so the same call works in an Rmd/Qmd knitting to PDF, HTML or Markdown. LaTeX output usesbooktabsand puts notes in athreeparttable, withnotes = TRUEgenerating a note recording the analysis, hypothesis and sample size. Infinite bounds render as the correct symbol per format rather than being blanked out.
regsensitivity 0.1.1
- Fix a Windows-only crash when the DIRECT optimizer (DMP analysis with
rybar < Infandcbar > 0) explored parameter points wherevarx_bounds()returned NA endpoints. The downstreamquad_ineq_bounds()did unguardedif (b1 <= r1 && b2 <= r1)comparisons which evaluated toNAand triggered “missing value where TRUE/FALSE needed”. Symptoms: BFG2020 vignette failed to build on Windows (Status: 1 ERROR on win-builder), but built clean on macOS due to floating-point determinism differences in nloptr’s DIRECT-L. - Audit all bare
if (...)numeric comparisons indmp.Rand guard against NA propagation. Every comparison now usesisTRUE()/isFALSE()so a single NA cannot poison the dispatch logic. - Add 13 NA-robustness regression tests pinning the failure mode (
tests/testthat/test-na-robustness.R).
regsensitivity 0.1.0
- Initial CRAN-ready release.
- Implements:
- DMP (2026) identified set and breakdown frontier (
rybar = Infanalytic,rybar < Inf, cbar = 0analytic,rybar < Inf, cbar > 0via DIRECT global optimization (nloptr)). - Oster (2019) identified set (
eqandboundmodes) and breakdown points. - Masten & Poirier (2026)
maxovbextension.
- DMP (2026) identified set and breakdown frontier (
- User API:
regsen_bounds(),regsen_breakdown(),regsen_summary(),regsensitivity()dispatcher, hypothesis helpers (bnd_lb(),bnd_ub(),bnd_eq()). - Visualization:
plot.regsensitivity()(ggplot2). - Vignettes:
-
regsensitivity: end-to-end tour with the BFG2020 application. -
dmp2022-replication: reproduces every table and figure of Diegert, Masten & Poirier (2026) that uses the bundled data. -
mp2022-stylized: reproduces the stylized Oster examples from Masten & Poirier (2026).
-
- Bundled data:
bfg2020(Bazzi, Fiszbein, Gebresilasse 2020, subset).
