Op deze pagina vind je een demonstratie van een statistische techniek aan de hand van een voorbeeld.

Meer informatie over hoe je deze pagina kan gebruiken vind je in deze handleiding.

De analyse gebeurt met behulp van R en RStudio. Een inleiding tot deze software vind je hier.


Referentie

Het voorbeeld op deze pagina is afkomstig uit een studie van Schmitz, Luminet, Klein, Morbée, Van den Bergh, Van Oost, Waterschoot, Yzerbyt & Van Steenkiste (2022). Om praktische en didactische redenen werden de hypothesen en de data enigszins aangepast.



1 Doel

Structural Equation Modelling (SEM)1 is een techniek die je kan gebruiken om de onderliggende mechanismen bloot te leggen die een fenomeen kunnen verklaren. In die zin lijkt het sterk op padanalyse.

Een belangrijk extra element is dat er bij SEM ook latente variabelen in het model aanwezig kunnen zijn. Dat zijn variabelen die niet rechtstreeks meetbaar zijn. Ze moeten indirect gemeten worden aan de hand van een reeks “indicatoren”, bv. vragen (“items”) in een enquête. Je mag er natuurlijk niet zomaar vanuit gaan dat een reeks indicatoren samen een goede meting zijn voor een bepaalde latente variabele. Dat moet je nagaan. Je moet bevestigen dat een vooropgestelde factorstructuur (= een aantal latente variabelen of “factoren”, elk met hun eigen set van indicatoren) daadwerkelijk terug te vinden is in de data. Je moet met andere woorden een confirmatorische factoranalyse (CFA) uitvoeren.

Met SEM ben je in staat om in één model padanalyse te combineren met confirmatorische factoranalyse.


Wat kan je leren uit SEM?

Met een SEM kan je verschillende zaken proberen te achterhalen. Belangrijke doelen van de analyse zijn

  • om te achterhalen of het model in zijn geheel goed fit met de data,
  • om alle coëfficiënten te schatten en
  • om te toetsen of deze coëfficiënten significant verschillen van nul.

De volgorde is hier belangrijk! Het heeft geen zin om conclusies te trekken over het al dan niet significant zijn van de afzonderlijke coëfficiënten in een model dat in zijn geheel niet goed fit met de data.


De studie van Schmitz et al. (2022)

Schmitz et al. (2022) probeerden de vaccinatie-intentie te verklaren. Dat is de intentie die mensen al dan niet hadden om zich te laten vaccineren tegen het coronavirus.2 Op basis van eerder onderzoek en van de Self-Determination Theory vermoedden de onderzoekers dat verschillende soorten motivatie een rol zouden kunnen spelen in de vaccinatie-intentie. Daarnaast zouden ook percepties van risico’s met betrekking tot infectie en de pandemie een effect kunnen hebben.

Op basis van deze hypothesen kwamen de onderzoekers tot een specifiek systeem van variabelen waarvan ze dachten dat het terug te vinden zou zijn in de data. Dit systeem van variabelen vind je in het diagram hieronder, binnen de rode stippellijn. Het wordt ook het “structurele gedeelte” van het SEM-model genoemd.

Belangrijk is dat de meeste variabelen “latent” zijn. Ze zijn met andere woorden niet rechtstreeks meetbaar. Ze moeten worden gemeten door verschillende indicatoren te combineren (in dit geval items in een vragenlijst). Er is naast het structurele gedeelte dus ook een “meetgedeelte” of “meetmodel” (in het Engels: “measurement model”). Dat is alles wat binnen de blauwe stippellijn staat in het diagram hieronder.

Klik op het diagram om het te vergroten.



Overzicht variabelen

Hieronder vind je een overzicht van alle variabelen in dit model. De linkerkolom bevat de variabelen die samen het structurele gedeelte van het model uitmaken. De tabel bevat de Engelse naam uit het artikel, een Nederlandse vertaling en de naam die de onderzoekers aan de variabele hebben gegeven in R.

Bij elke latente variabele vind je ook de indicatoren terug die gezamenlijk die variabele zouden meten. Hiervan geven we enkel de naam in R weer.

Toon/verberg het overzicht
Engels Nederlands Naam in R Indicatoren
Pandemic-related health concerns Pandemie-gerelateerde zorgen over gezondheid worries
  • worries_relatives
  • worries_health
Infection-related risk perception Infectie-gerelateerde perceptie van risico risks
  • risk_other
  • risk_self
Autonomous motivation Autonome motivatie motv_ident
  • motv_vaccin_ident_2
  • motv-vaccin_ident_3
  • motv_vaccin_ident_4
Controlled motivation Gecontroleerde motivatie motv_exter
  • motv_vaccin_exter_1
  • motv_vaccin_exter_3
Distrust-based amotivation Demotivatie door wantrouwen motv_distr
  • motv_vaccin_distrust_1
  • motv_vaccin_distrust_4
  • motv_vaccin_distrust_5
Effort-based amotivation Demotivatie door inspanning motv_effrt
  • motv_vaccin_effort_1
  • motv_vaccin_effort_2
  • motv_vaccin_effort_3
