library(dagitty)
library(ggdag)
library(dplyr)
# This is a cross-sectional study: every covariate is measured at a single
# interview, so we cannot causally order the time-varying covariates among
# themselves. We draw their mutual associations -- from shared, unobserved
# earlier circumstances -- as non-directional (bidirected) edges, instead of
# giving each covariate its own separate latent "past state" node.
demographics <- c("age", "sex", "ethn") # fixed at (or determined by) birth
covariates <- c(
"homeless", "polydrug", "hivstat", "sexabuse", "dprsn_dx", "hplsns"
)
# every unordered pair of time-varying covariates gets a bidirected edge
covariate_pairs <- utils::combn(covariates, 2)
bidirected_edges <- paste(covariate_pairs[1, ], "<->", covariate_pairs[2, ])
demographic_edges <- as.vector(
outer(demographics, covariates, function(a, b) paste(a, "->", b))
)
needles_dag <- dagitty(paste0("dag {", paste(c(
# birth-fixed demographics cause the current states, injection frequency,
# and the outcome
demographic_edges,
paste(demographics, "-> nivdu"),
paste(demographics, "-> shared_syr"),
# time-varying covariates are mutually associated, with no causal ordering
bidirected_edges,
# each time-varying covariate can affect injection frequency and the outcome
paste(covariates, "-> nivdu"),
paste(covariates, "-> shared_syr"),
# injecting is the only mediator: sharing a syringe requires injecting
"nivdu -> shared_syr",
# study inclusion is conditioned on; several covariates affect being sampled,
# so conditioning on `selected` induces associations (selection bias)
paste(c(demographics, "homeless", "polydrug", "hivstat"), "-> selected")
), collapse = "; "), "}"))
# layout: time flows left -> right (birth-fixed demographics -> current
# covariates -> injection frequency -> shared syringes)
y_cov <- seq(4, -4, length.out = length(covariates))
coordinates(needles_dag) <- list(
x = c(
age = -1, sex = -1, ethn = -1,
setNames(rep(2, length(covariates)), covariates),
nivdu = 5, shared_syr = 7, selected = 5
),
y = c(
age = 2, sex = 0, ethn = -2,
setNames(y_cov, covariates),
nivdu = 3.2, shared_syr = 0, selected = -5.5
)
)
needles_tidy <- needles_dag |>
tidy_dagitty() |>
mutate(role = case_when(
name == "homeless" ~ "exposure",
name == "shared_syr" ~ "outcome",
name == "nivdu" ~ "mediator",
name == "selected" ~ "study inclusion (conditioned on)",
name %in% demographics ~ "demographic (birth-fixed)",
TRUE ~ "time-varying covariate (current/recent status)"
))
# split edges so associations and causal arrows can be styled separately
directed_edges <- function(d) dplyr::filter(d, .data$direction == "->")
association_edges <- function(d) dplyr::filter(d, .data$direction == "<->")
needles_tidy |>
ggplot() +
aes(x = x, y = y, xend = xend, yend = yend) +
# non-directional associations among time-varying covariates: dashed grey arcs
geom_dag_edges_arc(
data = association_edges, curvature = 0.4, arrow = NULL,
edge_colour = "grey65", edge_linetype = "dashed", edge_width = 0.4,
start_cap = ggraph::circle(5, "mm"), end_cap = ggraph::circle(5, "mm")
) +
# causal (directed) edges: solid arrows
geom_dag_edges_link(
data = directed_edges, edge_width = 0.35, edge_colour = "grey20",
start_cap = ggraph::circle(5, "mm"), end_cap = ggraph::circle(5, "mm")
) +
geom_dag_point(aes(colour = role, shape = role), size = 7) +
geom_dag_text(colour = "black", size = 1.7) +
scale_shape_manual(values = c(
"exposure" = 16, "outcome" = 16, "mediator" = 16,
"study inclusion (conditioned on)" = 15,
"demographic (birth-fixed)" = 16,
"time-varying covariate (current/recent status)" = 16
)) +
scale_colour_manual(values = c(
"exposure" = "#d73027", "outcome" = "#4575b4", "mediator" = "#1a9850",
"study inclusion (conditioned on)" = "grey30",
"demographic (birth-fixed)" = "grey55",
"time-varying covariate (current/recent status)" = "grey80"
)) +
guides(colour = guide_legend(nrow = 2), shape = guide_legend(nrow = 2)) +
theme_dag() +
theme(legend.position = "bottom", legend.title = element_blank())