a ratio with its uncertainty kept
tulpaRatio fits Bayesian hierarchical models for ratios, rates and proportions, such as catch per unit effort, the share of one species in a sample, or successes out of a known number of trials.
The package models the numerator and the denominator as two processes with linked linear predictors, and derives the ratio from their joint posterior. The denominator is either a second response or a known exposure, and the two processes can share random effects. The ratio posterior carries the uncertainty of both components, so you can report the ratio, compare groups on it and decide which effects matter for your question. tulpaRatio supports:
ratiod_compare() ranks fitted
models by leave-one-out cross-validation or WAIC, and
ratiod_average() combines their ratio predictions with stacking or
pseudo-BMA weights.The default sampler is the No-U-Turn sampler with hand-coded gradients,
written in C++ on the tulpa engine,
so no Stan or JAGS installation is needed. Elliptical slice sampling,
Pólya-Gamma Gibbs sampling and a nested Laplace approximation fit the
subsets of models they apply to. Every fit reports the tier of guarantee
its inference carries. mode = "auto" picks an exact or structured
backend for the model, and the optimized tier runs only when you request
it.
The package compiles C++ code. Install the build tools for your operating system first:
| Operating system | Build tools |
|---|---|
| Windows | Rtools |
| macOS | Run xcode-select --install in Terminal |
| Debian / Ubuntu | Install r-base-dev
|
Check that R can find the compiler:
pkgbuild::has_build_tools(debug = TRUE)
Then install tulpaRatio:
# install.packages("pak")
# pak::pak("gcol33/tulpaRatio")
install.packages("tulpaRatio", repos = c('https://gcol33.r-universe.dev', 'https://cloud.r-project.org'))
The code is compiled as C++17. The Eigen headers come from RcppEigen and the sampler headers from tulpa, both installed as dependencies, so no system library is needed.
OpenMP lets the package run MCMC chains in parallel, set through cores
in the control argument of
tratio().
Rtools on Windows and the default compiler on Linux include OpenMP. Without it, the package installs and runs the chains one after another.
Apple’s compiler needs an additional OpenMP runtime. Install it in Terminal:
brew install libomp
Before installing tulpaRatio, add the settings for your Mac to
~/.R/Makevars, R’s personal compiler-settings file. Create the
directory and file if needed.
Apple Silicon:
CPPFLAGS += -I/opt/homebrew/opt/libomp/include -Xclang -fopenmp
LDFLAGS += -L/opt/homebrew/opt/libomp/lib -lomp
Intel:
CPPFLAGS += -I/usr/local/opt/libomp/include -Xclang -fopenmp
LDFLAGS += -L/usr/local/opt/libomp/lib -lomp
After installation, run:
tulpaRatio::ratiod_threads()
This returns the number of OpenMP threads available to the package.
tratio(). The formula names the numerator, then
the denominator after |, then the predictors.ratio().ratio_contrast().The input is a data frame with one row per observation. The family sets
the distribution of each process; ratiod_poisson_gamma() treats the
numerator as a count and the denominator as continuous effort.
library(tulpaRatio)
set.seed(1)
trawl <- data.frame(
site = factor(rep(1:12, each = 10)),
depth = rnorm(120),
season = factor(rep(c("summer", "winter"), 60))
)
trawl$effort <- rgamma(120, shape = 6, rate = 1)
trawl$catch <- rpois(120, trawl$effort * exp(0.5 + 0.3 * trawl$depth))
fit <- tratio(
catch | effort ~ depth + season + (1 | site),
data = trawl,
family = ratiod_poisson_gamma()
)
fit
#> ratio model fit: catch | effort ~ depth + season + (1 | site)
#> Family: poisson_gamma (Poisson numerator, Gamma denominator (CPUE-type))
#> Inference: Exact (Tier 1) via hmc; credible intervals are posterior uncertainty
#> Observations: 120; random effects: site (12)
#> Use summary() for estimates and ratio() for ratio posteriors.
The data simulate a trawl survey of 12 sites with 10 hauls each.
(1 | site) adds a site random effect, shared by catch and effort by
default. The printed fit states its inference tier.
The ratio here is catch per unit effort. ratio() returns its posterior
for every observation, summarised with summary = TRUE, and
ratio_contrast() compares the seasons on it:
cpue <- ratio(fit, summary = TRUE)
head(cpue)
#> tulpaRatio ratio summary (response scale)
#> ===========================================
#>
#> obs mean sd q2.5 q50 q97.5
#> 1 1.265194 0.1058698 1.0691947 1.259819 1.483303
#> 2 1.839405 0.1263989 1.5985339 1.835237 2.096420
#> 3 1.191068 0.1081630 0.9914735 1.185735 1.412453
#> 4 2.784215 0.2861958 2.2688690 2.765384 3.379809
#> 5 1.670186 0.1169916 1.4489908 1.667464 1.909893
#> 6 1.375038 0.1260676 1.1492059 1.370815 1.634085
ratio_contrast(fit, ~ season)
#> tulpaRatio ratio contrasts
#> =====================
#>
#> Type: difference
#> Variable: season
#> Reference: summer
#>
#> contrast mean sd q2.5 q50 q97.5
#> winter vs summer 0.1112132 0.09829348 -0.08689166 0.1124141 0.3020628
#> prob_positive
#> 0.869
Quick Start continues with the formula syntax, random effects and model checking.
A quotient of two measured quantities inherits the error of both. A haul
with more effort gives a more precise catch per unit effort, yet a model
fitted to the quotient weights every ratio equally. An offset,
offset(log(effort)), assumes that catch scales with effort with a
coefficient of exactly 1 and that effort has no structure of its own.
tulpaRatio fits both processes and derives the ratio from them. Why
Ratios Are Not
Data
sets out the argument with examples.
The numerator and the denominator can take their own predictors. The
formula then keeps the shared random effects, and formula_num and
formula_denom give each process its fixed effects:
fit2 <- tratio(
catch | effort ~ (1 | site),
formula_num = ~ depth + season,
formula_denom = ~ season,
data = trawl,
family = ratiod_poisson_gamma()
)
Random Effects covers correlated, uncorrelated, nested and crossed random effects and latent factors.
Each backend carries one of three tiers. Tier 1 covers the samplers that
converge to the exact posterior: the No-U-Turn sampler (the default),
elliptical slice sampling and Pólya-Gamma Gibbs sampling for binomial
models. Tier 2 is the nested Laplace approximation for large datasets,
which integrates the hyperparameters on an outer grid. Tier 3 covers
variational inference and stochastic-gradient MCMC, used only when you
request them through mode. inference_mode_info() lists the backends
and the models each one fits.
Start with Quick Start: one path from simulated trawl data to catch per unit effort, then formula syntax, random effects and model checking.
Questions and bug reports go to the GitHub issues. A bug report is easiest to act on with a small reproducible example, for instance made with reprex.
“Software is like sex: it’s better when it’s free.” — Linus Torvalds
I’m a PhD student who builds R packages in my free time because I believe good tools should be free and open. I started these projects for my own work and figured others might find them useful too.
If this package saved you some time, buying me a coffee is a nice way to say thanks. It helps with my coffee addiction.
tulpaRatio is licensed under MIT.
To cite tulpaRatio in publications, please use:
Colling G (2026). _tulpaRatio: Bayesian Hierarchical Models for Ratios,
Rates, and Proportions_. R package version 1.7.3,
<https://github.com/gcol33/tulpaRatio>.
BibTeX:
@Manual{colling2026tulparatio,
title = {tulpaRatio: Bayesian Hierarchical Models for Ratios, Rates, and Proportions},
author = {Gilles Colling},
year = {2026},
note = {R package version 1.7.3},
url = {https://github.com/gcol33/tulpaRatio},
}