
Visualize interaction effects with publication-ready forest plots.
An interaction p-value tells you whether the effect differs between
subgroups, but not which subgroup shows the effect, in which
direction, or by how much. foresty puts the interaction test and the
subgroup estimates side by side, making interaction effects easier to
interpret and report.
It also makes publication-ready forest plots easy to produce, with a Shiny app for exploring the results interactively and an HTML report that documents the model behind them.


Installation:
# install.packages("devtools")
remotes::install_github("AkiShiroshita/foresty")
Start with a model without an interaction term and let foresty
explore possible exposure–modifier interactions:
library(foresty)
fit <- glm(
asthma ~ no2 + sex + maternal_smoking + maternal_age,
family = binomial,
data = foresty_cohort
)
foresty_app(fit)
The app lets you select exposures and effect modifiers, choose how big a difference in the exposure each estimate is for — an interquartile range, two quantiles of it, an increment of your own — adjust the figure style, and produce forest plots and reports without having to remember the plotting code.
It opens in your browser, and its R code tab writes the code twice
over: the foresty call that drew the figures, and — under How to
calculate effect estimates for each subgroup — where those numbers come
from, in base R and the car package with nothing from this package in
them. So interactive exploration becomes a reproducible script either
way.
foresty provides several predefined styles:
| Style | Description |
|---|---|
"classic" |
Simple forest plot with a null line |
"jama" |
JAMA-inspired layout |
"nejm" |
NEJM-inspired layout |
"lancet" |
Lancet-inspired layout |
"bmj" |
BMJ-inspired layout |
"revman" |
Cochrane RevMan-inspired layout |
Styles are approximations of journal layouts rather than official journal templates.
The output can be customized further with ordinary ggplot2 layers, and
it can be passed to summary(), predict(), broom::tidy(), and
broom::glance(), etc.
foresty works with fitted models that provide coefficients, a
covariance matrix, and a model frame. Tested model classes include:
| Package | Models |
|---|---|
| base R | glm(), lm() |
survival |
coxph(), survreg() |
lme4 |
lmer(), glmer() |
| other | MASS::polr() (logistic), nnet::multinom(), geepack::geeglm() |
The effect measure is inferred from the model where possible, including odds ratios, hazard ratios, risk ratios, incidence rate ratios, and mean differences.
Robust and cluster-robust standard errors are also supported where applicable.
rms fits – lrm(), ols(), cph(), psm(), Glm(), orm() – are
not supported. A variable transformed by rms is a different
thing and is still read: rms::rcs(x, 4) inside a glm() formula is a
spline basis like any other, as are splines::ns(), splines::bs() and
stats::poly().
foresty is intentionally focused on two-way exposure × modifier
interactions.
foresty_combine(), but they are not crossed with each other.foresty estimates subgroup
effects from one model containing the interaction rather than fitting
separate models within each subgroup.foresty has four main functions (see vignette("forest") for
details):
foresty_main() — draws the effect of one or more exposures
from models you have already fitted.foresty_interaction() — the core of the package: it adds the
exposure × modifier interaction to a fitted model, estimates the
exposure effect within each level of the modifier, and tests the
interaction.foresty_combine() — puts an overall estimate and several
subgroup analyses into one publication-ready figure.foresty_data() — draws the same figure from estimates you
already have: a data frame, tibble or data.table with one row per
row of the figure, for numbers that did not come out of a model
foresty can read.Do you want to adjust for multiplicity (e.g., using the false discovery
rate)? Do you want to perform multiple imputation and present pooled,
subgroup-specific estimates? Of course, you can! Prepare the data you
want to present and use foresty_data().
subgroups <- data.frame(
subgroup = c("Overall", "Female", "Male", "Under 35", "35 and over"),
block = c("Overall", "Sex", "Sex", "Maternal age", "Maternal age"),
overall = c(TRUE, FALSE, FALSE, FALSE, FALSE),
estimate = c(1.24, 1.05, 1.48, 1.11, 1.39),
conf.low = c(1.08, 0.86, 1.21, 0.90, 1.14),
conf.high = c(1.42, 1.28, 1.81, 1.37, 1.69),
n = c(4000, 2009, 1991, 1832, 2168),
events = c(802, 327, 475, 341, 461),
p_int = c(NA, 0.012, NA, 0.106, NA)
)
library(foresty)
foresty_data(
subgroups,
label = "subgroup", group = "block", emphasis = "overall",
interaction_p = "p_int",
measure = "OR", outcome = "asthma", adjusted = TRUE
)

Claude Code (Anthropic’s Claude Opus 5) assisted with adding notes, testing and English-language proofreading. The design, decisions and final responsibility remain the author’s.
Subgroup-specific effect sizes are computed through car.
The forest plot designs took their cues from meta.
GPL-3.