Skip to content

Commit

Permalink
fix
Browse files Browse the repository at this point in the history
  • Loading branch information
strengejacke committed Sep 13, 2024
1 parent ded71b4 commit 5de1447
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 17 deletions.
63 changes: 54 additions & 9 deletions R/check_dag.R
Original file line number Diff line number Diff line change
Expand Up @@ -245,13 +245,33 @@ check_dag <- function(...,

# data for checking effects
checks <- lapply(c("direct", "total"), function(x) {
adjustment_set <- unlist(dagitty::adjustmentSets(dag, effect = x), use.names = FALSE)
if(is.null(adjusted)) {

Check warning on line 248 in R/check_dag.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/check_dag.R,line=248,col=7,[spaces_left_parentheses_linter] Place a space before left parenthesis, except in a function call.

Check warning on line 248 in R/check_dag.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/check_dag.R,line=248,col=7,[spaces_left_parentheses_linter] Place a space before left parenthesis, except in a function call.
adjustment_set <- unlist(dagitty::adjustmentSets(dag, effect = x), use.names = FALSE)
} else {
adjustment_set <- adjusted
}
adjustment_nodes <- unlist(dagitty::adjustedNodes(dag), use.names = FALSE)
minimal_adjustments <- as.list(dagitty::adjustmentSets(dag, effect = x))
collider <- adjustment_nodes[vapply(adjustment_nodes, ggdag::is_collider, logical(1), .dag = dag)]
if (!length(collider)) {

Check warning on line 256 in R/check_dag.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/check_dag.R,line=256,col=9,[if_not_else_linter] Prefer `if (A) x else y` to the less-readable `if (!A) y else x` in a simple if/else statement.

Check warning on line 256 in R/check_dag.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/check_dag.R,line=256,col=9,[if_not_else_linter] Prefer `if (A) x else y` to the less-readable `if (!A) y else x` in a simple if/else statement.
# if we don't have colliders, set to NULL
collider <- NULL
} else {
# if we *have* colliders, remove them from minimal adjustments
minimal_adjustments <- lapply(minimal_adjustments, function(ma) {

Check warning on line 261 in R/check_dag.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/check_dag.R,line=261,col=58,[unnecessary_lambda_linter] Pass setdiff directly as a symbol to lapply() instead of wrapping it in an unnecessary anonymous function. For example, prefer lapply(DF, sum) to lapply(DF, function(x) sum(x)).

Check warning on line 261 in R/check_dag.R

View workflow job for this annotation

GitHub Actions / lint / lint

file=R/check_dag.R,line=261,col=58,[unnecessary_lambda_linter] Pass setdiff directly as a symbol to lapply() instead of wrapping it in an unnecessary anonymous function. For example, prefer lapply(DF, sum) to lapply(DF, function(x) sum(x)).
setdiff(ma, collider)
})
}
list(
adjustment_not_needed = is.null(adjustment_set) && is.null(adjustment_nodes),
incorrectly_adjusted = is.null(adjustment_set) && !is.null(adjustment_nodes),
adjustment_not_needed = (is.null(adjustment_set) && is.null(adjustment_nodes) ||
(identical(sort(unique(adjustment_set)), sort(unique(setdiff(c(exposure, adjustment_nodes), collider)))))) &&
is.null(collider),
incorrectly_adjusted = (is.null(adjustment_set) && !is.null(adjustment_nodes)) ||
(!identical(sort(unique(adjustment_set)), sort(unique(setdiff(c(exposure, adjustment_nodes), collider))))) ||
(!is.null(collider) && collider %in% adjustment_nodes),
current_adjustments = adjustment_nodes,
minimal_adjustments = as.list(dagitty::adjustmentSets(dag, effect = x))
minimal_adjustments = minimal_adjustments,
collider = collider
)
})

Expand All @@ -260,6 +280,10 @@ check_dag <- function(...,
attr(dag, "exposure") <- exposure
attr(dag, "adjusted") <- adjusted
attr(dag, "adjustment_sets") <- checks[[1]]$current_adjustments
attr(dag, "collider") <- checks[[1]]$collider
# remove collider from sub-attributes
checks[[1]]$collider <- NULL
checks[[2]]$collider <- NULL
attr(dag, "check_direct") <- insight::compact_list(checks[[1]])
attr(dag, "check_total") <- insight::compact_list(checks[[2]])

Expand Down Expand Up @@ -296,6 +320,7 @@ as.dag <- function(x, ...) {
#' @export
print.check_dag <- function(x, ...) {
effect <- attributes(x)$effect
collider <- attributes(x)$collider

# header
cat(insight::print_color("# Check for correct adjustment sets", "blue"))
Expand All @@ -317,6 +342,16 @@ print.check_dag <- function(x, ...) {
)
}

# add information on colliders
if (!is.null(collider)) {
exposure_outcome_text <- paste0(
exposure_outcome_text,
"\n- Collider",
ifelse(length(collider) > 1, "s", ""),
": ", insight::color_text(datawizard::text_concatenate(collider), "cyan")
)
}

cat(exposure_outcome_text)
cat("\n\n")

Expand All @@ -331,12 +366,12 @@ print.check_dag <- function(x, ...) {
} else {
out <- attributes(x)$check_total
}
.print_dag_results(out, x, i, effect)
.print_dag_results(out, x, i, effect, collider)
}
}
}

