| Title: | Sensitivity Analysis and Bounds under Total Variation Neighborhoods |
|---|---|
| Description: | Implements the sensitivity analysis framework of Palomba (2026) "Sensitivity Analysis in Population Shares" <https://filippopalomba.github.io/#jmp> for randomized experiments with attrition, counterfactuals in structural models, and recentered instrumental variables. Computes and plots sensitivity bounds together with their confidence intervals and robustness summary measures. |
| Authors: | Filippo Palomba [aut, cre, cph] |
| Maintainer: | Filippo Palomba <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.1 |
| Built: | 2026-10-11 10:11:00 UTC |
| Source: | https://github.com/cran/tvbounds |
Displays the bounds stored in a tvbounds object as functions of the
budget of the total-variation (or contamination, or
divergence) neighborhood, supplied through the delta column of
x$bounds and drawn on the horizontal axis. The region between the
lower bound path and
the upper bound path is
shaded, the outer confidence band (when the object carries one) is drawn
as a lighter ribbon delimited by dashed lines, a dashed horizontal line
marks the reference value , a point marks
the baseline estimate at , and a dotted
vertical line marks the plug-in breakdown budget
when it is interior to the
budget grid.
## S3 method for class 'tvbounds' plot(x, ...) tvbounds_plot( x, bands = TRUE, baseline = TRUE, breakdown = TRUE, tau_star = 0, color = "#1F4E79", xlab = NULL, ylab = NULL, title = NULL, log_x = FALSE, ... )## S3 method for class 'tvbounds' plot(x, ...) tvbounds_plot( x, bands = TRUE, baseline = TRUE, breakdown = TRUE, tau_star = 0, color = "#1F4E79", xlab = NULL, ylab = NULL, title = NULL, log_x = FALSE, ... )
x |
A |
... |
For |
bands |
Logical; draw the outer confidence band when the object
carries one (columns |
baseline |
Logical; mark the baseline point estimate, that is, the
value of the estimand under |
breakdown |
Logical; draw a dotted vertical line, with a label, at
the plug-in breakdown budget
|
tau_star |
Reference value |
color |
Colour of the bounds, ribbons, and breakdown mark (default
|
xlab, ylab, title
|
Axis labels and plot title. |
log_x |
Logical; use a logarithmic budget axis. Rows with
|
The breakdown budget drawn by breakdown = TRUE is the plug-in
breakdown budget of tvbounds_summary(), the estimated counterpart of
:
the first budget at which the bound path adjacent to
reaches it — the lower path when the
baseline point estimate exceeds tau_star, the upper path otherwise.
The line is annotated with the value of
. No line is drawn when the
breakdown budget is censored at the right endpoint of the grid, when it
sits at the left endpoint, or when the baseline point estimate is
missing.
Rows of x$bounds with missing bound values (e.g. censored or
infeasible budgets) are omitted from the corresponding layer. For the
counterfactual application, whose divergence budgets may exceed one,
log_x = TRUE switches to a logarithmic budget axis.
A ggplot object, which prints to the active graphics device and can be modified further with ggplot2 layers.
Palomba, F. (2026). "Sensitivity Analysis in Population Shares." Working paper.
tvbounds_summary() for the numerical summary measures;
plot() dispatches here for tvbounds objects.
set.seed(123) n <- 400 d <- rbinom(n, 1, 0.5) s <- rbinom(n, 1, plogis(0.5 + 0.5 * d)) y <- ifelse(s == 1, 0.3 * d + rnorm(n), NA) dat <- data.frame(y = y, d = d, s = s) fit <- tvbounds_attrition(dat, outcome = "y", treatment = "d", response = "s", delta = seq(0, 1, by = 0.1), B = 100, seed = 1) tvbounds_plot(fit) # Bounds only, no confidence band and no breakdown mark: tvbounds_plot(fit, bands = FALSE, breakdown = FALSE)set.seed(123) n <- 400 d <- rbinom(n, 1, 0.5) s <- rbinom(n, 1, plogis(0.5 + 0.5 * d)) y <- ifelse(s == 1, 0.3 * d + rnorm(n), NA) dat <- data.frame(y = y, d = d, s = s) fit <- tvbounds_attrition(dat, outcome = "y", treatment = "d", response = "s", delta = seq(0, 1, by = 0.1), B = 100, seed = 1) tvbounds_plot(fit) # Bounds only, no confidence band and no breakdown mark: tvbounds_plot(fit, bands = FALSE, breakdown = FALSE)
Compact display of a tvbounds object: the application, the
neighborhood over which the sensitivity bounds were computed, the sample
size , the baseline point estimate — the value of the estimand
under the baseline distribution , reported at
— the grid of budgets on
which the bound paths
and were evaluated, and
whether inference is attached.
## S3 method for class 'tvbounds' print(x, digits = 3, ...)## S3 method for class 'tvbounds' print(x, digits = 3, ...)
x |
A |
digits |
Number of significant digits (default |
... |
Further arguments; ignored. |
x, invisibly.
Palomba, F. (2026). "Sensitivity Analysis in Population Shares." Working paper.
set.seed(1) n <- 200 d <- rbinom(n, 1, 0.5) s <- rbinom(n, 1, plogis(0.4 + 0.4 * d)) y <- ifelse(s == 1, 0.3 * d + rnorm(n), NA) fit <- tvbounds_attrition(data.frame(y = y, d = d, s = s), outcome = "y", treatment = "d", response = "s", delta = seq(0, 1, by = 0.1), bootstrap = FALSE) print(fit)set.seed(1) n <- 200 d <- rbinom(n, 1, 0.5) s <- rbinom(n, 1, plogis(0.4 + 0.4 * d)) y <- ifelse(s == 1, 0.3 * d + rnorm(n), NA) fit <- tvbounds_attrition(data.frame(y = y, d = d, s = s), outcome = "y", treatment = "d", response = "s", delta = seq(0, 1, by = 0.1), bootstrap = FALSE) print(fit)
Compact display of the summary measures computed by
tvbounds_summary(): the plug-in and certified breakdown budgets
and
, the
shadow price of robustness
,
the robustness standard error
,
and the certification frontier
, followed by any notes
on measures that could not be computed. The reference value of the
estimand, , is shown as tau_star,
matching the argument name.
## S3 method for class 'tvbounds_summary' print(x, digits = 3, ...)## S3 method for class 'tvbounds_summary' print(x, digits = 3, ...)
x |
A |
digits |
Number of significant digits (default |
... |
Further arguments; ignored. |
x, invisibly.
Palomba, F. (2026). "Sensitivity Analysis in Population Shares." Working paper.
set.seed(1) n <- 200 d <- rbinom(n, 1, 0.5) s <- rbinom(n, 1, plogis(0.4 + 0.4 * d)) y <- ifelse(s == 1, 0.3 * d + rnorm(n), NA) fit <- tvbounds_attrition(data.frame(y = y, d = d, s = s), outcome = "y", treatment = "d", response = "s", delta = seq(0, 1, by = 0.1), bootstrap = FALSE) # Without bootstrap draws the certified measures degrade to NA, with a # note explaining why: summary(fit)set.seed(1) n <- 200 d <- rbinom(n, 1, 0.5) s <- rbinom(n, 1, plogis(0.4 + 0.4 * d)) y <- ifelse(s == 1, 0.3 * d + rnorm(n), NA) fit <- tvbounds_attrition(data.frame(y = y, d = d, s = s), outcome = "y", treatment = "d", response = "s", delta = seq(0, 1, by = 0.1), bootstrap = FALSE) # Without bootstrap draws the certified measures degrade to NA, with a # note explaining why: summary(fit)
Computes the summary measures of Palomba (2026) for a tvbounds object:
the plug-in and certified breakdown budgets, the shadow price of
robustness, the robustness standard error, and the certification
frontier. The budget delta is the sensitivity parameter
of the total-variation (or contamination, or
divergence) neighborhood over which the bounds were computed.
## S3 method for class 'tvbounds' summary(object, ...) tvbounds_summary( object, delta = NULL, tau_star = 0, level = 0.95, cost_per_unit = 50, jump = 0.05, direction = c("auto", "lower", "upper") )## S3 method for class 'tvbounds' summary(object, ...) tvbounds_summary( object, delta = NULL, tau_star = 0, level = 0.95, cost_per_unit = 50, jump = 0.05, direction = c("auto", "lower", "upper") )
object |
A |
... |
For the |
delta |
Optional evaluation budget |
tau_star |
Reference value |
level |
Confidence level |
cost_per_unit |
Marginal cost of one additional sampled unit, used
for the cost equivalent of the certification frontier (default |
jump |
Increase in the certified budget that the frontier prices,
so that |
direction |
One of |
Write and
for the lower and upper
sensitivity bounds on the estimand at budget . The
first is nonincreasing and the second nondecreasing in the budget, and
both collapse at to the value of the
estimand under the baseline distribution . Robustness is
judged against a reference value of the
estimand, supplied through tau_star, and typically zero when it is the
sign of an effect rather than its magnitude that is of interest.
All measures are computed on the signed bound path adjacent to
, namely
when direction is "lower" and
when it is "upper", so that in both cases the path starts positive
when the conclusion holds at the baseline. The two directions are thus
treated symmetrically, as the paper treats them by replacing the
integrand by ; the displays below are written for
the lower path. With direction = "auto" (the default) the lower path
is used when the baseline point estimate exceeds tau_star and the
upper path otherwise.
The breakdown budget is the smallest budget at which the bounds cease to exclude the reference value,
with the convention that the infimum over the empty set equals one. That
convention is exactly the package's censoring rule: when the estimated
path never reaches on the supplied budget
grid, the breakdown is reported at the right endpoint of the grid, which
is one for the total-variation and contamination neighborhoods, and is
flagged by censored = TRUE rather than recorded as an inequality.
Sampling uncertainty is accounted for by the certified breakdown budget,
the largest budget at which the conclusion survives sampling
uncertainty. Here
estimates the lower bound path, is
the asymptotic standard deviation of that estimator in units of the
estimand and its
estimator, and is the two-sided
normal critical value at the confidence level level.
The shadow price of robustness is the marginal cost, in units of the
estimand, of one further unit of budget,
;
it is nonnegative and nonincreasing, and the package estimates it by
central differences on the budget grid. Dividing the sampling standard
deviation of the bound by the shadow price converts it from units of the
estimand into units of the budget, which yields the robustness standard
error
The certification frontier is the sample size at which the population
counterpart of the certified-breakdown inequality just clears the
reference value at budget ,
It is real-valued, so the smallest certifying integer sample size is
.
The frontier diverges as the budget approaches the breakdown budget and
is meaningful only below it: at and past the breakdown budget no sample
size certifies the conclusion, and the frontier is reported as NA. Its
semi-elasticity
,
the certification elasticity, gives the rate at which the required
sample size grows with the budget; cost_per_pp prices a discrete
version of it.
The one-row data frame measures reports the following, alongside the
symbol each one corresponds to in the paper:
delta_b — the plug-in breakdown budget
, obtained as the
first crossing of the estimated signed path with zero, linearly
interpolated between grid points; censored flags the empty-set
convention described above.
delta_b_ci — the certified breakdown budget
,
read off the outer confidence limit stored in the object (columns
ci_lower/ci_upper of object$bounds), that is, off the same band
the figures draw, so that tables and figures agree on one number. NA
when the object carries no band, and censored_ci flags censoring as
above.
delta_b_ci_norm — the same certified breakdown budget in its
normal-approximation form, the first crossing of
with . It estimates the same population
quantity as delta_b_ci and is retained as a diagnostic: the two
differ only when the bootstrap distribution of the bound is
asymmetric.
delta_eval — the budget at which the local
measures eta, se, varsigma, and varsigma_sc are evaluated.
eta — the shadow price of robustness
at delta_eval.
se — the estimated standard error of the bound in units of the
estimand at delta_eval, that is,
.
varsigma — the robustness standard error
,
computed as se * sqrt(n) / eta and therefore expressed in units of
the budget rather than in units of the estimand.
varsigma_sc — the finite-sample analogue se / eta, equal to
, which is the
sampling standard deviation of the breakdown budget itself at the
realized sample size.
frontier_at, n_cur, n_star, delta_n, cost_per_pp — the
certification frontier. frontier_at is the budget at which the
frontier is priced and n_cur the real-valued frontier
there, while n_star
is the smallest certifying integer sample size
at the budget raised by jump. delta_n is that sample size net of
the realized n, and cost_per_pp the implied cost of raising the
certified budget by one percentage point, at cost_per_unit per
sampled unit.
label, direction, n, point, tau_star — the estimand label,
the resolved direction, the sample size , the baseline
estimate of the estimand at , and the
reference value .
The evaluation budget for eta, se, and varsigma is the delta
argument when supplied; when delta = NULL (the default) it is the
plug-in breakdown budget, replaced by the certified breakdown budget
when the plug-in breakdown is censored, as in the paper. The frontier is
priced at the certified breakdown (default) or at the supplied delta.
Objects without inference (the recentered-IV and counterfactual
applications carry none by design) degrade gracefully: the plug-in
measures are reported, the certified and frontier measures are NA, and
the notes field explains why.
An object of class "tvbounds_summary": a list with a one-row
data frame measures (columns label, direction, n, point,
tau_star, delta_b, censored, delta_b_ci, censored_ci,
delta_b_ci_norm, delta_eval, eta, se, varsigma,
varsigma_sc, frontier_at, n_cur, n_star, delta_n,
cost_per_pp) and metadata fields (application, estimand_label,
neighborhood, divergence, direction, tau_star, delta_eval,
level, band_level, zc, cost_per_unit, jump, has_se,
has_ci, notes, call). Details gives the symbol of the paper each
column of measures corresponds to. Printed compactly by
print.tvbounds_summary().
Palomba, F. (2026). "Sensitivity Analysis in Population Shares." Working paper.
tvbounds_plot() to display the bounds; summary() dispatches
here for tvbounds objects.
set.seed(123) n <- 400 d <- rbinom(n, 1, 0.5) s <- rbinom(n, 1, plogis(0.5 + 0.5 * d)) y <- ifelse(s == 1, 0.3 * d + rnorm(n), NA) dat <- data.frame(y = y, d = d, s = s) fit <- tvbounds_attrition(dat, outcome = "y", treatment = "d", response = "s", delta = seq(0, 1, by = 0.1), B = 100, seed = 1) tvbounds_summary(fit) # Evaluate the measures at a chosen budget instead of the breakdown: tvbounds_summary(fit, delta = 0.2)set.seed(123) n <- 400 d <- rbinom(n, 1, 0.5) s <- rbinom(n, 1, plogis(0.5 + 0.5 * d)) y <- ifelse(s == 1, 0.3 * d + rnorm(n), NA) dat <- data.frame(y = y, d = d, s = s) fit <- tvbounds_attrition(dat, outcome = "y", treatment = "d", response = "s", delta = seq(0, 1, by = 0.1), B = 100, seed = 1) tvbounds_summary(fit) # Evaluate the measures at a chosen budget instead of the breakdown: tvbounds_summary(fit, delta = 0.2)
Computes sensitivity bounds on the average treatment effect for the
always-observed subpopulation of a randomized experiment with attrition,
following Palomba (2026). Writing for the
distribution of the data, the estimand is
the average treatment effect on the units that respond under either arm.
The bounds are indexed by a budget ,
supplied through delta, which caps the total variation distance between
the outcome distribution of the compliers (units that respond only under
treatment) and that of the always-observed units,
.
At the two distributions coincide and the
bounds collapse to the baseline difference in means among respondents (the
estimand under missingness completely at random); at
the restriction is vacuous and they equal the
trimming bounds of Lee (2009). Optionally computes a nonparametric
bootstrap (with clustering) for standard errors and a percentile
confidence band, and covariate-pooled bounds that allocate a single budget
optimally across covariate cells.
tvbounds_attrition( data, outcome, treatment, response, covariates = NULL, delta = seq(0, 1, by = 0.01), neighborhood = c("tv", "contamination"), bootstrap = TRUE, B = 1000, cluster = NULL, level = 0.95, min_obs = 5, seed = NULL, verbose = FALSE )tvbounds_attrition( data, outcome, treatment, response, covariates = NULL, delta = seq(0, 1, by = 0.01), neighborhood = c("tv", "contamination"), bootstrap = TRUE, B = 1000, cluster = NULL, level = 0.95, min_obs = 5, seed = NULL, verbose = FALSE )
data |
A data frame containing the columns named by |
outcome |
String; name of the numeric column holding the outcome
|
treatment |
String; name of the binary 0/1 column holding the
treatment |
response |
String; name of the binary 0/1 column holding the response
indicator |
covariates |
Optional character vector of column names to stratify
on, forming the discrete covariate |
delta |
Numeric vector of budget values
|
neighborhood |
Either |
bootstrap |
Logical; compute bootstrap standard errors and the
percentile confidence band. Default |
B |
Number of bootstrap replications |
cluster |
Optional string; name of a cluster identifier column.
When supplied, the bootstrap resamples whole clusters with
replacement. Default |
level |
Confidence level of the percentile band,
|
min_obs |
Minimum number of observed outcomes per arm for a
covariate cell to be retained. Default |
seed |
Optional integer seed for the bootstrap. When non- |
verbose |
Logical; emit progress messages. Default |
An object of class c("tvbounds_attrition", "tvbounds"): a list
with components
application: "attrition".
bounds: data frame with one row per requested budget and columns
delta, lower, upper, holding the sensitivity bounds
and
(or
their pooled covariate counterparts
and
when covariates is supplied), plus, when bootstrap = TRUE,
lower_se, upper_se, ci_lower, ci_upper (outer percentile
band endpoints at level).
point: the estimate of
at , where the bounds collapse to the
difference in means among respondents
(with covariates,
to its covariate-weighted analogue).
n: number of rows of data used.
neighborhood, level, B, estimand_label, call as documented
in the package overview (level and B are NA without
inference).
details: list with p_star (the complier share ,
which also sets the trimming mass), response_rate (the arm-specific
response rates )
and n_by_arm / n_respondents_by_arm (named, control and treated),
lee (list with the Lee-endpoint lower / upper,
and
, of the estimated
specification), lee_nocov (when covariates are used), breakdown
(list with the point-estimate breakdown budget
at the reference value
and, with inference, the
confidence-band breakdown budget; NA when censored beyond one),
pooled (with covariates: per-stratum table, coverage, dropped cells,
and the within-stratum reference curve pw,
and
,
under total variation), n_clusters (with cluster), and boot
(replicate counts, width standard errors, and — when
memory-reasonable — the matrices of bound draws).
Let be the binary treatment and, for
, let be the potential outcome
and the potential response
indicator. Their realized counterparts are
so that the observed data are : the outcome is recorded
only when . The response pattern partitions
the population into always-observed units
, never-observed units
, compliers and
defiers ; the estimand
is the average treatment
effect on the first of these groups.
Two assumptions are maintained. Random assignment enters as
,
labelled (MCAR), and the monotonicity condition of Lee (2009),
almost surely, labelled (Mono) —
treatment never causes a unit that would respond under control to attrit —
rules out defiers. Under (Mono) the outcome distribution of the observed
treated, , is a mixture of the complier and
always-observed outcome distributions,
where
and are
the arm-specific response rates and is the complier share. The
always-observed control mean is identified,
,
whereas the always-observed treated mean
is only partially
identified; the bounds on
follow by subtracting the identified control mean.
The complier share is estimated by
and returned as details$p_star, and the estimated response rates
as
details$response_rate. A negative unconstrained estimate of is
sampling noise under (Mono) and is projected to zero, in which case the
bounds collapse to the baseline difference in means at every budget.
Two robustness sets are available through neighborhood. Both are indexed
by the budget and both restrict the unobserved
complier outcome distribution relative to
the unobserved always-observed outcome distribution
:
"tv" (default): the total variation neighborhood of the paper,
,
so the two distributions may disagree on at most a
fraction of their mass.
"contamination": a one-sided strengthening in which
lies in the contamination neighborhood
of (Huber 1964), that is
for some distribution , equivalently
as measures: a -share of the compliers has
outcomes distributed exactly like the always-observed units, and only the
remaining -share may differ arbitrarily.
Under total variation the rescaling identity
recenters the restriction on the identified distribution
, so that the candidate complier distributions
, among which lies, range over the robustness set
where denotes the distributions on the
outcome space and the second restriction is the mixture structure of the
observed treated arm. The resulting sensitivity bounds
and
on
are available in closed
form as trimmed means of . Writing
for the quantile
function of the observed treated outcomes and
the upper bound is
and the lower bound is obtained symmetrically, trimming at
and
.
Under contamination, combining the same mixture identity with
pins the density
between
and , and the bounds are again trimmed means of
, now with effective trimming mass
. The contamination neighborhood is contained
in the total variation one at every budget, so its bounds are weakly
tighter, and the two families share the same endpoints: the baseline at
and, at , the Lee
(2009) bounds
and
,
where .
Both bound families are monotone in the budget by construction: the
robustness sets are nested in .
When covariates is supplied, units are stratified on the interaction of
the covariate columns, which plays the role of a discrete covariate
with support . Cells with fewer than
min_obs observed outcomes in either arm are dropped with a warning, and the
retained cells are weighted by their control-respondent shares, which
under (Mono) are the covariate distribution of the always-observed
population,
.
Within a cell the complier share and the observed
treated outcome distribution
are identified, and the cell-level
construction is the one above.
Two ways of spending the budget across cells are distinguished. The
within-stratum ("pointwise") restriction imposes
in every cell separately, giving one robustness set
per cell, whereas the pooled restriction caps only the average departure,
and so allows heterogeneity across cells inside the single robustness set
.
The pooled restriction is the weaker of the two, so
and
,
with equality at and at
, where both collapse to the covariate Lee
(2009) bounds
and
.
For neighborhood = "tv" the reported bounds are the pooled (joint)
bounds
and :
the budget allocation
subject to
is solved exactly by a greedy fill over the stratum value functions
, which are piecewise linear in the cell budget and
concave for the upper bound and convex for the lower one. The
within-stratum bounds
and
are returned in details$pooled$pw for reference. For
neighborhood = "contamination" the reported bounds impose the common
budget delta within every retained cell and aggregate; they remain
weakly inside the total variation bounds at every budget.
The bootstrap resamples the full observation
, together with the covariates, with
replacement — whole clusters when cluster is supplied — and recomputes
the entire bounds curve on each replicate, yielding
,
. Each replicate therefore redraws the
arm-specific response rates and hence , so the
reported standard errors carry the estimation uncertainty in
, which the naive variance
omits by treating as known; in the paper's
influence function
this uncertainty
is the term
. The reported
upper_se is the standard deviation of the draws
across replicates, that is the paper's
divided
by for a sample of size , and
lower_se is its counterpart for the lower bound. The reported confidence
band is the percentile band: writing for the value of
1 - level, ci_lower is the quantile of the
lower-bound draws and ci_upper the
quantile of the upper-bound draws, the
outer envelope of the identified set. Replicates on which the bounds
cannot be computed are dropped and counted (a warning reports their
number).
Palomba, F. (2026). "Sensitivity Analysis in Population Shares." Working paper.
Lee, D. S. (2009). "Training, Wages, and Sample Selection: Estimating Sharp Bounds on Treatment Effects." Review of Economic Studies, 76(3), 1071-1102.
Huber, P. J. (1964). "Robust Estimation of a Location Parameter." Annals of Mathematical Statistics, 35(1), 73-101.
tvbounds_plot() and tvbounds_summary() for reporting.
set.seed(123) n <- 400 d <- rbinom(n, 1, 0.5) s <- rbinom(n, 1, ifelse(d == 1, 0.9, 0.7)) y <- ifelse(s == 1, rnorm(n, mean = 0.3 * d), NA) x <- rbinom(n, 1, 0.5) dat <- data.frame(y = y, d = d, s = s, x = x) ## Total variation bounds with a small bootstrap fit <- tvbounds_attrition(dat, outcome = "y", treatment = "d", response = "s", delta = seq(0, 1, by = 0.1), B = 50, seed = 1) fit$bounds fit$details$lee ## Contamination neighborhood, no inference: weakly tighter bounds fit_c <- tvbounds_attrition(dat, outcome = "y", treatment = "d", response = "s", delta = seq(0, 1, by = 0.1), neighborhood = "contamination", bootstrap = FALSE) all(fit_c$bounds$lower >= fit$bounds$lower - 1e-12) ## Covariate-pooled bounds fit_x <- tvbounds_attrition(dat, outcome = "y", treatment = "d", response = "s", covariates = "x", delta = seq(0, 1, by = 0.1), bootstrap = FALSE)set.seed(123) n <- 400 d <- rbinom(n, 1, 0.5) s <- rbinom(n, 1, ifelse(d == 1, 0.9, 0.7)) y <- ifelse(s == 1, rnorm(n, mean = 0.3 * d), NA) x <- rbinom(n, 1, 0.5) dat <- data.frame(y = y, d = d, s = s, x = x) ## Total variation bounds with a small bootstrap fit <- tvbounds_attrition(dat, outcome = "y", treatment = "d", response = "s", delta = seq(0, 1, by = 0.1), B = 50, seed = 1) fit$bounds fit$details$lee ## Contamination neighborhood, no inference: weakly tighter bounds fit_c <- tvbounds_attrition(dat, outcome = "y", treatment = "d", response = "s", delta = seq(0, 1, by = 0.1), neighborhood = "contamination", bootstrap = FALSE) all(fit_c$bounds$lower >= fit$bounds$lower - 1e-12) ## Covariate-pooled bounds fit_x <- tvbounds_attrition(dat, outcome = "y", treatment = "d", response = "s", covariates = "x", delta = seq(0, 1, by = 0.1), bootstrap = FALSE)
Constructs the list of tuning options consumed by
tvbounds_counterfactual(). The defaults reproduce the settings used
for the counterfactual application in Palomba (2026).
tvbounds_control( maxsolves = 10, startptrange = 0.01, use_optim = FALSE, time_limit = 60, iterations = 100, outer_iterations = 3, inner_opt = NULL, outer_opt = NULL, knitro_options = list(), eta_min = 1e-120, lower_limit = -10, psi_tv_eps = 1e-04, tvac_tau = 0.001, tvmix_tau = 0.001, purekl_acap = 500, tvmix_kappa = NULL )tvbounds_control( maxsolves = 10, startptrange = 0.01, use_optim = FALSE, time_limit = 60, iterations = 100, outer_iterations = 3, inner_opt = NULL, outer_opt = NULL, knitro_options = list(), eta_min = 1e-120, lower_limit = -10, psi_tv_eps = 1e-04, tvac_tau = 0.001, tvmix_tau = 0.001, purekl_acap = 500, tvmix_kappa = NULL )
maxsolves |
Integer, number of multi-start restarts of the outer
optimization over the structural parameter |
startptrange |
Positive scalar; restarts after the first perturb the
initial |
use_optim |
Logical; if |
time_limit |
Positive scalar, wall-clock limit in seconds per outer
Optim run (used only when |
iterations |
Positive integer, inner iteration limit per outer
Optim run (used only when |
outer_iterations |
Positive integer, number of Fminbox outer
iterations per Optim run (used only when |
inner_opt |
Path to a KNITRO option file for the inner (dual)
problem, or |
outer_opt |
Path to a KNITRO option file for the outer problem
(over the structural parameter |
knitro_options |
Named list of individual KNITRO options (e.g.
|
eta_min |
Positive scalar, lower bound on the dual variable
|
lower_limit |
Scalar; inner (dual) objective values at or below this threshold are treated as unbounded below (the inner solver's infeasibility guard). |
psi_tv_eps |
Positive scalar, Huber smoothing scale for the kinked
total-variation conjugate |
tvac_tau |
Positive scalar, soft-max temperature for the
log-sum-exp term of the |
tvmix_tau |
Positive scalar, soft-max temperature for the
log-sum-exp term of the |
purekl_acap |
Positive scalar, overflow clamp on the exponent of
the conjugate of the pure Kullback-Leibler entropy
|
tvmix_kappa |
|
The options are stated in the notation of the paper. A candidate
distribution is measured against the baseline
by the divergence
generated by an entropy
function , and the budget caps it.
At a fixed structural parameter the inner problem is
solved in its dual form, over the multipliers
attached respectively to
the total-mass constraint, to the divergence budget – so that
is the shadow price of robustness – and to the moment
restrictions. The dual objective integrates against the baseline the
perspective of the convex conjugate ,
with the counterfactual criterion and the moment
function. Under total variation,
, the
conjugate is piecewise linear and
the perspective collapses to the kinked
; the
smoothing options below round those kinks off, always from above, so the
computed bounds stay outward-conservative. The contamination weight
of the mixture constraint
is set by tvmix_kappa.
A list of class "tvbounds_control" with the (validated)
options above.
Palomba, F. (2026). "Sensitivity Analysis in Population Shares." Working paper.
Christensen, T. and B. Connault (2023). "Counterfactual Sensitivity and Robustness." Econometrica, 91(1), 263-298.
ctrl <- tvbounds_control() ctrl$maxsolves # a faster configuration for exploratory grids tvbounds_control(maxsolves = 3, knitro_options = list(maxit = 200))ctrl <- tvbounds_control() ctrl$maxsolves # a faster configuration for exploratory grids tvbounds_control(maxsolves = 3, knitro_options = list(maxit = 200))
Computes the lower and upper sensitivity bounds
and
on a counterfactual
, when the distribution
of the latent variables ranges over a divergence
neighborhood of the simulated baseline with budget
delta, and the structural parameter ranges over the
values compatible with the moment conditions
, following Palomba (2026) and Christensen and Connault (2023).
This is the only function in the package that supports general
-divergences beyond total variation (spelled out at
first use; "TV" below) and contamination.
tvbounds_counterfactual( moments, d, theta_lb, theta_ub, delta, divergence = c("KL_chi2", "KL", "chi2", "TV", "TVmix", "TVmixC", "TVac"), side = c("both", "lower", "upper"), U = NULL, M = 50000, u_dim = NULL, gamma = NULL, gradient = NULL, theta_init = NULL, control = tvbounds_control(), seed = NULL, verbose = TRUE )tvbounds_counterfactual( moments, d, theta_lb, theta_ub, delta, divergence = c("KL_chi2", "KL", "chi2", "TV", "TVmix", "TVmixC", "TVac"), side = c("both", "lower", "upper"), U = NULL, M = 50000, u_dim = NULL, gamma = NULL, gradient = NULL, theta_init = NULL, control = tvbounds_control(), seed = NULL, verbose = TRUE )
moments |
The moment/counterfactual function: a length-2
character vector |
d |
Integer, the number of moment conditions
|
theta_lb, theta_ub
|
Numeric vectors of equal length: the box for
the structural parameter |
delta |
Numeric vector of strictly positive budgets
|
divergence |
Divergence keyword; one of |
side |
|
U |
Optional |
M |
Integer, the number |
u_dim |
Integer, the dimension |
gamma |
Optional R list: an arbitrary payload forwarded to the
moments function (as |
gradient |
How to differentiate the moments with respect to
|
theta_init |
Optional numeric vector, the initial structural
parameter |
control |
A list created by |
seed |
Optional integer seed for the scrambled-Halton draws (and for R's RNG, which is saved and restored so the call has no side effect on the caller's random-number stream). |
verbose |
Logical; print solver progress (default |
Setup. Let collect the latent variables
of the structural model (taste shocks, unobserved heterogeneity,
measurement errors) and let be the baseline distribution
the econometrician postulates for them. At a structural parameter
, a distribution is
compatible with the model when
, with the moment function and
the moment constraint set; the object of
interest is the counterfactual
, the expectation of a
known criterion that is linear in at fixed
. The robustness set collects the distributions that
are compatible with the model and within budget of the baseline,
where is the
-divergence selected by divergence and
the budget, and the reported bounds are the nested
extrema
The optimization over at a fixed is the
inner problem, solved in its dual form; the optimization over
is the outer problem.
Correspondence between the paper and the code. The paper writes the
counterfactual integrand as and the moment function as
. The interfaces below fill an array named K with the values
of and an array named G with the values of : read
K as and G as throughout. The solver takes the
moment conditions in centered equality form,
, so a nonzero
target is absorbed by centering the moment function. The
argument d is the number of moment conditions,
; the common length of theta_lb and theta_ub
is the dimension of ; and u_dim is the dimension
of a single latent draw.
Moments specification. moments can be supplied in three forms:
a length-2 character vector c(file, fname): file is the path
of a Julia source file that is included into the session, and
fname the name of a function defined there (at the top level, in
Main) with the in-place signature
moments!(K, G, theta, U, obj). The function must fill K (an
M-vector holding the counterfactual values
) and G (an M x d matrix
holding the moment functions
, one row per draw) and may
read the user payload as obj.gamma (an R list arrives in Julia as
an ordered dictionary keyed by symbols, so obj.gamma[:name]);
a single string naming a Julia function with the same signature that is already defined in the session;
an R function function(theta, U, gamma) returning a list with
components K (numeric of length M) and G (numeric
M x d matrix). This path is much slower (every objective
evaluation crosses the R/Julia boundary), and because ForwardDiff
cannot differentiate through R code the outer optimization needs
either a user-supplied gradient or finite differences (the
default for this path).
For Julia moments the outer envelope-theorem gradient differentiates
the moments by automatic differentiation (ForwardDiff), so the Julia
function should be written generically in the element type of
theta; pass gradient = "fd" for a non-generic function.
Latent draws. U is the M x u_dim matrix of latent draws
that discretizes the
baseline distribution , row j holding the draw
. When U = NULL the package generates M
scrambled-Halton points in the unit cube
of dimension u_dim (Owen,
2017), seeded by seed; the moments function is then responsible for
mapping the uniform coordinates into baseline draws (e.g. through
quantile transforms). The Halton generator supports u_dim <= 15;
supply U directly for higher-dimensional draws.
Divergences. divergence selects the entropy function
whose divergence
defines the neighborhood;
the budget grid delta must be strictly positive, and must lie in
for the total-variation family:
"KL_chi2" (default): the hybrid Kullback-Leibler/chi-square
divergence of Christensen and Connault (2023); any delta > 0.
"KL", "chi2": the pure Kullback-Leibler entropy
and the Pearson chi-square entropy; any delta > 0.
"TV": total variation,
, for
which . Its recession
function is finite,
, so
the dual keeps the pointwise constraint
, which discretizes into M linear feasibility
constraints (one per draw).
"TVmix": total variation intersected with the mixture
(contamination) constraint ,
that is, the perturbed distribution contains the baseline as a mixing
component with weight (or
control$tvmix_kappa); solved in the exact reduced form (Palomba,
2026).
"TVmixC": the literal dual of the same program, kept as a
cross-check of "TVmix"; it is slower, and it is the only mode
supporting a mixing weight
.
"TVac": total variation restricted to distributions absolutely
continuous with respect to the baseline,
.
The kinked total-variation conjugates
are Huber-smoothed and the
per-draw maxima in the dual objective log-sum-exp-smoothed (scales in
tvbounds_control()); both smoothings lie above the exact functions,
so computed bounds are outward-conservative (wider, never narrower) at
order 1e-3.
Optimization. For each budget (in increasing order) and each
side, the solver runs control$maxsolves multi-start outer
optimizations over (KNITRO, or Optim.jl when
control$use_optim = TRUE), warm-started at the previous budget's
optimum; the reported bound is the inner (dual) value re-solved at
the best candidate, which makes the bound curves monotone in the
budget by construction. A degenerate box (theta_lb == theta_ub)
skips the outer optimization and reports the bounds at the fixed
supplied through theta_init. Failed budgets are
reported as NA (for "TVmix" an NA typically signals an
infeasible moment condition at every in the box,
that is an empty robustness set at that budget).
An object of class c("tvbounds_counterfactual", "tvbounds"):
a list with the fields described in the package overview, in
particular bounds (data frame with columns delta, lower,
upper, holding ,
and
; no
standard-error or confidence-band columns, since this application
currently carries no inference), point (the plug-in counterfactual
at
theta_init, or NA when theta_init was not supplied),
divergence, and details, a list with:
solverdata frame of per-budget diagnostics: outer
multi-start flags, inner KNITRO status codes, and timings for
each side (-999 marks entries that do not apply, e.g. outer
flags in fixed-theta mode).
theta_lower, theta_upper
l x length(delta) matrices
of the outer-optimal structural parameters at
each budget, for the lower and the upper bound respectively.
M, u_dim
the number of simulated draws and
their dimension .
theta_lb, theta_ub, theta_init, fixed_theta
the parameter box, the initial point, and whether the box was degenerate.
controlthe resolved control list, including the option files actually used.
momentsa short description of the moments specification.
This function relies on Julia (>= 1.9) and on the commercial Artelys KNITRO solver, accessed through the 'JuliaCall' package and KNITRO.jl. A valid KNITRO license is required (free academic trials are available from Artelys at https://www.artelys.com/solvers/knitro/). On the first call in each R session the package initializes Julia, instantiates its Julia environment, and checks that KNITRO.jl loads and that a KNITRO solver context can be created (which exercises the license); a one-time message reports the outcome, and the call stops with installation and license guidance when the check fails. The check runs once per R session.
Palomba, F. (2026). "Sensitivity Analysis in Population Shares." Working paper.
Christensen, T. and B. Connault (2023). "Counterfactual Sensitivity and Robustness." Econometrica, 91(1), 263-298.
Owen, A. B. (2017). "A randomized Halton algorithm in R." arXiv:1706.02808.
tvbounds_control(), tvbounds_plot(), tvbounds_summary()
# The full solver requires Julia and a licensed KNITRO installation, # so a complete run cannot be executed on CRAN or in checks: ## Not run: # Toy model (shipped with the package): U ~ Uniform(0, 1), one moment # condition m(U; theta) = U - theta and counterfactual g(U; theta) = U, # so under the "TVmix" neighborhood the bounds equal the endpoints of # the theta box. In the Julia file, m is written into G and g into K. toy <- system.file("julia", "examples", "toy.jl", package = "tvbounds") fit <- tvbounds_counterfactual( moments = c(toy, "tvb_toy_moments!"), d = 1, theta_lb = 0.4, theta_ub = 0.6, delta = c(0.5, 1), divergence = "TVmix", M = 500, u_dim = 1, theta_init = 0.5, control = tvbounds_control(maxsolves = 2), seed = 1234) fit$bounds ## End(Not run) # The control constructor is pure R and always available: tvbounds_control(maxsolves = 3)$maxsolves# The full solver requires Julia and a licensed KNITRO installation, # so a complete run cannot be executed on CRAN or in checks: ## Not run: # Toy model (shipped with the package): U ~ Uniform(0, 1), one moment # condition m(U; theta) = U - theta and counterfactual g(U; theta) = U, # so under the "TVmix" neighborhood the bounds equal the endpoints of # the theta box. In the Julia file, m is written into G and g into K. toy <- system.file("julia", "examples", "toy.jl", package = "tvbounds") fit <- tvbounds_counterfactual( moments = c(toy, "tvb_toy_moments!"), d = 1, theta_lb = 0.4, theta_ub = 0.6, delta = c(0.5, 1), divergence = "TVmix", M = 500, u_dim = 1, theta_init = 0.5, control = tvbounds_control(maxsolves = 2), seed = 1234) fit$bounds ## End(Not run) # The control constructor is pure R and always available: tvbounds_control(maxsolves = 3)$maxsolves
Computes sensitivity bounds on a recentered (formula) instrumental-variables
estimate when the postulated distribution of the shocks is allowed to vary
within a total variation or a contamination neighborhood of the baseline
assignment distribution , as in Palomba (2026). The leading
use case is the recentered instruments of Borusyak and Hull (2023), whose
validity rests on a researcher-postulated distribution for the shock
process: the bounds quantify how far the estimate can move when up to a
fraction delta of that postulated probability mass is misspecified.
tvbounds_riv( y, x, z, Fmat, p = NULL, controls = NULL, delta = seq(0, 1, by = 0.002), neighborhood = c("tv", "contamination"), tau_star = 0, verbose = FALSE )tvbounds_riv( y, x, z, Fmat, p = NULL, controls = NULL, delta = seq(0, 1, by = 0.002), neighborhood = c("tv", "contamination"), tau_star = 0, verbose = FALSE )
y |
Numeric n-vector, the outcome |
x |
Numeric n-vector, the endogenous regressor |
z |
Numeric n-vector, the realized (un-recentered) candidate
instrument: |
Fmat |
Numeric n x S matrix of counterfactual instrument draws:
|
p |
Optional numeric S-vector holding the probabilities that the
postulated assignment distribution |
controls |
Optional numeric matrix or data frame of control
variables (n rows) to be partialled out of |
delta |
Numeric vector of sensitivity budgets
|
neighborhood |
Either |
tau_star |
Numeric scalar reference value |
verbose |
Logical; if |
The exercise is conducted conditionally on the realized sample, so every
bound is a deterministic function of the data and of the budget delta;
accordingly, and by design (as in the paper), no standard errors or
confidence bands are produced for this application.
The model and the recentered estimate. For units
the structural equation is
with the parameter of interest, the
outcome, the endogenous regressor and
the unobserved residual. Let denote
the vector of exogenous shocks, taking values in a space
, let collect predetermined covariates, and
let
be the known formula that maps a shock configuration into the instrument of
unit , so that is the
candidate instrument at the realized shocks. For a distribution
on , the expected instrument and the recentered
instrument are
Borusyak and Hull (2023) postulate an assignment distribution
for the shocks and recenter at it.
The two criterion functions. The formula enters only through the two sample aggregates
whose recentered values are the reduced form
and
the first stage
.
The estimate at a candidate assignment distribution is the ratio
and the reported estimate is
.
How the arguments encode the shock space. The package represents
by S shock configurations
: entry Fmat[i, s] holds
, the formula of unit i at the s-th
configuration, p[s] holds the probability that assigns to
that configuration, and z[i] holds .
The shock space is therefore taken to be the finite set
,
over which the candidate distributions range.
Neighborhoods. The bounds report the range of
as ranges over the chosen
neighborhood of , intersected with the set
of distributions at which
the ratio is defined:
neighborhood = "tv" (the default): the total variation ball
which delivers the bounds
and
. They
are computed from the closed form the paper gives for the criterion
bounds
and
,
the smallest and the largest value of
over the ball: writing for
the quantile function of a criterion under ,
and , for its
extremes over ,
that is, the baseline mean of once a tail of mass
has been trimmed and relocated to the most (least)
favorable configuration. Applied to the one-parameter family of criteria
, for which
holds exactly when
, each bound is the
unique root in of a strictly monotone function and is located
by bracketed root finding;
neighborhood = "contamination": the contamination neighborhood
in which the contamination share is the budget itself,
delivering the bounds
and .
These are attained by contaminating distributions degenerate at
a single shock configuration, so they are obtained by enumerating the S
configurations rather than by optimization.
Every member of
lies in
, so
the contamination bounds are weakly tighter at every budget.
First-stage breakdown. Once the budget is large enough that some
distribution in the neighborhood makes the recentered first stage
vanish, is no
longer well defined over the whole neighborhood and the identified set is
the entire real line. The smallest such budget is the first-stage
breakdown budget,
for the total
variation ball and
for the
contamination neighborhood; since the contamination neighborhood is the
smaller of the two,
. The
one for the neighborhood in use is reported in details$delta_fs.
Following the paper, the infimum over an empty set is set to 1, so a
reported value of 1 carrying delta_fs_censored = TRUE means that the
first stage never breaks down over the budget range, not that breakdown
occurs at 1. Rows of bounds at budgets where the bounds are vacuous
carry NA.
Controls. When controls is supplied, y, x, z, and every column
of Fmat are residualized on the controls and a constant (when
controls = NULL, on the constant alone) before the bounds are computed.
By the Frisch–Waugh–Lovell theorem this leaves the just-identified
two-stage least squares coefficient on x unchanged, because the
projection matrix is idempotent; the baseline estimate then replicates the
estimate from the full regression with controls.
An object of class c("tvbounds_riv", "tvbounds"), a list with
entries
application: "riv".
bounds: data frame with one row per budget and columns delta,
lower, upper. Rows at budgets where the neighborhood contains a
distribution collapsing the recentered first stage carry NA (the
bounds are vacuous there). No standard error or confidence interval
columns are attached: this application carries no inference by design.
point: the reported recentered IV estimate
, which is the common value of the two bounds at
delta = 0.
n: number of observations.
neighborhood: the neighborhood used.
level, B: NA (no inference).
estimand_label: "IV coefficient".
call: the matched call.
details: a list with
criteria: the reduced criterion object consumed by the internal
kernels, holding the realized values and
, the two S-vectors
and ,
and the baseline aggregates
and
; bounds at additional
budgets can be recomputed from it without touching the n x S design
again.
delta_fs, delta_fs_censored: the first-stage breakdown budget
for the neighborhood used,
or
, and
whether it is censored at 1 (TRUE when the first stage never
breaks down on , so 1 is the empty-set convention rather
than an actual breakdown).
delta_fs_tv, delta_fs_cont: both first-stage breakdown budgets,
and
(each
carrying its "censored" attribute).
delta_breakdown, delta_breakdown_censored: the smallest budget
at which the bounds
for the chosen neighborhood cover ,
reported as 1 with delta_breakdown_censored = TRUE when
is never covered on .
tau_star: the reference value used.
n_controls: number of control columns partialled out (0 when
controls = NULL).
Palomba, F. (2026). "Sensitivity Analysis in Population Shares." Working paper.
Borusyak, K. and Hull, P. (2023). "Nonrandom Exposure to Exogenous Shocks." Econometrica, 91(6), 2155–2185.
# A small simulated formula-instrument design: S counterfactual shock # configurations, a realized instrument, a first stage, and an outcome. set.seed(123) n <- 80 S <- 50 Fmat <- matrix(rnorm(n * S), n, S) # Fmat[i, s] = f_i(v^(s); w) z <- rowMeans(Fmat) + rnorm(n) # realized instrument z_i = f_i(v; w) x <- z + 0.5 * rnorm(n) # endogenous regressor y <- 0.4 * x + rnorm(n) # outcome fit <- tvbounds_riv(y, x, z, Fmat, delta = seq(0, 1, by = 0.05)) fit$point # recentered IV estimate at P_* head(fit$bounds) # bounds along the budget grid fit$details$delta_fs # first-stage breakdown budget fit$details$delta_breakdown # breakdown budget for the sign # Contamination neighborhood, with controls partialled out. W <- data.frame(w1 = rnorm(n), w2 = rnorm(n)) fit_cont <- tvbounds_riv(y, x, z, Fmat, controls = W, delta = seq(0, 1, by = 0.05), neighborhood = "contamination") fit_cont$point# A small simulated formula-instrument design: S counterfactual shock # configurations, a realized instrument, a first stage, and an outcome. set.seed(123) n <- 80 S <- 50 Fmat <- matrix(rnorm(n * S), n, S) # Fmat[i, s] = f_i(v^(s); w) z <- rowMeans(Fmat) + rnorm(n) # realized instrument z_i = f_i(v; w) x <- z + 0.5 * rnorm(n) # endogenous regressor y <- 0.4 * x + rnorm(n) # outcome fit <- tvbounds_riv(y, x, z, Fmat, delta = seq(0, 1, by = 0.05)) fit$point # recentered IV estimate at P_* head(fit$bounds) # bounds along the budget grid fit$details$delta_fs # first-stage breakdown budget fit$details$delta_breakdown # breakdown budget for the sign # Contamination neighborhood, with controls partialled out. W <- data.frame(w1 = rnorm(n), w2 = rnorm(n)) fit_cont <- tvbounds_riv(y, x, z, Fmat, controls = W, delta = seq(0, 1, by = 0.05), neighborhood = "contamination") fit_cont$point