Vaccination intention Vaccinatie-intentie vaccination_intention Niet van toepassing3



2 Packages en dataset

Voor de analyse van een SEM kan je functies uit het R-package lavaan gebruiken.

install.packages('lavaan') # eenmalig installeren

library(lavaan) # bij de start van elke sessie


De dataset bevat gegevens van 32 variabelen geobserveerd bij 8887 individuen.

Deze dataset kan je inladen met read.csv(). De data kan je best meteen in een object vaccin onderbrengen zodat je die later makkelijk opnieuw kan oproepen.

vaccin <- read.csv('https://statlas.ugent.be/datasets/vaccin.csv')


Met str() krijg je een opsomming van alle variabelen in de dataset. Je vindt er ook telkens bij om welk datatype het gaat. Afhankelijk van het datatype zal je sommige functies wel of juist niet kunnen gebruiken. Het heeft bijvoorbeeld geen zin om een gemiddelde te berekenen van een variabele van type chr.

str(vaccin)
'data.frame':   8887 obs. of  32 variables:
 $ X                     : int  1 2 3 4 5 6 7 8 9 10 ...
 $ date                  : chr  "2020-11-25" "2020-11-25" "2020-11-25" "2020-11-25" ...
 $ age                   : int  70 40 30 51 34 63 65 25 54 66 ...
 $ gender                : chr  "Female" "Female" "Female" "Male" ...
 $ comorbidity           : chr  "No" "No" "No" "No" ...
 $ comorbidity.N_Y       : int  0 0 0 0 0 0 0 0 0 0 ...
 $ edu.cat               : chr  "Master" "Secundary superior" "Master" "Bachelor technical" ...
 $ edu                   : int  7 4 7 5 4 4 3 5 5 3 ...
 $ vaccination_intention : int  5 2 3 1 1 5 5 3 5 4 ...
 $ vaccin_campaign       : int  3 1 3 2 1 3 4 4 4 3 ...
 $ vaccin_encouragement  : int  5 2 4 3 3 5 4 4 4 4 ...
 $ risk_probability_self : int  2 3 3 1 2 3 3 2 2 3 ...
 $ risk_consequence_self : int  3 2 2 3 2 5 5 2 3 3 ...
 $ risk_self             : num  1.83 1.83 1.83 1.33 1.5 ...
 $ risk_probability_other: int  3 4 3 4 4 3 4 4 3 3 ...
 $ risk_consequence_other: int  4 3 3 3 4 5 5 4 3 3 ...
 $ risk_other            : num  2.83 2.83 2.33 2.83 3.5 ...
 $ worries_relatives     : int  3 3 2 2 4 3 5 4 4 3 ...
 $ worries_health        : int  3 1 1 2 3 2 4 4 2 3 ...
 $ motv_vaccin_ident_2   : int  5 2 3 2 1 5 5 2 5 4 ...
 $ motv_vaccin_ident_3   : int  5 2 4 2 1 5 5 3 5 4 ...
 $ motv_vaccin_ident_4   : int  5 2 4 2 1 5 5 3 5 4 ...
 $ motv_vaccin_exter_1   : int  1 3 2 5 2 4 2 4 2 2 ...
 $ motv_vaccin_exter_2   : int  1 3 2 3 1 3 1 4 2 2 ...
 $ motv_vaccin_exter_3   : int  3 3 3 4 2 2 3 4 2 2 ...
 $ motv_vaccin_distrust_1: int  1 4 5 4 4 4 4 5 2 3 ...
 $ motv_vaccin_distrust_4: int  1 5 4 3 3 3 2 4 3 2 ...
 $ motv_vaccin_distrust_5: int  1 5 5 3 3 3 2 5 3 2 ...
 $ motv_vaccin_effort_1  : int  1 2 2 2 3 1 1 1 1 2 ...
 $ motv_vaccin_effort_2  : int  1 2 1 1 2 1 1 1 1 2 ...
 $ motv_vaccin_effort_3  : int  1 1 1 1 1 1 1 1 1 2 ...
 $ gender.M_F            : int  1 1 1 0 1 1 1 1 1 1 ...

In de output van str() kan je zien dat er inderdaad 32 variabelen zijn met telkens 8887 observaties.



3 Analyses vooraf

Het is een goed idee om niet overhaast te werk te gaan. Specifieker, het is aan te raden om eerst het meetmodel goed te krijgen om pas in een volgende fase het volledige SEM-model (meetmodel + structuurmodel) te fitten. Waarom? De reden om een SEM-analyse uit te voeren is op de eerste plaats om na te gaan of het structureel model goed fit met de data. Daarin zitten namelijk de wetenschappelijke probleemstelling en hypothesen vervat. Als je meteen een SEM-model zou fitten en de fit blijkt niet goed te zijn, dan kan je niet zomaar achterhalen of het ligt aan een slecht fittend meetmodel of een slecht fittend structureel model.