.print_dag_results <- function(out, x, i, effect) {
.print_dag_results <- function(out, x, i, effect, collider = NULL) {
# missing adjustements - minimal_adjustment can be a list of different
# options for minimal adjustements, so we check here if any of the minimal
# adjustments are currently sufficient
Expand All @@ -356,8 +391,18 @@ print.check_dag <- function(x, ...) {
attributes(x)$outcome,
"`."
)
} else if (!is.null(collider)) {
# Scenario 2: adjusted for (downstream) collider
msg <- paste0(
insight::color_text("Incorrectly adjusted!", "red"),
"\nYour model adjusts for a (downstream) collider, ",
insight::color_text(datawizard::text_concatenate(collider, enclose = "`"), "cyan"),
". To estimate the ", i, " effect, do ",
insight::color_text("not", "italic"),
" adjust for it, to avoid collider-bias."
)
} else if (isTRUE(out$incorrectly_adjusted)) {
# Scenario 2: incorrectly adjusted, adjustments where none is allowed
# Scenario 3: incorrectly adjusted, adjustments where none is allowed
msg <- paste0(
insight::color_text("Incorrectly adjusted!", "red"),
"\nTo estimate the ", i, " effect, do ",
Expand All @@ -367,13 +412,13 @@ print.check_dag <- function(x, ...) {
"."
)
} else if (any(sufficient_adjustments)) {
# Scenario 3: correct adjustment
# Scenario 4: correct adjustment
msg <- paste0(
insight::color_text("Model is correctly specified.", "green"),
"\nAll minimal sufficient adjustments to estimate the ", i, " effect were done."
)
} else {
# Scenario 4: missing adjustments
# Scenario 5: missing adjustments
msg <- paste0(
insight::color_text("Incorrectly adjusted!", "red"),
"\nTo estimate the ", i, " effect, ",
Expand Down
184 changes: 176 additions & 8 deletions tests/testthat/_snaps/check_dag.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,128 @@
# check_dag

Code
print(dag)
Output
# Check for correct adjustment sets
- Outcome: y
- Exposure: x
Identification of direct and total effects
Model is correctly specified.
No adjustment needed to estimate the direct and total effect of `x` on `y`.

---

Code
print(dag)
Output
# Check for correct adjustment sets
- Outcome: y
- Exposure: x
- Adjustment: b
Identification of direct and total effects
Incorrectly adjusted!
To estimate the direct and total effect, do not adjust for `b`.

---

Code
print(dag)
Output
# Check for correct adjustment sets
- Outcome: y
- Exposure: x
Identification of direct and total effects
Incorrectly adjusted!
To estimate the direct and total effect, do not adjust for .

---

Code
print(dag)
Output
# Check for correct adjustment sets
- Outcome: y
- Exposure: x
- Adjustment: c
Identification of direct and total effects
Incorrectly adjusted!
To estimate the direct and total effect, do not adjust for `c`.

---

Code
print(dag)
Output
# Check for correct adjustment sets
- Outcome: y
- Exposure: x
- Adjustment: c
Identification of direct and total effects
Incorrectly adjusted!
To estimate the direct and total effect, do not adjust for `c`.

---

Code
print(dag)
Output
# Check for correct adjustment sets
- Outcome: mpg
- Exposure: wt
- Adjustments: cyl, disp and gear
Identification of direct and total effects
Model is correctly specified.
No adjustment needed to estimate the direct and total effect of `wt` on `mpg`.

# check_dag, multiple adjustment sets

Code
print(dag)
Output
# Check for correct adjustment sets
- Outcome: exam
- Exposure: podcast
Identification of direct and total effects
Incorrectly adjusted!
To estimate the direct and total effect, do not adjust for .

---

Code
print(dag)
Output
# Check for correct adjustment sets
- Outcome: exam
- Exposure: podcast
- Adjustments: alertness and prepared
Identification of direct and total effects
Incorrectly adjusted!
To estimate the direct and total effect, do not adjust for `alertness` and `prepared`.

# check_dag, different adjustements for total and direct

Code
Expand All @@ -10,12 +135,12 @@
Identification of direct effects
Incorrectly adjusted!
To estimate the direct effect, at least adjust for `x1` and `x2`. Currently, the model does not adjust for any variables.
To estimate the direct effect, do not adjust for .
Identification of total effects
Incorrectly adjusted!
To estimate the total effect, at least adjust for `x1`. Currently, the model does not adjust for any variables.
To estimate the total effect, do not adjust for .

---
Expand All @@ -31,12 +156,12 @@
Identification of direct effects
Incorrectly adjusted!
To estimate the direct effect, at least adjust for `x1` and `x2`. Currently, the model only adjusts for `x1`. You possibly also need to adjust for `x2` to block biasing paths.
To estimate the direct effect, do not adjust for `x1`.
Identification of total effects
Model is correctly specified.
All minimal sufficient adjustments to estimate the total effect were done.
Incorrectly adjusted!
To estimate the total effect, do not adjust for `x1`.

---
Expand All @@ -52,7 +177,7 @@
Identification of direct effects
Incorrectly adjusted!
To estimate the direct effect, at least adjust for `x1` and `x2`. Currently, the model only adjusts for `x2`. You possibly also need to adjust for `x1` to block biasing paths.
To estimate the direct effect, do not adjust for `x2`.
Identification of total effects
Expand All @@ -72,12 +197,55 @@
Identification of direct effects
Model is correctly specified.
All minimal sufficient adjustments to estimate the direct effect were done.
Incorrectly adjusted!
To estimate the direct effect, do not adjust for `x1` and `x2`.
Identification of total effects
Incorrectly adjusted!
To estimate the total effect, do not adjust for `x1` and `x2`.

# check_dag, collider bias

Code
print(dag)
Output
# Check for correct adjustment sets
- Outcome: SMD_ICD11
- Exposure: agegroup
- Adjustments: edgroup3, gender_kid, pss4_kid_sum_2sd and residence
Identification of direct effects
Model is correctly specified.
No adjustment needed to estimate the direct effect of `agegroup` on `SMD_ICD11`.
Identification of total effects
Model is correctly specified.
No adjustment needed to estimate the total effect of `agegroup` on `SMD_ICD11`.

---

Code
print(dag)
Output
# Check for correct adjustment sets
- Outcome: SMD_ICD11
- Exposure: agegroup
- Adjustments: edgroup3, gender_kid, pss4_kid_sum_2sd, residence and sm_h_total_kid
- Collider: sm_h_total_kid
Identification of direct effects
Incorrectly adjusted!
Your model adjusts for a (downstream) collider, `sm_h_total_kid`. To estimate the direct effect, do not adjust for it, to avoid collider-bias.
Identification of total effects
Incorrectly adjusted!
Your model adjusts for a (downstream) collider, `sm_h_total_kid`. To estimate the total effect, do not adjust for it, to avoid collider-bias.

28 changes: 28 additions & 0 deletions tests/testthat/test-check_dag.R
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,31 @@ test_that("check_dag, different adjustements for total and direct", {
)
expect_snapshot(print(dag))
})

test_that("check_dag, collider bias", {
dag <- check_dag(
SMD_ICD11 ~ agegroup + gender_kid + edgroup3 + residence + pss4_kid_sum_2sd + sm_h_total_kid,
pss4_kid_sum_2sd ~ gender_kid,
sm_h_total_kid ~ gender_kid + agegroup,
adjusted = c(
"agegroup", "gender_kid", "edgroup3", "residence",
"pss4_kid_sum_2sd"
),
outcome = "SMD_ICD11",
exposure = "agegroup"
)
expect_snapshot(print(dag))

dag <- check_dag(
SMD_ICD11 ~ agegroup + gender_kid + edgroup3 + residence + pss4_kid_sum_2sd + sm_h_total_kid,
pss4_kid_sum_2sd ~ gender_kid,
sm_h_total_kid ~ gender_kid + agegroup,
adjusted = c(
"agegroup", "gender_kid", "edgroup3", "residence",
"pss4_kid_sum_2sd", "sm_h_total_kid"
),
outcome = "SMD_ICD11",
exposure = "agegroup"
)
expect_snapshot(print(dag))
})

0 comments on commit 5de1447

Please sign in to comment.