The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
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.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.