Welke stappen kan je ondernemen om het meetmodel goed te krijgen?

  • Om het meetmodel goed te krijgen kan je bijvoorbeeld eerst een CFA uitvoeren op elke latente variabele afzonderlijk. Voor latente variabelen waarvoor een gevestigd, algemeen aanvaard meetinstrument bestaat, mag je er meestal wel van uitgaan dat de items een goede meting zijn van de latente variabele. Als je echter zelf een meetinstrument hebt samengesteld om een latente variabele te meten, dan kan het zeker de moeite lonen om eerst, vooraf, een CFA uit te voeren voor die latente variabele apart.

  • Nadat je vrij zeker bent van elke latente variabele afzonderlijk loont het nog de moeite om een CFA uit te voeren voor alle latente variabelen tegelijk, in één meetmodel. Schmitz et al. (2020) doen dit zelfs verschillende keren. Ze construeren onder meer een meetmodel waarbij er slechts één latente variabele “motivatie” is, die wordt gemeten door alle indicatoren \(Y_5\) tot en met \(Y_{16}\). Dit meetmodel vergelijken ze met een meetmodel waarbij er vier afzonderlijke latente variabelen m.b.t. motivatie zijn (zoals in het diagram hierboven). Dit tweede model blijkt beter te fitten met de data dan het eerste, waardoor de onderzoekers meer zekerheid inbouwen dat er wel degelijk vier duidelijk te onderscheiden latente variabelen bestaan.

In de demonstratie hieronder tonen we enkel de SEM-analyse. Meer info over CFA, inclusief een demonstratie, vind je hier.

Het is dus aan te raden om pas aan de “echte” SEM-analyse te beginnen nadat je enige zekerheid hebt gekregen over het meetmodel. Maar zelfs die SEM-analyse zou je stapsgewijs kunnen uitvoeren, te beginnen met een deel van het model om geleidelijk aan uit te breiden en samen te voegen in een groter geheel. Dat kan helpen om oorzaken van eventuele slechte fit te identificeren.



4 Modelspecificatie met lavaan

Verbanden tussen variabelen specifiëren

Eerst leg je vast welke verbanden je model moet bevatten tussen welke variabelen. Die structuur stop je in een object, hier mijnSEM genaamd. Vergeet de aanhalingstekens ' helemaal vooraan en achteraan niet. De modelsyntax vormt immers een “string” (een stuk tekst) in R. Deze modelspecificatie zal in een volgende fase aan de functie sem() worden gegeven, die schattingen van alle modelparameters zal teruggeven. Straks meer daarover.

In het model hieronder zijn verschillende soorten verbanden opgenomen, elk met een eigen syntax. Die zullen we een voor een bespreken.4

mijnSEM <- '
# Latente variabelen
  worries =~ worries_relatives + worries_health
  risks =~ risk_other + risk_self
  motv_ident =~ motv_vaccin_ident_2 + motv_vaccin_ident_3 + motv_vaccin_ident_4
  motv_exter =~ motv_vaccin_exter_1 + motv_vaccin_exter_3
  motv_distr =~ motv_vaccin_distrust_1 + motv_vaccin_distrust_4 + motv_vaccin_distrust_5
  motv_effrt =~ motv_vaccin_effort_1 + motv_vaccin_effort_2 + motv_vaccin_effort_3

# Factor covarianties
  worries ~~ risks
  motv_ident ~~ motv_exter
  motv_ident ~~ motv_distr
  motv_ident ~~ motv_effrt
  motv_exter ~~ motv_distr
  motv_exter ~~ motv_effrt
  motv_distr ~~ motv_effrt
  
# Directe effecten
  motv_ident ~ worries + risks
  motv_exter ~ worries + risks
  motv_distr ~ worries + risks
  motv_effrt ~ worries + risks
  vaccination_intention ~ worries + risks + motv_ident + motv_exter + motv_distr + motv_effrt
'


Latente variabelen

Hier worden verbanden tussen indicatoren en potentiële factoren vastgelegd (i.e. de “factorstructuur”).

Deze code weerspiegelt dus de hypothesen die de onderzoekers bij voorbaat hadden over welke indicatoren welke latente variabele meten. Het gelijkheidsteken = in combinatie met een tilde ~ kan je lezen als “wordt gemeten door”. Dus kan je de eerste regel hierboven begrijpen als “de latente variabele worries wordt gemeten door de indicatoren worries_relatives en worries_health.5


Factor covarianties

Covarianties tussen factoren kunnen ook worden geschat en weergegeven. Als je niet uitdrukkelijk de covarianties neerschrijft in de modelspecificatie, dan zullen enkel covarianties tussen de zuiver exogene variabelen6 automatisch worden opgenomen, i.c. worries en risks.

Covarianties tussen mediatoren (hier: motv_ident, motv_exter, motv_distr en motv_effrt) worden dus niet standaard opgenomen. Hier is het aan de onderzoeker om te bepalen of er goede inhoudelijke/theoretische redenen zijn om covarianties tussen (sommige) mediatoren toe te laten. Vaak specifiëren onderzoekers covarianties tussen alle mediatoren. Merk op dat het toelaten van zulke covarianties meteen ook een “toegeving” inhoudt dat het model en de onderliggende theorie erg onvolledig zijn: dan zijn er namelijk nog verbanden tussen de mediatoren die op geen enkele manier in dit model worden verklaard.

