| Title: | Forest Plots and Subgroup Effects from Fitted Regression Models |
| Version: | 0.1.0 |
| Description: | Draws forest plots of exposure effects from fitted regression models. Name an exposure and 'foresty' plots its effect. Name an effect modifier as well and it refits the model with the interaction term, estimates the exposure effect within each level of the modifier as a linear combination of the coefficients, and reports the joint interaction test beside those estimates. It takes one exposure and one modifier at a time, so the interaction is always a two-way one. Rows of the plot and of the table beside it share one scale, in a layout that can follow a journal's house style. The same results go to a self-contained HTML page holding the subgroup estimates, the joint test and the coefficient table. The 'car' package computes the linear combinations and their tests. Models fitted by stats::glm(), stats::lm(), the 'survival' package, the 'lme4' package and the 'geepack' package are supported, as is any fit supplying coef() and vcov(). Ordinal outcomes are supported through the 'MASS' package and nominal ones through the 'nnet' package, where the figure carries one row per level of the outcome and the interaction is tested jointly across the equations. Fits from the 'rms' package are refused, naming the function that fits the same model in their place. The estimation of an exposure effect within a level of a modifier, and the test of the difference between such estimates, follow Altman and Bland (2003) <doi:10.1136/bmj.326.7382.219> and VanderWeele and Knol (2014) <doi:10.1515/em-2013-0005>; the reporting of subgroup effects beside the interaction test follows Wang et al. (2007) <doi:10.1056/NEJMsr077003>, and the figure itself the forest plot described by Lewis and Clarke (2001) <doi:10.1136/bmj.322.7300.1479>. |
| License: | GPL-3 |
| URL: | https://github.com/AkiShiroshita/foresty, https://akishiroshita.github.io/foresty/ |
| BugReports: | https://github.com/AkiShiroshita/foresty/issues |
| Encoding: | UTF-8 |
| Language: | en-GB |
| Depends: | R (≥ 4.1) |
| Imports: | car (≥ 3.1.0), checkmate (≥ 2.1.0), ggplot2 (≥ 3.4.0), grDevices, grid, patchwork (≥ 1.1.0), scales (≥ 1.2.0), stats, utils |
| Suggests: | base64enc, broom (≥ 1.0.0), bslib (≥ 0.5.0), data.table, geepack, gt (≥ 0.9.0), Hmisc, knitr, lme4, MASS, nnet, ragg, rmarkdown, rms, sandwich (≥ 3.0.0), shiny (≥ 1.7.0), survival (≥ 3.2.0), svglite, tibble, testthat (≥ 3.0.0), zip |
| VignetteBuilder: | knitr |
| LazyData: | true |
| Config/testthat/edition: | 3 |
| Config/roxygen2/version: | 8.0.0 |
| NeedsCompilation: | no |
| Packaged: | 2026-09-03 16:56:17 UTC; shiroa1 |
| Author: | Akihiro Shiroshita [aut, cre, cph], Yuki Kataoka [aut] |
| Maintainer: | Akihiro Shiroshita <akihirokun8@gmail.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-09-12 14:40:22 UTC |
foresty: Forest Plots and Subgroup Effects from Fitted Regression Models
Description
Draws forest plots of exposure effects from fitted regression models. Name an exposure and 'foresty' plots its effect. Name an effect modifier as well and it refits the model with the interaction term, estimates the exposure effect within each level of the modifier as a linear combination of the coefficients, and reports the joint interaction test beside those estimates. It takes one exposure and one modifier at a time, so the interaction is always a two-way one. Rows of the plot and of the table beside it share one scale, in a layout that can follow a journal's house style. The same results go to a self-contained HTML page holding the subgroup estimates, the joint test and the coefficient table. The 'car' package computes the linear combinations and their tests. Models fitted by stats::glm(), stats::lm(), the 'survival' package, the 'lme4' package and the 'geepack' package are supported, as is any fit supplying coef() and vcov(). Ordinal outcomes are supported through the 'MASS' package and nominal ones through the 'nnet' package, where the figure carries one row per level of the outcome and the interaction is tested jointly across the equations. Fits from the 'rms' package are refused, naming the function that fits the same model in their place. The estimation of an exposure effect within a level of a modifier, and the test of the difference between such estimates, follow Altman and Bland (2003) doi:10.1136/bmj.326.7382.219 and VanderWeele and Knol (2014) doi:10.1515/em-2013-0005; the reporting of subgroup effects beside the interaction test follows Wang et al. (2007) doi:10.1056/NEJMsr077003, and the figure itself the forest plot described by Lewis and Clarke (2001) doi:10.1136/bmj.322.7300.1479.
Author(s)
Maintainer: Akihiro Shiroshita akihirokun8@gmail.com [copyright holder]
Authors:
Akihiro Shiroshita akihirokun8@gmail.com [copyright holder]
Yuki Kataoka youkiti@gmail.com
See Also
Useful links:
Report bugs at https://github.com/AkiShiroshita/foresty/issues
Model methods for foresty figures
Description
These pass through to the model the figure was drawn from, so that a foresty result can be used wherever the fit itself would have been.
Usage
## S3 method for class 'foresty'
predict(object, ..., model = NULL)
## S3 method for class 'foresty'
coef(object, ..., model = NULL)
## S3 method for class 'foresty'
vcov(object, ..., model = NULL)
## S3 method for class 'foresty'
formula(x, ..., model = NULL)
## S3 method for class 'foresty'
nobs(object, ..., model = NULL)
## S3 method for class 'foresty'
model.frame(formula, ..., model = NULL)
Arguments
object, x, formula |
A |
... |
Passed on to the method for the underlying fit. |
model |
Which model is meant, when the figure covers several. |
Value
Whatever the corresponding method for the underlying model returns.
Examples
fit <- glm(asthma ~ no2 + sex, family = binomial, data = foresty_cohort)
x <- foresty_interaction(fit, exposure = "no2", interaction = "sex")
head(predict(x, type = "response"))
formula(x)
nobs(x)
Turn a foresty figure into a data frame
Description
tidy() returns the estimates the figure was drawn from, one row apiece, in
the columns broom uses and in the order it puts them, so the result reads
beside a tidied model and drops into the same pipelines. glance() returns
one row describing the fit.
Usage
## S3 method for class 'foresty'
tidy(
x,
what = c("estimates", "coefficients"),
conf.int = TRUE,
model = NULL,
...
)
## S3 method for class 'foresty'
glance(x, model = NULL, ...)
## S3 method for class 'foresty'
as.data.frame(x, row.names = NULL, optional = FALSE, ...)
Arguments
x |
A |
what |
|
conf.int |
Whether to include the confidence interval. Defaults to
|
model |
Which model to take the coefficients from, when the figure
covers several. |
... |
Ignored. |
row.names, optional |
Ignored, present for consistency with the generic. |
Details
as.data.frame() returns everything the figure carries instead, the display
labels included, which is what to use when the estimates are going back into
a plot rather than into a table. It is the one of the three that needs
nothing installed.
tidy() and glance() are broom's generics, and foresty registers its
methods on them rather than carrying broom itself: install it if you want
them, and call them as broom::tidy(x) or after library(broom).
Value
A data frame.
Examples
fit <- glm(asthma ~ no2 + sex + maternal_age, family = binomial,
data = foresty_cohort)
x <- foresty_interaction(fit, exposure = "no2", interaction = "sex")
as.data.frame(x)
if (requireNamespace("broom", quietly = TRUE)) {
broom::tidy(x)
broom::glance(x)
}
Build a forest plot by clicking
Description
Opens a Shiny app on a model you have already fitted, so that the exposures,
the modifiers and the layout can be chosen from menus rather than typed,
retyped and re-run. Every figure the app draws is drawn by
foresty_interaction(), foresty_main() or foresty_combine(), and the
code that drew it is shown in a tab of its own for copying back into your
script.
Usage
foresty_app(
fit,
measure = NULL,
launch = interactive(),
launch.browser = TRUE,
...
)
Arguments
fit |
A fitted model, as passed to |
measure |
The effect measure, as in |
launch |
Whether to start the app. |
launch.browser |
Passed to |
... |
Passed to |
Value
The Shiny app object, invisibly when the app was run.
What the app does not do
It does not fit your model. The estimates a forest plot reports come out of
the coefficients and the covariance matrix of a fit, so the model has to
exist before there is anything to draw, and which model to fit is the part of
the analysis that should be written down in a script rather than clicked
together and forgotten. The app is for the part that is fiddling –
foresty_layout() alone has some thirty arguments, and finding the figure a
journal wants by editing a call and re-running it is slow.
It does not change the type of your variables either. A modifier stored as a
number but taking only two values is drawn as the two subgroups it is,
needing no change; one taking three or more is refused, and the refusal is
shown where the figure would have been, because turning it into a factor
means fitting a different model and that is a decision to make in the script,
not a checkbox. See the interaction argument of foresty_interaction().
More than one pair
The exposure and the modifier menus both take as many variables as you
select, and what is drawn is every pair of them: three exposures and two
modifiers are six figures. The overall effect, which comes from the model
with no interaction term in it, is a checkbox of its own, so it can be drawn
beside the subgroups rather than instead of them. Where more than one figure
is drawn the app offers to combine them, which is foresty_combine() and
follows its rules: figures of one exposure become the blocks of one figure,
and figures of different exposures stay apart, since rows reporting the
effects of different exposures are not read against each other.
How big a difference in the exposure each estimate is for – one unit, an
interquartile range, an increment you name, its two ends either way round,
the first to the third quantile, two values you name – is asked once for each
continuous exposure rather than once for all of them, since two exposures
rarely want the same answer. The lowest and highest values it takes are
written under the choice, and are where naming two values starts from.
Whichever was chosen is written on the figure wherever the exposure is
named, one unit included – "no2 (per 1)", "no2 (per 10)",
"no2 (per IQR, 8.44)" – since a reader comparing two figures across a
screen should not have to know that a row saying nothing means one.
Which way round the comparison runs is written with an arrow rather than as
"from ... to ...", since that is the part a reader gets wrong: Lowest value -> Highest value is the effect of being at the top of the exposure rather
than the bottom, and Reverse the direction turns it into the same
estimate the other way up, which is how a protective effect is drawn as one.
The two boxes that name two values are headed the same way, and so is the
figure: two values compared are written beside the exposure as
"no2 (4.102 -> 38.72)" rather than as "38.72 vs 4.102", which says which
two values were compared without saying which of them the estimate is the
effect of moving to.
From one quantile to another is not the interquartile range, though it
begins at the quartiles: the range is a width, and an effect per one of it
is a step along a slope, whereas the quantiles are two values of the
exposure and are compared as at = c(from, to). So it can be asked of a
splined exposure, which has no single slope to report a width per, and the
two probabilities can be set to anything – the 10th against the 90th, the
median against the top decile – rather than only 0.25 and 0.75.
One reference group instead of one per subgroup
A subgroup figure reads each subgroup against its own reference level, so the rows of one subgroup are not comparisons with the rows of another. Where the exposure and the modifier both have levels there is a second question: what every combination of the two comes to against one of them, which is the table of groups against a baseline group a paper reports. Model asks it once per effect modifier chosen – Compare every combination of the exposure and ... with one group – and the menus under the box say which combination the rest are read against: a level of the modifier, and a level of each exposure that has levels.
Every row is then that whole cell – this level of the exposure, this level
of the modifier – against the one chosen, all of it out of the same
interaction model, and the cell chosen is drawn as the reference it is. The
p-value beside the rows is the same joint test of the interaction either way:
it asks whether the effect of the exposure depends on the modifier, and it is
not a test of the rows. See the reference argument of
foresty_interaction().
A continuous exposure has no levels to combine, so a pair with one is drawn as it always was whatever the box says: its rows are the effect of a difference in the exposure rather than groups of people.
Figure style names
The rows, the axis and the columns of a figure are named after columns of a
data set, and a column is called what the data set calls it. Figure style
renames the outcome – which is otherwise taken from the left of your
formula, asthma_ever_dx and all – replaces the label under the plot
outright, and names each selected exposure. A model carrying person-time is
also asked what unit to count it in – any number, 1,000 and 100,000 being
how a paper usually reports a rate and six figures beside the plot being
width the plot could have had.
A model too slow to redraw
Every change to a control refits the model once per pair of menus, and while that is happening the app looks exactly as it did before, so a line saying that it is working is shown above the tabs and the figure on the screen is the previous one until it goes.
On a model where that wait is not seconds but minutes, Write the R code only stops it: nothing is fitted and nothing is drawn, the menus and the style controls go on working, and the R code tab goes on writing the call they come to, which is what to paste into the script and run once. The other tabs report on models the app fitted and say so instead.
Colors and the panel
A figure is drawn in one color, which is asked for per exposure and can be
named as a place in a ColorBrewer palette – the third color of Dark2 –
or typed as a hex code, which is how a figure is drawn in the color a
journal or a slide deck already uses. What the colors change with draws
it in more than one instead: a color per category of the rows, which is the
levels of a categorical exposure or the subgroups of the modifier, or a
color per row. Those come from a palette, or from hex codes typed beside it
– #1B9E77, #D95F02 – which is the same list colors takes. See
color_by in foresty_layout().
The tabs
Plot draws them, each figure carrying the exposure it is of under its
title, which is a line that can be turned off on its own where the title
says it already. A row of a forest plot is usually half of a comparison, and
the columns of counts carry the other half beside it: a row of suburban
children reads 822 vs 468, those being the 822 the row is of and the 468
rural ones its odds ratio was estimated against. A line under the figure
says which two groups they are, and for a multinomial fit that the estimate
did not come out of the two of them alone. Nothing is said, and nothing is
paired, on a figure whose rows compare no two groups of people – a
continuous exposure and a binary outcome. Count each row's own group only
goes back to one number a row. The line is on the screen rather than in the
figure, so what the download buttons write is unchanged. See What the
counts beside the rows count in foresty_main(). Models writes out, in R, how each figure was arrived at: the term
added to your model, the linear combination each subgroup estimate is, and
the test reported beside them. That code is meant to be run: it is the call
foresty made, with the same design matrix, the same coefficients and
covariance, the same degrees of freedom and the same test, so pasting it
beside the model reproduces the numbers on the figure rather than
approximating them. summary(fit) is the summary of the model you fitted
and of every model the app fitted from it by adding an interaction term.
Both tabs head each block with the outcome, the exposure and the effect
modifier the figure under it is of, since a coefficient table says none of
them. R code is the code twice over. With foresty is the code that
drew what is beside it, deparsed rather than reconstructed, so it cannot
drift from what you are looking at; several pairs are written as a loop over
the pairs rather than as one call apiece. It names the model with the name
you passed to foresty_app(), so pasting it into a script next to that
model works as it stands. How to calculate effect estimates for each
subgroup is where those numbers come from, in base R and the car package
with nothing from this package in them: the interaction term, the linear
combination of the coefficients each subgroup estimate is, and the joint
test reported beside them. It is there for a reader deciding whether to take
the dependency at all, and for one who would rather see what is being done
on their behalf than take it on trust. It comes to the estimates on the Plot
tab exactly – the same design matrix, the same coefficients and covariance,
the same degrees of freedom and the same test – and draws nothing: drawing
them is what the call above it is for. A multinomial fit is written out too:
it holds one equation for each level of the outcome other than the one it
was fitted against, so an estimate is a difference of blocks of coefficients
rather than a difference of two rows of the design, and the script does that
placing where it does the rest of the arithmetic.
What comes out
The figure downloads as PNG and as SVG, at the size and resolution set beside
the buttons; where the pairs are not being combined, one file per figure
comes down in a zip. The reports do the same, one HTML page per model. The
objects themselves download as an .rds file holding a named list – one
element per pair, plus the combined figure where there is one – so that a
session that ended in the app can go on in a script: figures <- readRDS("...") and then summary(figures[[1]]),
as.data.frame(figures[[1]]) or foresty_report(figures[[1]], "x.html").
See Also
foresty_interaction(), foresty_main(), foresty_combine(),
foresty_layout().
Examples
fit <- glm(asthma ~ no2 + sex + maternal_smoking + maternal_age,
family = binomial, data = foresty_cohort)
# Opens the app in an interactive session.
if (interactive()) {
foresty_app(fit)
}
A simulated birth cohort
Description
Four thousand simulated children with an air pollution exposure in infancy, asthma by school age, and a time to first wheeze episode. The data are made up, and are here so that the examples run without a real cohort.
Usage
foresty_cohort
Format
A data frame with 4,000 rows and 13 columns:
- asthma
Asthma by school age, 1 or 0.
- asthma_severity
Asthma severity by school age, an ordered factor:
None<Mild<Moderate<Severe. Drawn on its own rather than fromasthma.- wheeze
The event indicator for
followup_years: 1 where a first wheeze episode was seen, 0 where the child was censored without one.- wheeze_phenotype
Wheeze phenotype, an unordered factor:
None,TransientorPersistent.- followup_years
Years to the wheeze episode or to censoring.
- no2
Nitrogen dioxide during infancy, in parts per billion.
- black_carbon
Black carbon during infancy, in micrograms per cubic metre.
- sex
Child's sex,
FemaleorMale.- maternal_smoking
Smoking during pregnancy,
NoorYes.- maternal_asthma
Maternal history of asthma,
NoorYes.- maternal_age
Maternal age at delivery, in years.
- birth_year
Year of birth, 2005 to 2014, as a factor.
- urbanicity
Rural,SuburbanorUrban. Exposure rises with it, so it confounds the comparison.
Details
The exposure effect was simulated to be about twice as large in boys as in girls, and to be the same whether or not the mother smoked, although maternal smoking raises the risk of asthma on its own. The two modifiers therefore show what a real interaction and an absent one look like when the subgroup estimates are drawn beside the joint test.
The outcome comes in four shapes, so that the same interaction can be
followed through the model classes that carry it: binary (asthma),
time to event (wheeze with followup_years), ordinal
(asthma_severity) and nominal (wheeze_phenotype). The ordinal outcome
was drawn from a latent logistic variable cut at three fixed thresholds, so
it is a proportional odds model by construction; the nominal one was drawn
from two multinomial logits whose exposure effects differ, so its levels
cannot be collapsed into an ordering.
The four are four separate draws, sharing the covariates and the same
exposure effect rather than describing one event four ways: each is there
to be the outcome of a model, and a child may be an asthma case in one of
them and not in another. So asthma is not the asthma_severity of the
same child collapsed to two levels, and cross-tabulating one outcome
against another says nothing about anything. Fit them one at a time.
Source
Simulated by data-raw/foresty_cohort.R.
The qualitative palettes a figure can be colored from
Description
The ColorBrewer qualitative palettes, as RColorBrewer::brewer.pal() gives
them, so that color and colors in foresty_layout() can be answered
by naming a palette and a place in it rather than by pasting a hex code.
The values are the palettes' own and are held here so that the package
needs nothing installed to draw in them.
Usage
foresty_colors(palette = c("Dark2", "Set1", "Set2"), n = NULL, start = 1)
Arguments
palette |
Name of the palette: |
n |
How many colors to return. |
start |
Which color of the palette to begin at, counting from 1. The palette is rotated rather than trimmed, so that every color is still available after it. |
Value
A character vector of colors.
See Also
foresty_layout(), whose color and colors arguments these
are for.
Examples
foresty_colors("Dark2")
# The third color of Dark2, for one figure drawn in one color.
foresty_colors("Dark2")[3]
# A palette beginning there, for a figure whose rows are colored by their
# category.
foresty_colors("Dark2", start = 3)
Combine several foresty figures into one forest plot
Description
Draws the rows of figures already made – an overall effect from
foresty_main(), the same effect within the levels of sex from
foresty_interaction(), within the levels of ethnicity from another – as
one figure, each of them a block of rows under a heading of its own. This is
the subgroup panel a paper prints: the overall estimate at the top, the
subgroup analyses under it, and the interaction p-value beside each block.
Usage
foresty_combine(
...,
emphasize = "auto",
outcome = NULL,
table = TRUE,
columns = NULL,
person_time = NULL,
layout = NULL,
title = NULL,
subtitle = NULL,
xlab = NULL
)
Arguments
... |
Figures returned by |
emphasize |
Which blocks are drawn as the figure's summary rather than
as another subgroup: |
outcome |
What to call the outcome, as |
table |
Whether to draw the table of numbers beside the plot. Defaults
to |
columns |
Which columns the table carries, from |
person_time |
The unit person-time is reported in. |
layout |
How the figure is drawn: the name of a style, as
|
title |
Plot title. The default names the measure, the outcome it is a
measure of and the exposure the figure reports, as |
subtitle |
Plot subtitle. |
xlab |
The label under the plot, which by default names the measure and
the outcome, as |
Details
Nothing is refitted and nothing is re-estimated. Each figure keeps the
estimates it was drawn from, so a block means exactly what it meant on its
own figure, and the estimates behind the combined one are still reached with
summary(), tidy() and as.data.frame().
The figures have to agree on what they are measuring: the same effect measure and the same confidence level, since the rows are read against one axis. They do not have to come from the same model, and usually will not, each subgroup analysis being its own interaction model.
Value
A ggplot2 object, of class foresty, carrying every estimate
drawn on it – or, where the figures combined report more than one
exposure, a list of one such object per exposure, named for them and of
class foresty_figures. See More than one exposure.
More than one exposure
Every row of a combined figure is read against the rows above it, and rows reporting the effect of different exposures are not comparable that way, however alike their axes happen to be. So the figures handed in are sorted by the exposure they report and one figure is drawn for each: NO2 overall and within every subgroup on the first, black carbon overall and within every subgroup on the second, each titled with its own exposure.
A call covering one exposure – which is the usual one – returns that
figure. A call covering several returns the figures in a list, named for the
exposures, which draws them one after another when it is printed and holds
foresty figures that are used singly as any other is:
figures <- foresty_combine(Overall = overall, Sex = by_sex)
figures # draws each in turn
figures[["NO2"]] # one of them
ggplot2::ggsave("no2.png", figures[["NO2"]])
Naming the blocks
A named argument names its block. An unnamed one is named for itself: a
foresty_interaction() figure by its modifier, a foresty_main() figure
"Overall".
A block holding a single row – the overall estimate, most often – is drawn as one row carrying the block's name, rather than as a heading with a single row indented under it.
Singling out the overall estimate
The overall estimate is not one of the subgroups; it is what the subgroups are read against, and a figure that draws it as another row of the same kind invites a reader to compare it with them as though it were one. It is therefore drawn apart from them: a filled diamond on its interval, half again the size of the squares under it, its label in bold, and a rule between it and the subgroups, drawn whether or not the style rules between subgroups. The interval is a plain line, as every other interval on the figure is, so the whole figure is read the same way and the diamond says which row is the summary.
emphasize says which blocks are drawn that way. "auto", the default,
takes the blocks that came from foresty_main(), which are the ones with no
modifier behind them; a figure of nothing but those has none singled out,
since every row would be. Name blocks to choose them yourself, as
emphasize = c("Overall", "Pooled"), and pass NULL to draw every block
alike. How they are drawn is set in foresty_layout(), through
emphasis_shape, emphasis_height, emphasis_face and emphasis_gap.
What the counts beside the rows count
The table beside the plot reports the number of people behind each row and, where the outcome is an event, how many of them had it. Both are counted over the rows the model was fitted to and not over the data frame it was fitted from, so anyone missing the outcome, the exposure or any covariate in the formula is left out of them – which is the same complete-case set the estimates come from, and is what makes the two agree. A column that is not in the model does not affect them however much of it is missing.
A row of a forest plot is usually half of a comparison, so by default the
other half is beside it: a row of suburban children reads 822 vs 468,
those being the 822 the row is of and the 468 rural ones its odds ratio was
estimated against. counts = "row" in foresty_layout() holds the row's
own group alone instead. What the two groups are depends on what the row
compares:
A continuous exposure compares no two groups of people – its row is a step along a slope – so the count is the one group the row is of: every row of the model's data in a
foresty_main()figure, everyone in the subgroup in aforesty_interaction()one.A categorical exposure compares one of its levels with the level the contrast is from, so a row carries the two:
822 vs 468people and125 vs 59events. The reference row is the group the others are compared with rather than a comparison of its own, so it carries one number.A multinomial fit (
nnet::multinom()) of an exposure with no levels compares two levels of the outcome, and the two numbers are how many people were at each: a "Transient vs None" row of women reads637 vs 1,050. That is the only pair of counts such a row has – the events column would repeat it – so it goes under the sizes and the events column is left off. Of a categorical exposure, the row compares the exposure's levels within one comparison of the outcome, so the sizes are the two exposure groups whatever their outcome and the events are the people at the row's outcome level within each.An ordinal fit (
MASS::polr()) has one set of coefficients for the whole outcome, so its rows carry sizes and no count of events.
A multinomial estimate is worth one caution the counts cannot give: it did
not come out of the two groups beside it alone. All the equations are fitted
over the whole outcome at once, so the people at the levels a row is not
about bear on it too, and a row reading 637 vs 1,050 is not the logistic
regression of those 1,687.
The figures whose rows compare two groups say what the counts are under the
plot in foresty_app() and under the table of estimates in
foresty_report(); the ones that do not say nothing.
Adjusting the figure
The result is a ggplot2 object, so layers, scales and themes are added to
it as usual, and + always reaches the forest:
foresty_main(list(fit), "no2") + ggplot2::coord_cartesian(xlim = c(0.8, 2)) foresty_main(list(fit), "no2") + ggplot2::theme_minimal(base_size = 14)
& reaches every panel of a figure that has more than one, which is worth
knowing about but rarely what you want: a scale or a coordinate system
applied to the table of numbers beside the plot will spoil it. Use +.
See Also
foresty_main(), foresty_interaction(), foresty_layout().
Examples
fit <- glm(asthma ~ no2 + sex + maternal_smoking + maternal_age,
family = binomial, data = foresty_cohort)
overall <- foresty_main(list(fit), exposure = "no2",
labels = c(no2 = "NO2"))
by_sex <- foresty_interaction(fit, exposure = "no2", interaction = "sex")
by_smoking <- foresty_interaction(fit, exposure = "no2",
interaction = "maternal_smoking")
foresty_combine(Overall = overall, Sex = by_sex,
`Maternal smoking` = by_smoking)
# The rest of the ways the blocks can be combined, drawn one after
# another. Each is quick; there are simply several of them.
# In the layout of a journal, the interaction p-value written once against
# each block, and without the numbers beside the plot.
foresty_combine(overall, by_sex, by_smoking, layout = "jama")
foresty_combine(overall, by_sex, by_smoking, table = FALSE)
# Every block drawn alike, the overall estimate included.
foresty_combine(Overall = overall, Sex = by_sex, emphasize = NULL)
# Two exposures are two figures, one apiece, named for them.
fit_bc <- glm(asthma ~ black_carbon + sex + maternal_smoking + maternal_age,
family = binomial, data = foresty_cohort)
figures <- foresty_combine(
Overall = foresty_main(list(fit, fit_bc),
exposure = c(NO2 = "no2",
`Black carbon` = "black_carbon")),
Sex = by_sex,
`Black carbon by sex` = foresty_interaction(fit_bc, "black_carbon", "sex")
)
names(figures)
figures[["NO2"]]
Draw a forest plot and its table from a data frame of estimates
Description
Takes the estimates you already have – one row per row of the figure, with
the effect and the two ends of its interval – and draws the figure that
foresty_app() draws: the forest plot and, beside it, the table of numbers,
aligned row for row, in any of the journal styles of foresty_layout().
Usage
foresty_data(
data,
estimate = NULL,
conf.low = NULL,
conf.high = NULL,
label = NULL,
group = NULL,
n = NULL,
events = NULL,
person_time = NULL,
p = NULL,
interaction_p = NULL,
reference = NULL,
emphasis = NULL,
measure = "OR",
outcome = NULL,
ratio = NULL,
adjusted = FALSE,
ci_level = 0.95,
table = TRUE,
columns = NULL,
person_time_unit = NULL,
layout = NULL,
label_header = NULL,
title = NULL,
subtitle = NULL,
xlab = NULL
)
Arguments
data |
The estimates, one row per row of the figure. A data frame, a
|
estimate, conf.low, conf.high |
The columns holding the effect and the
two ends of its interval. |
label |
The column holding what each row is called. |
group |
The column that blocks the rows into subgroups, or |
n, events, person_time |
The columns of counts to write in the table beside the plot, where the data carries them. |
p |
The column of p-values for the rows themselves. A figure carrying a
test of the interaction leaves this column off unless |
interaction_p |
The column of p-values for the interaction, written once per block. |
reference |
A logical column marking rows that are a definition rather
than an estimate – the reference level of a categorical exposure – which
are drawn without an interval and written "1.00 (reference)". |
emphasis |
A logical column marking the rows drawn for emphasis. |
measure |
What the estimates are, as one of |
outcome |
What the measure is a measure of, named so that the axis reads "Odds ratio for incident asthma" rather than "Odds ratio". |
ratio |
Whether the estimates are ratios, drawn about 1 on a scale
where the null is 1, rather than differences drawn about 0. |
adjusted |
Whether the estimates are adjusted, which is a word the axis
and the heading of the table say if so. |
ci_level |
The confidence level the intervals were formed at, which is what the heading of the table reports. It is not used to compute anything: the interval is the one in the data. |
table |
Whether to draw the table of numbers beside the plot. |
columns |
Which columns of that table to draw, and in what order. See
|
person_time_unit |
The unit person-time is reported in, as
|
layout |
The style of the figure, as a name or a |
label_header |
What to write over the column of row labels. |
title, subtitle, xlab |
The title over the figure, the line under it, and
the label under the plot. |
Details
This is the entry point for numbers that did not come out of a model this
package can read. A meta-analysis, a table being redrawn from a paper, a
model fitted by something foresty does not support, a set of results typed
out by hand: pass the data frame, name the columns holding the estimate and
its interval, and the figure is the same figure.
Value
A foresty figure: a ggplot/patchwork object carrying its
estimates, which prints as the figure and can be added to as any ggplot
can.
The data
A data.frame, a tibble, a data.table or a matrix, all of which are
read as the plain data frame the columns are taken from. One row is one row
of the figure, drawn in the order the rows are in, so sort the data the way
the figure should read.
The estimate and the two ends of its interval are the only columns the
figure cannot be drawn without. They are looked for by name when they are
not named here – estimate, conf.low and conf.high, and the usual
alternatives (lower/upper, lcl/ucl, ci_low/ci_high) – so a
frame that came out of broom::tidy(conf.int = TRUE) needs nothing said
about it at all.
Subgroups
group names a column that blocks the rows: its values become the headings
the rows sit under, in the order they first appear. A block holding a single
row takes no heading of its own – the row is labelled with the block
instead – which is what makes an "Overall" row a row rather than a heading
with one line under it.
emphasis names a logical column marking the rows drawn for emphasis: a
bolder label, a diamond rather than a square, and a rule setting the block
off from the ones around it. It is how the overall estimate is set apart
from the subgroups read against it. See foresty_layout().
interaction_p names a column of p-values for the interaction. One test
covers the block it was taken across, so it is written once, against the
first row of the block, rather than repeated down the column.
What is not here
The figure carries no model, because it was not given one. summary(),
foresty_report(), coef(), vcov() and the coefficient table are
properties of a model and refuse on a figure drawn this way, naming
foresty_main() and foresty_interaction() as the way to have them.
as.data.frame(), broom::tidy() and print() work as they do on any
other foresty figure.
See Also
foresty_main() and foresty_interaction(), which start from a
fitted model; foresty_layout() for the styles; foresty_app(), whose
figures this one reproduces.
Examples
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)
)
foresty_data(
subgroups,
label = "subgroup", group = "block", emphasis = "overall",
interaction_p = "p_int",
measure = "OR", outcome = "asthma", adjusted = TRUE
)
# Every combination of two categorical variables against one of them, which
# is what foresty_interaction(reference = ) draws from a model: the blocks
# are the levels of the modifier, the rows the levels of the exposure, and
# one row of the figure is the group the rest are read against.
cells <- data.frame(
ecog = c("0-1", ">=2", "0-1", ">=2"),
mutation = c("Negative", "Negative", "Positive", "Positive"),
estimate = c(1.00, 1.94, 0.62, 1.21),
conf.low = c(NA, 1.42, 0.44, 0.83),
conf.high = c(NA, 2.65, 0.87, 1.76),
n = c(212, 168, 190, 145),
events = c(126, 131, 92, 96),
reference = c(TRUE, FALSE, FALSE, FALSE),
p_int = c(0.031, NA, NA, NA)
)
foresty_data(
cells,
label = "ecog", group = "mutation", reference = "reference",
interaction_p = "p_int", measure = "HR", outcome = "death",
adjusted = TRUE, layout = "jama"
)
Exposure effect within each level of an effect modifier
Description
Takes a model that does not yet interact the exposure with the modifier, updates it with the interaction term, and estimates the exposure effect separately within every level of the modifier. The estimates are drawn one level above the other, a row to a level, and can be written to a self-contained HTML page carrying the joint test of the interaction.
Usage
foresty_interaction(
fit,
exposure,
interaction,
measure = NULL,
exponentiate = TRUE,
labels = NULL,
level_labels = NULL,
reference = NULL,
outcome = NULL,
outcome_reference = NULL,
outcome_reference_row = FALSE,
ci_level = 0.95,
contrast = NULL,
at = NULL,
vcov = NULL,
cluster = NULL,
test = c("lrt", "wald", "both"),
table = TRUE,
columns = NULL,
person_time = NULL,
layout = NULL,
title = NULL,
subtitle = NULL,
xlab = NULL,
html = FALSE
)
Arguments
fit |
A fitted model. If it already contains the exposure by modifier interaction it is used as it stands; otherwise it is updated to add it. |
exposure |
Name of the exposure variable, as a character string.
Naming it, as |
interaction |
Name of the effect modifier, as a character string, and
named as |
measure |
Effect measure, one of |
exponentiate |
Whether a ratio measure is drawn as a ratio. |
labels |
Named character vector giving the label to draw for a
variable, as |
level_labels |
Named character vector giving the label to draw for a
level of the modifier, as |
reference |
The one combination of the exposure and the modifier every
row is compared with, as a named character vector naming a level of each:
|
outcome |
What to call the outcome, as |
outcome_reference |
For a multinomial logistic regression, the level of
the outcome every estimate is read against, as
|
outcome_reference_row |
Whether that level is drawn as a row of its
own. |
ci_level |
Confidence level of the intervals. Defaults to |
contrast |
For a continuous exposure, the increment the effect is
reported per. |
at |
The two values of the exposure to contrast, as |
vcov |
Robust standard errors. |
cluster |
Cluster-robust standard errors, passed to
|
test |
How the interaction is tested: |
table |
Whether to draw the table of numbers beside the plot. Defaults
to |
columns |
Which columns the table carries, from |
person_time |
The unit person-time is reported in, for a model that
carries any. |
layout |
How the figure is drawn: the name of a style, as
|
title |
Plot title. The default names the measure, the outcome it is a
measure of, the exposure, the modifier and the fact that every subgroup
estimate comes out of one model carrying their interaction term, as
|
subtitle |
Plot subtitle. |
xlab |
The label under the plot, which by default names the measure and
the outcome, as |
html |
Whether to write the HTML report. |
Details
The point is to make an interaction readable. The p-value of an interaction term reports only that the subgroups depart from a common effect; it does not say which subgroup carries the effect, in which direction, or how far apart they are, and a small p-value from a large study can accompany subgroup estimates that are practically identical. Seeing the subgroup-specific estimates and the test together is what settles whether an interaction is worth reporting.
Each subgroup effect is a linear combination of the coefficients of the
single interaction model, not a separate model fitted in each subgroup: in
the reference level of the modifier it is the exposure term alone, and in
the other levels it is the exposure term plus the relevant interaction
terms. Every subgroup therefore shares one estimate of the covariate
effects, and the interaction can be tested. The combinations and their
tests are computed by car::linearHypothesis().
Value
A ggplot2 object, of class foresty, carrying the estimates and
the interaction test. Print it to draw it. summary() reports the
coefficients and the test, and tidy() returns the estimates as a data
frame.
One reference group for the whole figure
By default every row is the effect of the exposure inside one subgroup, so
each subgroup is read against its own reference level and the rows of one
subgroup are not comparisons with the rows of another. Where the exposure and
the modifier are both categorical there is a second question a figure can
answer: what every combination of the two comes to against one of them.
reference asks it. reference = c(ecog = "0-1", egfr = "Negative") names
the one combination the rest are compared with, and each row is then that
whole cell – this level of the exposure and this level of the modifier –
against that cell, all of it out of the same interaction model. The cell
named carries no estimate of its own and is drawn as the reference it is.
The two figures answer different questions and the difference is worth being
clear about. Without reference, a row says how much the exposure matters in
that subgroup, and the interaction test beside the rows says whether those
effects differ. With it, a row says how far that combination of the two
variables is from one chosen combination, which is what a table of six groups
against one baseline group reports, and the interaction test beside them is
the same test of the same coefficients: it still asks whether the effect of
the exposure depends on the modifier, and it is not a test of the rows.
reference = TRUE takes the first level of each, which is the cell a model
fitted under R's default treatment contrasts takes as its own baseline. Which
cell that is makes no difference to the estimates – each row is a difference
between two rows of the design matrix, so it comes out the same however the
factors were coded – but it is what the rest of the figure is read against,
so name the cell with reference = c(...) where the first level is not the
one a reader should start from.
Testing the interaction
The p-value beside the subgroups is the joint test that every exposure by modifier coefficient is zero. It is the likelihood ratio test by default, comparing the likelihood of the model carrying the interaction with that of the same model without it.
test = "wald" takes the joint Wald test over the coefficients and their
covariance instead, which is what a model summary reports and what a robust
or cluster-robust variance changes. The two tests answer the same question
and usually agree; they part company where the Wald approximation is poor –
a small study, a rare outcome, a sparse subgroup, an estimate far from the
null – and the likelihood ratio test is the more trustworthy of the two
there, which is why it is the default. test = "both" reports each in a
column of its own, which is a way of showing that the reported p-value does
not turn on which test was chosen.
The likelihood ratio test is computed from the likelihood, so it has no
robust form: it takes no account of a variance passed through vcov or
cluster. Where it would not match the intervals drawn beside it for that
reason, the Wald test is reported in its place and foresty says so; ask for
the likelihood ratio test by name there and it is given, with a warning that
it does not match them.
A model with no likelihood at all is a different case, and no likelihood
ratio test is reported for one however it is asked for. A GEE is the one that
comes up: it estimates its coefficients from estimating equations rather than
from a likelihood, and its standard errors are the sandwich ones from the
start, so the joint Wald test is the test of it. A quasi-likelihood family is
the same case. test = "lrt" on either is an error naming the reason rather
than a number that would look like a likelihood ratio test and not be one;
the default and test = "both" report the Wald test and say that they did.
For a linear model the likelihood ratio test is the chi-square form rather than the exact F test, which is what the Wald test gives there.
What the counts beside the rows count
The table beside the plot reports the number of people behind each row and, where the outcome is an event, how many of them had it. Both are counted over the rows the model was fitted to and not over the data frame it was fitted from, so anyone missing the outcome, the exposure or any covariate in the formula is left out of them – which is the same complete-case set the estimates come from, and is what makes the two agree. A column that is not in the model does not affect them however much of it is missing.
A row of a forest plot is usually half of a comparison, so by default the
other half is beside it: a row of suburban children reads 822 vs 468,
those being the 822 the row is of and the 468 rural ones its odds ratio was
estimated against. counts = "row" in foresty_layout() holds the row's
own group alone instead. What the two groups are depends on what the row
compares:
A continuous exposure compares no two groups of people – its row is a step along a slope – so the count is the one group the row is of: every row of the model's data in a
foresty_main()figure, everyone in the subgroup in aforesty_interaction()one.A categorical exposure compares one of its levels with the level the contrast is from, so a row carries the two:
822 vs 468people and125 vs 59events. The reference row is the group the others are compared with rather than a comparison of its own, so it carries one number.A multinomial fit (
nnet::multinom()) of an exposure with no levels compares two levels of the outcome, and the two numbers are how many people were at each: a "Transient vs None" row of women reads637 vs 1,050. That is the only pair of counts such a row has – the events column would repeat it – so it goes under the sizes and the events column is left off. Of a categorical exposure, the row compares the exposure's levels within one comparison of the outcome, so the sizes are the two exposure groups whatever their outcome and the events are the people at the row's outcome level within each.An ordinal fit (
MASS::polr()) has one set of coefficients for the whole outcome, so its rows carry sizes and no count of events.
A multinomial estimate is worth one caution the counts cannot give: it did
not come out of the two groups beside it alone. All the equations are fitted
over the whole outcome at once, so the people at the levels a row is not
about bear on it too, and a row reading 637 vs 1,050 is not the logistic
regression of those 1,687.
The figures whose rows compare two groups say what the counts are under the
plot in foresty_app() and under the table of estimates in
foresty_report(); the ones that do not say nothing.
Adjusting the figure
The result is a ggplot2 object, so layers, scales and themes are added to
it as usual, and + always reaches the forest:
foresty_main(list(fit), "no2") + ggplot2::coord_cartesian(xlim = c(0.8, 2)) foresty_main(list(fit), "no2") + ggplot2::theme_minimal(base_size = 14)
& reaches every panel of a figure that has more than one, which is worth
knowing about but rarely what you want: a scale or a coordinate system
applied to the table of numbers beside the plot will spoil it. Use +.
See Also
foresty_main(), foresty_layout(), foresty_report().
Examples
fit <- glm(asthma ~ no2 + sex + maternal_smoking + maternal_age,
family = binomial, data = foresty_cohort)
# Sex, where the interaction is real.
by_sex <- foresty_interaction(fit, exposure = "no2", interaction = "sex")
by_sex
summary(by_sex)
# The rest of what the function can be asked for, drawn one after
# another. Each is quick; there are simply several of them.
# Maternal smoking, where it is not.
foresty_interaction(fit, exposure = "no2", interaction = "maternal_smoking")
# Naming the modifier labels it, and both tests of the interaction can be
# reported side by side.
foresty_interaction(fit, exposure = c(NO2 = "no2"),
interaction = c(Sex = "sex"), test = "both")
# Every combination of two categorical variables against one of them, which
# is the other question a two-way table of groups asks.
fit2 <- glm(asthma ~ urbanicity + sex + maternal_age, family = binomial,
data = foresty_cohort)
foresty_interaction(fit2, exposure = "urbanicity", interaction = "sex",
reference = c(urbanicity = "Rural", sex = "Female"))
# The subgroup estimates, the numbers behind them and the test of the
# interaction, in the layout of a journal.
foresty_interaction(fit, exposure = "no2", interaction = "sex",
layout = "jama")
# A multinomial outcome. Each subgroup carries one row per comparison
# between outcome levels, and the interaction is tested jointly across the
# equations, so it has one degree of freedom for each of them.
if (requireNamespace("nnet", quietly = TRUE)) {
fit_phenotype <- nnet::multinom(
wheeze_phenotype ~ no2 + sex + maternal_smoking,
data = foresty_cohort, trace = FALSE
)
print(foresty_interaction(fit_phenotype, exposure = "no2",
interaction = "sex", contrast = 10))
}
Layout and style of a foresty figure
Description
Every drawing decision the figure makes is held in one object: the colors,
the marks, the rules between the rows, how the numbers beside the plot are
written, and where the columns sit. Pass the name of a style to
foresty_main() or foresty_interaction() as layout = "jama", or build
one here and change any part of it.
Usage
foresty_layout(
style = c("classic", "jama", "nejm", "lancet", "bmj", "revman"),
base_size = NULL,
family = NULL,
color = NULL,
color_by = NULL,
colors = NULL,
theme = NULL,
palette = NULL,
point_shape = NULL,
point_size = NULL,
emphasis_shape = NULL,
emphasis_height = NULL,
emphasis_size = NULL,
emphasis_face = NULL,
emphasis_gap = NULL,
interval_width = NULL,
null_line = NULL,
grid = NULL,
axis_line = NULL,
band = NULL,
rules = NULL,
separators = NULL,
group_position = NULL,
table_side = NULL,
header_face = NULL,
group_face = NULL,
digits = NULL,
p_format = NULL,
decimal_mark = NULL,
ci_separator = NULL,
ci_brackets = NULL,
column_gap = NULL,
counts = NULL,
headings = NULL,
xlim = NULL,
arrows = NULL,
arrows_position = NULL,
plot_width = NULL,
min_plot_width = NULL,
auto_labels = NULL
)
Arguments
style |
Name of the style to start from, one of |
base_size |
Base font size in points. Everything else is drawn relative to it. |
family |
Font family, as |
color |
One color for the estimates and their intervals, which is the
usual thing to change. |
color_by |
What the colors change with, for a figure drawn in more
than one. |
colors |
The colors |
theme |
The |
palette |
Named character vector overriding single colors, as
|
point_shape, point_size |
Plotting symbol and its size. The default is a filled square, as a forest plot is usually drawn with. |
emphasis_shape, emphasis_height, emphasis_size, emphasis_face |
How a row
singled out for emphasis is drawn – the overall estimate on a
|
emphasis_gap |
Whether a row singled out for emphasis is set apart from
the rest by a rule of its own, drawn whether or not |
interval_width |
Line width of the confidence intervals. |
null_line |
Line type of the line at the null, as |
grid |
Whether to draw vertical grid lines behind the estimates. |
axis_line |
Whether to draw the axis line under the plot. |
band |
Whether to shade alternate rows, which helps a reader carry the eye across a wide table of numbers. |
rules |
Where to rule the figure: |
separators |
Whether to draw a thin rule between one subgroup and the next. |
group_position |
Where the name of a subgroup goes: |
table_side |
Which side of the plot the numbers are written on,
|
header_face, group_face |
Font face of the column headings and of the
subgroup names, as |
digits |
Digits the estimates are written to. |
p_format |
How p-values are written. |
decimal_mark |
Decimal point, |
ci_separator |
What goes between the confidence limits. The default
picks |
ci_brackets |
The pair of brackets the interval is written in, as
|
column_gap |
Space between one column of numbers and the next, in ems, an em being about two digits wide. Lower it to draw the numbers tighter and leave the plot more of the figure. |
counts |
What the columns of counts hold on a row that compares two
groups of people. |
headings |
Named character vector renaming the column headings, as
|
xlim |
Limits of the plot, as |
arrows |
Two labels for the directions of the effect, as
|
arrows_position |
Whether those labels go at the |
plot_width |
How much of the figure the plot itself takes. |
min_plot_width |
The least of the figure the plot is allowed to be left
with, as a fraction. What the columns of text leave depends on the width
the figure is drawn at, which is not known while it is being built, so a
wide table – a survival model carrying N, events and person-time – can
leave the plot a centimetre and squash the axis under it into a row of
overprinted numbers. This is the floor: where the columns of text would
leave the plot less than this share of the width it is being drawn at, the
plot is given that share and the columns of text share the rest in
proportion to what they hold. |
auto_labels |
Whether |
Details
The styles are the ones a paper is usually asked for, read off the forest
plots those journals print and the layouts of the meta package, which
foresty follows here. They are approximations of a house style, not the
style sheet itself: a journal will still ask for its own fonts and sizes,
and every element below can be overridden.
"classic"The default. Black marks on white, a dashed line at the null, a rule under the column headings and thin rules between subgroups.
"jama"Numbers to the left of the plot, rules above and below the whole block, navy squares, a solid line at the null, p-values without their leading zero (
.03,<.001), and "No." and "P value" as headings. The title and subtitle thatforesty_interaction()writes for itself are left off, the caption being where a journal puts them."nejm"Numbers to the right, blue squares, en dashes between the confidence limits, "P value" as the heading.
"lancet"As
"nejm", in the Lancet's blue, with the middle dot for a decimal point, so that 1.17 is written with one, and en dashes."bmj"The BMJ's violet, grey rules, and "to" between the confidence limits.
"revman"Cochrane's RevMan: blue squares and intervals written
1.15 [0.93, 1.42].
Value
An object of class foresty_layout.
See Also
foresty_main(), foresty_interaction().
Examples
fit <- glm(asthma ~ no2 + sex + maternal_smoking + maternal_age,
family = binomial, data = foresty_cohort)
foresty_interaction(fit, "no2", "sex", table = TRUE, layout = "jama")
# A style, changed where it needs to be.
foresty_interaction(
fit, "no2", "sex", table = TRUE,
layout = foresty_layout("jama", color = "#B24745", base_size = 11)
)
# Trimming a wide interval rather than letting it flatten the figure.
fit_urban <- glm(asthma ~ urbanicity + sex + maternal_age,
family = binomial, data = foresty_cohort)
foresty_main(list(fit_urban), "urbanicity", table = TRUE,
layout = foresty_layout("classic", xlim = c(0.8, 2.5),
arrows = c("Lower risk", "Higher risk")))
Forest plot of exposure effects across models
Description
Draws the effect of one exposure from each of several fitted models, one
row apiece, so that exposures that were each fitted in their own model come
onto a single figure. None of the models may interact its exposure with
anything; use foresty_interaction() for those.
Usage
foresty_main(
fits,
exposure,
measure = NULL,
exponentiate = TRUE,
labels = NULL,
outcome = NULL,
outcome_reference = NULL,
outcome_reference_row = FALSE,
ci_level = 0.95,
contrast = NULL,
at = NULL,
vcov = NULL,
cluster = NULL,
table = TRUE,
columns = NULL,
person_time = NULL,
layout = NULL,
title = NULL,
subtitle = NULL,
xlab = NULL,
html = FALSE
)
Arguments
fits |
A list of fitted models. Models fitted by |
exposure |
Name of the exposure variable in each model, as a character
vector: either one name for all of them, or one name per model. Naming an
element, as |
measure |
Effect measure, one of |
exponentiate |
Whether a ratio measure is drawn as a ratio. |
labels |
Named character vector giving the label to draw for a
variable, as |
outcome |
What to call the outcome, as |
outcome_reference |
For a multinomial logistic regression, the level of
the outcome every estimate is read against, as
|
outcome_reference_row |
Whether that level is drawn as a row of its
own. |
ci_level |
Confidence level of the intervals. Defaults to |
contrast |
For a continuous exposure, the increment the effect is
reported per. |
at |
The two values of the exposure to contrast, as |
vcov |
Robust standard errors. |
cluster |
Cluster-robust standard errors, passed to
|
table |
Whether to draw the table of numbers beside the plot. Defaults
to |
columns |
Which columns the table carries, from |
person_time |
The unit person-time is reported in, for a model that
carries any. |
layout |
How the figure is drawn: the name of a style, as
|
title |
Plot title. The default names the measure, the outcome it is a
measure of, the exposure it is reported for and the fact that the estimate
comes from a model with no interaction term in it, as |
subtitle |
Plot subtitle. |
xlab |
The label under the plot, which by default names the measure and
the outcome, as |
html |
Whether to write the HTML report – the model it was drawn from,
the estimates, the figure and the whole coefficient table, on a page that
is a single file and can be sent on. |
Details
The effect is the difference between two rows of the model's own design
matrix, one at the baseline value of the exposure and one at the value it is
compared with, so it is read off correctly whether the exposure is
continuous, binary or a factor with several levels, and whatever contrast
coding the fitting function used. The estimate and its confidence interval
are computed by car::linearHypothesis().
A categorical exposure gets one row per level, the reference level included and marked as such, with the levels named on the rows and the variable named once at the left of the figure.
Value
A ggplot2 object, of class foresty, carrying the estimates it
was drawn from. Print it to draw it. summary() reports the coefficients
and tests behind it, tidy() returns the estimates as a data frame, and
predict() passes through to the underlying model.
A splined exposure
An exposure entered as a spline has no single effect to report: the difference it makes depends on where along the curve it is taken. So the two values are named, and the row says which they were:
fit <- glm(asthma ~ splines::ns(no2, 3) + sex, family = binomial, data = d) foresty_main(list(fit), exposure = "no2", at = c(10, 20))
The basis has to be built inside the formula, by splines::ns(),
splines::bs(), stats::poly() or another function of the variable, so the
two design-matrix rows can be evaluated at the two values.
A basis computed before the fit and entered as columns of its own –
Hmisc::rcspline.eval() written into spline1, spline2, spline3 and
then fitted as y ~ spline1 + spline2 + spline3 – cannot be handled that
way, because nothing in the fit records that those three columns are one
variable or how to recompute them at another value. Name the exposure and
the model refuses, saying that it is not in the model. Put the basis in the
formula instead, which fits exactly the same model:
knots <- quantile(d$age, probs = c(0.05, 0.35, 0.65, 0.95)) fit <- glm(y ~ splines::ns(age, 4) + sex, family = binomial, data = d) foresty_main(list(fit), exposure = "age", at = c(30, 60))
What the counts beside the rows count
The table beside the plot reports the number of people behind each row and, where the outcome is an event, how many of them had it. Both are counted over the rows the model was fitted to and not over the data frame it was fitted from, so anyone missing the outcome, the exposure or any covariate in the formula is left out of them – which is the same complete-case set the estimates come from, and is what makes the two agree. A column that is not in the model does not affect them however much of it is missing.
A row of a forest plot is usually half of a comparison, so by default the
other half is beside it: a row of suburban children reads 822 vs 468,
those being the 822 the row is of and the 468 rural ones its odds ratio was
estimated against. counts = "row" in foresty_layout() holds the row's
own group alone instead. What the two groups are depends on what the row
compares:
A continuous exposure compares no two groups of people – its row is a step along a slope – so the count is the one group the row is of: every row of the model's data in a
foresty_main()figure, everyone in the subgroup in aforesty_interaction()one.A categorical exposure compares one of its levels with the level the contrast is from, so a row carries the two:
822 vs 468people and125 vs 59events. The reference row is the group the others are compared with rather than a comparison of its own, so it carries one number.A multinomial fit (
nnet::multinom()) of an exposure with no levels compares two levels of the outcome, and the two numbers are how many people were at each: a "Transient vs None" row of women reads637 vs 1,050. That is the only pair of counts such a row has – the events column would repeat it – so it goes under the sizes and the events column is left off. Of a categorical exposure, the row compares the exposure's levels within one comparison of the outcome, so the sizes are the two exposure groups whatever their outcome and the events are the people at the row's outcome level within each.An ordinal fit (
MASS::polr()) has one set of coefficients for the whole outcome, so its rows carry sizes and no count of events.
A multinomial estimate is worth one caution the counts cannot give: it did
not come out of the two groups beside it alone. All the equations are fitted
over the whole outcome at once, so the people at the levels a row is not
about bear on it too, and a row reading 637 vs 1,050 is not the logistic
regression of those 1,687.
The figures whose rows compare two groups say what the counts are under the
plot in foresty_app() and under the table of estimates in
foresty_report(); the ones that do not say nothing.
Adjusting the figure
The result is a ggplot2 object, so layers, scales and themes are added to
it as usual, and + always reaches the forest:
foresty_main(list(fit), "no2") + ggplot2::coord_cartesian(xlim = c(0.8, 2)) foresty_main(list(fit), "no2") + ggplot2::theme_minimal(base_size = 14)
& reaches every panel of a figure that has more than one, which is worth
knowing about but rarely what you want: a scale or a coordinate system
applied to the table of numbers beside the plot will spoil it. Use +.
See Also
foresty_interaction(), foresty_layout(), foresty_report().
Examples
fit_no2 <- glm(asthma ~ no2 + sex + maternal_smoking + maternal_age,
family = binomial, data = foresty_cohort)
fit_bc <- glm(asthma ~ black_carbon + sex + maternal_smoking + maternal_age,
family = binomial, data = foresty_cohort)
foresty_main(
list(fit_no2, fit_bc),
exposure = c("no2", "black_carbon"),
labels = c(no2 = "NO2", black_carbon = "Black carbon")
)
# A categorical exposure: the levels are named, the variable once at the left.
fit_urban <- glm(asthma ~ urbanicity + sex + maternal_age,
family = binomial, data = foresty_cohort)
foresty_main(list(fit_urban), exposure = "urbanicity")
# Every variation the figure has, drawn one after another. They are
# skipped by the timed run of the examples only because there are many
# of them, not because any one is slow.
# In the layout of a journal, and without the numbers beside the plot.
foresty_main(list(fit_urban), exposure = "urbanicity", layout = "jama")
foresty_main(list(fit_urban), exposure = "urbanicity", table = FALSE)
# Per 10 units of the exposure rather than per 1, which the row says.
foresty_main(list(fit_no2), exposure = "no2", contrast = 10)
# A rate model. The offset is the time each child was followed for, so the
# measure is an incidence rate ratio and the person-time behind each row is
# drawn beside the counts.
fit_rate <- glm(asthma ~ no2 + sex + maternal_age +
offset(log(followup_years)),
family = poisson, data = foresty_cohort)
foresty_main(list(fit_rate), exposure = "no2",
labels = c(no2 = "NO2"), contrast = 10)
# A splined exposure: the two values being compared are named.
fit_spline <- glm(asthma ~ splines::ns(no2, 3) + sex + maternal_age,
family = binomial, data = foresty_cohort)
foresty_main(list(fit_spline), exposure = "no2", at = c(10, 20))
# On the scale the model was fitted on, as a log odds ratio about zero.
foresty_main(list(fit_no2), exposure = "no2", exponentiate = FALSE)
# The exposure, the outcome and the axis all named by hand.
foresty_main(list(fit_no2), exposure = c(`NO2, ug/m3` = "no2"),
outcome = "incident asthma by age 8",
xlab = "Adjusted odds ratio (95% CI)")
# Person-time reported per 1,000 rather than as the total.
foresty_main(list(fit_rate), exposure = "no2", person_time = 1000)
# A multinomial logistic regression has one equation per non-reference level
# of the outcome, so the exposure has one effect per level and the figure
# has one row per level. `outcome_reference` says which level they are all
# read against.
if (requireNamespace("nnet", quietly = TRUE)) {
fit_phenotype <- nnet::multinom(
wheeze_phenotype ~ no2 + sex + maternal_smoking,
data = foresty_cohort, trace = FALSE
)
# Against "None", which is the level the model itself was fitted against,
# and drawn as a row of its own so that the figure says so.
print(foresty_main(list(fit_phenotype), exposure = "no2", contrast = 10,
outcome_reference_row = TRUE))
# The same estimates read against another level instead.
against_transient <- foresty_main(list(fit_phenotype), exposure = "no2",
contrast = 10,
outcome_reference = "Transient")
summary(against_transient)
}
Write a foresty figure and its numbers to a self-contained HTML page
Description
Writes what an interaction p-value was standing in for: the exposure effect within each level of the modifier, the joint test of the interaction, the ratio of the effects between levels, the whole coefficient table of the updated model, and the figure. Everything is inlined, so the file can be opened or sent on its own.
Usage
foresty_report(
x,
file,
title = NULL,
model = NULL,
width = 10,
height = NULL,
open = FALSE
)
Arguments
x |
An object returned by |
file |
Path to write to. A |
title |
Browser page title. The default names the exposure and the modifier; it is not displayed in the report body. |
model |
Which model to report the coefficients of, when the figure covers several. The default reports every one of them. |
width, height |
Size of the embedded plot in inches. |
open |
Whether to open the file when it has been written. Defaults to
|
Details
A figure drawn from several models – foresty_main() over a list of them –
describes each of them on the page and gives each its own coefficient table,
headed with the exposure it was fitted for. model cuts those sections down
to one. The rest of the page is the figure itself – the estimates, the test
and the plot – and is the whole of it whichever model is named, a figure
being one figure however many models went into it.
Value
The path written to, invisibly.
Examples
# Writing the page renders the figure and lays the tables out with `gt`,
# which is the slow part, so it is left out of the timed run.
fit <- glm(asthma ~ no2 + sex + maternal_age, family = binomial,
data = foresty_cohort)
x <- foresty_interaction(fit, exposure = "no2", interaction = "sex")
foresty_report(x, file = file.path(tempdir(), "no2_by_sex.html"))
# The same page can be asked for as the figure is made. `html = TRUE`
# writes it under a name taken from the variables it is about -- here
# no2_sex.html -- into the working directory; a path writes it there
# instead, which is what these examples do so that nothing is written
# outside the session's temporary directory.
foresty_interaction(fit, exposure = "no2", interaction = "sex",
html = file.path(tempdir(), "no2_sex.html"))
foresty_main(list(fit), exposure = "no2",
html = file.path(tempdir(), "no2.html"))
Draw a forest plot
Description
Draws the figure, having first made sure the plot in it has room for its
axis. A column of text is as wide as what it holds, and the plot is given
what those columns leave, which is a decision that cannot be made while the
figure is being built: how much they leave depends on the width the figure is
drawn at. So a table wide enough – a survival model reporting N, events and
person-time beside the estimates – can leave the plot a centimetre and the
numbers under its axis printed one on top of another. The floor is applied
here, where the width being drawn at is known: see the min_plot_width
argument of foresty_layout(). Where it has to act, the columns of text
stop being as wide as what they hold and share what the plot leaves in
proportion to it, which is where the room for the axis comes from; a figure
with room for its columns already is drawn exactly as it was built.
Usage
## S3 method for class 'foresty'
print(x, ...)
## S3 method for class 'foresty'
plot(x, ...)
## S3 method for class 'foresty'
grid.draw(x, recording = TRUE)
Arguments
x |
A figure returned by |
... |
Passed on to patchwork's and ggplot2's own print methods. |
recording |
Whether to record the drawing on the display list, as in
|
Value
x, invisibly.
Examples
fit <- glm(asthma ~ no2 + sex, family = binomial, data = foresty_cohort)
print(foresty_interaction(fit, "no2", "sex", table = TRUE))
Summarize a foresty figure
Description
Reports what the figure was drawn from: the model, its whole coefficient
table with standard errors and p-values in the manner of summary.glm(),
the exposure or subgroup estimates on the reported scale, and the joint test
of the interaction where there is one.
Usage
## S3 method for class 'foresty'
summary(object, model = NULL, ...)
## S3 method for class 'summary.foresty'
print(x, ...)
Arguments
object |
A |
model |
Which model to report, when the figure covers several. Defaults to the only one. |
... |
Ignored. |
x |
A |
Details
The coefficient table is on the scale the model was fitted on, as a model summary is, so a logistic regression reports log odds. The estimates underneath are on the reported scale, exponentiated where the measure is a ratio.
Value
An object of class summary.foresty, a list with elements call,
coefficients, estimates and, for an interaction, interaction_test.
Examples
fit <- glm(asthma ~ no2 + sex + maternal_age, family = binomial,
data = foresty_cohort)
summary(foresty_interaction(fit, exposure = "no2", interaction = "sex"))