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.

Getting started with respondeR

library(respondeR)

The idea

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.

Data format

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):

impute_change_sd("prepost", sd_baseline = 2.1, sd_final = 2.4, r = 0.5)
#> [1] 2.26495

A first 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.

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    NA

summary(res) adds the settings and the per-study weights.

Which way is “better”?

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

Per-study results and a forest plot

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.3826682
plot(res)

Forest plot of per-study responder risk differences with pooled estimates

Random effects and heterogeneity

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

How much does the MID matter?

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.

curve <- responder_curve(sample_responder_data, mids = seq(0, 2, by = 0.25))
plot(curve, at = 1)

Pooled risk difference against the MID threshold with pointwise confidence bands

Summary-of-findings rows

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)

A threshold-free alternative

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.

cles <- responder_cles(sample_responder_data)
sprintf("CLES = %.1f%% (%.1f%% to %.1f%%)",
        100 * cles$cles, 100 * cles$cles_lb, 100 * cles$cles_ub)
#> [1] "CLES = 69.0% (65.1% to 72.7%)"

A real example: VAS pain after exercise therapy

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

Fitted meta-analyses

If you already have a meta::metacont() or metafor::rma() fit, pass it to responder_analysis() directly; see vignette("meta-analysis-objects").

The Shiny application

Everything above is available in a point-and-click app:

launch_responder_analysis()

The same tool runs in the browser, with no installation, at https://choxos.github.io/respondeR/app/.

Where next

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.