In de output bij dit voorbeeld zal je schattingen van covarianties zien tussen de exogene factoren worries en risks en ook tussen alle mediatoren. In de plot die het model visualiseert (zie eerder) zie je deze covarianties getekend als dubbele pijlen met stippellijn.


Directe effecten

Directe effecten zijn de effecten in het structurele gedeelte van het model. Elk direct effect stemt overeen met een enkele pijl in het rode gedeelte van het diagram van hierboven. Een voorbeeld is het effect van autonome motivatie op de vaccinatie-intentie. Deze effecten codeer je met de operator ~.

In de modelspecificatie hierboven is er een lijn code per afhankelijke variabele. Als je dat wenst kan je de code wat meer opsplitsen. Voor de effecten op bijvoorbeeld motv_ident wordt dat dan:

'
motv_ident ~ worries
motv_ident ~ risks
'


Parameters schatten

Nadat de specificatie van het model is afgerond, laat je R alle parameters schatten met de functie sem(). Deze functie heeft de specificatie van het model nodig (object mijnSEM) en de data.7

fit <- sem(mijnSEM, data = vaccin)



5 Resultaten tonen

Om de resultaten te zien gebruik je de functie summary(). Het object fit kan uiteraard niet ontbreken. Met de argumenten kan je verder bepalen wat je precies in de output wil zien. (De interpretatie van de output behandelen we verderop.)

summary(fit, fit.measures = TRUE, standardized = TRUE, rsquare = TRUE)


De argumenten geven enkele keuzes aan die je kan maken bij de summary()-functie:


fit.measures = TRUE

Indien fit.measures = FALSE - wat standaard het geval is - wordt enkel een \(\chi^2\)-toets uitgevoerd als maat voor de fit van het model. Deze toets is niet altijd voldoende om de model fit te beoordelen, zeker bij hele grote steekproeven. In Schmitz et al. (2020) zijn er 8887 respondenten, wat veel is.

Het is aangeraden om sowieso bijkomende fitmaten te laten berekenen en te rapporteren in je publicatie, zoals CFI/TLI, RMSEA en SRMR. Al deze fitmaten zijn op een verschillende manier gevoelig voor misspecificatie van het model. Voeg deze fitmaten toe door eenvoudigweg het argument fit.measures = TRUE te gebruiken. De lezer van je onderzoek kan zo zelf een oordeel vellen.

Meer uitgebreide uitleg over deze fitmaten (en meer) vind je in hoofdstuk 12 van

→ Kline, R. B. (2015). Principles and practice of structural equation modeling (Fourth Edition). New York: Guilford Press.



standardized = TRUE

Het is niet zomaar mogelijk om de padcoëfficiënten onderling te vergelijken en om uitspraken te doen over welke groter of kleiner is. Dat komt omdat de getalwaarde ervan afhangt van de meeteenheid van de variabelen.

Met het argument standardized = TRUE krijg je een extra kolom Std.all met schattingen voor gestandaardiseerde padcoëfficiënten. Wanneer er nu twee pijlen in eenzelfde variabele toekomen, zoals bij motv_ident, kan je wel de grootte van de verschillende effecten vergelijken.



rsquare = TRUE

Met dit argument kan je een schatting van de proportie verklaarde variantie (m.a.w. de determinatiecoëfficiënt \(R^2\)) van elke endogene variabele laten berekenen.



6 Resultaten interpreteren

Wat kan je leren uit de output van summary(fit, fit.measures = TRUE, standardized = TRUE, rsquare = TRUE)?


lavaan 0.6.16 ended normally after 70 iterations

  Estimator                                         ML
  Optimization method                           NLMINB
  Number of model parameters                        52

  Number of observations                          8887

Model Test User Model:
                                                      
  Test statistic                              1974.297
  Degrees of freedom                                84
  P-value (Chi-square)                           0.000

Model Test Baseline Model:

  Test statistic                             92690.428
  Degrees of freedom                               120
  P-value                                        0.000

User Model versus Baseline Model:

  Comparative Fit Index (CFI)                    0.980
  Tucker-Lewis Index (TLI)                       0.971

Loglikelihood and Information Criteria:

  Loglikelihood user model (H0)            -173231.586
  Loglikelihood unrestricted model (H1)    -172244.438
                                                      
  Akaike (AIC)                              346567.172
  Bayesian (BIC)                            346935.974
  Sample-size adjusted Bayesian (SABIC)     346770.727

Root Mean Square Error of Approximation:

  RMSEA                                          0.050
  90 Percent confidence interval - lower         0.048
  90 Percent confidence interval - upper         0.052
  P-value H_0: RMSEA <= 0.050                    0.388
  P-value H_0: RMSEA >= 0.080                    0.000

Standardized Root Mean Square Residual:

  SRMR                                           0.036

Parameter Estimates:

  Standard errors                             Standard
  Information                                 Expected
  Information saturated (h1) model          Structured

