Introduction to RASbound

The problem

Classic RAS adjusts a seed matrix x0 so that its row and column sums match target totals. It is also called biproportional matrix balancing, iterative proportional fitting (IPF) or raking.

RASbound adds one thing: each cell can have a lower and an upper bound. This matters when you update or regionalize an input-output table and some cells must stay within a plausible range.

A minimal example

library(RASbound)

x0 <- matrix(c(2, 4, 6, 8), nrow = 2)
row_targets <- c(20, 20)
col_targets <- c(15, 25)
upper <- matrix(c(Inf, Inf, 12, Inf), nrow = 2)  # cap cell [1, 2] at 12

fit <- bras_freeze_sim(x0, row_targets, col_targets, upper = upper)
fit
#> <bras_fit> method: freeze_simultaneous 
#>   converged: TRUE  | feasible: TRUE  | outer passes: 2 
#>   cells fixed at a bound: 1
fit$matrix
#>      [,1] [,2]
#> [1,]    8   12
#> [2,]    7   13
summary(fit)
#>                method converged feasible n_outer_iter n_fixed_cells
#> 1 freeze_simultaneous      TRUE     TRUE            2             1
#>   max_row_margin_error max_col_margin_error
#> 1         8.121948e-09         3.552714e-15

All functions take the same arguments and return the same kind of object (bras_fit), so you can swap one for another and compare the results.

Which function should I use?

Function Bound handling Cells frozen permanently? Diagnoses infeasibility?
ras() none (classic RAS) – –
bras_freeze_sim() fixes all violating cells per pass yes yes
bras_freeze_seq() fixes only the most extreme violating cell per pass yes yes
bras_freeze_hybrid() simultaneous, with sequential fallback yes yes
bras_naive() reclamps every cell every pass (reference) no no
bras_soft() clamps and redistributes by remaining headroom (reference) no no
bras_cap_iter() clamps and rescales uncapped cells uniformly (reference) no no
bras_calibrate_logit() wraps survey::calibrate(calfun = "logit") n/a no
bras_calibrate_linear() wraps survey::calibrate(calfun = "linear") no no
bras_calibrate_raking() wraps survey::calibrate(calfun = "raking") no no

For adjusting real data, use bras_freeze_hybrid(). The rest of this page explains the three groups of functions.

The hard-freeze family

bras_freeze_sim(), bras_freeze_seq() and bras_freeze_hybrid() are the functions to use on real data. They all work in the same way:

  1. Solve the classic RAS problem on the cells that are still free.
  2. Find the free cells that ended up outside [lower, upper].
  3. Fix those cells at the bound they violated.
  4. Subtract the fixed values from the row and column targets.
  5. Repeat on the smaller problem that remains.

A fixed cell is never changed again, and the residual targets are recomputed exactly. So when the process finishes, the row and column totals are matched exactly and no cell is outside its bounds.

The three functions differ in how many cells they fix per pass:

If the bounds cannot be reconciled with the targets, these functions raise an error (on_infeasible = "error"). fit$diagnostics$infeasible_at shows which rows and columns are involved. If a solution probably exists but max_outer ran out, you get a warning and converged = FALSE. In that case, increase max_outer. See ?bras_fit to tell the two cases apart in code.

The reference variants

ras(), bras_naive(), bras_soft() and bras_cap_iter() are there for comparison, not for production use.

These three bounded variants do not guarantee exact margins or respected bounds. They are there so that a simulation study can measure how much that guarantee is worth.

The survey wrappers

bras_calibrate_logit(), bras_calibrate_linear() and bras_calibrate_raking() are thin wrappers around survey::calibrate(). They let you compare against a well-known calibration engine without leaving the RASbound interface. They require the survey package.

They solve the problem in a different way from the freeze family. They optimize the whole matrix in one step, with the Newton-Raphson algorithm of Deville, Saerndal and Sautory (1993). Bounds are enforced as real constraints inside that step. The three wrappers differ only in the distance function they minimize:

All three compute converged from the actual margin error of the returned matrix. See ?bras_calibrate_logit for the meaning of feasible in each case.

There is no wrapper for ipf::rake(). Its code needs a Rust toolchain to install, which is a heavy requirement for one comparator.

Where to go next