tvbounds
Sensitivity analysis and bounds under total variation neighborhoods, the
companion Python module to Palomba (2026), "Sensitivity Analysis in
Population Shares." It is a function-by-function port of the R package of
the same name: the same functions, the same options, the same manual
(docs/tvbounds-manual.pdf), and the same numbers.
Installation
pip install tvbounds
Only numpy, pandas, and matplotlib are required at runtime. The
structural-counterfactual solver additionally needs the juliacall
package (pip install "tvbounds[julia]"), a Julia installation, and the
Artelys KNITRO solver (see below).
KNITRO requirement for structural counterfactuals
tvbounds_counterfactual() solves its optimization problems in Julia
through the commercial Artelys KNITRO
solver, so it requires Julia (>= 1.9), the juliacall Python package, and
a valid KNITRO license. Students can request a free one-year KNITRO
license through Artelys' academic program on the same page. The package
checks for KNITRO once per Python session, on the first call. The Julia
solver module is the one shipped with the R package, byte for byte.
The Julia dependencies (KNITRO.jl, ForwardDiff, Optim, ...) are declared in
tvbounds/juliapkg.json and installed by juliacall into its own Julia
environment on first use. KNITRO.jl downloads the KNITRO library through
KNITRO_jll, whose newest release may be a KNITRO version that your license
does not cover (KNITRO licenses are bound to a release date; a license for
KNITRO 15.1 rejects KNITRO 16 with error -520). In that case pin the
release your license covers before the first call, for example
python -m juliapkg add KNITRO_jll --uuid 0e6b36f8-8e90-4eb5-b54e-06f667ea875c --version "=15.1.0"
or point KNITRO.jl at a local installation through the KNITRODIR
environment variable, as described in the KNITRO.jl documentation.
Quick tour
The commands below reproduce, in brief, what the manual develops in detail.
Randomized experiments with attrition
import numpy as np
import pandas as pd
from tvbounds import tvbounds_attrition, tvbounds_summary
## a small experiment with village-level assignment and selective attrition
rng = np.random.default_rng(20260820)
n = 500
village = np.repeat(np.arange(1, 51), 10)
d = rng.binomial(1, 0.5, 50)[village - 1]
x = rng.binomial(1, 0.4, n)
ability = rng.normal(size=n)
s = (rng.uniform(size=n) < 1 / (1 + np.exp(-(0.2 + 1.2 * d + 0.5 * ability)))).astype(int)
y = np.where(s == 1, 1 + 0.35 * d + 0.5 * x + ability + 0.5 * rng.normal(size=n), np.nan)
rct = pd.DataFrame({"y": y, "d": d, "s": s, "x": x, "village": village})
## total variation bounds with a bootstrap confidence band
fit_tv = tvbounds_attrition(rct,
outcome="y", treatment="d", response="s",
delta=np.linspace(0, 1, 21), B=200, seed=1)
print(fit_tv)
fit_tv.plot()
## contamination neighborhood: weakly tighter bounds, same endpoints
fit_ct = tvbounds_attrition(rct,
outcome="y", treatment="d", response="s",
delta=np.linspace(0, 1, 21), neighborhood="contamination",
bootstrap=False)
## cluster bootstrap and covariate-pooled bounds
fit_cl = tvbounds_attrition(rct,
outcome="y", treatment="d", response="s",
delta=np.linspace(0, 1, 21), B=200, cluster="village", seed=1)
fit_x = tvbounds_attrition(rct,
outcome="y", treatment="d", response="s", covariates="x",
delta=np.linspace(0, 1, 21), B=200, seed=1)
## summary measures: breakdown budgets, shadow price, robustness
## standard error, certification frontier
print(fit_tv.summary())
print(tvbounds_summary(fit_tv, delta=0.1))
Recentered instrumental variables
import numpy as np
from tvbounds import tvbounds_riv
## a shift-share design: exposure shares W, S counterfactual shock draws
rng = np.random.default_rng(1901)
n, K, S = 150, 10, 80
W = rng.exponential(size=(n, K)) ** 2
W = W / W.sum(axis=1, keepdims=True)
g0 = rng.normal(loc=0.3, size=K)
G = rng.normal(size=(K, S))
z = W @ g0 # realized formula instrument
Fmat = W @ G # n x S counterfactual draws
x = z + rng.normal(size=n)
y = 0.5 * x + 0.4 * (W @ rng.normal(size=K)) + 0.5 * rng.normal(size=n)
## bounds under both neighborhoods (deterministic; no inference by design)
riv_tv = tvbounds_riv(y, x, z, Fmat, delta=np.linspace(0, 1, 101))
riv_ct = tvbounds_riv(y, x, z, Fmat, delta=np.linspace(0, 1, 101),
neighborhood="contamination")
print(riv_tv)
riv_tv.plot()
## first-stage breakdown budget and its censoring flag
riv_tv.details["delta_fs"], riv_tv.details["delta_fs_censored"]
Counterfactuals in structural models (requires KNITRO)
import tvbounds
from tvbounds import tvbounds_counterfactual, tvbounds_control
## a toy model shipped with the package: moments in Julia, Bundle signature
toy = tvbounds.julia_file("examples", "toy.jl")
fit_cf = tvbounds_counterfactual(
moments = (toy, "tvb_toy_moments!"), # Julia file + function name
d = 1, # number of moment conditions
theta_lb = 0.4, theta_ub = 0.6, # box for the structural parameter
delta = [0.05, 0.1, 0.25, 0.5, 1], # budgets (strictly positive)
divergence = "TVmix",
side = "both",
M = 5000, u_dim = 1, # scrambled-Halton draws
theta_init = 0.5,
seed = 1234,
control = tvbounds_control(maxsolves = 5))
fit_cf.bounds
fit_cf.plot()
Agreement with the R package
Seeded bootstraps use an R-compatible random-number stream, so every
number a seeded call returns (bounds, standard errors, percentile bands,
breakdown budgets, summary measures) reproduces the R package's output
at machine precision. The validation/ folder of the source repository
holds the cross-checks against the R package and against the empirical
results of the paper. Two platform notes: the last-bit agreement relies on
the arithmetic of R's compiled kernels (fused multiply-adds on Apple
Silicon builds; other R builds may differ in the last unit in place of a
standard error or a breakdown budget), and covariate cells are ordered as
R orders factor levels in an ICU (en_US) locale, so string-valued
covariates under a C locale in R may list the strata in a different order
(the bounds themselves are unaffected beyond the last bit).
Citation
If you use tvbounds, please cite:
Palomba, F. (2026). "Sensitivity Analysis in Population Shares." Working paper.
License
MIT © Filippo Palomba. See LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tvbounds-0.1.1.tar.gz.
File metadata
- Download URL: tvbounds-0.1.1.tar.gz
- Upload date:
- Size: 597.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.9.17
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
16fbff1242d1f6d0e43fadbd0a9ace6ebd44c230b34d25c3aaaf2b457bbaf1d4
|
|
| MD5 |
af545f05c9231fd6826f196435e94237
|
|
| BLAKE2b-256 |
d48d5c9a9ad97a7f85f22c3aa3ea5cf137a355c017120dad3ff5a728aaaad1bf
|
File details
Details for the file tvbounds-0.1.1-py3-none-any.whl.
File metadata
- Download URL: tvbounds-0.1.1-py3-none-any.whl
- Upload date:
- Size: 126.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.9.17
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d6ff3fbc822d811d1403d8c9284c1f5864a7ac411818cdb6e7d2321d3ca47b15
|
|
| MD5 |
1480255ee01607164af7c6004fe945a0
|
|
| BLAKE2b-256 |
f195d3c6dc243458be693f532b351f2cf07023a24f1a0940cb675a541629a1a0
|