Skip to content

Commit a92d670

Browse files
Dev (#62)
* add varying paths * Update dsem_control.Rd * fix option for TMBad * Using TMB@dev for now * simplify CPP * fix last commit * updates * add logistic-regression demo * Update test_deterministic_with_measurements.R * add control$stabilize_Q * Update NEWS.md * augment estimate_mu * Update DESCRIPTION * Update model-description.Rmd * update vignettes * Update model-description.Rmd * Update model-description.Rmd * add data * add vignettes * Update statistical_interactions.Rmd * Create utils.hpp * fix defaults for estimate_mu = NULL... ... i.e., estimating mu for all variables with >= 1 observation * update to use `gmrf_project` ... ... by default * renaming options for `gmrf_parameterization` * try to make CI happy * try again to make CI happy * Update dynamic_factor_analysis.Rmd * still trying to make CI happy * Create dsemRTMB.Rd * another attempt to make CI happy * updates * Update dynamic_factor_analysis.Rmd * loosen test tol to 0.001 * updates to vignettes * add informative error ... ... when using gmrf_parameterization = "full" and a zero-exogenous variance variable * fix bug in previous error-check * add integrated tests for varying-slopes * make check_win_devel happy * Create explore_varying_slopes.R * Update model-description.Rmd * add rk4sys for vignettes * add GLM example * improved `family` interface * fix docs for family-list * add branch to CI * fix vignettes and bugs * make CI happy * adding `gaussian_fixed_sd` * Update test-distributions.R * make CI happy * add lognormal family * fix typo * make CI happy * Update beverton-holt.R * Create explore_GARCH.R * add error check * Update dsem.R * add informative error for projecting moderators * Update dsem.R * add GARCH vignette * add informative error ... ... for when `sem` includes variables not in `tsdata` (i.e., due to typos) * adding tweeide and make_msv * change version number for dev * fix bug from in merge * Update model-description.Rmd * Update _pkgdown.yml * Update _pkgdown.yml * Update make_dsem_ram.R * make R_CMD_check happy * update docs * fix typo in nonlinear vignette ... ... h/t Gabriel Leiva-Gomez * Create github_notes.R * Update loo_residuals.Rd * update Version to prep for CRAN * Update NEWS.md * add checkmate assertions --------- Co-authored-by: Jim Thorson <50178738+James-Thorson-NOAA@users.noreply.github.com> Co-authored-by: James Thorson <James.Thorson@noaa.gov>
1 parent 81d3d81 commit a92d670

35 files changed

Lines changed: 1235 additions & 183 deletions

.github/workflows/R-CMD-check.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
33
on:
44
push:
5-
branches: [main, dev]
5+
branches: [main, dev, dev_add-family]
66
pull_request:
7-
branches: [main, dev]
7+
branches: [main, dev, dev_add-family]
88

99
name: R-CMD-check
1010

.github/workflows/test-coverage.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
33
on:
44
push:
5-
branches: [main, dev]
5+
branches: [main, dev, dev_add-family]
66
pull_request:
7-
branches: [main, dev]
7+
branches: [main, dev, dev_add-family]
88

99
name: test-coverage
1010

DESCRIPTION

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
Package: dsem
22
Type: Package
33
Title: Dynamic Structural Equation Models
4-
Version: 2.0.1
5-
Date: 2026-05-12
4+
Version: 3.0.0
5+
Date: 2026-06-10
66
Authors@R:
77
c(person(given = "James",
88
family = "Thorson",
@@ -34,7 +34,8 @@ Imports:
3434
ggplot2,
3535
grid,
3636
methods,
37-
stats
37+
stats,
38+
checkmate
3839
Depends:
3940
R (>= 4.0.0),
4041
Suggests:

NAMESPACE

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,22 @@ export(classify_variables)
1515
export(convert_equations)
1616
export(dsem)
1717
export(dsem_control)
18+
export(fixed)
19+
export(gaussian_fixed_sd)
1820
export(list_parameters)
21+
export(lognormal)
1922
export(loo_residuals)
2023
export(make_dfa)
2124
export(make_dsem_ram)
2225
export(make_matrices)
26+
export(make_msv)
2327
export(parse_path)
2428
export(partition_variance)
2529
export(rk4sys)
2630
export(stepwise_selection)
2731
export(test_dsep)
2832
export(total_effect)
33+
export(tweedie)
2934
importFrom(Matrix,Cholesky)
3035
importFrom(Matrix,Diagonal)
3136
importFrom(Matrix,bandSparse)
@@ -53,6 +58,10 @@ importFrom(TMB,config)
5358
importFrom(TMB,dynlib)
5459
importFrom(TMB,sdreport)
5560
importFrom(TMB,summary.sdreport)
61+
importFrom(checkmate,assertClass)
62+
importFrom(checkmate,assertList)
63+
importFrom(checkmate,assertLogical)
64+
importFrom(checkmate,assertNames)
5665
importFrom(ggplot2,aes)
5766
importFrom(ggraph,create_layout)
5867
importFrom(ggraph,geom_edge_arc)
@@ -71,13 +80,17 @@ importFrom(methods,is)
7180
importFrom(stats,"tsp<-")
7281
importFrom(stats,.preformat.ts)
7382
importFrom(stats,AIC)
83+
importFrom(stats,Gamma)
84+
importFrom(stats,binomial)
85+
importFrom(stats,gaussian)
7486
importFrom(stats,logLik)
7587
importFrom(stats,na.omit)
7688
importFrom(stats,nlminb)
7789
importFrom(stats,optimHess)
7890
importFrom(stats,pchisq)
7991
importFrom(stats,plogis)
8092
importFrom(stats,pnorm)
93+
importFrom(stats,poisson)
8194
importFrom(stats,rbinom)
8295
importFrom(stats,rgamma)
8396
importFrom(stats,rnorm)

NEWS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
# dsem 3.0.0
2+
3+
* Change to requiring `family` options that agree with `glm`
4+
* Adding `family = gaussian_fixed_sd()` option to pre-specify a known standard deviation
5+
* Adding `family = lognormal()` option (e.g., for linearized Beverton-Holt stock recruit analysis)
6+
* Add `family = tweedie()` option (e.g., for paleo Lake cores)
7+
* Adding option for moderator variables affecting exogenous variance in Gamma (not just paths in Rho)
8+
* adding `make_msv` for helping multivariate stochastic volatility models
9+
* Adding MGARCH vignette
10+
* Adding `checkmate` assertions for user inputs
11+
112
# dsem 2.0.1
213

314
* Fix an "out-of-bounds read" error identified by SAN

R/dsem.R

Lines changed: 134 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@
77
#' \code{\link[dsem]{make_dsem_ram}} for more description
88
#' @param tsdata time-series data, as outputted using \code{\link[stats]{ts}}, with \code{NA}
99
#' for missing values.
10-
#' @param family Character-vector listing the distribution used for each column of \code{tsdata}, where
11-
#' each element must be \code{fixed} (for no measurement error),
12-
#' \code{normal} for normal measurement error using an identity link,
13-
#' \code{gamma} for a gamma measurement error using a fixed CV and log-link,
14-
#' \code{bernoulli} for a Bernoulli measurement error using a logit-link, or
15-
#' \code{poisson} for a Poisson measurement error using a log-link.
16-
#' \code{family="fixed"} is default behavior and assumes that a given variable is measured exactly.
17-
#' Other options correspond to different specifications of measurement error.
10+
#' @param family A named list of families, each returning a class \code{family},
11+
#' including [fixed()], [gaussian()], [binomial()], [Gamma()], [poisson()],
12+
#' [lognormal()], [tweedie()], or [gaussian_fixed_sd()] with names that match levels of
13+
#' \code{colnames(tsdata)} to allow different
14+
#' families by variable. Family [fixed()] specifies that states
15+
#' are known (i.e., measurements for that variable have no error).
16+
#' Other families allow users to supply a link function including `identity`,
17+
#' `log`, `logit`, or `cloglog`. For example
18+
#' \code{family = list(y = binomial("logit"), x = fixed())}
19+
#' would specify logit-linked Bernoulli distribution for variable `tsdata$y`
20+
#' and a fixed (no measurement error) distribution for `tsdata$x`.
21+
#' For many variables, it is convenient to do e.g.,
22+
#' \code{family = Map(function(.) gaussian(), colnames(tsdata))} rather than writing
23+
#' them all manually.
1824
#' @param estimate_delta0 Boolean indicating whether to estimate deviations from equilibrium in initial year
1925
#' as fixed effects, or alternatively to assume that dynamics start at some stochastic draw away from
2026
#' the stationary distribution
@@ -42,14 +48,17 @@
4248
#' settings, and see documentation for that function for details.
4349
#'
4450
#' @importFrom TMB compile dynlib MakeADFun sdreport summary.sdreport config
45-
#' @importFrom stats AIC sd .preformat.ts na.omit nlminb optimHess pnorm rbinom rgamma rpois rnorm simulate time tsp<- plogis pchisq
51+
#' @importFrom stats AIC sd .preformat.ts na.omit nlminb optimHess pnorm rbinom
52+
#' @importFrom stats rgamma rpois rnorm simulate time tsp<- plogis pchisq
53+
#' @importFrom stats gaussian poisson Gamma binomial
4654
#' @importFrom Matrix solve Cholesky sparseMatrix mat2triplet drop0 t
4755
#' @importFrom igraph plot.igraph graph_from_data_frame with_sugiyama layout_ graph.adjacency clusters
4856
#' @importFrom ggraph ggraph geom_edge_arc create_layout rectangle geom_node_text theme_graph
4957
#' @importFrom ggplot2 aes
5058
#' @importFrom grid arrow
5159
#' @importFrom methods is
5260
#' @importFrom utils combn
61+
#' @importFrom checkmate assertList assertClass assertNames assertLogical
5362
#'
5463
#' @details
5564
#' A DSEM involves (at a minimum):
@@ -186,10 +195,12 @@
186195
#' "gwage","invest","capital")]
187196
#'
188197
#' # Fit model
189-
#' fit = dsem( sem=sem,
190-
#' tsdata = tsdata,
191-
#' estimate_delta0 = TRUE,
192-
#' control = dsem_control(quiet=TRUE) )
198+
#' fit = dsem(
199+
#' sem = sem,
200+
#' tsdata = tsdata,
201+
#' estimate_delta0 = TRUE,
202+
#' control = dsem_control(quiet=TRUE)
203+
#' )
193204
#' summary( fit )
194205
#' plot( fit )
195206
#' plot( fit, edge_label="value" )
@@ -199,22 +210,32 @@
199210
dsem <-
200211
function( sem,
201212
tsdata,
202-
family = rep("fixed",ncol(tsdata)),
213+
family = Map(function(.) fixed(), colnames(tsdata)),
203214
estimate_delta0 = FALSE,
204215
estimate_mu = NULL,
205216
prior_negloglike = NULL,
206217
control = dsem_control(),
207218
covs = colnames(tsdata) ){
208219
start_time = Sys.time()
209220

221+
# Temporary warning
222+
if( isTRUE(is(family, "character")) ){
223+
stop("starting with release 3.0.0, `family` must be a named list of ")
224+
}
225+
210226
# General error checks
211-
if( isFALSE(is(control, "dsem_control")) ) stop("`control` must be made by `dsem_control()`")
227+
#if( isFALSE(is(control, "dsem_control")) ) stop("`control` must be made by `dsem_control()`")
212228
if( isTRUE(control$gmrf_parameterization=="project") ){
213229
if( isTRUE(any(family=="fixed" & colSums(!is.na(tsdata))>0)) ){
214230
stop("`family` cannot be `fixed` using `gmrf_parameterization=projection` for any variable with data")
215231
}
216232
}
217-
if( isFALSE(is(tsdata,"ts")) ) stop("`tsdata` must be a `ts` object")
233+
#if( isFALSE(is(tsdata,"ts")) ) stop("`tsdata` must be a `ts` object")
234+
assertClass(control, classes = "dsem_control")
235+
assertClass(tsdata, classes = "ts")
236+
assertNames( names(family), must.include = colnames(tsdata))
237+
assertList(family, types = "family")
238+
assertLogical(estimate_delta0, len = 1)
218239

219240
# General warnings
220241
if( isFALSE(control$quiet) ){
@@ -249,7 +270,7 @@ function( sem,
249270
stop("If specifying `lower` or `upper`, please set `dsem_control('newton_loops'=0)`")
250271
}
251272
}
252-
if( any(ram$heads==0) ){
273+
if( any(ram$heads %in% c(3,4)) ){
253274
# Using moderating variables, their raw values are used to construct Rho_kk and raw values follow standard-normal distribution
254275
# so Gamma_kk and Rho_kk are not properly applied to moderating variables
255276
if( control$gmrf_parameterization %in% c("project") ){
@@ -261,7 +282,8 @@ function( sem,
261282
options = c(
262283
switch(control$gmrf_parameterization, "full" = 0, "project" = 1, "mvn_project" = 2, "gmrf_project" = 3, NA),
263284
switch(control$constant_variance, "conditional"=0, "marginal"=1, "diagonal"=2),
264-
ifelse( isTRUE(control$stabilize_Q), 1, 0 )
285+
ifelse( isTRUE(control$stabilize_Q), 1, 0 ),
286+
ifelse( isTRUE(control$logscale_moderating_variance), 1, 0)
265287
)
266288

267289
# define variables to project to `project_k` unless provided by user
@@ -304,14 +326,89 @@ function( sem,
304326
}
305327
}
306328

329+
# Check for errors
330+
if( !all(colnames(tsdata) %in% names(family)) ){
331+
stop("some variable in `colnames(tsdata)` not found in `names(family)`")
332+
}
333+
334+
# distribution/link
335+
build_distributions <-
336+
function( variables ){
337+
338+
# Construct log_sigma based on family
339+
remove_last = function(x) x[-length(x)]
340+
341+
# Fixed values
342+
sigma_j = lapply( family, FUN=function(x){
343+
switch( x$family[length(x$family)],
344+
"fixed" = c(),
345+
"gaussian" = NA,
346+
"poisson" = c(),
347+
"binomial" = c(),
348+
"bernoulli" = c(),
349+
"Gamma" = NA,
350+
"gaussian_fixed_sd" = c(),
351+
"lognormal" = NA,
352+
"tweedie" = c(NA, NA)
353+
)} )
354+
Nsigma_j = sapply(sigma_j, length)
355+
sigmastart_j = remove_last(cumsum(c(0,Nsigma_j)))
356+
names(sigmastart_j) = variables
357+
358+
# Extract known SDs
359+
sd_tj = sapply( family,
360+
FUN = function(x){
361+
if(length(x$fixed_sd)==0){
362+
return(rep(0, nrow(tsdata)))
363+
}else{
364+
sd = rep(0, nrow(tsdata))
365+
sd[] = x$fixed_sd
366+
return(sd)
367+
}
368+
}
369+
)
370+
371+
#
372+
family_code = sapply( family, FUN=function(x){
373+
c("fixed" = 0,
374+
"gaussian" = 1,
375+
"binomial" = 2,
376+
"bernoulli" = 2,
377+
"poisson" = 3,
378+
"Gamma" = 4,
379+
"gaussian_fixed_sd" = 5,
380+
"lognormal" = 6,
381+
"tweedie" = 7
382+
)[x$family]} )
383+
link_code = sapply( family, FUN=function(x){
384+
c("identity" = 0,
385+
"log" = 1,
386+
"logit" = 2,
387+
"cloglog" = 3
388+
)[x$link]} )
389+
out = list(
390+
family_code = family_code,
391+
link_code = link_code,
392+
Nsigma_j = Nsigma_j,
393+
sigmastart_j = sigmastart_j,
394+
sigma_j = sigma_j,
395+
sd_tj = sd_tj
396+
)
397+
return(out)
398+
}
399+
family = family[match(colnames(tsdata), names(family))]
400+
distributions = build_distributions( colnames(tsdata) )
401+
307402
#
308403
Data = list(
309-
"options" = options,
310-
#"RAM" = as.matrix(na.omit(ram[,1:4])),
311-
"RAM" = as.matrix(ram[,-5]),
312-
"RAMstart" = as.numeric(ram[,5]),
313-
"familycode_j" = sapply(family, FUN=switch, "fixed"=0, "normal"=1, "bernoulli"=2, "poisson"=3, "gamma"=4 ),
314-
"y_tj" = tsdata
404+
options = options,
405+
RAM = as.matrix(ram[,-5]),
406+
RAMstart = as.numeric(ram[,5]),
407+
familycode_j = distributions$family_code,
408+
linkcode_j = distributions$link_code,
409+
sigmastart_j = distributions$sigmastart_j,
410+
eps_tj = distributions$sd_tj,
411+
y_tj = tsdata
315412
)
316413

317414
# Default ... unobs_idx for fixed variables with zero variance
@@ -323,10 +420,10 @@ function( sem,
323420
# Construct parameters
324421
if( is.null(control$parameters) ){
325422
Params = list(
326-
beta_z = rep(0,max(ram[,4],na.rm=TRUE)), # NA for spatially-varying paths in ram
327-
lnsigma_j = rep(0,ncol(tsdata)),
328-
mu_j = rep(0,ncol(tsdata)),
329-
delta0_j = rep(0,ncol(tsdata)),
423+
beta_z = rep(0, max(ram[,4],na.rm=TRUE)), # NA for spatially-varying paths in ram
424+
lnsigma_z = rep(0, sum(distributions$Nsigma_j)),
425+
mu_j = rep(0, ncol(tsdata)),
426+
delta0_j = rep(0, ncol(tsdata)),
330427
x_tj = ifelse( is.na(tsdata), 0, tsdata )
331428
)
332429
#if( control$gmrf_parameterization=="full" ){
@@ -368,9 +465,9 @@ function( sem,
368465
if( is.null(control$map) ){
369466
Map = list()
370467
# Map off x_tj for fixed when data is available
371-
Map$x_tj = ifelse( is.na(as.vector(tsdata)) | (Data$familycode_j[col(tsdata)] %in% c(1,2,3,4)), seq_len(prod(dim(tsdata))), NA )
468+
Map$x_tj = ifelse( is.na(as.vector(tsdata)) | (Data$familycode_j[col(tsdata)] %in% c(1,2,3,4,5,6,7)), seq_len(prod(dim(tsdata))), NA )
372469
# Map off sigma_j for fixed / bernoulli / Poisson
373-
Map$lnsigma_j = factor( ifelse(Data$familycode_j %in% c(0,2,3), NA, seq_along(Params$lnsigma_j)) )
470+
# Map$lnsigma_j = factor( ifelse(Data$familycode_j %in% c(0,2,3), NA, seq_along(Params$lnsigma_j)) )
374471

375472
# Map off mean for latent variables
376473
Map$mu_j = factor( ifelse(colnames(tsdata) %in% estimate_mu, seq_len(ncol(tsdata)), NA) )
@@ -645,6 +742,9 @@ function( sem,
645742
#' are then replaced with Inf and avoided during estimation
646743
#' @param stabilize_Q add \code{stability_eps = 1e-10} to stabilize precision
647744
#' (experimental)
745+
#' @param logscale_moderating_variance When users supply a moderating variable
746+
#' for an exogenous variance (i.e., double-headed arrow), whether to
747+
#' exponentiate the variable prior to
648748
#'
649749
#' @return
650750
#' An S3 object of class "dsem_control" that specifies detailed model settings,
@@ -673,7 +773,8 @@ function( nlminb_loops = 1,
673773
upper = Inf,
674774
project_k = NULL,
675775
suppress_nlminb_warnings = TRUE,
676-
stabilize_Q = FALSE ){
776+
stabilize_Q = FALSE,
777+
logscale_moderating_variance = FALSE ){
677778

678779
gmrf_parameterization = match.arg(gmrf_parameterization)
679780
constant_variance = match.arg(constant_variance)
@@ -701,7 +802,8 @@ function( nlminb_loops = 1,
701802
upper = upper,
702803
project_k = project_k,
703804
suppress_nlminb_warnings = suppress_nlminb_warnings,
704-
stabilize_Q = stabilize_Q
805+
stabilize_Q = stabilize_Q,
806+
logscale_moderating_variance = logscale_moderating_variance
705807
), class = "dsem_control" )
706808
}
707809

0 commit comments

Comments
 (0)