Latent Variables:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
  worries =~                                                            
    worries_reltvs    1.000                               0.776    0.710
    worries_health    1.058    0.024   43.366    0.000    0.820    0.698
  risks =~                                                              
    risk_other        1.000                               0.620    0.725
    risk_self         0.730    0.017   43.248    0.000    0.453    0.657
  motv_ident =~                                                         
    mtv_vccn_dnt_2    1.000                               1.373    0.952
    mtv_vccn_dnt_3    0.924    0.005  174.332    0.000    1.269    0.930
    mtv_vccn_dnt_4    0.854    0.006  138.195    0.000    1.173    0.868
  motv_exter =~                                                         
    mtv_vccn_xtr_1    1.000                               1.320    0.999
    mtv_vccn_xtr_3    0.488    0.014   35.062    0.000    0.644    0.524
  motv_distr =~                                                         
    mtv_vccn_dst_1    1.000                               1.031    0.805
    mtv_vccn_dst_4    1.201    0.011  106.600    0.000    1.238    0.933
    mtv_vccn_dst_5    1.179    0.012   98.508    0.000    1.215    0.880
  motv_effrt =~                                                         
    mtv_vccn_ffr_1    1.000                               0.634    0.782
    mtv_vccn_ffr_2    1.162    0.018   66.327    0.000    0.737    0.857
    mtv_vccn_ffr_3    0.794    0.015   52.715    0.000    0.504    0.595

Regressions:
                          Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
  motv_ident ~                                                                 
    worries                  0.146    0.049    2.960    0.003    0.082    0.082
    risks                    0.847    0.064   13.169    0.000    0.383    0.383
  motv_exter ~                                                                 
    worries                  0.281    0.050    5.562    0.000    0.165    0.165
    risks                   -0.869    0.066  -13.229    0.000   -0.408   -0.408
  motv_distr ~                                                                 
    worries                  0.129    0.040    3.258    0.001    0.097    0.097
    risks                   -0.548    0.051  -10.709    0.000   -0.330   -0.330
  motv_effrt ~                                                                 
    worries                  0.097    0.026    3.756    0.000    0.118    0.118
    risks                   -0.310    0.033   -9.389    0.000   -0.303   -0.303
  vaccination_intention ~                                                      
    worries                  0.006    0.027    0.229    0.819    0.005    0.003
    risks                    0.063    0.034    1.843    0.065    0.039    0.027
    motv_ident               0.661    0.015   44.354    0.000    0.908    0.619
    motv_exter               0.028    0.008    3.701    0.000    0.037    0.025
    motv_distr              -0.451    0.019  -23.719    0.000   -0.465   -0.317
    motv_effrt               0.033    0.016    2.034    0.042    0.021    0.014

Covariances:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
  worries ~~                                                            
    risks             0.365    0.010   35.857    0.000    0.759    0.759
 .motv_ident ~~                                                         
   .motv_exter       -0.811    0.021  -37.766    0.000   -0.526   -0.526
   .motv_distr       -1.035    0.021  -49.342    0.000   -0.848   -0.848
   .motv_effrt       -0.364    0.011  -31.987    0.000   -0.480   -0.480
 .motv_exter ~~                                                         
   .motv_distr        0.696    0.018   38.303    0.000    0.557    0.557
   .motv_effrt        0.276    0.011   25.499    0.000    0.356    0.356
 .motv_distr ~~                                                         
   .motv_effrt        0.303    0.009   32.001    0.000    0.494    0.494

Variances:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
   .worries_reltvs    0.590    0.015   38.915    0.000    0.590    0.495
   .worries_health    0.707    0.017   40.660    0.000    0.707    0.512
   .risk_other        0.348    0.009   36.840    0.000    0.348    0.475
   .risk_self         0.270    0.006   46.250    0.000    0.270    0.568
   .mtv_vccn_dnt_2    0.195    0.005   38.045    0.000    0.195    0.094
   .mtv_vccn_dnt_3    0.252    0.005   47.315    0.000    0.252    0.135
   .mtv_vccn_dnt_4    0.450    0.008   57.848    0.000    0.450    0.247
   .mtv_vccn_xtr_1    0.003    0.040    0.067    0.947    0.003    0.002
   .mtv_vccn_xtr_3    1.097    0.019   57.828    0.000    1.097    0.725
   .mtv_vccn_dst_1    0.579    0.010   58.340    0.000    0.579    0.353
   .mtv_vccn_dst_4    0.227    0.006   35.411    0.000    0.227    0.129
   .mtv_vccn_dst_5    0.429    0.009   50.347    0.000    0.429    0.225
   .mtv_vccn_ffr_1    0.255    0.006   41.540    0.000    0.255    0.388
   .mtv_vccn_ffr_2    0.196    0.007   27.977    0.000    0.196    0.265
   .mtv_vccn_ffr_3    0.464    0.008   59.310    0.000    0.464    0.647
    worries           0.601    0.020   30.347    0.000    1.000    1.000
    risks             0.385    0.012   30.847    0.000    1.000    1.000
   .motv_ident        1.506    0.028   54.419    0.000    0.799    0.799
   .motv_exter        1.581    0.048   33.176    0.000    0.908    0.908
   .motv_distr        0.989    0.023   43.488    0.000    0.930    0.930
   .motv_effrt        0.382    0.010   38.021    0.000    0.948    0.948
   .vaccintn_ntntn    0.442    0.008   58.239    0.000    0.442    0.205

