Compiles a theory's constructs and propositions into lavaan model
syntax written for lavaan::sem(). Constructs with measurement
indicators become a latent measurement model (=~), propositions
become structural paths (~) and covariances (~~), and the
covariances the theory rules out are fixed at zero. Comments say what to
check before a fit. The output is deterministic.
Arguments
- theory
A theory object (named list), e.g. from
tf_read().
The structural model
Each proposition becomes a line in file order: a regression
(to ~ from) for causes, increases, decreases and
mediates, and a covariance (from ~~ to) for
associates. A moderates proposition gives the moderator's
main effect, to ~ from, unless a path relation already writes that
line. A comment follows on the product term, which has to be added by hand:
to ~ x:from for an observed predictor x, and
lavaan::sam() or the modsem package when the variables are latent.
The propositions form the graph that tf_implications() reads. Two of its
constructs that are both exogenous, with no proposition pointing into
either, or both terminal, with neither pointing to another construct, are
unrelated by the theory's account unless an association joins them. By
default, lavaan::sem() frees the covariance of two exogenous latent
variables and of two terminal variables, and takes that of two exogenous
observed variables from the data. A fit with those defaults could not refute
the claim. In the modality-switching example, two of the six independencies
tf_implications() derives would go untested. The syntax therefore ends
with a block that fixes each such covariance at zero (a ~~ 0*b), and
deleting the block restores lavaan's defaults. A pair with a moderator keeps
those defaults, because the product term changes the moderator's role.
lavaan takes the covariances of observed exogenous variables from the data
only until a covariance line names one of them. It then treats that variable
as random and fixes at zero each of its covariances that the syntax does not
write. Where a zero line names such a variable, the block therefore ends by
freeing each covariance between two observed exogenous variables that no
association names, when either is a moderator (x ~~ m). The moderator
then keeps lavaan's defaults.
Before fitting
The syntax is a starting point for a fit. Indicator names are the sanitised
measurement entries and must match columns of the data, so entries written
as column names, such as heart_rate, give syntax that fits as it
stands. A comment lists each construct with a single indicator, whose
residual variance lavaan::sem() fixes at zero, so the construct is
treated as measured without error. A comment flags each feedback loop. A
non-recursive model may not be identified (Bollen, 1989), and the
panic-network example is not, so check identification before fitting. Some
constructs have to be re-specified by hand. A manipulation is better entered
as a coded observed variable and a categorical predictor with more than two
levels as dummy variables. A covariate that a measurement entry only
describes needs a variable of its own. Once the block names an observed
predictor, a product term added by hand needs a free covariance with each
observed exogenous variable that a covariance line names. lavaan fixes each
one left unwritten at zero, and each takes a line of its own
(x:m ~~ x).
Names
Every name is one lavaan reads as written. Construct ids that lavaan would
refuse or misread, such as self-efficacy, 1arousal, NA
or its own keyword efa, are renamed. A comment after the header
records each renaming
(# renamed for lavaan: 'self-efficacy' -> self_efficacy). An
indicator is its measurement entry folded and lowercased, with each run of
other characters made one underscore. i_ goes in front when the
result would still be refused, so 7-point Likert rating gives
i_7_point_likert_rating. A comment writes each control character of
an id, and each character outside the Basic Multilingual Plane, as
<U+XXXX>, so that it stays one line that lavaan reads as a comment.
API_SPEC.md section 19 states the rules.
The function stops when two different ids would share a name, since lavaan
would merge them. Such a pair is two constructs, two indicators of one
construct or a construct and an indicator. A construct and an indicator
collide when the construct is measured by its own name or named like an
indicator of another construct. An indicator shared by two constructs, a
cross-loading, is allowed. The Python twin raises ValueError with the
same message text.
References
Bollen, K. A. (1989). Structural equations with latent variables. Wiley. doi:10.1002/9781118619179
See also
tf_implications() for the independencies the zero block encodes.
Examples
# Measurement entries written as column names give syntax that fits as it
# stands. Threat and avoidance both follow from arousal alone, so the
# syntax fixes the covariance of their disturbances at zero.
theory <- tf_theory("demo-1", "A demonstration theory") |>
tf_add_construct("c_arousal", "Arousal", "Bodily activation.",
measurement = c("heart_rate", "arousal_rating")) |>
tf_add_construct("c_threat", "Perceived threat", "Appraised danger.",
measurement = "threat_rating") |>
tf_add_construct("c_avoidance", "Avoidance", "Withdrawal from the trigger.",
measurement = c("avoidance_task", "avoidance_diary")) |>
tf_add_proposition("p1", "c_arousal", "c_threat", "increases") |>
tf_add_proposition("p2", "c_arousal", "c_avoidance", "increases")
cat(tf_compile_sem(theory))
#> # lavaan model generated by theoryforge for demo-1
#> # Written for lavaan::sem(); indicator names are the sanitised measurement entries and must match columns of the data.
#> # Measurement model
#> c_arousal =~ heart_rate + arousal_rating
#> c_threat =~ threat_rating
#> c_avoidance =~ avoidance_task + avoidance_diary
#> # Single-indicator constructs (lavaan fixes the indicator's residual variance at zero, so each is treated as measured without error): c_threat
#> # Structural model
#> c_threat ~ c_arousal
#> c_avoidance ~ c_arousal
#> # Covariances the theory fixes at zero (lavaan::sem() frees them by default; delete this block to restore its defaults)
#> c_threat ~~ 0*c_avoidance