## ----include = FALSE----------------------------------------------------------
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)

diagram_helper <- if (file.exists("diagram-helpers.Rinc")) {
  "diagram-helpers.Rinc"
} else {
  file.path("vignettes", "diagram-helpers.Rinc")
}
sys.source(diagram_helper, envir = knitr::knit_global())

diagram_device <- .vignette_diagram_device()
knitr::opts_chunk$set(dev = diagram_device, fig.ext = diagram_device)

## ----setup--------------------------------------------------------------------
library(dyadMLM)
has_glmmTMB <- requireNamespace("glmmTMB", quietly = TRUE)
dsm_fitted_alt <- "Fitted DSM diagram unavailable."

## ----prepare-cross-dsm--------------------------------------------------------
cross_dsm_data <- dyadMLM::prepare_dyad_data(
  dyads_cross,
  dyad = coupleID,
  member = personID,
  role = gender,
  predictors = provided_support,
  model_types = "dsm",
  # All three observed compositions in `dyads_cross` are detected and retained by
  # default. This example focuses on `female-male` dyads, so we restrict the
  # analysis here.
  keep_compositions = "female-male",
  dsm_role_order = c("female", "male")
)

print(cross_dsm_data, n = 4)

## ----conceptual-dsm-diagram, echo=FALSE, fig.width=9, fig.height=4.8, out.width="100%", fig.cap="Conceptual cross-sectional DSM. Predictor mean and predictor difference each predict both outcome scores.", fig.alt="Path diagram for a cross-sectional dyadic score model. The centered female-male predictor mean and female-minus-male predictor difference each predict the female-male outcome mean and female-minus-male outcome difference. Paths are labelled a11, a12, a21, and a22, and outcome intercepts are labelled a10 and a20."----
draw_dsm_diagram()

## ----conceptual-dsm-member-diagram, echo=FALSE, fig.width=9, fig.height=5.6, out.width="100%", fig.cap="Individual-level representation of the cross-sectional DSM used for the long-format multilevel model. The centered predictor mean and female-minus-male predictor difference appear on both member rows. For the female outcome, the intercept and slopes add one-half of the corresponding outcome-difference parameters. For the male outcome, they subtract one-half. The female and male residuals may have different variances and covary.", fig.alt="Two-panel path diagram for a female-minus-male dyadic score model. Both panels contain the centered predictor mean and female-minus-male predictor difference. For the female outcome, the intercept is a10 plus half a20, the predictor-mean coefficient is a11 plus half a21, and the predictor-difference coefficient is a12 plus half a22. For the male outcome, the same combinations use minus signs. The two member residuals covary."----
draw_dsm_member_diagram()

## ----fit-cross-dsm, eval=has_glmmTMB------------------------------------------
dsm_model <- glmmTMB::glmmTMB(
  closeness ~

    # Outcome-level intercept
    1 +

    # Predictor level -> outcome level (a11)
    .dy_provided_support_dyad_mean_gmc +

    # Predictor difference -> outcome level (a12)
    .dy_provided_support_within_dyad_diff +

    # Outcome-difference intercept (a20)
    .dy_dsm_role_contrast +

    # Predictor level -> outcome difference (a21)
    .dy_provided_support_dyad_mean_gmc:.dy_dsm_role_contrast +

    # Predictor difference -> outcome difference (a22)
    .dy_provided_support_within_dyad_diff:.dy_dsm_role_contrast +

    # Outcome-level and outcome-difference residual variances and their covariance
    us(1 + .dy_dsm_role_contrast | coupleID),
  dispformula = ~ 0,
  family = gaussian(),
  data = cross_dsm_data
)

summary(dsm_model)

## ----prepare-fitted-dsm-diagram, include=FALSE, eval=has_glmmTMB--------------
dsm_diagram_values <- .extract_dsm_diagram_values(dsm_model)
dsm_diagram_estimates <- dsm_diagram_values$estimates
dsm_diagram_residuals <- dsm_diagram_values$residuals

dsm_fitted_alt <- sprintf(
  paste(
    "Fitted DSM. Intercepts a10 %.2f and a20 %.2f; paths a11 %.2f, a12 %.2f,",
    "a21 %.2f, and a22 %.2f; residual SDs %.2f and %.2f, with correlation %.2f."
  ),
  dsm_diagram_estimates[["a10"]],
  dsm_diagram_estimates[["a20"]],
  dsm_diagram_estimates[["a11"]],
  dsm_diagram_estimates[["a12"]],
  dsm_diagram_estimates[["a21"]],
  dsm_diagram_estimates[["a22"]],
  dsm_diagram_residuals[["sd_mean"]],
  dsm_diagram_residuals[["sd_difference"]],
  dsm_diagram_residuals[["correlation"]]
)

