mxPath unexpected behavior with all = TRUE

7 replies [Last post]
mspiegel's picture
Offline
Joined: 07/31/2009

Imagine I want to create the following model:

I try creating this model fragment using the following R code:

lVars <- c('a', 'b')
freeParams <- c('p1', 'p2', 'p3')
 
model <- mxModel('model', type = 'RAM',
   latentVars = lVars,
   mxPath(from = lVars, to = lVars, arrows = 2, 
      all = TRUE, labels = freeParams))

But this code generates the model:

What happened here? The explanation: When the mxPath() function is called with all = TRUE, it will generate n x m paths, where n is the length of 'from' argument and 'm' is the length of the 'to' argument. Keep in mind that when a path is created, it will clobber any existing path that has the same 'from', 'to', and number of arrows. In the case of symmetric paths, the 'from' and 'to' arguments can be switched with impunity. So the following paths are created:

From To Label
a a p1
a b p2
b a p3
b b p1

This is a bug, right? The expected behavior when all = TRUE and arrows = 2 is not to clobber 1/2 of the generated paths. Does anyone have an example where the behavior shown here is the desired behavior?

AttachmentSize
correct.jpg69.54 KB
incorrect.jpg69.4 KB
mspiegel's picture
Offline
Joined: 07/31/2009
Update: the following code

Update: the following code snippet now works in the source code repository, as of revision 1738. This functionality will be available in OpenMx 1.2.

lVars <- c('a', 'b')
freeParams <- c('p1', 'p2', 'p3')
 
model <- mxModel('model', type = 'RAM',
   latentVars = lVars,
   mxPath(from = lVars, to = lVars, arrows = 2, 
      connect = "unique.pairs", labels = freeParams))

mspiegel's picture
Offline
Joined: 07/31/2009
I advocate that the behavior

I advocate that the behavior is a bug. If 'arrows' is '1' and 'all' is 'TRUE', then the number of paths should be |to| x |from| for all cases.

If 'arrows' is '2' and 'all' is 'TRUE', then the number of paths should be |to| x |from| if and only if 'to' and 'from' contain non-overlapping elements. If 'to' and 'from' contain overlapping elements, then the mxPath() function should not generate duplicate paths that overwrite old paths.

tbates's picture
Offline
Joined: 07/31/2009
guiding rule: call never stomps on something it made

I think the guiding rule should be that the function never stomps on something that this call to it created.

That would mean
1. accumulating a list of from-to paths
2. removing duplicates (including mirror images)
3. then creating them, using the provided labels.

Only if the final list doesn't match the label length should a warning be thrown.

This way from and too lists can overlap partly or completely and you always get one connection from everthing on the left to everything on the right, which is a nice intention-oriented interpretation of "ALL" as "all unique combinations".

tbrick's picture
Offline
Joined: 07/31/2009
We need to think about

We need to think about consistency with other functions, too.

paste() doesn't have an all=TRUE flag, but we're in the process of implementing mxPaste() that does. We will want the behavior of mxPaste() to be consistent with mxPath() for any value of 'arrows'.

That is, we want to be able to do mxPath(from=latents, to=latents, arrows=x, all=TRUE, labels=mxPaste(latentNames, latentNames, all=TRUE)) to work regardless of whether x is 1 or 2.

There's also the complexity argument--at least with the current setup, the behavior is always the same. In this set up, what happens if from and to are overlapping but not identical? What if there are duplicates in from= or to=? We at least need a simple guiding rule.

The prospect of creating a path then immediately klobbering it doesn't seem right either--if we can come up with an easy guiding rule, I think I can get behind the change in semantics. What is the easy way to automatically generate labels for mxPath(from=vars, arrows=2, all=TRUE)?

