tulpaRatio

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:

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.

Installation

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.

Optional: multithreading

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.

macOS setup

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

Check available threads

After installation, run:

tulpaRatio::ratiod_threads()

This returns the number of OpenMP threads available to the package.

Usage

  1. Fit the model with tratio(). The formula names the numerator, then the denominator after |, then the predictors.
  2. Get the ratio posterior with ratio().
  3. Compare groups on the ratio with 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.

Modelling the counts instead of the quotient

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.

Different predictors for numerator and denominator

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.

Inference backends and tiers

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.

Documentation

Start with Quick Start: one path from simulated trawl data to catch per unit effort, then formula syntax, random effects and model checking.

Getting help

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.

Support

“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.

Buy Me A Coffee

License

tulpaRatio is licensed under MIT.

Citation

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},
}

Download the citation as BibTeX or RIS.