Skip to content

2.2 Editing asn MCMSEM Model

Michel Nivard edited this page Sep 12, 2024 · 1 revision

Editing an MCM model

We try to cover major model changes in the MCMmodel() function, but we cannot possibly account for all possible models you might want to run using MCMSEM. Therefore we have included the MCMedit() function to allow you to make detailed changes to an MCM model.

The general syntax of MCMedit() is the following: adjusted_model <- MCMedit(base_model, pointer, name, value) where:

  • base_model is the base model to be changed
  • pointer points to the location of values need to be changed, i.e. one of the matrices (A, Fm, S, Sk, or K), bounds, or starts
  • name represents the name or coordinate(s) of values to be changed
  • value the new value, this can be a new name (to create a new free parameter), a fixed value, or 0 (to disable a parameter for example)

This will return the edited model adjusted model, which is another MCM model object with the edits applied.

For the purposes of this wiki we will start with the following base model:

my_model <- MCMmodel(data_summary, n_latent=2, scale_data=FALSE)

Figure 1

Dropping parameters

Our aim for this section is to:

  • Drop the paths from f1 to x4 and x5
  • Drop the paths from f2 to x4 and x5
  • Drop the causal paths from x1 to x5 (both ways)

So our goal for this section is to end up with the following model:

Figure 2

There are a number of ways to do this.

By coordinates

First parameters can be dropped based on their coordinates in their respective matrix. All a (factorloadings) and b (causal path) parameters reside in the A matrix. In order to change parameters by their coordinates we will first need to know their position in the matrix. Therefore we will start by displaying the A matrix:

print(my_model, matrix="A")

Which prints the following:

     [,1] [,2] [,3]   [,4]   [,5]   [,6]   [,7]  
[1,] "0"  "0"  "0"    "0"    "0"    "0"    "0"   
[2,] "0"  "0"  "0"    "0"    "0"    "0"    "0"   
[3,] "a1" "0" "0"    "b2_1" "b3_1" "b4_1" "b5_1"
[4,] "a1" "a2" "b1_2" "0"    "b3_2" "b4_2" "b5_2"
[5,] "a1" "a2" "b1_3" "b2_3" "0"    "b4_3" "b5_3"
[6,] "a1" "a2" "b1_4" "b2_4" "b3_4" "0"    "b5_4"
[7,] "a1" "a2" "b1_5" "b2_5" "b3_5" "b4_5" "0"   

From this matrix we can observe that a1 paths we want to drop are located at [6, 1] (path f1 -> x4) and [7, 1] (path f1 -> x5). Dropping a parameter is done by setting its value to 0. So by running the followig

my_model2 <- MCMedit(my_model,         # The base model
                     pointer = "A",    # We are changing values in the A matrix
                     name = c(6, 1),   # We want to change the values at coordinate [6, 1]
                     value = 0)        # We want to set that value to 0 (i.e. remove the parameter)
    
my_model2 <- MCMedit(my_model2, pointer="A", name=c(7, 1), value=0) # Same for [7, 1]

There are more a2 paths we need to drop, namely those located at [3, 2] (path f2 -> x1), [4, 2] (path f2 -> x2) and [5, 2] (path f2 -> x3). This can be done in a similar fashion, but you can also provide multiple coordinates at once by using a 2-element list with x-coordinates, and y-coordinates:

coords <- list( c(3, 4, 5),
                c(2, 2, 2) )
my_model2 <- MCMedit(my_model, pointer="A", name=coords, value=0)

Turning your model into a uni-directional model, for instance, is likely also easiest via coordinates, for example, let's say we want to drop all b parameters in the upper diagonal (i.e. all "reverse" causal paths):

nvars <- 5
n_latent <- 2
coords <- expand.grid(x=1:nvars+n_latent, y=1:nvars+n_latent)[upper.tri(matrix(0, nvars, nvars), diag=FALSE), ]
my_model2 <- MCMedit(my_model, pointer="A", name=list(coords$x, coords$y), value=0)

To verify our edits worked we can run print(my_model2, matrix="A") again, which will now return the following:

     [,1] [,2] [,3]   [,4]   [,5]   [,6]   [,7]
[1,] "0"  "0"  "0"    "0"    "0"    "0"    "0" 
[2,] "0"  "0"  "0"    "0"    "0"    "0"    "0" 
[3,] "a1" "0"  "0"    "0"    "0"    "0"    "0" 
[4,] "a1" "a2" "b1_2" "0"    "0"    "0"    "0" 
[5,] "a1" "a2" "b1_3" "b2_3" "0"    "0"    "0" 
[6,] "a1" "a2" "b1_4" "b2_4" "b3_4" "0"    "0" 
[7,] "a1" "a2" "b1_5" "b2_5" "b3_5" "b4_5" "0" 