What about mxPath(from=vars1, to=vars2, arrows=2, all=TRUE) if vars1 and vars2 are overlapping but not identical? (When I ran this by Mike Spiegel, he validly asked "when would anyone do that?" I don't have an answer, so if anybody does, please let me know.)

Either way, I tend to agree with Ryne--it makes sense to at least throw a warning if people provide a the number of elements being labeled that isn't a clean multiple of the number of labels. Yes, people will probably ignore it. But at least we tried. This warning-throwing behavior for mismatched numbers of values is pretty common in R.

Ryne's picture
Offline
Joined: 07/31/2009
So this should be smarter,

So this should be smarter, but we run the risk of having different functionality for 1 and 2 headed arrows. One way to restate what you're pointing out is that when arrows=2, mxPath populates a symmetric matrix, so our usual tricks for populating symmetric matrices should work. The difference between mxPath and mxMatrix is that mxMatrix requires that the vector of labels be a multiple of the number of elements in the matrix, whereas mxPath allows you to supply 3 labels for four paths.

This is a bigger discussion, and one I would have thought we'd have had before. I think at least a warning is warranted, depending on whether we're telling people that they gave 3 labels for 4 paths or telling them that they assigned the same parameter two different labels.

Just as a reminder, this is what we do if someone tried to pull Mike's dual assignment of the a b covariance in a matrix:

mxMatrix("Symm", 2, 2, labels=letters[1:4])
Error: Labels matrix of symmetric matrix 'untitled36' is not symmetric!

tbates's picture
Offline
Joined: 07/31/2009
"all" is is every-possible,

"all" is is every-possible, so I think this is doing the requested behaviour, but no-one would desire it :-)

So I think you are requesting and getting
from a to a
from a to b
from b to a
from b to b
When the third path is not wanted, and shifts your labels.

I accomplish this using

lVars = c('a', 'b');
model = mxModel('model', type = 'RAM',
	latentVars = lVars,
	mxPath(from = lVars, arrows = 2, labels = paste(lVars,"_var", sep="")), # <i>variances</i>
	mxPath(from = lVars, to = lVars, arrows = 2,  all = T, excludeself=T, labels = 'p2') # <i>covariances</i>
)

tbates's picture
Offline
Joined: 07/31/2009
test case from the examples

data(HS.fake.data)  #load the data
names(HS.fake.data)
Spatial   <- c("visual","cubes","paper") # the manifest variables loading on each proposed latent variable
Verbal    <- c("general","paragrap","sentence")
Math      <- c("numeric","series","arithmet")
 
latents   <- c("vis","math","text")
manifests <-  c(Spatial,Math,Verbal)
 
model <- mxModel("Holzinger and Swineford 1939", type="RAM", 
	manifestVars = manifests, # list the measured variables (boxes)
	latentVars   = latents,   # list the latent variables (circles)
    # factor loadings from latents to  manifests
	mxPath(from="vis",  to=Spatial),# factor loadings
    mxPath(from="math", to=Math),   # factor loadings
    mxPath(from="text", to=Verbal), # factor loadings
 
	# Allow latent variables to covary 
	# mxPath(from="vis" , to="math", arrows=2, free=T, labels=c("cov1")),
	# mxPath(from="vis" , to="text", arrows=2, free=T, labels=c("cov2")),
	# mxPath(from="math", to="text", arrows=2, free=T, labels=c("cov3")),
 
	# I should be equivalent to the above three
	mxPath(from=latents, arrows=2, free=T, all=T, labels=paste("cov", 1:3, sep=""), excludeSelf=T),
 
    # Allow latent variables to have variance (first fixed  @ 1)
	mxPath(from=latents, arrows=2, free=c(F,T,T), values=1), 
    # Manifest have residual variance
	mxPath(from=manifests, arrows=2),   
	# the data to be analysed
    mxData(cov(HS.fake.data[,manifests]), type="cov", numObs=301)
)
model@matrices$S

Should look like this:

@labels
         visual cubes paper numeric series arithmet general paragrap sentence vis    math   text  
...
vis      NA     NA    NA    NA      NA     NA       NA      NA       NA       NA     "cov1" "cov2"
math     NA     NA    NA    NA      NA     NA       NA      NA       NA       "cov1" NA     "cov3"
text     NA     NA    NA    NA      NA     NA       NA      NA       NA       "cov2" "cov3" NA   

not this
@labels
         visual cubes paper numeric series arithmet general paragrap sentence vis    math   text  
...
vis      NA     NA    NA    NA      NA     NA       NA      NA       NA       NA     "cov1" "cov1"
math     NA     NA    NA    NA      NA     NA       NA      NA       NA       "cov1" NA     "cov2"
text     NA     NA    NA    NA      NA     NA       NA      NA       NA       "cov1" "cov2" NA