Trials of continuous outcomes usually report a mean change and standard deviation in each arm. Those are hard to communicate: a standardized mean difference of 0.3 means little to a patient. A responder analysis translates the continuous result into something concrete: the proportion of patients who improve by at least a minimal important difference (MID). It contrasts the arms on familiar scales: risk difference, risk ratio, odds ratio, number needed to treat.
respondeR does this from summary statistics alone, using the cut-point approach of Anzures-Cabrera, Sarpatwari & Higgins (2011). It never needs individual patient data.
One row per study (or per comparison). The experimental arm columns
end in _e, the control arm in _c:
sample_responder_data
#> study change_e sd_e n_e change_c sd_c n_c
#> 1 Study 1 0.9581395 1.257593 43 0.217777778 1.195501 45
#> 2 Study 2 0.7920863 1.281364 139 0.003448276 1.324629 145
#> 3 Study 3 1.0230769 1.341201 156 -0.041975309 1.263178 162| Column | Meaning |
|---|---|
study |
Study (comparison) label |
change_e, change_c |
Mean change per arm |
sd_e, sd_c |
SD of change per arm |
n_e, n_c |
Sample size per arm |
trial (optional) |
Trial label, shared by the comparisons of one multi-arm trial |
If a trial reports baseline and final SDs instead of the SD of the
change, reconstruct it with impute_change_sd() (and vary
the assumed correlation as a sensitivity analysis):
Suppose a change above 1 is clinically meaningful. With
the default settings, responder_analysis() returns one row
per pooling method, printed as a compact table with the number needed to
treat:
res <- responder_analysis(sample_responder_data, mid = 1)
res
#> Responder analysis (respondeR)
#> MID = 1 (a higher change is a response); k = 3 studies; fixed
#> effects; delta per-study variance
#> Percentages; 95% confidence intervals.
#>
#> Method PE PC RD RR
#> Individual - - 25.5 (20.0 to 31.0) 2.15 (1.79 to 2.58)
#> Weighted mean 47.6 22.1 25.5 (20.0 to 31.1) 2.16 (1.80 to 2.59)
#> Unweighted mean 47.7 22.8 24.9 2.09
#> Median 48.7 21.5 27.2 2.26
#> OR NNT
#> 3.20 (2.45 to 4.18) NNTB 3.9 (3.2 to 5.0)
#> 3.21 (2.46 to 4.19) NNTB 3.9 (3.2 to 5.0)
#> 3.09 NNTB 4.0
#> 3.46 NNTB 3.7
#>
#> Use as.data.frame() for every column, summary() for details, plot() for a forest plot.individual method pools per-study contrasts, so it
has no single pair of pooled proportions (shown as “-”).The result is still a data frame, so columns are available directly,
and as.data.frame() gives every column, including the
heterogeneity statistics:
res$rd
#> [1] 0.2548985 0.2551921 0.2487438 0.2718912
head(as.data.frame(res), 2)
#> method pooling k p_e p_c rd rd_lb rd_ub
#> 1 individual fixed 3 NA NA 0.2548985 0.1997582 0.3100388
#> 2 weighted fixed 3 0.4757294 0.2205372 0.2551921 0.1998579 0.3105264
#> rr rr_lb rr_ub or or_lb or_ub nnt nnt_lb
#> 1 2.148360 1.787463 2.582123 3.197922 2.447367 4.178656 3.923130 3.225403
#> 2 2.157138 1.795354 2.591827 3.207140 2.455182 4.189402 3.918616 3.220338
#> nnt_ub var_rd tau2 i2 q q_p pi_lb pi_ub
#> 1 5.006053 0.0007914838 0.0006313298 0.1925461 2.476921 0.28983 NA NA
#> 2 5.003555 0.0007970618 NA NA NA NA NA NAsummary(res) adds the settings and the per-study
weights.
By default a higher change is a response. For outcomes where
lower is better (pain, symptom scores), set
direction = "lower" and give the MID as the required change
(for example mid = -1.5 for a 1.5-point reduction).
responder_rd_individual() returns the per-study risk
differences, and plot() draws them with the pooled
estimates:
responder_rd_individual(sample_responder_data, mid = 1)
#> study p_e p_c rd se ci_lb ci_ub
#> 1 Study 1 0.4867232 0.2564577 0.2302655 0.08067806 0.07213941 0.3883916
#> 2 Study 2 0.4355507 0.2259278 0.2096229 0.04394149 0.12349918 0.2957466
#> 3 Study 3 0.5068639 0.2047187 0.3021452 0.04108392 0.22162219 0.3826682When studies disagree, use random effects. With few studies, prefer
the Hartung-Knapp-Sidik-Jonkman interval with its ad hoc floor
(ci_method = "hksj_adhoc"), which is never narrower than
the Wald interval:
responder_analysis(sample_responder_data, mid = 1, method = "individual",
pooling = "random", ci_method = "hksj_adhoc")
#> Responder analysis (respondeR)
#> MID = 1 (a higher change is a response); k = 3 studies; random
#> effects; delta per-study variance
#> Percentages; 95% confidence intervals.
#>
#> Method PE PC RD RR OR
#> Individual - - 25.4 (11.5 to 39.3) 2.15 (1.43 to 3.22) 3.19 (1.72 to 5.91)
#> NNT
#> NNTB 3.9 (2.5 to 8.7)
#>
#> individual: I-squared 19%, tau-squared 0.000631, Q 2.48 (p 0.290); prediction interval -26.5 to 77.4
#>
#> Use as.data.frame() for every column, summary() for details, plot() for a forest plot.responder_curve() repeats the analysis across
thresholds. Report the curve, not just one value; open circles mark
thresholds where a single study would hold most of the weight.
responder_sof() turns the pooled relative effect into
absolute effects per 1000 patients at an assumed control risk, by
default the median control-arm responder risk, in GRADE wording. Give
several control risks for low, moderate and high baseline risk:
ind <- responder_analysis(sample_responder_data, mid = 1, method = "individual")
responder_sof(ind)
#> Summary of findings (responders)
#>
#> Method Control risk Intervention risk
#> individual 226 per 1000 483 per 1000 (417 to 549)
#> Difference Relative effect
#> 257 more per 1000 (191 more to 324 more) OR 3.20 (2.45 to 4.18)
responder_sof(ind, control_risk = c(0.1, 0.4))
#> Summary of findings (responders)
#>
#> Method Control risk Intervention risk
#> individual 100 per 1000 262 per 1000 (214 to 317)
#> individual 400 per 1000 681 per 1000 (620 to 736)
#> Difference Relative effect
#> 162 more per 1000 (114 more to 217 more) OR 3.20 (2.45 to 4.18)
#> 281 more per 1000 (220 more to 336 more) OR 3.20 (2.45 to 4.18)Choosing a MID is sometimes contentious. The common-language effect size sidesteps it: the probability that a randomly chosen treated patient has a better change than a randomly chosen control.
The package bundles a real dataset, vas_pain: the 20
exercise-versus-control comparisons pooled for the visual analogue scale
(VAS) pain outcome by Li, Bao, Wang and Zhao (2025). Change scores are
post minus baseline VAS on a 0 to 10 cm scale, so a more negative value
is a larger pain reduction. Two of the trials had three arms and share
one control group between two comparisons; the trial column
marks them and respondeR splits each shared control arm so it is not
counted twice. Using a 1.5 cm reduction as the MID:
vas <- responder_analysis(vas_pain, mid = -1.5, direction = "lower",
pooling = "random", ci_method = "hksj_adhoc")
#> Split the shared control arm of multi-arm trial(s) Farzaneh 2022, Peng Qiu 2014 across their comparisons (control n divided equally) to avoid double counting. This message is shown once per session.
vas
#> Responder analysis (respondeR)
#> MID = -1.5 (a lower change is a response); k = 20 studies; random
#> effects; delta per-study variance
#> Percentages; 95% confidence intervals.
#>
#> Method PE PC RD RR
#> Individual - - 17.5 (10.8 to 24.3) 1.20 (1.09 to 1.33)
#> Weighted mean 79.2 52.3 26.9 (18.9 to 34.8) 1.51 (1.25 to 1.83)
#> Unweighted mean 78.7 47.1 31.6 1.67
#> Median 83.9 37.3 46.7 2.25
#> OR NNT
#> 3.90 (2.54 to 5.99) NNTB 5.7 (4.1 to 9.3)
#> 3.47 (2.44 to 4.92) NNTB 3.7 (2.9 to 5.3)
#> 4.14 NNTB 3.2
#> 8.81 NNTB 2.1
#>
#> individual: I-squared 62%, tau-squared 0.00712, Q 49.67 (p 0.000); prediction interval -1.5 to 36.5
#>
#> Use as.data.frame() for every column, summary() for details, plot() for a forest plot.Pooling the per-study estimates (the individual method,
which keeps every comparison within its randomized trial), about 18 more
exercise patients per 100 reach a 1.5 cm pain reduction than controls.
The descriptive unweighted and median
summaries are larger and more dispersed; that spread reflects the
heterogeneity across trials (I-squared above 50 percent), and they carry
no interval. In summary-of-findings form:
responder_sof(vas[vas$method == "individual", ])
#> Summary of findings (responders)
#>
#> Method Control risk Intervention risk
#> individual 418 per 1000 737 per 1000 (646 to 812)
#> Difference Relative effect
#> 319 more per 1000 (228 more to 393 more) OR 3.90 (2.54 to 5.99)and without any threshold:
cles <- responder_cles(vas_pain, direction = "lower", pooling = "random")
sprintf("A treated patient has less pain than a control %.0f%% of the time (%.0f%% to %.0f%%)",
100 * cles$cles, 100 * cles$cles_lb, 100 * cles$cles_ub)
#> [1] "A treated patient has less pain than a control 73% of the time (67% to 78%)"(Data from Li et al. (2025), Frontiers in Sports and Active Living, doi:10.3389/fspor.2025.1614906, Figure 3, reproduced under CC BY 4.0.)
If you already have a meta::metacont() or
metafor::rma() fit, pass it to
responder_analysis() directly; see
vignette("meta-analysis-objects").
Everything above is available in a point-and-click app:
The same tool runs in the browser, with no installation, at https://choxos.github.io/respondeR/app/.
See vignette("methodology") for the full statistical
detail: each method’s estimator and variance, the relative measures, the
SMD bridge, the interval options, assumptions and a method-choice
guide.