By name

Lastly, parameters can be dropped using their name. We will use this method to drop path x1 -> x5 and x5 -> x1, which are labelled b1_5 and b5_1 respectively.

my_model2 <- MCMedit(my_model, pointer="A", name=c("b1_5", "b5_1"), value=0)

To verify our edits worked we can run print(my_model2, matrix="A") again, which will now return the following:

     [,1] [,2] [,3]   [,4]   [,5]   [,6]   [,7]  
[1,] "0"  "0"  "0"    "0"    "0"    "0"    "0"   
[2,] "0"  "0"  "0"    "0"    "0"    "0"    "0"   
[3,] "a1" "0"  "0"    "b2_1" "b3_1" "b4_1" "0"  
[4,] "a1" "a2" "b1_2" "0"    "b3_2" "b4_2" "b5_2"
[5,] "a1" "a2" "b1_3" "b2_3" "0"    "b4_3" "b5_3"
[6,] "a1" "a2" "b1_4" "b2_4" "b3_4" "0"    "b5_4"
[7,] "a1" "a2" "0"    "b2_5" "b3_5" "b4_5" "0"   

Confirming our edits are properly applied

Splitting parameters

In this section we will continue from the previous section we will do the following:

  • Split the a1 parameter into a1_1, a1_2 and a1_3 so the paths from f1 to x1, x2 and x3 are free.
  • Split the a2 parameter into a2_4 and a2_5 so the paths from f2 to x4 and x5 are free.

So our goal for this section is to end up with the following model:

Figure 3

Note: Splitting of factor loadings is typically more efficiently achieved by adding contrained_a=FALSE to MCMmodel(). However, for the purposes of this demonstration we will do it manually.

By coordinate

This works similar to dropping parameters, only now instead of providing the value 0, we provide the new parameter names. Looking at our A matrix again print(my_model, matrix="A")

     [,1] [,2] [,3]   [,4]   [,5]   [,6]   [,7]  
[1,] "0"  "0"  "0"    "0"    "0"    "0"    "0"   
[2,] "0"  "0"  "0"    "0"    "0"    "0"    "0"   
[3,] "a1" "0"  "0"    "b2_1" "b3_1" "b4_1" "0"  
[4,] "a1" "a2"  "b1_2" "0"    "b3_2" "b4_2" "b5_2"
[5,] "a1" "a2"  "b1_3" "b2_3" "0"    "b4_3" "b5_3"
[6,] "a1" "a2" "b1_4" "b2_4" "b3_4" "0"    "b5_4"
[7,] "a1" "a2" "0"    "b2_5" "b3_5" "b4_5" "0"

We can see that the coordinates of a1 are [3,1], [4,1] and [5,1]. So using similar semantics as previously described:

coords <- list( c(3, 4, 5),  c(2, 2, 2) )  # We start by making each factor load on 1 variable again
my_model_split_A <- MCMedit(my_model, pointer="A", name=coords, value=0)

coords <- list( c(3, 4, 5),
                c(1, 1, 1) )
new_names <- c("a1_1", "a1_2", "a1_3")
my_model_split_A <- MCMedit(my_model_split_A, pointer="A", name=coords, value=new_names)

By name

Typically splitting your parameter by its name is the easier solution. Remember we have already dropped 3 of the 5 paths of a2 previously, so two paths remain (those to x4 and x5). Thus we will have to provide two new parameter names in this case.

new_names <- c("a2_4", "a2_5")
my_model_split_A <- MCMedit(my_model_split_A, pointer="A", name="a2", value=new_names)

Again we can verify these changes worked by running print(my_model_split_A, matrix="A")

     [,1]   [,2]   [,3]   [,4]   [,5]   [,6]   [,7]  
[1,] "0"    "0"    "0"    "0"    "0"    "0"    "0"   
[2,] "0"    "0"    "0"    "0"    "0"    "0"    "0"   
[3,] "a1_1" "0"    "0"    "b2_1" "b3_1" "b4_1" "0"   
[4,] "a1_2" "0"    "b1_2" "0"    "b3_2" "b4_2" "b5_2"
[5,] "a1_3" "0"    "b1_3" "b2_3" "0"    "b4_3" "b5_3"
[6,] "0"    "a2_4" "b1_4" "b2_4" "b3_4" "0"    "b5_4"
[7,] "0"    "a2_5" "0"    "b2_5" "b3_5" "b4_5" "0" 

