diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 82570215d..9dd5ab6b7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -15,7 +15,7 @@ jobs: fail-fast: true matrix: version: - - '1.7.3' + - '1' os: - ubuntu-latest arch: diff --git a/.github/workflows/CI_ecosystem.yml b/.github/workflows/CI_ecosystem.yml index a3fa15887..98a79eb0b 100644 --- a/.github/workflows/CI_ecosystem.yml +++ b/.github/workflows/CI_ecosystem.yml @@ -18,7 +18,8 @@ jobs: fail-fast: false matrix: version: - - '1.7.3' + - '1' + - '1.9' os: - ubuntu-latest - macos-latest @@ -42,4 +43,4 @@ jobs: - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v2 with: - file: lcov.info \ No newline at end of file + file: lcov.info diff --git a/.github/workflows/CI_extended.yml b/.github/workflows/CI_extended.yml index f6857a517..4cc24d90d 100644 --- a/.github/workflows/CI_extended.yml +++ b/.github/workflows/CI_extended.yml @@ -18,7 +18,8 @@ jobs: fail-fast: false matrix: version: - - '1.7.3' + - '1' + - '1.9' os: - ubuntu-latest - macos-latest @@ -42,4 +43,4 @@ jobs: - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v2 with: - file: lcov.info \ No newline at end of file + file: lcov.info diff --git a/.github/workflows/TagBot b/.github/workflows/TagBot.yml similarity index 100% rename from .github/workflows/TagBot rename to .github/workflows/TagBot.yml diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index beac68371..e7dacab73 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@latest with: - version: '1.7.2' + version: '1' - name: Install dependencies run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' - name: Build and deploy diff --git a/.github/workflows/preview-documentation.yml b/.github/workflows/preview-documentation.yml index 9bc3297fd..29373527b 100644 --- a/.github/workflows/preview-documentation.yml +++ b/.github/workflows/preview-documentation.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@latest with: - version: '1.6' + version: '1' - name: Install dependencies run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' - name: Build and deploy @@ -36,4 +36,4 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, body: 'Docs can be previewed here: ${{ env.GHP_URL }}previews/PR${{ env.PR_NUMBER }}' - }) \ No newline at end of file + }) diff --git a/Project.toml b/Project.toml index e2062f5a7..c8176911b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "StructuralEquationModels" uuid = "383ca8c5-e4ff-4104-b0a9-f7b279deed53" authors = ["Maximilian Ernst", "Aaron Peikert"] -version = "0.1.0" +version = "0.2.0" [deps] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" @@ -24,7 +24,7 @@ StenoGraphs = "78862bba-adae-4a83-bb4d-33c106177f81" Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" [compat] -julia = "1.7" +julia = "1.9" StenoGraphs = "0.2" DataFrames = "1" Distributions = "0.25" diff --git a/README.md b/README.md index ea15eeb1e..cb50cfea8 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ |:-------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------:| | [![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://structuralequationmodels.github.io/StructuralEquationModels.jl/) [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://structuralequationmodels.github.io/StructuralEquationModels.jl/dev/) | [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) [![Github Action CI](https://github.com/StructuralEquationModels/StructuralEquationModels.jl/workflows/CI_extended/badge.svg)](https://github.com/StructuralEquationModels/StructuralEquationModels.jl/actions/) [![codecov](https://codecov.io/gh/StructuralEquationModels/StructuralEquationModels.jl/branch/main/graph/badge.svg?token=P2kjzpvM4V)](https://codecov.io/gh/StructuralEquationModels/StructuralEquationModels.jl) | [![DOI](https://zenodo.org/badge/228649704.svg)](https://zenodo.org/badge/latestdoi/228649704) | +# What is this Package for? This is a package for Structural Equation Modeling. It is still *in development*. @@ -14,6 +15,8 @@ Models you can fit include - Multigroup SEM - Sums of arbitrary loss functions (everything the optimizer can handle). +# What are the merrits? + We provide fast objective functions, gradients, and for some cases hessians as well as approximations thereof. As a user, you can easily define custom loss functions. For those, you can decide to provide analytical gradients or use finite difference approximation / automatic differentiation. @@ -21,7 +24,8 @@ You can choose to mix and match loss functions natively found in this package an In such cases, you optimize over a sum of different objectives (e.g. ML + Ridge). This mix and match strategy also applies to gradients, where you may supply analytic gradients or opt for automatic differentiation or mix analytical and automatic differentiation. -You may consider using this package if: +# You may consider using this package if: + - you want to extend SEM (e.g. add a new objective function) and need an extendable framework - you want to extend SEM, and your implementation needs to be fast (because you want to do a simulation, for example) - you want to fit the same model(s) to many datasets (bootstrapping, simulation studies) @@ -33,7 +37,7 @@ The package makes use of - Optim.jl and NLopt.jl to provide a range of different Optimizers/Linesearches. - FiniteDiff.jl and ForwardDiff.jl to provide gradients for user-defined loss functions. -At the moment, we are still working on +# At the moment, we are still working on: - optimizing performance for big models (with hundreds of parameters) # Questions? diff --git a/docs/src/developer/loss.md b/docs/src/developer/loss.md index 30461bdb3..8bd654bf1 100644 --- a/docs/src/developer/loss.md +++ b/docs/src/developer/loss.md @@ -217,7 +217,7 @@ Let's make a sligtly more complicated example: we will reimplement maximum likel To keep it simple, we only cover models without a meanstructure. The maximum likelihood objective is defined as ```math -F_{ML} = \log \det \Sigma_i + \mathrm{tr}(\Sigma_i \Sigma_o) +F_{ML} = \log \det \Sigma_i + \mathrm{tr}\left(\Sigma_{i}^{-1} \Sigma_o \right) ``` where ``\Sigma_i`` is the model implied covariance matrix and ``\Sigma_o`` is the observed covariance matrix. We can query the model implied covariance matrix from the `imply` par of our model, and the observed covariance matrix from the `observed` path of our model. @@ -269,4 +269,4 @@ model_ml = SemFiniteDiff( model_fit = sem_fit(model_ml) ``` -If you want to differentiate your own loss functions via automatic differentiation, check out the [AutoDiffSEM](https://github.com/StructuralEquationModels/AutoDiffSEM) package (spoiler allert: it's really easy). \ No newline at end of file +If you want to differentiate your own loss functions via automatic differentiation, check out the [AutoDiffSEM](https://github.com/StructuralEquationModels/AutoDiffSEM) package (spoiler allert: it's really easy). diff --git a/src/StructuralEquationModels.jl b/src/StructuralEquationModels.jl index 17aa46d04..131ec206f 100644 --- a/src/StructuralEquationModels.jl +++ b/src/StructuralEquationModels.jl @@ -90,6 +90,7 @@ export AbstractSem, SemConstant, SemWLS, loss, SemOptimizer, SemOptimizerEmpty, SemOptimizerOptim, SemOptimizerNLopt, NLoptConstraint, + optimizer, n_iterations, convergence, SemObserved, SemObservedData, SemObservedCovariance, SemObservedMissing, observed, sem_fit, diff --git a/src/frontend/fit/standard_errors/hessian.jl b/src/frontend/fit/standard_errors/hessian.jl index 1300d7cb4..2afb08822 100644 --- a/src/frontend/fit/standard_errors/hessian.jl +++ b/src/frontend/fit/standard_errors/hessian.jl @@ -26,7 +26,7 @@ function se_hessian(sem_fit::SemFit; hessian = :finitediff) elseif hessian == :expected throw(ArgumentError("standard errors based on the expected hessian are not implemented yet")) else - throw(ArgumentError("I dont know how to compute `$how` standard-errors")) + throw(ArgumentError("I dont know how to compute `$hessian` standard-errors")) end invH = c*inv(H) @@ -44,9 +44,14 @@ H_scaling(model::AbstractSemSingle) = model.optimizer, model.loss.functions...) -H_scaling(model, obs, imp, optimizer, lossfun::Union{SemML, SemWLS}) = +H_scaling(model, obs, imp, optimizer, lossfun::SemML) = 2/(n_obs(model)-1) +function H_scaling(model, obs, imp, optimizer, lossfun::SemWLS) + @warn "Standard errors for WLS are only correct if a GLS weight matrix (the default) is used." + return 2/(n_obs(model)-1) +end + H_scaling(model, obs, imp, optimizer, lossfun::SemFIML) = 2/n_obs(model) diff --git a/src/frontend/specification/EnsembleParameterTable.jl b/src/frontend/specification/EnsembleParameterTable.jl index c1edc8d44..8504be567 100644 --- a/src/frontend/specification/EnsembleParameterTable.jl +++ b/src/frontend/specification/EnsembleParameterTable.jl @@ -69,7 +69,22 @@ end # Sorting ---------------------------------------------------------------------------------- -# todo +# Sorting ---------------------------------------------------------------------------------- + +function sort!(ensemble_partable::EnsembleParameterTable) + + for partable in values(ensemble_partable.tables) + sort!(partable) + end + + return ensemble_partable +end + +function sort(partable::EnsembleParameterTable) + new_partable = deepcopy(partable) + sort!(new_partable) + return new_partable +end # add a row -------------------------------------------------------------------------------- diff --git a/src/imply/RAM/symbolic.jl b/src/imply/RAM/symbolic.jl index bcb4c3b67..23362646b 100644 --- a/src/imply/RAM/symbolic.jl +++ b/src/imply/RAM/symbolic.jl @@ -238,7 +238,7 @@ identifier(imply::RAMSymbolic) = imply.identifier n_par(imply::RAMSymbolic) = imply.n_par function update_observed(imply::RAMSymbolic, observed::SemObserved; kwargs...) - if n_man(observed) == size(imply.Σ) + if Int(n_man(observed)) == size(imply.Σ, 1) return imply else return RAMSymbolic(;observed = observed, kwargs...) diff --git a/src/imply/empty.jl b/src/imply/empty.jl index 7b1de7e73..65d0e3259 100644 --- a/src/imply/empty.jl +++ b/src/imply/empty.jl @@ -3,6 +3,7 @@ ############################################################################################ """ Empty placeholder for models that don't need an imply part. +(For example, models that only regularize parameters.) # Constructor @@ -11,6 +12,10 @@ Empty placeholder for models that don't need an imply part. # Arguments - `specification`: either a `RAMMatrices` or `ParameterTable` object +# Examples +A multigroup model with ridge regularization could be specified as a `SemEnsemble` with one +model per group and an additional model with `ImplyEmpty` and `SemRidge` for the regularization part. + # Extended help ## Interfaces diff --git a/test/examples/multigroup/build_models.jl b/test/examples/multigroup/build_models.jl index db2f10dfa..4738ab4a6 100644 --- a/test/examples/multigroup/build_models.jl +++ b/test/examples/multigroup/build_models.jl @@ -16,10 +16,8 @@ model_g2 = Sem( model_ml_multigroup = SemEnsemble(model_g1, model_g2; optimizer = semoptimizer) -############################################################################################ -### test gradients -############################################################################################ +# gradients @testset "ml_gradients_multigroup" begin @test test_gradient(model_ml_multigroup, start_test; atol = 1e-9) end @@ -49,6 +47,69 @@ end lav_groups = Dict(:Pasteur => 1, :Grant_White => 2)) end +############################################################################################ +# ML estimation - sorted +############################################################################################ + +partable_s = sort(partable) + +specification_s = RAMMatrices(partable_s) + +specification_g1_s = specification_s[:Pasteur] +specification_g2_s = specification_s[:Grant_White] + +model_g1 = Sem( + specification = specification_g1_s, + data = dat_g1, + imply = RAMSymbolic +) + +model_g2 = Sem( + specification = specification_g2_s, + data = dat_g2, + imply = RAM +) + +model_ml_multigroup = SemEnsemble(model_g1, model_g2; optimizer = semoptimizer) + +# gradients +@testset "ml_gradients_multigroup | sorted" begin + @test test_gradient(model_ml_multigroup, start_test; atol = 1e-2) +end + +grad = similar(start_test) +gradient!(grad, model_ml_multigroup, rand(36)) +grad_fd = FiniteDiff.finite_difference_gradient(x -> objective!(model_ml_multigroup, x), start_test) + +# fit +@testset "ml_solution_multigroup | sorted" begin + solution = sem_fit(model_ml_multigroup) + update_estimate!(partable_s, solution) + @test compare_estimates( + partable, + solution_lav[:parameter_estimates_ml]; atol = 1e-4, + lav_groups = Dict(:Pasteur => 1, :Grant_White => 2)) +end + +@testset "fitmeasures/se_ml | sorted" begin + solution_ml = sem_fit(model_ml_multigroup) + @test all(test_fitmeasures( + fit_measures(solution_ml), + solution_lav[:fitmeasures_ml]; rtol = 1e-2, atol = 1e-7)) + + update_partable!( + partable_s, identifier(model_ml_multigroup), se_hessian(solution_ml), :se) + @test compare_estimates( + partable_s, + solution_lav[:parameter_estimates_ml]; atol = 1e-3, + col = :se, lav_col = :se, + lav_groups = Dict(:Pasteur => 1, :Grant_White => 2)) +end + +@testset "sorted | LowerTriangular A" begin + @test imply(model_ml_multigroup.sems[2]).A isa LowerTriangular +end + ############################################################################################ # ML estimation - user defined loss function ############################################################################################ diff --git a/test/examples/multigroup/multigroup.jl b/test/examples/multigroup/multigroup.jl index e7d87fa4a..a86371caa 100644 --- a/test/examples/multigroup/multigroup.jl +++ b/test/examples/multigroup/multigroup.jl @@ -1,5 +1,5 @@ using StructuralEquationModels, Test, FiniteDiff -import LinearAlgebra: diagind +import LinearAlgebra: diagind, LowerTriangular # import StructuralEquationModels as SEM include( joinpath(chop(dirname(pathof(StructuralEquationModels)), tail = 3), diff --git a/test/examples/political_democracy/constraints.jl b/test/examples/political_democracy/constraints.jl index 5044f7540..a9bc7aa1a 100644 --- a/test/examples/political_democracy/constraints.jl +++ b/test/examples/political_democracy/constraints.jl @@ -61,6 +61,6 @@ end solution_constrained = sem_fit(model_ml_constrained) @test solution_constrained.solution[31]*solution_constrained.solution[30] >= 0.6 @test all(abs.(solution_constrained.solution) .< 10) - @test_skip solution_constrained.optimization_result.result[3] == :FTOL_REACHED + @test solution_constrained.optimization_result.result[3] == :FTOL_REACHED skip=true @test abs(solution_constrained.minimum - 21.21) < 0.01 end \ No newline at end of file