R-Square:
                   Estimate
    worries_reltvs    0.505
    worries_health    0.488
    risk_other        0.525
    risk_self         0.432
    mtv_vccn_dnt_2    0.906
    mtv_vccn_dnt_3    0.865
    mtv_vccn_dnt_4    0.753
    mtv_vccn_xtr_1    0.998
    mtv_vccn_xtr_3    0.275
    mtv_vccn_dst_1    0.647
    mtv_vccn_dst_4    0.871
    mtv_vccn_dst_5    0.775
    mtv_vccn_ffr_1    0.612
    mtv_vccn_ffr_2    0.735
    mtv_vccn_ffr_3    0.353
    motv_ident        0.201
    motv_exter        0.092
    motv_distr        0.070
    motv_effrt        0.052
    vaccintn_ntntn    0.795


Model fit

Of het model in zijn geheel goed fit met de data kan je achterhalen met behulp van de verschillende fitmaten.

Let op! De \(\chi^2\)-toets onder Model Test User Model interpreteren is hier anders dan wat je misschien intuïtief zou denken. Hier stelt de nulhypothese dat er een goede fit is. Dus als je een p-waarde kleiner dan 0.05 ziet, dan is de conclusie dat je de nulhypothese van goede fit moet verwerpen. Dat is in ons model het geval.

Met het argument fit.measures = TRUE heb je naast de \(\chi^2\)-toets nog andere fitmaten opgevraagd. Er bestaat geen consensus over de precieze waarde vanaf dewelke je zeker kan spreken van een goede fit. Vuistregels voor deze fitmaten die in de literatuur voorkomen zijn:

  • CFI/TLI > 0.95 of > 0.90
  • RMSEA < 0.05, < 0.06 of < 0.08
  • SRMR < 0.06 of < 0.08


De waarden voor deze drie fitmaten zijn goed, in tegenstelling tot wat de \(\chi^2\)-toets ons leerde. De conclusie die we in dit geval kunnen trekken is dat de fit goed is, omdat de “slechte” uitkomst van de \(\chi^2\)-toets kan worden verklaard door de hoge steekproefgrootte. Zie ook eerder.

De aanvaardbare fit betekent dat je verder kan gaan met de analyse van de afzonderlijke parameters als je dat wil. Je kan directe en indirecte effecten en andere geschatte parameters beoordelen. Als de model fit niet aanvaardbaar was geweest, dan zou dit niet zinvol zijn. In dat geval zou je eerst op zoek moeten naar een beter fittend model!


Modification indices

Als de fit van je model niet goed is, kan je eventueel de functie modificationIndices() gebruiken om suggesties te krijgen voor verbeteringen. In de kolom mi lees je welke bijkomende parameters een substantiële impact zouden hebben op de fit.8


Hier moet je wel heel voorzichtig mee omspringen! Het is verleidelijk om je model fit te verbeteren door zonder nadenken wat extra connecties te maken tussen indicatoren en/of factoren. Eén van de problemen daarmee is dat elke extra connectie de fit van het model minstens een beetje zal verbeteren, ook als die connectie geen reële inhoudelijke betekenis heeft. Je mag een connectie enkel toevoegen als je er goede theoretische redenen voor hebt, dus als je echt reden hebt om te denken dat die relatie tussen indicatoren en/of factoren er in werkelijkheid zou kunnen zijn.


modificationIndices(fit, 
                    sort=TRUE, # Sorteer van grootste naar kleinste effect op de model fit (kolom 'mi')
                    maximum.number=5L # Toon enkel de eerste vijf
                    )
               lhs op                    rhs      mi    epc sepc.lv sepc.all sepc.nox
66         worries =~ motv_vaccin_distrust_1 377.179  0.262   0.204    0.159    0.159
92      motv_ident =~ motv_vaccin_distrust_4 347.659 -0.278  -0.381   -0.287   -0.287
79           risks =~ motv_vaccin_distrust_1 302.022  0.304   0.188    0.147    0.147
150 worries_health ~~              risk_self 283.791  0.124   0.124    0.284    0.284
91      motv_ident =~ motv_vaccin_distrust_1 260.014  0.235   0.323    0.252    0.252


Factorladingen

De factorladingen geven informatie over de samenhang tussen de indicatoren. Je kan ze interpreteren als regressiecoëfficiënten in een model waarbij de indicator de afhankelijke variabele is en de factor de onafhankelijke variabele. De factorlading bij de eerste indicator van elke factor wordt door de functie sem() automatisch gefixeerd op 1. Zo wordt ervoor gezorgd dat de factor (ruwweg) de meeteenheid overneemt van één van de indicatoren.

\[y_1 = \lambda_{11}\eta_1 + \epsilon_1\]

Niet vergeten: eigenlijk heeft het geen zin om factorladingen (of andere parameters) te interpreteren en beoordelen in een model dat in zijn geheel niet goed fit met de data.


Onder Latent Variables vind je in de kolom Std.all de gestandaardiseerde factorladingen. Deze kan je met elkaar vergelijken om te bepalen welke groter en kleiner zijn.