A note on the K matrix

The K matrix contains free parameters, as well as products of the S matrix (depending on the layout of the model). Running print(my_model, matrix="K") will print a simplified version of the K matrix which only displays the free parameters in the K matrix, and 0 otherwise. In order to visualize the full product of K, run the following:

print(MCMparseK(my_model))

IMPORTANT: This is by no means necessary to run the model, this is purely for visualizing the result of matrix operations on K before loss is determined. Additionally, parsing the K matrix in this way may take a long time for larger models.

Constraining parameters

Currently, constraining parameters can only be done via coordinates. For example, to undo our previous change and constrain a2_4 and a2_5 to the same parameter, we can do the following:

my_model_single_a2 <- MCMedit(my_model_split_A, pointer="A", name=list(c(6, 7), c(2, 2)), value="a2")

Note this result is stored in a different model as the rest of this wiki will assume a2_4 and a2_5 are still separate paths.

Adding new parameters

Adding new parameters works the same as dropping parameters, but can only be done via coordinates (as there is no old name to replace).

In this example we will undo our previous change and reinstate the paths x1 -> x5 and x5 -> x1:

my_model <- MCMedit(my_model, pointer="A", name=list(c(7, 3), c(3, 7)), value=c("b1_5", "b5_1"))

Changing start values

To see the current starting values per parameter, see

my_model$start_values

Changing starting values can only be done by name (as the starting values are bound to the parameters themselves, not the matrices). You can change starting values by providing the pointer "start", and providing either the name of a single parameter, or a vector with parameter names to set multiple start values to the same value simultaneously:

my_model <- MCMedit(my_model, pointer="start", name="a2_4", value=0.1)  # Set a2_4 to 0.1
my_model <- MCMedit(my_model, pointer="start", name="a2_5", value=0.1)  # Set a2_4 to 0.1 
my_model <- MCMedit(my_model, pointer="start", name=c("a1_1", "a1_2", "a1_3"), value=0.1)  # Set a1_1, a1_2 a1_3 start values to 0.1

We recognized that this can become messy in larger models, therefore we have also implemented a way to change the starting values of all parameters of a certain type in one go. So a more efficient way of applying the previous change (since it encompassess all a parameters) would be:

my_model <- MCMedit(my_model, pointer="start", name="a", value=0.1)

In some cases you may want to set all starting values simultaneously, in which case you can run the following:

my_model <- MCMedit(my_model, pointer="start", name="all", value=0)

Note: Starting values cannot be changed using my_model$start_values["start", "a1"] <- .5

Changing bounds

Note: If you are using bounds, ensure use_bounds is set to TRUE in MCMfit

To see the current bounds per parameter, see

my_model$bounds

Upper and lower bound

To change the upper and lower bound of a single parameter, use the pointer bound:

my_model <- MCMedit(my_model, pointer="bound", name="a1_1", value=c(-1, 1))

Alternatively you can change bounds for multiple parameters by supplying a vector of parameter names For example, let's set the bounds of a1_1, a1_2 and a1_3 to [-1, 1]

my_model <- MCMedit(my_model, pointer="bound", name=c("a1_1", "a1_2", "a1_3"), value=c(-1, 1))

Changing bounds of a whole group of parameters works the same as starting values, e.g. to change the bounds of all a parameters to [-1, 1]:

my_model <- MCMedit(my_model, pointer="bound", name="a", value=c(-1, 1))

Upper or lower bounds separately

You can change the lower and upper bounds separately by using the pointers lbound and ubound respectively.

Changes in other matrices

Throughout this wiki we have only made changes to the A matrix as it is likely to be your first target for making changes to your model. Changes to other matrices work the exact same way as changes to the A matrix as outlined above, with a few caveats for the Sk and K matrices:

  • Adding parameters to the Sk or K matrix is tricky as they are 2D representations of 3D and 4D matrices, so interpreting what the coordinates mean, and which cell is which co-kurtosis is not trivial. It is therefore not recommended to change these yourself.
  • Starting values for sk and k are determined algorithmically (from the skew and kurtosis of the observed data), which is very likely to be the best starting value. It is therefore not recommended to change these unless you absolutely have to (for example if you want to compare fit performance of 2 models at the exact same starting point).