Ting asked me how the “true-score MTMM” model can be estimated in R. This post demonstrates that using ESS 3 data.
Download this Rmd and data used here:
First the theory.
The TS-MTMM model was formulated by Saris & Andrews (1991). It is mathametically equivalent to the regular MTMM model. However, in contrast witht the regular MTMM, it directly yields estimates of the “true score reliability” and “true score validity”. These are interesting because they, in turn connect with the theory of Lord & Novick (1969). In this theory, every survey answer has some expectation, which is called the “true score” (TS). So the “true score” is not literally a true score (“Platonic true score” in Lord & Novick’s terms), but just the expectation of the answer: \[ \tau := E(y) \] and \[ y := \tau + \epsilon \] Note that this is just a definition (hence the \(:=\)) and you can’t really argue with it. The existence of the true score is not, repeat not, an assumption.
What the expectation might be over is a point of some contention. Most commonly, people ask you to imagine that you ask a question, wipe the person’s memory, and “immediately” ask it again. That way, the second time the answer will be different purely due to “measurement error” (things that aren’t interesting). So whatever you’d like to define as “measurement error” is what the expectation is over, and I’ll leave it there. There is more in the Lord & Novick book and Denny Borsboom wrote an interesting dissertation on these concepts.
So anyway,
Measurement error (\(\epsilon\)) is just defined as whatever the current answer’s deviation is from the average answer I’d give.
The TS might be biased, in the sense that each person will, for the same true stimulus, give a different answer: each person has a “response function” that determines how you respond on average to a question compared with other people, for the same underlying feeling. For example, mine might be \(\tau_{\text{Daniel}} = \eta + 1\) and yours \(\tau_{\text{Ting}} = \eta - 1\). That means I’m always biased upwards (by +1 relative to our average) and you downwards (by -1). We’ll give different answers even though we had the same true underlying value, \(\eta\). (The existence of an \(\eta\) is an assumption.) We’ll call this personal bias \(\xi\).
Now, this personal bias might happen on more than one question. For example, Krosnick showed that if you ask people completely unrelated agree-disagree questions, some people tend to “agree” with all of them. Even if they’re contradictory of have no content at all. So that shows that the same \(\xi\) operates on different \(\tau\)’s: \[ y_1 = \tau_1 + \epsilon_1\\ \tau_1 = \eta_1 + \xi \] and \[ y_2 = \tau_2 + \epsilon_2\\ \tau_2 = \eta_2 + \xi \] Where the \(\xi\) is the same (for example because they’re both agree-disagree questions) but the \(\eta\)’s and \(\tau\)’s differ (because they’re on different topics).
Now back to “measurement error”. We’d like to know \(\eta\) but all we got was \(y\). There are two reasons for that:
Actually the name “systematic” is extremely confusing here because most people use that term to mean “bias in the average”. Here it does not mean that but rather “person-specific bias that’s the same across questions”. Another term for \(\xi\) is “method factor” and \(\eta\) is called the “trait factor”.
So now we’re ready to see what is meant by “true score reliability” and “true score validity” (again a super-confusing term but bear with me):
To separate these, the most straightforward method is just to formulate the model above directly as a structural equation model, e.g. in lavaan
. Note that \(\tau\) is a “phantom” latent variable here: it is just defined as trait PLUS method, without any further residual (unique variance). If we copy that in the syntax, we’ll get the right correlations back as standardized loadings. This is shown below.
I’m using the “enjoying life” items from ESS 3. The exact questions are in the main and supplementary questionnaire.
We’ll use tidyverse
to munge data and lavaan
to fit models.
library(tidyverse)
## Loading tidyverse: ggplot2
## Loading tidyverse: tibble
## Loading tidyverse: tidyr
## Loading tidyverse: readr
## Loading tidyverse: purrr
## Loading tidyverse: dplyr
## Conflicts with tidy packages ----------------------------------------------
## filter(): dplyr, stats
## lag(): dplyr, stats
library(lavaan)
## This is lavaan 0.5-22
## lavaan is BETA software! Please report any bugs.
load("ESS3_merged.rdata")
First select the nine variables that form a part of the experiment. The experiment is also described in Melanie’s paper.
mtmm_sub <- ess3.mgd %>%
select(idno, cntry, lrnnew, accdng, plprftr, testb7, testb8, testb9, testb19, testb20, testb21) %>%
filter(cntry == "NL")
mtmm_sub <- mtmm_sub %>% mutate(idno = as.factor(idno)) %>% purrr::map_if(is.numeric, ~ .x - mean(.x, na.rm = TRUE)) %>% as_data_frame
I’m using only Dutch data here because the full dataset has a waiting time that’s too long for my limited patience. But you’re welcome to change this.
As an exploratory move, show the correlatiosn:
mtmm_sub %>% select(-(1:2)) %>% cor(use = "pair") %>% round(2)
## lrnnew accdng plprftr testb7 testb8 testb9 testb19 testb20 testb21
## lrnnew 1.00 0.25 0.20 0.63 0.20 0.19 -0.58 -0.30 -0.23
## accdng 0.25 1.00 0.21 0.24 0.45 0.17 -0.20 -0.42 -0.16
## plprftr 0.20 0.21 1.00 0.21 0.16 0.70 -0.11 -0.14 -0.61
## testb7 0.63 0.24 0.21 1.00 0.29 0.20 NA NA NA
## testb8 0.20 0.45 0.16 0.29 1.00 0.22 NA NA NA
## testb9 0.19 0.17 0.70 0.20 0.22 1.00 NA NA NA
## testb19 -0.58 -0.20 -0.11 NA NA NA 1.00 0.48 0.35
## testb20 -0.30 -0.42 -0.14 NA NA NA 0.48 1.00 0.34
## testb21 -0.23 -0.16 -0.61 NA NA NA 0.35 0.34 1.00
Note that some are NA
, missing, because this is a split-ballot questionnaire: people only got the main questionnaire (version 1 of the questions) plus version 2 OR plus version 3. Nobody got both versions 2 and 3.
A basic sanity check is to forget about the MTMM for a moment and check that a reasonable factor model results from just letting each of the different versions of the same question load on a single factor.
trait_basic <- "
T1 =~ 1*lrnnew + testb7 + testb19
T2 =~ 1*accdng + testb8 + testb20
T3 =~ 1*plprftr+ testb9 + testb21
"
fit_trait_basic <- cfa(trait_basic, data = mtmm_sub, missing = "ml")
## Warning in lav_data_full(data = data, group = group, group.label =
## group.label, : lavaan WARNING: due to missing values, some pairwise
## combinations have less than 10% coverage
summary(fit_trait_basic, standardized = TRUE)
## lavaan (0.5-22) converged normally after 72 iterations
##
## Number of observations 1771
##
## Number of missing patterns 12
##
## Estimator ML
## Minimum Function Test Statistic 108.185
## Degrees of freedom 24
## P-value (Chi-square) 0.000
##
## Parameter Estimates:
##
## Information Observed
## Standard Errors Standard
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## T1 =~
## lrnnew 1.000 0.552 0.699
## testb7 1.304 0.094 13.913 0.000 0.720 0.892
## testb19 -3.657 0.272 -13.441 0.000 -2.019 -0.893
## T2 =~
## accdng 1.000 0.424 0.548
## testb8 1.365 0.139 9.830 0.000 0.579 0.747
## testb20 -4.036 0.386 -10.443 0.000 -1.711 -0.875
## T3 =~
## plprftr 1.000 0.671 0.685
## testb9 1.374 0.117 11.743 0.000 0.922 0.980
## testb21 -3.623 0.334 -10.850 0.000 -2.432 -0.967
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## T1 ~~
## T2 0.129 0.014 9.473 0.000 0.553 0.553
## T3 0.129 0.016 8.142 0.000 0.349 0.349
## T2 ~~
## T3 0.108 0.015 7.418 0.000 0.381 0.381
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .lrnnew 0.001 0.019 0.053 0.958 0.001 0.001
## .testb7 0.015 0.028 0.545 0.586 0.015 0.019
## .testb19 0.037 0.080 0.462 0.644 0.037 0.016
## .accdng 0.002 0.018 0.099 0.921 0.002 0.002
## .testb8 0.003 0.030 0.116 0.908 0.003 0.004
## .testb20 0.032 0.073 0.434 0.665 0.032 0.016
## .plprftr 0.001 0.023 0.032 0.975 0.001 0.001
## .testb9 0.012 0.032 0.388 0.698 0.012 0.013
## .testb21 0.058 0.087 0.664 0.506 0.058 0.023
## T1 0.000 0.000 0.000
## T2 0.000 0.000 0.000
## T3 0.000 0.000 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .lrnnew 0.319 0.021 14.995 0.000 0.319 0.511
## .testb7 0.133 0.037 3.607 0.000 0.133 0.204
## .testb19 1.034 0.301 3.431 0.001 1.034 0.202
## .accdng 0.419 0.021 20.381 0.000 0.419 0.700
## .testb8 0.265 0.041 6.412 0.000 0.265 0.442
## .testb20 0.892 0.280 3.183 0.001 0.892 0.234
## .plprftr 0.509 0.039 12.955 0.000 0.509 0.531
## .testb9 0.035 0.069 0.510 0.610 0.035 0.040
## .testb21 0.415 0.543 0.763 0.445 0.415 0.066
## T1 0.305 0.026 11.839 0.000 1.000 1.000
## T2 0.180 0.021 8.608 0.000 1.000 1.000
## T3 0.450 0.045 10.067 0.000 1.000 1.000
Looks fine to me. Note the third method gives negative loadings because the question answer options are reversed. You can see this in the correlation matrix too. This is just SEM taking care of reverse coding automatically. We can generally ignore signs.
(The 10% coverage warning is due to the missing correlations. It can also be safely ignored in this case.)
Below I show how to fit an MTMM that not a true-score MTMM. This is the standard thing most people do when they MTMM. I’ve left out the second method (so it’s MTMM-1, see Eid 2000).
mtmm_basic <- "
T1 =~ lrnnew + testb7 + testb19
T2 =~ accdng + testb8 + testb20
T3 =~ plprftr+ testb9 + testb21
M1 =~ 1*lrnnew + 1*accdng + 1*plprftr
M3 =~ 1*testb19 + 1*testb20 + 1*testb21
T1~~1*T1
T2~~1*T2
T3~~1*T3
T1~~T2+T3
T2~~T3
"
fit_mtmm_basic <- lavaan(mtmm_basic, data = mtmm_sub, missing = "ml",
auto.fix.first = FALSE, auto.var = TRUE)
## Warning in lav_data_full(data = data, group = group, group.label =
## group.label, : lavaan WARNING: due to missing values, some pairwise
## combinations have less than 10% coverage
summary(fit_mtmm_basic, standardized = TRUE)
## lavaan (0.5-22) converged normally after 66 iterations
##
## Number of observations 1771
##
## Number of missing patterns 12
##
## Estimator ML
## Minimum Function Test Statistic 25.967
## Degrees of freedom 31
## P-value (Chi-square) 0.723
##
## Parameter Estimates:
##
## Information Observed
## Standard Errors Standard
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## T1 =~
## lrnnew 0.585 0.029 20.292 0.000 0.585 0.739
## testb7 0.708 0.041 17.421 0.000 0.708 0.878
## testb19 -1.704 0.115 -14.876 0.000 -1.704 -0.773
## T2 =~
## accdng 0.470 0.032 14.912 0.000 0.470 0.608
## testb8 0.585 0.047 12.550 0.000 0.585 0.758
## testb20 -1.382 0.119 -11.665 0.000 -1.382 -0.714
## T3 =~
## plprftr 0.732 0.043 17.115 0.000 0.732 0.748
## testb9 0.871 0.053 16.358 0.000 0.871 0.927
## testb21 -2.119 0.147 -14.449 0.000 -2.119 -0.845
## M1 =~
## lrnnew 1.000 0.168 0.212
## accdng 1.000 0.168 0.217
## plprftr 1.000 0.168 0.171
## M3 =~
## testb19 1.000 0.891 0.404
## testb20 1.000 0.891 0.460
## testb21 1.000 0.891 0.355
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## T1 ~~
## T2 0.475 0.034 13.931 0.000 0.475 0.475
## T3 0.302 0.032 9.601 0.000 0.302 0.302
## T2 ~~
## T3 0.327 0.035 9.223 0.000 0.327 0.327
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .lrnnew 0.000 0.000 0.000
## .testb7 0.000 0.000 0.000
## .testb19 0.000 0.000 0.000
## .accdng 0.000 0.000 0.000
## .testb8 0.000 0.000 0.000
## .testb20 0.000 0.000 0.000
## .plprftr 0.000 0.000 0.000
## .testb9 0.000 0.000 0.000
## .testb21 0.000 0.000 0.000
## T1 0.000 0.000 0.000
## T2 0.000 0.000 0.000
## T3 0.000 0.000 0.000
## M1 0.000 0.000 0.000
## M3 0.000 0.000 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## T1 1.000 1.000 1.000
## T2 1.000 1.000 1.000
## T3 1.000 1.000 1.000
## .lrnnew 0.256 0.027 9.571 0.000 0.256 0.409
## .testb7 0.150 0.043 3.504 0.000 0.150 0.230
## .testb19 1.160 0.257 4.509 0.000 1.160 0.239
## .accdng 0.349 0.025 13.933 0.000 0.349 0.583
## .testb8 0.254 0.044 5.726 0.000 0.254 0.426
## .testb20 1.040 0.231 4.509 0.000 1.040 0.278
## .plprftr 0.393 0.053 7.413 0.000 0.393 0.411
## .testb9 0.125 0.077 1.609 0.108 0.125 0.141
## .testb21 1.006 0.468 2.150 0.032 1.006 0.160
## M1 0.028 0.010 2.718 0.007 1.000 1.000
## M3 0.793 0.137 5.771 0.000 1.000 1.000
Here is the true-score MTMM model. The first part defines the observed variables as equal to true score (e.g. lrnnew_TS =~ 1*lrnnew
), except for random error variance (e.g. lrnnew~~lrnnew
). The second part defines each true score to exactly equal trait PLUS method. E.g. T1 =~ lrnnew_TS
and M1 =~ 1*lrnnew_TS
and there’s no further variance (auto.var = FALSE
in the call below). The methods don’t correlate but the traits do.
mtmm_ts <- "
lrnnew_TS =~ 1*lrnnew
accdng_TS =~ 1*accdng
plprftr_TS =~ 1*plprftr
testb7_TS =~ 1*testb7
testb8_TS =~ 1*testb8
testb9_TS =~ 1*testb9
testb19_TS =~ 1*testb19
testb20_TS =~ 1*testb20
testb21_TS =~ 1*testb21
T1 =~ lrnnew_TS + testb7_TS + testb19_TS
T2 =~ accdng_TS + testb8_TS + testb20_TS
T3 =~ plprftr_TS + testb9_TS + testb21_TS
M1 =~ 1*lrnnew_TS + 1*accdng_TS + 1*plprftr_TS
M3 =~ 1*testb19_TS + 1*testb20_TS + 1*testb21_TS
lrnnew~~lrnnew
accdng~~accdng
plprftr~~plprftr
testb7 ~~testb7
testb8 ~~testb8
testb9 ~~testb9
testb19~~testb19
testb20~~testb20
testb21~~testb21
M1~~M1
M3~~M3
T1~~1*T1
T2~~1*T2
T3~~1*T3
T1~~T2+T3
T2~~T3
"
fit_mtmm_ts <- lavaan(mtmm_ts, data = mtmm_sub, missing = "ml",
auto.fix.first = FALSE, auto.var = FALSE)
## Warning in lav_data_full(data = data, group = group, group.label =
## group.label, : lavaan WARNING: due to missing values, some pairwise
## combinations have less than 10% coverage
The model estimates are shown below. Note they’re different than for the run-of-the-mill MTMM. But the model fit and df are the same, demonstrating mathematical equivalence:
fit_mtmm_ts
## lavaan (0.5-22) converged normally after 66 iterations
##
## Number of observations 1771
##
## Number of missing patterns 12
##
## Estimator ML
## Minimum Function Test Statistic 25.967
## Degrees of freedom 31
## P-value (Chi-square) 0.723
fit_mtmm_basic
## lavaan (0.5-22) converged normally after 66 iterations
##
## Number of observations 1771
##
## Number of missing patterns 12
##
## Estimator ML
## Minimum Function Test Statistic 25.967
## Degrees of freedom 31
## P-value (Chi-square) 0.723
As per usual, the “true score validity” (don’t get me started on this term) estimates are pretty high, e.g. 0.961 for lrnnew
and 1 by definition for the second method, but the reliabilities are around 0.8, e.g. standardized lrnnew_TS =~ lrnnew
is 0.769. This is typical in ESS.
summary(fit_mtmm_ts, standardized = TRUE)
## lavaan (0.5-22) converged normally after 66 iterations
##
## Number of observations 1771
##
## Number of missing patterns 12
##
## Estimator ML
## Minimum Function Test Statistic 25.967
## Degrees of freedom 31
## P-value (Chi-square) 0.723
##
## Parameter Estimates:
##
## Information Observed
## Standard Errors Standard
##
## Latent Variables:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## lrnnew_TS =~
## lrnnew 1.000 0.608 0.769
## accdng_TS =~
## accdng 1.000 0.499 0.646
## plprftr_TS =~
## plprftr 1.000 0.751 0.768
## testb7_TS =~
## testb7 1.000 0.708 0.878
## testb8_TS =~
## testb8 1.000 0.585 0.758
## testb9_TS =~
## testb9 1.000 0.871 0.927
## testb19_TS =~
## testb19 1.000 1.923 0.873
## testb20_TS =~
## testb20 1.000 1.644 0.850
## testb21_TS =~
## testb21 1.000 2.299 0.917
## T1 =~
## lrnnew_TS 0.585 0.029 20.292 0.000 0.961 0.961
## testb7_TS 0.708 0.041 17.421 0.000 1.000 1.000
## testb19_TS -1.704 0.115 -14.876 0.000 -0.886 -0.886
## T2 =~
## accdng_TS 0.470 0.032 14.912 0.000 0.942 0.942
## testb8_TS 0.585 0.047 12.550 0.000 1.000 1.000
## testb20_TS -1.382 0.119 -11.665 0.000 -0.841 -0.841
## T3 =~
## plprftr_TS 0.732 0.043 17.115 0.000 0.975 0.975
## testb9_TS 0.871 0.053 16.358 0.000 1.000 1.000
## testb21_TS -2.119 0.147 -14.449 0.000 -0.922 -0.922
## M1 =~
## lrnnew_TS 1.000 0.276 0.276
## accdng_TS 1.000 0.336 0.336
## plprftr_TS 1.000 0.223 0.223
## M3 =~
## testb19_TS 1.000 0.463 0.463
## testb20_TS 1.000 0.542 0.542
## testb21_TS 1.000 0.387 0.387
##
## Covariances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## T1 ~~
## T2 0.475 0.034 13.931 0.000 0.475 0.475
## T3 0.302 0.032 9.601 0.000 0.302 0.302
## T2 ~~
## T3 0.327 0.035 9.223 0.000 0.327 0.327
##
## Intercepts:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .lrnnew 0.000 0.000 0.000
## .accdng 0.000 0.000 0.000
## .plprftr 0.000 0.000 0.000
## .testb7 0.000 0.000 0.000
## .testb8 0.000 0.000 0.000
## .testb9 0.000 0.000 0.000
## .testb19 0.000 0.000 0.000
## .testb20 0.000 0.000 0.000
## .testb21 0.000 0.000 0.000
## lrnnew_TS 0.000 0.000 0.000
## accdng_TS 0.000 0.000 0.000
## plprftr_TS 0.000 0.000 0.000
## testb7_TS 0.000 0.000 0.000
## testb8_TS 0.000 0.000 0.000
## testb9_TS 0.000 0.000 0.000
## testb19_TS 0.000 0.000 0.000
## testb20_TS 0.000 0.000 0.000
## testb21_TS 0.000 0.000 0.000
## T1 0.000 0.000 0.000
## T2 0.000 0.000 0.000
## T3 0.000 0.000 0.000
## M1 0.000 0.000 0.000
## M3 0.000 0.000 0.000
##
## Variances:
## Estimate Std.Err z-value P(>|z|) Std.lv Std.all
## .lrnnew 0.256 0.027 9.571 0.000 0.256 0.409
## .accdng 0.349 0.025 13.933 0.000 0.349 0.583
## .plprftr 0.393 0.053 7.413 0.000 0.393 0.411
## .testb7 0.150 0.043 3.504 0.000 0.150 0.230
## .testb8 0.254 0.044 5.726 0.000 0.254 0.426
## .testb9 0.125 0.077 1.609 0.108 0.125 0.141
## .testb19 1.160 0.257 4.509 0.000 1.160 0.239
## .testb20 1.040 0.231 4.509 0.000 1.040 0.278
## .testb21 1.006 0.468 2.150 0.032 1.006 0.160
## M1 0.028 0.010 2.718 0.007 1.000 1.000
## M3 0.793 0.137 5.771 0.000 1.000 1.000
## T1 1.000 1.000 1.000
## T2 1.000 1.000 1.000
## T3 1.000 1.000 1.000
## lrnnew_TS 0.000 0.000 0.000
## accdng_TS 0.000 0.000 0.000
## plprftr_TS 0.000 0.000 0.000
## testb7_TS 0.000 0.000 0.000
## testb8_TS 0.000 0.000 0.000
## testb9_TS 0.000 0.000 0.000
## testb19_TS 0.000 0.000 0.000
## testb20_TS 0.000 0.000 0.000
## testb21_TS 0.000 0.000 0.000
The reliability coefficients, validity coefficients, and method effects are above and also here:
std_ts <- standardizedsolution(fit_mtmm_ts) %>% filter(op == "=~") %>% select(1:4)
std_ts
## lhs op rhs est.std
## 1 lrnnew_TS =~ lrnnew 0.7689815
## 2 accdng_TS =~ accdng 0.6458489
## 3 plprftr_TS =~ plprftr 0.7675850
## 4 testb7_TS =~ testb7 0.8777321
## 5 testb8_TS =~ testb8 0.7576548
## 6 testb9_TS =~ testb9 0.9268046
## 7 testb19_TS =~ testb19 0.8725297
## 8 testb20_TS =~ testb20 0.8498356
## 9 testb21_TS =~ testb21 0.9165255
## 10 T1 =~ lrnnew_TS 0.9612032
## 11 T1 =~ testb7_TS 1.0000000
## 12 T1 =~ testb19_TS -0.8862848
## 13 T2 =~ accdng_TS 0.9418774
## 14 T2 =~ testb8_TS 1.0000000
## 15 T2 =~ testb20_TS -0.8406231
## 16 T3 =~ plprftr_TS 0.9747377
## 17 T3 =~ testb9_TS 1.0000000
## 18 T3 =~ testb21_TS -0.9218924
## 19 M1 =~ lrnnew_TS 0.2758413
## 20 M1 =~ accdng_TS 0.3359567
## 21 M1 =~ plprftr_TS 0.2233529
## 22 M3 =~ testb19_TS 0.4631407
## 23 M3 =~ testb20_TS 0.5416206
## 24 M3 =~ testb21_TS 0.3874459
std_basic <- standardizedsolution(fit_mtmm_basic) %>%
filter(op == "=~") %>% select(1:4)
std_basic
## lhs op rhs est.std
## 1 T1 =~ lrnnew 0.7391475
## 2 T1 =~ testb7 0.8777321
## 3 T1 =~ testb19 -0.7733098
## 4 T2 =~ accdng 0.6083105
## 5 T2 =~ testb8 0.7576548
## 6 T2 =~ testb20 -0.7143914
## 7 T3 =~ plprftr 0.7481940
## 8 T3 =~ testb9 0.9268046
## 9 T3 =~ testb21 -0.8449379
## 10 M1 =~ lrnnew 0.2121169
## 11 M1 =~ accdng 0.2169773
## 12 M1 =~ plprftr 0.1714423
## 13 M3 =~ testb19 0.4041040
## 14 M3 =~ testb20 0.4602884
## 15 M3 =~ testb21 0.3551041
The standardized coefficients of one model can be calculated from the other. For example, starting from the TS solution, we get the “basic” trait and method loadings by multiplying reliability with validity and method effect, respectively. Here’s an example for the lrnnew
variable (T1 and M1):
reliability_coefficient <- std_ts$est.std[std_ts$rhs == "lrnnew"]
val_and_met_coefficients <- std_ts$est.std[std_ts$rhs == "lrnnew_TS"]
reliability_coefficient * val_and_met_coefficients
## [1] 0.7391475 0.2121169
This can be verified by looking at the standardized solution of the “basic” model directly:
std_basic %>% filter(rhs == "lrnnew")
## lhs op rhs est.std
## 1 T1 =~ lrnnew 0.7391475
## 2 M1 =~ lrnnew 0.2121169
Note these are identical even though obtained from different models.
Of course, we can also reverse the calculations: starting from the “basic” solution, calculate the standardized coefficients of the TS model without actually estimating that model. Calling the standardized trait and method factor loading from the basic model \(\lambda\) and \(\gamma\) respectively, we have \(\lambda = r \cdot v\), \(\gamma = r \cdot m\), and since \(\tau\) doesn’t contain any unique variance, \(m^2 + v^2 = 1\), implying that \[ r = \sqrt{\lambda^2 + \gamma^2},\\ v = \lambda / r, \text{and}\\ m = \gamma / r. \]
basic_coefs <- std_basic %>% filter(rhs == "lrnnew") %>% .$est.std
lambda <- basic_coefs[1]
gamma <- basic_coefs[2]
r <- sqrt(lambda^2 + gamma^2)
v <- lambda / r
m <- gamma / r
c(r=r, v=v, m=m)
## r v m
## 0.7689815 0.9612032 0.2758413
which are indeed identical to reliability_coefficient
and val_and_met_coefficients
obtained from the basic model:
reliability_coefficient
## [1] 0.7689815
val_and_met_coefficients
## [1] 0.9612032 0.2758413