sfcheung / semptools Goto Github PK
View Code? Open in Web Editor NEWHelper functions for customizing plots by semPlot::semPaths
Home Page: https://sfcheung.github.io/semptools
License: GNU General Public License v3.0
Helper functions for customizing plots by semPlot::semPaths
Home Page: https://sfcheung.github.io/semptools
License: GNU General Public License v3.0
auto_factor_point_to()
Work in the branch auto_factor_point_to
.
This is from the vignette of layout_matrix()
point_to <- layout_matrix(left = c(1, 1),
left = c(3, 1),
down = c(2, 2),
up = c(2, 3))
How about using a layout matrix as input?
m_sem <- layout_matrix(f1 = c(1, 1),
f2 = c(3, 1),
f3 = c(2, 2),
f4 = c(2, 3))
m_sem
point_to <- auto_factor_point_to(factor_layout = m_sem,
f1 = "left",
f2 = "left",
f3 = "down",
f4 = "up")
We can also revise set_sem_layout()
and set_cfa_layout()
to do this automatically if they detect that the argument of factor_point_to
is a named vector rather than a matrix.
Dear Dr. Cheung and Dr. Lai,
Thanks for writing these helper functions for users in psychology. I think these functions are really easy to use and they are very helpful for customizing graphs. I tested this package on my own project and everything works out pretty well. However, I am a little confused about setting the m matrix for semPlot
in the following lines:
m <- matrix(c("x1", NA, NA, NA,
NA, "x3", NA, "x4",
"x2", NA, NA, NA), byrow = TRUE, 3, 4)
p_pa <- semPaths(fit_pa, whatLabels = "est",
sizeMan = 10,
edge.label.cex = 1.15,
style = "ram",
nCharNodes = 0, nCharEdges = 0,
layout = m)
I think adding an explanation about this m matrix and how to set it would be greatly appreciated by new users like me.
This can be done manually using edge.label.position
. The goal is to make this task user friendly, and users can specify only edge labels that they want to move.
Do this in its own branch.
Confirmed that graphAttributes$Nodes$loopRotation
of a qgraph object also controls the position of lisrel style residuals. Double check whether it works in rotate_resid.
semptools
functions will return an error.fiml
in lavaan
will have a mean structure, resulting in intercepts.intercepts = FALSE
when using semPaths
. All semptools
functions should then work.set_cfa_layout assumes there are more than one factor and encounters error when there is only one factors.
There is a quick fix. To be done in its own branch.
Since R 4.0.0 was out, I tested whether the package can be installed with the newest version of R. So far it can be installed without issues, and runs fine with all vignettes built successfully. I'll continue to run a few tests on it.
auto_indicator_config()
. It returns a named vector of he indicator order, names being the factor names.set_sem_layout()
and set_cfa_layou()
: If either indicator_order
or indicator_factor
is not set, or both are not set, it will check whether (a) a lavaan
model syntax is supplied, or (b) a lavaan-class
object is supplied. If yes, it will adopt the appropriate option.auto_indicator_order()
.add_object()
, for future use.lavaan_indicator_order()
.set_cfa_layout()
.set_sem_layout()
.Work in layout_helpers
.
In set_sem_layout
and set_cfa_layout
, users need to supply a character vector to match indicators to factors and set the orders. This can be improved by writing a helper function to generate the two vectors. E.g.,
x <-
"
f2 =~ x1 + x3 + x4
f1 =~ x5 + x7 + x6
"
The function should generate these two vectors:
a1 <- c(""x1", "x3", "x4", "x5", "x7", "x6")
a2 <- c("f2", "f2", "f2", "f1", "f1", "f1")
Because users of semptools
are also users of lavaan
, it is easier for them to use syntax similar to that of lavaan
.
We used vectors as the input because the output of semPlot::semPaths()
does not store the output of lavaan
. However, we already have functions (e.g., mark_sig()
) that asks for the output of lavaan
.
Therefore, instead of asking users to specify the syntax again, we can just write simple helper functions to generate the vectors, instead of asking users to do the following:
indicator_order <- c("x04", "x05", "x06", "x07",
"x01", "x02", "x03",
"x11", "x12", "x13", "x14",
"x08", "x09", "x10")
indicator_factor <- c( "f2", "f2", "f2", "f2",
"f1", "f1", "f1",
"f4", "f4", "f4", "f4",
"f3", "f3", "f3")
we can write two helpers to do this:
auto_indicator_order(fit)
auto_indicator_factor(fit)
These functions will inspect the parameter table and then generate these two vectors automatically.
The description currently says that it takes input
The object used by semPaths to generate the plot. Use the same argument name used in semPaths to make the meaning of this argument obvious.
However, I believe currently it only works for lavaan
output objects, while semPaths()
can be used on outputs other than lavaan
outputs. Shall we update the descriptions?
Hi, I was hoping to use your package for my SEM model which includes single manifest variables in my regressions that are not indicators. I didn't find a way to integrate them into your framework. Did I miss something or is this just not possible yet? If it isn't, this would make the package very powerful. If it is, sorry in advance!
Hello Dr. Cheung and Dr. Lai,
This is a really cool and helpful package for customizing plots of path models in a convenient way with huge flexibility.
I was testing out this package with an output from a multigroup confirmatory factor analysis model which contains two path models of two groups of gender. The semPlot object contains two lists and the functions of semptools do not seem to handle semPlot objects with more than one list.
For example, after fitting a configural model into the data
configural_fit <- cfa(model = model_helpful,
data = helpful,
estimator = "WLSMV",
ordered = c(paste0("H", 1:7)),
group = "gender",
meanstructure = TRUE,
parameterization = "theta")
summary(configural_fit)
and plotting the path models via semPaths, which yields two plots,
conf_pa <- semPaths(configural_fit, whatLabels = "est",
nCharNodes = 0, nCharEdges = 0)
passing through the object "conf_pa"
conf_pa_ast <- mark_sig(conf_pa, configural_fit)
plot(conf_pa_ast)
will yield an error message as shown below.
rlang::last_error()
<error/rlang_error_data_pronoun_not_found>
Columnfrom_names
not found in.data
Backtrace:
- semptools::mark_sig(conf_pa, configural_fit)
rlang::last_trace()
<error/rlang_error_data_pronoun_not_found>
Columnfrom_names
not found in.data
Backtrace:
█
- ├─semptools::mark_sig(conf_pa, configural_fit)
- │ ├─dplyr::select(...)
- │ └─dplyr:::select.data.frame(...)
- │ └─tidyselect::vars_select(tbl_vars(.data), !!!enquos(...))
- │ └─tidyselect:::eval_select_impl(...)
- │ ├─tidyselect:::with_subscript_errors(...)
- │ │ ├─base::tryCatch(...)
- │ │ │ └─base:::tryCatchList(expr, classes, parentenv, handlers)
- │ │ │ └─base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
- │ │ │ └─base:::doTryCatch(return(expr), name, parentenv, handler)
- │ │ └─tidyselect:::instrument_base_errors(expr)
- │ │ └─base::withCallingHandlers(...)
- │ └─tidyselect:::vars_select_eval(...)
- │ └─tidyselect:::walk_data_tree(expr, data_mask, context_mask)
- │ └─tidyselect:::eval_c(expr, data_mask, context_mask)
- │ └─tidyselect:::reduce_sels(node, data_mask, context_mask, init = init)
- │ └─tidyselect:::walk_data_tree(new, data_mask, context_mask)
- │ └─base::eval(expr, data_mask)
- │ └─base::eval(expr, data_mask)
- │ ├─from_names
- │ └─rlang:::
$.rlang_data_pronoun
(.data, from_names)- │ └─rlang:::data_pronoun_get(x, nm)
- └─rlang:::abort_data_pronoun(x)
If the lists inside of "conf_pa" are read one by one (e.g., by specifying conf_pa[[1]]), functions in this package work well.
conf_pa_ast <- mark_sig(conf_pa[[1]], configural_fit)
plot(conf_pa_ast)
library(lavaan)
library(semPlot)
library(semptools)
mod_pa <-
'x1 ~~ x2
x3 ~ x1 + x2
x4 ~ x1 + x3
'
fit_pa <- lavaan::sem(mod_pa, pa_example)
In semPaths()
one can specify custom labels using the nodeLabels
argument.
# Use custom labels
m <- matrix(c("Var1", NA, NA, NA,
NA, "Var3", NA, "Var4",
"Var2", NA, NA, NA), byrow = TRUE, 3, 4)
p_pa <- semPaths(fit_pa, whatLabels = "est",
sizeMan = 10,
edge.label.cex = 1.15,
style = "ram",
nodeLabels = c("Var3", "Var4", "Var1", "Var2"),
layout = m)
However, using custom labels result in errors from mark_sig()
and
similarly mark_se()
, because the node labels are different from the
ones in the lavaan
object.
p_pa2 <- mark_sig(p_pa, fit_pa)
plot(p_pa2) # No "***" added
There may not be an easy fix as the qgraph
object does not seem to
contain the original variable names. One option is to have users input
the custom labels as a named list. Another is to print out a warning
cautioning that the use of custom labels is not supported.
It is solvable if users specify a named vector as custom labels
# Use custom labels with named vector
p_pa <- semPaths(fit_pa, whatLabels = "est",
sizeMan = 10,
edge.label.cex = 1.15,
style = "ram",
nodeLabels = c(
"x3" = "Var3",
"x4" = "Var4",
"x1" = "Var1",
"x2" = "Var2"),
layout = m)
In which case the mapping of the variable names and custom labels are
stored
p_pa$graphAttributes$Nodes$names
## x3 x4 x1 x2
## "Var3" "Var4" "Var1" "Var2"
P.S. A similar issue occurs when semPaths()
abbreviate the names of
the variables.
No need for the original output. Just need to make sure that the estimates are standardized.
Can use the position of the two nodes to determine the actual sign internally.
Need to check whether a dependent variable is a latent variable. If so and unstandardized solution is requested, the proportions of explained variance should not be computed and the parameter should be labelled in a meaningful way.
Do this in its own branch.
Describe how to use named vectors or lists (an option added in v0.2.8.1) in vignettes. Update the quick starts because named list and vector should be the default way to specify the changes.
No need to do this. set_cfa_layout
does not need named vectors.
Write it in this branch:
https://github.com/sfcheung/semptools/tree/vignettes_cfa_sem
set_cfa_layout()
.set_sem_layout()
has a similar problem.Work in orthogonal_cfa
.
There are cases in which the factors are orthogonal and so the covariances are not plotted. set_cfa_layout()
will throw an error.
Check if factor covariances are present. If not, skip them.
Shu Fai, I personally think that showing the standard errors in parentheses for the path coefficients is more informative than the asterisks. Should we make a function for that, like mark_sig()
?
$graphAttributes$Nodes$names
can be changed by change_node_label()
and some other functions. The following functions need to be revised such that if no match in node names is found in $graphAttributes$Nodes$names
, then the names of this vector will be searched:
The semPaths()
function can work with Mplus output (see here), although it's not clear to me whether it can extract parameter estimates. So it should work with the following functions that do not depend on a lavaan fit object:
rotate_resid()
set_curve()
set_edge_label_position()
set_cfa_layout()
set_sem_layout()
This is not urgent as the priority should be to make the functions work without bugs for lavaan
.
Hi @sfcheung, I was using the semptools
package and wanted to create a diagram just using lavaan
syntax. The lavaanify()
function allows converting syntax to a lavaan
model object. I then ran into an error when using drop_nodes()
:
library(semptools)
m2 <- " eta_Y =~ 1 * fs_y
eta_M =~ 1 * fs_m
fs_y ~~ theta_y * fs_y
fs_m ~~ theta_m * fs_m
eta_Y ~ b2 * X + b3 * eta_M
eta_M ~ b1 * X "
pm2 <- semPlot::semPlotModel(
lavaan::lavaanify(m2)
)
pm2_2 <- drop_nodes(pm2, c("fs_y", "fs_m"))
#> Error in object@ObsCovs[[1]]: subscript out of bounds
Created on 2022-02-26 by the reprex package (v2.0.1)
Do you think we can make drop_nodes()
change the ObsCovs
and the ImpCovs
slots only when the input semPlotModel
contains those? I've added two conditional statements in a separate branch if you think it's okay to merge. Thanks!
Mark
This can be done manually using curve
. The goal is to make this task user friendly, and users can specify only edges that they want to change.
Do this in its own branch.
Checked and some functions work for some metaSEM models if the output is not required. However, some functions may need to be updated to handle issues unique to MASEM.
Sometimes semPaths draw two or more path coefficients on nearly the same position. It would be great to automatically move them away from each other. I have a rough idea on how to do this. May not be that difficult. Just receives a qgraph object, checks the implied positions for all edge labels, adjusts those with identical positions, and returns an adjusted vector of edge label positions.
This is not urgent because if the number of overlapping edge labels are small, this can be adjusted manually by set_edge_label_position
.
2023-10-15: I think this is difficult. The drawing functions are internal functions and there is no simple way to know the product without actually drawing product.
Draft a brief vignette to illustrate how to use the functions.
Do this in it's own branch. This branch should not involve changing the code.
When a label is changed, the original option such as font size may no longer be appropriate and needs to be changed. However, this can be done when a graph is first generated by semPlot::semPaths
because the labels are not specified in this step. Using qgraph::qgraph
again is not an option because it will change rebuild the graph and override changes made by semptools
functions.
Address this issue in the branch update_change_node_label
.
change_node_label
to allow modifying graphAttributes$Nodes$label.cex
.change_node_label
to allow modifying plotOptions$label.scale
.change_node_label
to allow modifying plotOptions$label.prop
.change_node_label
to allow modifying plotOptions$label.norm
.The generated output is a qgraph object. Therefore, it is possible to write some tests for the functions to see if the changes to a qgraph is the intended ones.
This can be done manually using a layout matrix. However, it will be helpful to have function to do this in a user-friendly way.
One possible approach is to read the content of an semPath output and output the layout in a meaningful format. Users can then customize the layout using labels instead of numbers as in the layout argument.
Do this in its own branch:
The build did not pass the test "Curve and edge label position can be changed after changing labels" on my Ubuntu machine. It seems that on @sfcheung's machine, the return value of
all.equal(p_pa2$graphAttributes$Edges,
p_pa2_curve$graphAttributes$Edges)
is
Component \"curve\": Mean absolute difference: 1
but on my machine, the return value is
Component “curve”: Mean absolute difference: 1
(with curly double quotes).
I'll soon push a quick fix by replacing all curly double quotes to \"
before passing the values to expect_match()
, so that it should work on both machines
In semPaths
, to specify the layout of nodes, we need to prepare a matrix:
m <- matrix(c("x1", NA, NA, NA,
NA, "x3", NA, "x4",
"x2", NA, NA, NA), byrow = TRUE, 3, 4)
This will become inefficient to create and modify if the number of variables is large. We can write a function to create the matrix this way:
m <- fct(x1 = c(2, 1),
x2 = c(3, 1),
x3 = c(2, 2),
x4 = c(2, 4))
This may look less intuitive, but is much easier to modify when the number of variables is large.
Done (bfd921e)
As illustrated below, semPaths()
gives a named vector for the labels
without specifying nCharNodes
, but gives an unnamed vector when nCharNodes = 0
.
library(lavaan)
#> This is lavaan 0.6-7
#> lavaan is BETA software! Please report any bugs.
library(semPlot)
#> Registered S3 methods overwritten by 'huge':
#> method from
#> plot.sim BDgraph
#> print.sim BDgraph
library(semptools)
mod_pa <-
'x1 ~~ x2
x3 ~ x1 + x2
x4 ~ x1 + x3
'
fit_pa <- lavaan::sem(mod_pa, pa_example)
# Use custom labels
m <- matrix(c("x1", NA, NA, NA,
NA, "x3", NA, "x4",
"x2", NA, NA, NA), byrow = TRUE, 3, 4)
p_pa <- semPaths(fit_pa, whatLabels = "est",
sizeMan = 10,
edge.label.cex = 1.15,
layout = m)
p_pa$graphAttributes$Nodes$labels
#> x3 x4 x1 x2
#> "x3" "x4" "x1" "x2"
p_pa2 <- semPaths(fit_pa,
whatLabels = "est",
nCharNodes = 0,
sizeMan = 10,
edge.label.cex = 1.15,
layout = m
)
p_pa2$graphAttributes$Nodes$labels
#> [1] "x3" "x4" "x1" "x2"
Created on 2020-12-31 by the reprex package (v0.3.0)
This causes an issue as when the labels are not named, the labels after applying change_node_labels()
will also be unnamed, and the original variable names are lost, causing set_cfa_layout()
and set_sem_layout()
to fail.
A solution is to modify change_node_labels()
to add the original labels
as names when the original labels
is an unnamed vector. I've made the changes and will push to a new branch before merging.
x ~~ y
to both x ~~ y
and y ~~ x
.Done in #112 .
See this issue:
#6 (comment)
Some functions require users to specify the changes as a list of lists. E.g.,
list(list(node = "x3", rotate = 45),
list(node = "x4", rotate = -45),
list(node = "x2", rotate = -90))
This can be difficult to read and write. We can write a function to convert a named list, or even a named vector, to this kind of list. E.g.,
# Option A:
fct(c(x3 = 45, x4 = -45, x2 = -90)) # Return the list in the block above.
# Option B:
fct(list(x3 = 45, x4 = -45, x3 = -90)) # Return the list in the block above.
It is better to write a separate function to do this, for two reasons.
This function can be be used for all other semptools
functions.
We do not need to modify the existing functions.
Drafted, to_list_of_lists
. Preliminary tests passed.
Functions revised (preliminary tests passed).
rotate_resid
, for the argument rotate_resid_list
.
set_curve
, for the argument curve_list
.
set_edge_label_position
, for the argument position_list
.
set_sem_layout
, for the argument indicator_push
, indicator_spread
, and loading_position
Final checks before pushing v0.2.9 (work on this branch: devel_final_check
testthat
.devtools::check
.In indicator_order
, use NA
for the "indicators" of 2nd order factors. Then adding this
The attempts at commit 3e33ba9 tries to remove the need for NA
. Actually, factors that appear in both indicator_factor
and indicator_order
will be changed to NA
in indicator_order
.
The following
tmp <- sapply(residual_rotate, function(x) !is.na(x$node))
residual_rotate <- residual_rotate[tmp]
can be added before
semPaths_plot <- rotate_resid(semPaths_plot, residual_rotate)
will do.
setcolor has been drafted but not yet ported to the package.
Do this in its own branch.
Two tasks converted to issues: #83 , #82
layout_matrix
in quick start. (Do this in the branch vignette_layout_matrix) (ecb1628)drop_nodes
and keep_nodes
in quick start. (84057cf)change_node_label
(added in v0.2.8.1) in vignettes. (a88ddb8)master
branch, created the GitHub action to build the pages at the branch gh-pages
. Added /docs
to .gitignore
. Merged the commits to devel
.It should be possible to separate the pkgdown
files (in docs
) from the master branch, such that we can rebuild the site without updating the master branch.
2021-07-27: Tested in another package and it works. Will do this in the Version 0.2.9, after fixing the issue with change_node_label
.
Currently, only one cutoff is used, and only one level, "*", will be used in the label. Should allow at least three levels, conventionally, .05, .01, and .001.
Do this in its own branch.
Done in version 0.2.9.
%>%
by the native forward pipe operator |>
, introduced in R 4.1.0. Will remark the %>%
can be used in older version of R.%>%
by |>
in all filesmagrittr
from depends.Describe how to use named vectors or lists (an option added in v0.2.8.1) in vignettes. Update the quick starts because named list and vector should be the default way to specify the changes.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.