Een goede vuistregel stelt dat een gestandaardiseerde factorlading boven 0.70 moet liggen om de indicator als een degelijke meting te beschouwen. Boven 0.80 spreken we van een echt goede meting. In ons model voldoen veel factorladingen aan deze vuistregels, maar enkele doen dat duidelijk niet.

Als je gebruik hebt gemaakt van gevestigde meetinstrumenten, die eerder al grondig zijn onderzocht, dan is een zwakkere factorlading niet erg. Heb je daarentegen bijvoorbeeld zelf een vragenlijst samengesteld, dan kan je overwegen om het item met de lage factorlading weg te laten. Besef wel dat een lagere factorlading automatisch ook een kleinere rol speelt in het bredere model. De keuze om zo’n item erin te laten of te schrappen zal dus relatief weinig impact hebben op conclusies over het structurele model.


Covarianties tussen de factoren

Onder Covariances vind je in de kolom Std.all de gevraagde covarianties tussen de variabelen in het structurele model.


Varianties

Onder Variances vind je in de kolom Std.all de schattingen van de residuele varianties van elke indicator en van elke variabele in het structureel model. Dit is niet iets wat je uitdrukkelijk hebt gevraagd in de modelspecificatie. De functie sem() vult dit automatisch aan.9

De residuele variantie van een indicator is de variantie van de indicator die niet gedeeld wordt met de andere indicatoren van dezelfde factor. Het vormt het spiegelbeeld van de factorladingen: als de factorlading hoog is, dan moet de residuele variantie laag zijn.

De variabelen in het structureel model vallen uiteen in twee soorten: de exogene en de endogene. De exogene factoren (worries en risks) zijn te herkennen doordat er geen punt voor staat. Zij worden door geen enkele andere variabele in het model verklaard. De output geeft dan gewoon de variantie weer.

Van de endogene factoren (bv. motv_ident) wordt de residuele variantie gegeven, dus de variantie die niet verklaard wordt in het model.


Directe effecten

De schattingen van padcoëfficiënten (die overeenstemmen met directe effecten) zijn te vinden onder Regressions. De waarden onder Std.all kan je vergelijken met elkaar om te bepalen welk effect groter of kleiner is. Of een direct effect verschillend is van 0 kan je beoordelen aan de hand van de p-waarde of overschrijdingskans in de kolom P(>|z|).

In dit model zie je dat de meeste vooropgestelde directe effecten significant blijken te zijn op het 5%-significantieniveau. De uitzonderingen zijn de directe effecten van worries op vaccination_intention en van risks op vaccination_intention.


Indirecte en totale effecten laten berekenen

Het is mogelijk om indirecte en totale effecten te laten berekenen. Dit doe je best pas nadat je hebt vastgesteld dat het model in zijn geheel goed fit met de data.

Hoe ga je te werk? In de modelspecificatie voeg je labels (a11, a21,…) toe aan de verschillende directe effecten. De labels stellen schattingen van padcoëfficiënten (de directe effecten) voor. Aan de hand daarvan kan je nu de indirecte effecten laten berekenen. Daarvoor vermenigvuldig je de padcoëfficiënten op een pad tussen twee variabelen. Die berekeningen zie je in de modelspecificatie hieronder. Het effect dat je wil berekenen geef je een naam, gevolgd door de operator := met daarachter de gepaste formule met labels.

Totale effecten berekenen kan ook. Die zijn niets anders dan de som van alle indirecte en directe effecten die te vinden zijn tussen twee variabelen.

Een voorbeeld: de onderzoekers willen in de output het totale effect zien van worries op vaccination_intention. Daarvoor gaan ze na welke directe en indirecte paden allemaal lopen van worries naar vaccination_intention. Al die paden brengen ze samen in een totaal effect c1.

c1 := cp1 + a11*b1 + a12*b2 + a13*b3 + a14*b4

Schattingen van indirecte en totale effecten die je op deze manier opvraagt in de modelspecificatie (met de operator :=) komen in de output. Je vindt ze onder Defined parameters. Ook hier kan je de schattingen in de kolom Std.all vergelijken met elkaar. De significantie van het effect kan je beoordelen aan de hand van de p-waarde of overschrijdingskans in de kolom P(>|z|).

mijnSEM <- '
# Latente variabelen
  worries =~ worries_relatives + worries_health
  risks =~ risk_other + risk_self
  motv_ident =~ motv_vaccin_ident_2 + motv_vaccin_ident_3 + motv_vaccin_ident_4
  motv_exter =~ motv_vaccin_exter_1 + motv_vaccin_exter_3
  motv_distr =~ motv_vaccin_distrust_1 + motv_vaccin_distrust_4 + motv_vaccin_distrust_5
  motv_effrt =~ motv_vaccin_effort_1 + motv_vaccin_effort_2 + motv_vaccin_effort_3

# Factor covarianties
  worries ~~ risks
  motv_ident ~~ motv_exter
  motv_ident ~~ motv_distr
  motv_ident ~~ motv_effrt
  motv_exter ~~ motv_distr
  motv_exter ~~ motv_effrt
  motv_distr ~~ motv_effrt
  