## ----fitted-dsm-diagram, echo=FALSE, eval=has_glmmTMB, fig.width=9, fig.height=4.8, out.width="100%", fig.cap="Fitted cross-sectional DSM for the example data. The nodes identify the mean and difference scores; edge and intercept labels show the estimated DSM coefficients, and the residual labels show the estimated score-component standard deviations and correlation.", fig.alt=dsm_fitted_alt----
draw_dsm_diagram(
  model = dsm_model,
  labels = c(predictor = "provided support", outcome = "closeness")
)

## ----prepare-cross-dsm-inverted-----------------------------------------------
cross_dsm_data_inverted <- dyadMLM::prepare_dyad_data(
  dyads_cross,
  dyad = coupleID,
  member = personID,
  role = gender,
  predictors = provided_support,
  # Request APIM columns too for comparison below.
  model_types = c("dsm", "apim"),
  keep_compositions = "female-male",
  dsm_role_order = c("male", "female")
)

## ----fit-cross-dsm-inverted, eval=has_glmmTMB---------------------------------
dsm_model_inverted <- glmmTMB::glmmTMB(
  closeness ~
    .dy_provided_support_dyad_mean_gmc +
    .dy_provided_support_within_dyad_diff +
    .dy_dsm_role_contrast +
    .dy_provided_support_dyad_mean_gmc:.dy_dsm_role_contrast +
    .dy_provided_support_within_dyad_diff:.dy_dsm_role_contrast +
    us(1 + .dy_dsm_role_contrast | coupleID),
  dispformula = ~ 0,
  family = gaussian(),
  data = cross_dsm_data_inverted
)

female_minus_male <- glmmTMB::fixef(dsm_model)$cond
male_minus_female <- glmmTMB::fixef(dsm_model_inverted)$cond

knitr::kable(
  data.frame(
    `model term` = names(female_minus_male),
    `female - male` = unname(female_minus_male),
    `male - female` = unname(male_minus_female),
    check.names = FALSE
  ),
  digits = 3,
  align = c("l", "r", "r")
)

## ----fit-cross-apim, eval=has_glmmTMB-----------------------------------------
apim_model <- glmmTMB::glmmTMB(
  closeness ~
    # Role-specific intercepts
    0 +
    .dy_is_female_x_male_female +
    .dy_is_female_x_male_male +

    # Role-specific actor effects
    .dy_is_female_x_male_female:.dy_provided_support_actor +
    .dy_is_female_x_male_male:.dy_provided_support_actor +

    # Role-specific partner effects
    .dy_is_female_x_male_female:.dy_provided_support_partner +
    .dy_is_female_x_male_male:.dy_provided_support_partner +

    # Role-specific Gaussian residual covariance structure
    us(0 +
         .dy_is_female_x_male_female +
         .dy_is_female_x_male_male
       | coupleID),
  dispformula = ~ 0,
  family = gaussian(),
  data = cross_dsm_data_inverted
)

## ----compare-cross-fit, eval=has_glmmTMB--------------------------------------
data.frame(
  model = c("DSM: female - male", "DSM: male - female", "APIM"),
  AIC = round(c(AIC(dsm_model), AIC(dsm_model_inverted), AIC(apim_model)), 3),
  BIC = round(c(BIC(dsm_model), BIC(dsm_model_inverted), BIC(apim_model)), 3),
  logLik = round(c(
    as.numeric(logLik(dsm_model)),
    as.numeric(logLik(dsm_model_inverted)),
    as.numeric(logLik(apim_model))
  ), 3)
)

## ----compare-cross-coefficients, eval=has_glmmTMB, echo=FALSE-----------------
apim_coef <- glmmTMB::fixef(apim_model)$cond
dsm_coef <- glmmTMB::fixef(dsm_model)$cond

b0_female <- apim_coef[[".dy_is_female_x_male_female"]]
b0_male <- apim_coef[[".dy_is_female_x_male_male"]]
b_actor_female <- apim_coef[[
  ".dy_is_female_x_male_female:.dy_provided_support_actor"
]]
b_actor_male <- apim_coef[[
  ".dy_is_female_x_male_male:.dy_provided_support_actor"
]]
b_partner_female <- apim_coef[[
  ".dy_is_female_x_male_female:.dy_provided_support_partner"
]]
b_partner_male <- apim_coef[[
  ".dy_is_female_x_male_male:.dy_provided_support_partner"
]]

mu_X <- mean(
  (cross_dsm_data_inverted$.dy_provided_support_actor +
     cross_dsm_data_inverted$.dy_provided_support_partner) / 2 -
    cross_dsm_data_inverted$.dy_provided_support_dyad_mean_gmc,
  na.rm = TRUE
)

b_sum_female <- b_actor_female + b_partner_female
b_sum_male <- b_actor_male + b_partner_male
b_difference_female <- b_actor_female - b_partner_female
b_difference_male <- b_actor_male - b_partner_male

