skills/relsa-severity-assessment/references/relsa-method.md
RELSA (RELative Severity Assessment) turns several welfare outcome measures into one
interpretable number per animal per time point. It was introduced in Talbot et al. (2022),
Front. Vet. Sci. 9:937711, and implemented in the R package
mytalbot/RELSA (GPL-3). scripts/relsa_score.py is a
Python port of that implementation.
Every variable must be declared as falling or rising under worsening welfare. The default assumption is that a decrease means a worse outcome (body weight, activity, burrowing, wheel running, food intake). Variables that increase are turned: clinical scores, inflammatory biomarkers, fever, tachycardia.
Directionality is model-specific and getting it wrong silently zeroes a variable's contribution, because deviations in the "wrong" direction are floored at 0. Body temperature is the classic trap: it falls in CLP sepsis and endotoxaemia (hypothermia predicts death) and rises in fever models.
build_reference() warns when a variable's only observed deviation runs against its declared
direction, and rejects one that never deviates at all. It cannot do better than that: in the
published sepsis data activity swings 530% above baseline and 100% below, so "which direction
is worse" is not recoverable from the data and has to come from the biology of the model.
Each variable is divided by that animal's own baseline value and expressed as a percentage, so every trajectory starts at 100%:
x_norm(t) = 100 * x(t) / x(baseline)
Using each animal's own baseline is what makes RELSA robust to between-animal variation in
absolute values. The baseline may be one time point (the RELSA convention codes it as
day = -1) or the mean of a baseline window — pass several times to --baseline-time.
Two variable types must not be normalized again:
Variables already expressed as percent change from baseline, such as body weight change
(bwc [%]) in the published datasets.
Ordinal severity scores whose healthy baseline is 0. 0/0 is undefined, so ratio
normalization cannot represent them at all. Use score_to_percent() /
--score-scale COL=MAX, which maps the score's scale instead of its ratio: the healthy
score becomes 100, the worst possible score becomes 200, and one score point is worth
100 / (max - baseline) percent. The variable is then a turned variable like any other.
This mapping is this skill's convention, not something the paper specifies. It is a
choice about how much a score point is worth relative to a percent of body weight, and it
should be stated in the methods. The defensible alternative is to keep the score out of
RELSA entirely and use it as an independent endpoint criterion, which is what the DSS
blood-sampling model in the paper does (its clinical score of 5 is an endpoint trigger,
while RELSA is computed from bwc and wheel running).
The reference set is the cohort assumed to carry the greatest burden in the model, and it fixes the meaning of the scale. For each variable, RELSA records the most extreme normalized value reached anywhere in that cohort:
maxsev_i = min over reference set (or max, for turned variables)
maxdelta_i = |100 - maxsev_i|
The paper uses "the animal in the treatment group suspected to experience the greatest burden under the respective model" — e.g. the highest DSS dose with phlebotomy in the DSS blood sampling dataset.
This is the single most consequential choice in the whole procedure. RELSA is relative:
change the reference set and every score changes. A reference cohort that is too mild pushes
scores above 1; one that is too severe compresses everything toward 0. A score is
meaningless without the reference set it came from, which is why ReferenceModel carries a
label and --save-reference writes it to JSON for reuse on later cohorts.
A variable that never deviates in the reference set has maxdelta = 0, would divide by zero,
and is rejected with an error rather than silently dropped.
delta_i(t) = 100 - x_norm,i(t) (turned: x_norm,i(t) - 100), floored at 0
RW_i(t) = delta_i(t) / maxdelta_i
RELSA(t) = sqrt( (1/n) * sum_i RW_i(t)^2 ) over the n variables measured at t
The root-mean-square, rather than the arithmetic mean, is deliberate: severity is signalled by extremes, so squaring gives a large deviation in one variable more influence than the mean would. A single variable at the reference maximum with three others at baseline gives RELSA = 0.5, not 0.25.
Missing values are dropped from the mean, never imputed and never treated as 0 — treating a missing measurement as "no deviation" would bias every score downward. This is why a score is defined whenever at least one variable was measured.
Interpretation. RELSA = 0 is baseline; 0.73 means the animal reached 73% of the reference set's maximum deviation; above 1 means it exceeded the reference set. The score is dimensionless and comparable within a reference frame, not across reference sets or models.
Because the score averages over whichever variables were measured, a variable that appears
or disappears mid-trajectory moves the score by itself. In the published sepsis dataset,
body weight is recorded only on the day of euthanasia. Include bwc in that model and mouse
ID_801's endpoint score falls from 0.93 to 0.83 — not because the animal improved, but
because a variable with a low weight (0.16) joined the mean at exactly that time point. The
paper's sepsis model uses only the four telemetry parameters, which are present throughout.
Score the variables measured throughout the trajectory; keep the intermittent ones as
separate endpoint criteria. relsa_scores() warns when the composition changes.
relsa_score.py reproduces the R package's own published worked example — the surgery
dataset, animal Ca_001, variables bwc, burON, hr, hrv, temp, act, turned hr, temp — to
the two decimals the package prints: every normalized value, every weight, and the RELSA
scores 0.00, 0.73, 0.55, 0.44, 0.44, 0.41 for days -1 to 4, including the NA weight where
burON is missing. The test suite pins this.
Details worth knowing if you compare against R directly:
round_digits=None /
--full-precision skips it, which changes scores in the third decimal — and, because KDE
minima are sensitive to the granularity of the score distribution, can change the number of
thresholds found. Keep the default when reproducing published work.relsa()'s wf column is not the score. The R function returns both a mean weight
factor (wf) and the root-mean-square (rms); the RELSA score is rms. In the released
package wf divides the weight sum by the count of missing variables rather than the
count of present ones (the vignette has the intended form), and because wf is used to
mask rms, a complete row sitting exactly at baseline is returned as NA instead of 0 by
that code path. The rendered vignette prints 0.00 for the baseline day, so the port
returns 0.0, matching the published output and the formula.set[, 4:ncol] positionally;
this port uses named id / time columns.From Lutscher et al. (2026) and the studies it re-analyses. Use it as a template for declaring your own model, not as a set of defaults to copy.
| Model / intervention | Variables in RELSA | Turned | Humane endpoint criterion |
|---|---|---|---|
| CLP sepsis (telemetry) | hr, hrv, temp, act | none | >25% temperature loss over two consecutive monitoring intervals |
| DSS colitis + restraint stress | hr, hrv, temp, act, bwc | hr, temp | 20% body weight loss |
| DSS colitis + facial vein blood sampling | bwc, vwr (voluntary wheel running) | none | 20% body weight loss or clinical score 5 |
| Pancreatic cancer (6606PDA) | bwc, vwr | none | 20% body weight loss |
| Neurosurgery (intracranial electrode) | bwc, nesting score, Neuro Score (modified Irwin) | nesting, neuro | total clinical score of 7 |
Heart rate, heart rate variability and temperature were averaged per interval; activity was summed. Clinical scoring differed between laboratories and models, so the paper states plainly that clinical scores are not directly comparable across those studies — one of its central caveats about a generalized RELSA scale.