# Directe effecten
  motv_ident ~ a11*worries + a21*risks
  motv_exter ~ a12*worries + a22*risks
  motv_distr ~ a13*worries + a23*risks
  motv_effrt ~ a14*worries + a24*risks
  vaccination_intention ~ cp1*worries + cp2*risks + b1*motv_ident + b2*motv_exter + b3*motv_distr + b4*motv_effrt
  
# Indirecte effecten
  a11b1 := a11*b1
  a12b2 := a12*b2
  a13b3 := a13*b3
  a14b4 := a14*b4
  
  a21b1 := a21*b1
  a22b2 := a22*b2
  a23b3 := a23*b3
  a24b4 := a24*b4
  
  a1b := a11*b1 + a12*b2 + a13*b3 + a14*b4
  a2b := a21*b1 + a22*b2 + a23*b3 + a24*b4

# Total effects
  c1 := cp1 + a11*b1 + a12*b2 + a13*b3 + a14*b4
  c2 := cp2 + a21*b1 + a22*b2 + a23*b3 + a24*b4
'


Als je indirecte effecten laat berekenen kan je het model fitten met de volgende code.

fit <- sem(mijnSEM, data=vaccin, se="bootstrap")


In de code zie je een extra argument se = 'bootstrap'. Dit slaat op de methode om de standaardfouten (“standard error”) van parameters te berekenen. Wanneer je bv. een indirect effect schat, dan is de parameter een product van twee andere parameters en dus typisch niet normaal verdeeld over vele steekproeven heen. Met de “gewone” methode van standaardfouten berekenen kan dit tot verkeerde conclusies leiden over de significantie van het indirect effect. Daarom is het sterk aangeraden om hier steeds voor se = 'bootstrap' te kiezen.



7 Visualiseren van een SEM

Er bestaan packages die toelaten om automatisch een diagram te maken van je model. Het is duidelijk dat een visualisatie van een model met veel variabelen al gauw ingewikkeld en onoverzichtelijk wordt, terwijl een visualisatie precies de omgekeerde bedoeling heeft! Mogelijk is het interessanter om bijvoorbeeld enkel het structurele gedeelte in een diagram te tonen.

Een package dat je kan gebruiken is bijvoorbeeld lavaanPlot. Meer info vind je hier, voornamelijk onder “Documentation”.

install.packages("lavaanPlot") # eenmalig het package installeren

library(lavaanPlot) # package laden voor gebruik

lavaanPlot(model = fit,
           node_options = list(shape = "box", fontname = "Helvetica"),
           edge_options = list(color = "grey"),
           coefs = TRUE,
           sig=.05 # toon enkel significante coëfficiënten
           )

Klik op de afbeelding om te vergroten.


Een alternatief is het package semPlot. Meer info vind je hier, voornamelijk onder “Documentation”.

install.packages('semPlot') # eenmalig het package installeren

library(semPlot) # package laden voor gebruik

semPaths(fit, layout='circle')

Klik op de afbeelding om te vergroten.



8 Meer weten?

Online en in handboeken is leermateriaal te vinden dat wat meer diepgang biedt dan deze demonstratie. Enkele bronnen waarvan we de kwaliteit kunnen verzekeren zijn:



9 Voetnoten


10 Referenties

Kline R.B. (2016). Principles and practice of structural equation modeling. Fourth edition. New York ; London: The Guilford Press.

Schmitz M., Luminet O., Klein O., Morbée S., Van den Bergh O., Van Oost P., Waterschoot J., Yzerbyt V. & Van Steenkiste M. (2022). Predicting vaccine uptake during COVID-19 crisis: A motivational approach. Vaccine 40 (2), pp.288-297. doi: 10.1016/j.vaccine.2021.11.068


  1. In het Nederlands: Structureel vergelijkingsmodel↩︎

  2. Schmitz et al. (2022) onderzochten ook de effecten op andere afhankelijke variabelen. Dat deel van hun studie wordt op deze pagina niet besproken.↩︎

  3. Vaccinatie-intentie is rechtstreeks meetbaar. Het is m.a.w. een manifeste variabele, geen latente variabele. Er zijn dan ook geen indicatoren.↩︎

  4. Alles wat rechts van een # staat is commentaar. Het is enkel bedoeld om wat duiding te geven bij de code.↩︎

  5. Of dat werkelijk het geval is zal natuurlijk moeten blijken uit de analyse. Op dit moment is het nog maar een hypothese!↩︎

  6. Exogene variabelen zijn variabelen die niet door andere variabelen in het model verklaard worden. In het diagram bovenaan de pagina zijn het variabelen waar geen enkele pijl toekomt.↩︎

  7. In plaats van de volledige dataset wordt vaak enkel de variantie-covariantiematrix aan sem() gegeven.↩︎

  8. Bij de verbanden die al in je model zitten, staat er logischerwijs een waarde 0 in deze kolom.↩︎

  9. De functie sem() is gebruiksvriendelijk in de zin dat het heel wat van je syntax automatisch aanvult. Als je dit niet wil, dan kan je de functie lavaan() gebruiken in de plaats.↩︎