a11_from_apim <- (b_sum_female + b_sum_male) / 2
a21_from_apim <- b_sum_female - b_sum_male
a12_from_apim <- (b_difference_female - b_difference_male) / 4
a22_from_apim <- (b_difference_female + b_difference_male) / 2

apim_to_dsm <- c(
  a10 = (b0_female + b0_male) / 2 + mu_X * a11_from_apim,
  a11 = a11_from_apim,
  a12 = a12_from_apim,
  a20 = b0_female - b0_male + mu_X * a21_from_apim,
  a21 = a21_from_apim,
  a22 = a22_from_apim
)

a10_dsm <- dsm_coef[["(Intercept)"]]
a11_dsm <- dsm_coef[[".dy_provided_support_dyad_mean_gmc"]]
a12_dsm <- dsm_coef[[".dy_provided_support_within_dyad_diff"]]
a20_dsm <- dsm_coef[[".dy_dsm_role_contrast"]]
a21_dsm <- dsm_coef[[
  ".dy_provided_support_dyad_mean_gmc:.dy_dsm_role_contrast"
]]
a22_dsm <- dsm_coef[[
  ".dy_provided_support_within_dyad_diff:.dy_dsm_role_contrast"
]]

knitr::kable(
  data.frame(
    `DSM path` = names(apim_to_dsm),
    `From APIM transformation` = round(unname(apim_to_dsm), 3),
    `From DSM model` = round(c(
      a10_dsm,
      a11_dsm,
      a12_dsm,
      a20_dsm,
      a21_dsm,
      a22_dsm
    ), 3),
    check.names = FALSE
  ),
  align = c("l", "r", "r"),
  caption = paste0(
    "APIM-to-DSM fixed-effect transformation (centering constant = ",
    round(mu_X, 3),
    ")."
  )
)

## ----compare-cross-random-effects, eval=has_glmmTMB---------------------------
apim_vcov <- as.matrix(glmmTMB::VarCorr(apim_model)$cond$coupleID)
dsm_vcov <- as.matrix(glmmTMB::VarCorr(dsm_model)$cond$coupleID)

rotation <- rbind(
  outcome_level = c(0.5, 0.5),
  outcome_difference = c(1, -1)
)
apim_to_dsm_vcov <- rotation %*% apim_vcov %*% t(rotation)

data.frame(
  parameter = c(
    "Var(outcome mean)",
    "Cov(outcome mean, outcome diff)",
    "Var(outcome diff)"
  ),
  from_DSM = round(c(
    dsm_vcov[1, 1],
    dsm_vcov[1, 2],
    dsm_vcov[2, 2]
  ), 3),
  from_APIM_transformation = round(c(
    apim_to_dsm_vcov[1, 1],
    apim_to_dsm_vcov[1, 2],
    apim_to_dsm_vcov[2, 2]
  ), 3)
)

## ----prepare-ild-dsm----------------------------------------------------------
ild_dsm_data <- dyadMLM::prepare_dyad_data(
  dyads_ild,
  dyad = coupleID,
  member = personID,
  role = gender,
  time = diaryday,
  predictors = provided_support,
  model_types = "dsm",
  keep_compositions = "female-male",
  dsm_role_order = c("female", "male")
)

print(ild_dsm_data, n = 4)

## ----fit-ild-dsm, eval = has_glmmTMB------------------------------------------

dsm_ILD <- glmmTMB::glmmTMB(
  closeness ~

    # Outcome-level intercept and linear time trend
    1 +
    diaryday +

    # Within-person predictor level -> outcome level
    .dy_provided_support_cwp_dyad_mean +

    # Within-person predictor difference -> outcome level
    .dy_provided_support_cwp_within_dyad_diff +

    # Between-person predictor level -> outcome level
    .dy_provided_support_cbp_dyad_mean +

    # Between-person predictor difference -> outcome level
    .dy_provided_support_cbp_within_dyad_diff +

    # Outcome-difference intercept and linear time trend
    .dy_dsm_role_contrast +
    diaryday:.dy_dsm_role_contrast +

    # Within-person predictor level and difference -> outcome difference
    .dy_provided_support_cwp_dyad_mean:.dy_dsm_role_contrast +
    .dy_provided_support_cwp_within_dyad_diff:.dy_dsm_role_contrast +

    # Between-person predictor level and difference -> outcome difference
    .dy_provided_support_cbp_dyad_mean:.dy_dsm_role_contrast +
    .dy_provided_support_cbp_within_dyad_diff:.dy_dsm_role_contrast +

    # Stable outcome-level and outcome-difference covariance
    us(1 + .dy_dsm_role_contrast | coupleID) +

    # Same-day outcome-level and outcome-difference covariance
    us(1 + .dy_dsm_role_contrast | coupleID:diaryday),
  dispformula = ~ 0,
  family = gaussian(),
  data = ild_dsm_data
)

summary(dsm_ILD)


