Multi-node structural equation models: piecewiseSEM and drmSEM
Source:vignettes/symbolizer-sem.Rmd
symbolizer-sem.RmdA piecewise structural equation model is a system of regressions — one fitted model per response, wired together into a hypothesized causal graph. This article shows how
symbolize()reads the whole system: it walks each node, symbolizes it with the node’s own class method, and hands back a collator whose equations, assumption table, and HTML widget are stacked node by node. Two flavours are covered: mean-only piecewise SEMs frompiecewiseSEM, and distributional (location-scale — modelling spread or skew, not just the mean) piecewise SEMs fromdrmSEM.
symbolize() reports the structure of a
model system — its responses, predictors, links, and distributional
assumptions — not the fitted coefficients, significance, or
goodness-of-fit. The directed paths below are the analyst’s
assumed structure; whether the data support them is a separate
question (see Shipley’s d-separation test in
piecewiseSEM).
A SEM is a list of regressions
Where a single GLM has one response, a piecewise SEM has several — each its own fitted model, each a node in a directed graph. A variable that is a predictor in one node is a response in another.
We use the keeley data bundled with
piecewiseSEM: 90 plots of California shrubland surveyed
after wildfire (Grace & Keeley 2006). Five of its columns appear
below — stand age, fire severity
(firesev), an abiotic favourability index
(abiotic), plant cover, and species
richness (rich). A simplified slice of the
Grace & Keeley causal backbone posits four directed paths plus one
correlated-error arc:
-
firesev ~ age— the model treats fire severity as a response to stand age (older stands carry more accumulated fuel), -
cover ~ firesev— plant cover as a response to fire severity, -
rich ~ abiotic + firesev— richness as a response to abiotic favourability and fire severity, -
rich %~~% cover— richness and cover are allowed a residual covariance (some shared unmeasured cause beyond their common dependence on fire), declared as a bidirected arc, not a regression. (This arc is added here to illustrate the feature; the age→firesev→cover/rich paths are the simplified published backbone.)
piecewiseSEM::psem() glues those fitted nodes into one
object. symbolize() reads it as a unit.
library(piecewiseSEM)
data(keeley)
sem <- psem(
lm(firesev ~ age, data = keeley),
lm(cover ~ firesev, data = keeley),
lm(rich ~ abiotic + firesev, data = keeley),
rich %~~% cover,
data = keeley
)
#> Warning: the 'nobars' function has moved to the reformulas package. Please update your imports, or ask an upstream package maintainer to do so.
#> This warning is displayed once per session.
sym <- symbolize(
sem,
symbols = c(firesev = "F_i", cover = "C_i", rich = "R_i",
age = "A_i", abiotic = "B_i")
)
sym
#> <symbolized_psem>: 3 nodes
#> - firesev (lm, gaussian)
#> - cover (lm, gaussian)
#> - rich (lm, gaussian)
#> 1 residual covariance arc: rich ~~ coversymbolize() returns a symbolized_psem — a
collator holding one symbolized_model per node
(sym$parts), the node names in declared order
(sym$node_names), and any %~~% arcs
(sym$cov_arcs). Every node was dispatched on its own
class:
The equations, node by node
equations() row-binds every node’s equation block with a
leading node column, so you can work with the whole system
in R. The rendered widget below shows the same node equations without
exposing raw LaTeX in the article:
equations(sym)as_latex() concatenates the per-node LaTeX under
## Node: headers, ready to splice into a manuscript. The
command is run during the vignette build, but the raw LaTeX string is
not printed in the HTML article:
The assumptions — including the covariance arc
assumption_table() row-binds each node’s assumptions,
again with a leading node column. The %~~% arc
surfaces here as a Residual cov(...) row — and
only here. A bidirected arc (the %~~% arc)
is a covariance, not a regression, so it never appears as an equation.
It records only that two residuals covary; it neither names the
source nor implies one response affects the other:
assumption_table(sym)| assumption | expression | biological meaning | status |
|---|---|---|---|
| conditional_distribution | F_i \mid \mu_i,\, \sigma_i \sim \mathrm{Normal}(\mu_i,\, \sigma_i^2) | firesev varies normally around its expected value | explicit |
| linear_predictor | \mu_i = \beta_0 + \sum_k \beta_k X_{ki} | Expected firesev is a linear combination of the mean-model predictors | explicit |
| independence | F_i \perp F_j \mid X \text{ for } i \ne j | Observations are conditionally independent given the predictors | follows from the formula |
| no_missing_at_random | — | Observations are assumed not missing in a way that depends on the unobserved response | your responsibility |
| conditional_distribution | C_i \mid \mu_i,\, \sigma_i \sim \mathrm{Normal}(\mu_i,\, \sigma_i^2) | cover varies normally around its expected value | explicit |
| linear_predictor | \mu_i = \beta_0 + \sum_k \beta_k X_{ki} | Expected cover is a linear combination of the mean-model predictors | explicit |
| independence | C_i \perp C_j \mid X \text{ for } i \ne j | Observations are conditionally independent given the predictors | follows from the formula |
| no_missing_at_random | — | Observations are assumed not missing in a way that depends on the unobserved response | your responsibility |
| conditional_distribution | R_i \mid \mu_i,\, \sigma_i \sim \mathrm{Normal}(\mu_i,\, \sigma_i^2) | rich varies normally around its expected value | explicit |
| linear_predictor | \mu_i = \beta_0 + \sum_k \beta_k X_{ki} | Expected rich is a linear combination of the mean-model predictors | explicit |
| independence | R_i \perp R_j \mid X \text{ for } i \ne j | Observations are conditionally independent given the predictors | follows from the formula |
| no_missing_at_random | — | Observations are assumed not missing in a way that depends on the unobserved response | your responsibility |
| Residual cov(rich, cover) | \mathrm{Cov}(\varepsilon_{rich}, \varepsilon_{cover}) \neq 0 | Residual covariance between rich and cover, declared explicitly (a bidirected arc, not a regression). | explicit |
Three views of the whole SEM
as_html_three_views() renders the interactive
three-views widget for every node, stacked under node
headers: the per-observation Index form, the Matrix
form, and the matrix form populated with the node’s own real data (see
vignette("symbolizer") for what each view shows).
as_html_three_views(sym, id = "psem")Node: firesev
What happens for each observation i – the per-individual reading.
Each observation is normally distributed around a mean that may shift with the predictors; the residual SD is constant across observations.
Coefficient reading. On the response scale, \hat\beta is the additive change in the mean of the response for a one-unit increase in the predictor (identity link – no back-transformation needed).
where:
- F_i — response variable \mathbb{R}^{90}
- A_i — continuous predictor column of X (length 90)
- \mu_i — conditional mu of firesev \mathbb{R}^{90}
- \sigma — residual standard deviation of firesev scalar
- \beta_{0}, \beta_{1} — mu submodel coefficients \mathbb{R}^{2}
The same model in matrix form – the structural contract every textbook past chapter 4 switches to.
Each observation is normally distributed around a mean that may shift with the predictors; the residual SD is constant across observations.
where:
- \mathbf{f} — response variable \mathbb{R}^{90}
- \boldsymbol{\mu} — conditional mu of firesev \mathbb{R}^{90}
- \boldsymbol{\sigma} — residual standard deviation of firesev scalar
- \boldsymbol{\beta} — mu submodel coefficients \mathbb{R}^{2}
- \mathbf{X} — mu submodel design matrix \mathbb{R}^{90 \times 2}
The same matrix equation, with your actual numbers stacked inside the brackets – what the computer multiplies. Showing first 5 and last 2 rows of n = 90.
Each observation is normally distributed around a mean that may shift with the predictors; the residual SD is constant across observations.
Matrix-form expansion of the model. Each row shows the response y_i and the corresponding row of the design matrix X (showing head and tail rows of the n total observations), with the coefficient vector beta listed below.For observation i = 1 of your data:
Stacking the same response equation for all n = 90 observations:
Left: observed vector \mathbf{f}. Middle: the prediction \mathbf{X}\hat{\boldsymbol{\beta}} = \hat{\boldsymbol{\mu}}. Right: the residual vector \hat{\boldsymbol{\varepsilon}} = \mathbf{f} - \hat{\boldsymbol{\mu}}. Every row of this matrix equation is one of the response-equation rows from the worked row above.
Node: cover
What happens for each observation i – the per-individual reading.
Each observation is normally distributed around a mean that may shift with the predictors; the residual SD is constant across observations.
Coefficient reading. On the response scale, \hat\beta is the additive change in the mean of the response for a one-unit increase in the predictor (identity link – no back-transformation needed).
where:
- C_i — response variable \mathbb{R}^{90}
- F_i — continuous predictor column of X (length 90)
- \mu_i — conditional mu of cover \mathbb{R}^{90}
- \sigma — residual standard deviation of cover scalar
- \beta_{0}, \beta_{1} — mu submodel coefficients \mathbb{R}^{2}
The same model in matrix form – the structural contract every textbook past chapter 4 switches to.
Each observation is normally distributed around a mean that may shift with the predictors; the residual SD is constant across observations.
where:
- \mathbf{c} — response variable \mathbb{R}^{90}
- \boldsymbol{\mu} — conditional mu of cover \mathbb{R}^{90}
- \boldsymbol{\sigma} — residual standard deviation of cover scalar
- \boldsymbol{\beta} — mu submodel coefficients \mathbb{R}^{2}
- \mathbf{X} — mu submodel design matrix \mathbb{R}^{90 \times 2}
The same matrix equation, with your actual numbers stacked inside the brackets – what the computer multiplies. Showing first 5 and last 2 rows of n = 90.
Each observation is normally distributed around a mean that may shift with the predictors; the residual SD is constant across observations.
Matrix-form expansion of the model. Each row shows the response y_i and the corresponding row of the design matrix X (showing head and tail rows of the n total observations), with the coefficient vector beta listed below.For observation i = 1 of your data:
Stacking the same response equation for all n = 90 observations:
Left: observed vector \mathbf{c}. Middle: the prediction \mathbf{X}\hat{\boldsymbol{\beta}} = \hat{\boldsymbol{\mu}}. Right: the residual vector \hat{\boldsymbol{\varepsilon}} = \mathbf{c} - \hat{\boldsymbol{\mu}}. Every row of this matrix equation is one of the response-equation rows from the worked row above.
Node: rich
What happens for each observation i – the per-individual reading.
Each observation is normally distributed around a mean that may shift with the predictors; the residual SD is constant across observations.
Coefficient reading. On the response scale, \hat\beta is the additive change in the mean of the response for a one-unit increase in the predictor (identity link – no back-transformation needed).
where:
- R_i — response variable \mathbb{R}^{90}
- B_i — continuous predictor column of X (length 90)
- F_i — continuous predictor column of X (length 90)
- \mu_i — conditional mu of rich \mathbb{R}^{90}
- \sigma — residual standard deviation of rich scalar
- \beta_{0}, \beta_{1}, \beta_{2} — mu submodel coefficients \mathbb{R}^{3}
The same model in matrix form – the structural contract every textbook past chapter 4 switches to.
Each observation is normally distributed around a mean that may shift with the predictors; the residual SD is constant across observations.
where:
- \mathbf{r} — response variable \mathbb{R}^{90}
- \boldsymbol{\mu} — conditional mu of rich \mathbb{R}^{90}
- \boldsymbol{\sigma} — residual standard deviation of rich scalar
- \boldsymbol{\beta} — mu submodel coefficients \mathbb{R}^{3}
- \mathbf{X} — mu submodel design matrix \mathbb{R}^{90 \times 3}
The same matrix equation, with your actual numbers stacked inside the brackets – what the computer multiplies. Showing first 5 and last 2 rows of n = 90.
Each observation is normally distributed around a mean that may shift with the predictors; the residual SD is constant across observations.
Matrix-form expansion of the model. Each row shows the response y_i and the corresponding row of the design matrix X (showing head and tail rows of the n total observations), with the coefficient vector beta listed below.For observation i = 1 of your data:
Stacking the same response equation for all n = 90 observations:
Left: observed vector \mathbf{r}. Middle: the prediction \mathbf{X}\hat{\boldsymbol{\beta}} = \hat{\boldsymbol{\mu}}. Right: the residual vector \hat{\boldsymbol{\varepsilon}} = \mathbf{r} - \hat{\boldsymbol{\mu}}. Every row of this matrix equation is one of the response-equation rows from the worked row above.
The distributional flavour: drmSEM
A piecewiseSEM node is mean-only — one linear predictor
per response. It can model how the mean of cover
responds to fire severity, but not how the spread of
cover changes. When a path needs a distributional
structure (a scale submodel, a zero-inflation component, a residual
correlation between two responses), the node is a drmTMB
location-scale fit and the SEM is built with drmSEM::drm_sem().
symbolize() reads it through the drmSEM-side
symbolize.drm_sem method, which delegates each node to
symbolize.drmTMB.
The code below is not run here (it needs the
drmSEM and drmTMB packages); it continues the
same keeley story. Suppose we wanted to ask whether the
residual spread of cover widens at high fire severity —
plausible when severe burns produce divergent recovery — which the
mean-only lm() node cannot express:
library(drmSEM)
library(drmTMB)
sem <- drm_sem(
# The `cover` node gains a SCALE submodel: `sigma ~ firesev` lets the
# variability of cover -- not just its mean -- depend on fire severity.
cover = drm_node(drm_formula(cover ~ firesev, sigma ~ firesev),
family = gaussian()),
firesev = drm_node(drm_formula(firesev ~ age),
family = gaussian()),
data = keeley
)
sym <- symbolize(sem) # dispatches to drmSEM::symbolize.drm_sem
sym # <symbolized_drm_sem>: 2 nodes ...
equations(sym) # node column + per-node mu AND sigma rows
assumption_table(sym) # per-node assumptions, distributional
as_html_three_views(sym) # the same stacked widget, location-scale nodesEverything downstream — the equations, the assumption table, the HTML
widget — works identically on the drm_sem, because both
collators share one parent class. See the drmSEM site for runnable
distributional-SEM examples.
Why the same methods work on both
Every renderer you just saw — equations(),
as_latex(), assumption_table(),
as_html_three_views() — works identically on a
drm_sem. Here is the mechanism. symbolize.psem
tags its output symbolized_psem, which inherits from
symbolized_model_set; drmSEM’s
symbolize.drm_sem tags its output
symbolized_drm_sem, which inherits from the same parent.
Both store their nodes in a named parts
list. So the rendering generics dispatch on the shared parent and read
names(x$parts) — one implementation, two collators. The
capability registry records which package owns each method:
caps <- symbolizer_capabilities()
caps[caps$class %in% c("piecewiseSEM", "drm_sem"),
c("class", "status", "since", "lives_in", "notes")]
#> # A tibble: 2 × 5
#> class status since lives_in notes
#> <chr> <chr> <chr> <chr> <chr>
#> 1 piecewiseSEM Stable 0.22.4 symbolizer Walks psem list-of-fits; each fitted no…
#> 2 drm_sem Stable 0.22.4 drmSEM Method lives in drmSEM::symbolize.drm_s…
piecewiseSEM (psem) |
drmSEM (drm_sem) |
|
|---|---|---|
| Per-node model |
lm / glm / lmer /
glmer / glmmTMB
|
drmTMB location-scale |
| Structure per node | mean only (one linear predictor) | distributional (mu, sigma,
nu, zi, hu,
rho12) |
| Method lives in |
symbolizer (bundled) |
drmSEM |
| Collator class |
symbolized_psem →
symbolized_model_set
|
symbolized_drm_sem →
symbolized_model_set
|
%~~% / residual covariance |
Residual cov(...) assumption row |
per-node rho12 distributional component |
Reach for piecewiseSEM when each response is adequately
summarised by its mean; reach for drmSEM when a path’s
variance, skew, or zero-inflation is itself part of the
story.
References
Grace, J. B., & Keeley, J. E. (2006). A structural equation model analysis of postfire plant diversity in California shrublands. Ecological Applications, 16(2), 503–514. https://doi.org/10.1890/1051-0761(2006)016%5B0503:ASEMAO%5D2.0.CO;2
Lefcheck, J. S. (2016). piecewiseSEM: Piecewise structural equation modelling in R for ecology, evolution, and systematics. Methods in Ecology and Evolution, 7(5), 573–579. https://doi.org/10.1111/2041-210X.12512
Shipley, B. (2009). Confirmatory path analysis in a generalized multilevel context. Ecology, 90(2), 363–368. https://doi.org/10.1890/08-1034.1