---
title: "Get started with DAGassist"
author: "Graham Goff and Michael Denly"
output: rmarkdown::html_vignette
bibliography: references.bib
link-citations: true
vignette: >
  %\VignetteIndexEntry{Get started with DAGassist}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  dpi = 150,
  fig.width = 7
)
options(DAGassist.color = FALSE, crayon.enabled = FALSE)

# chunks that need optional packages run only when those packages are installed
has_plot <- requireNamespace("ggdag", quietly = TRUE) &&
  requireNamespace("ggplot2", quietly = TRUE)
has_tables <- requireNamespace("modelsummary", quietly = TRUE)
has_estimands <- has_tables &&
  requireNamespace("WeightIt", quietly = TRUE) &&
  requireNamespace("marginaleffects", quietly = TRUE) &&
  requireNamespace("DirectEffects", quietly = TRUE)
```

`DAGassist` checks your regression against your causal diagram. It infers which controls identify the estimand of interst, re-fits the model with the controls the diagram implies, and reports the comparison. This guide walks through the workflow in five steps:

1. Declare the effect you want to estimate.
2. Draw a DAG of how you think the data were generated.
3. Classify each control by its causal role.
4. Re-estimate the model with DAG-derived adjustment sets.
5. Target your estimand explicitly.

The running example asks whether higher income increases voter turnout. The data, `turnout_data`, are simulated, so the right answers are known.

```{r setup}
library(DAGassist)
```

## Step 1: Declare your estimand

Before choosing controls, decide which effect you want [@FindleyKikutaDenly2021]. Income could affect turnout in two ways: directly, and by raising political interest, which in turn raises turnout. That gives two different questions:

- **The total effect.** How much does turnout change when income rises, through every path? In `turnout_data`, the answer is **0.50**.
- **The direct effect.** How much does turnout change when income rises *but political interest is held fixed*? The answer is **0.30**.

The choice matters because the same control can be required for one question and inadmissable for the other. This guide targets the total effect.

## Step 2: Draw your DAG

A directed acyclic graph (DAG) encodes your assumptions about how the data were generated. Each edge (arrow) is a direct causal effect; each missing edge is an assumption that there is none. For introductions to DAGs, see @Pearl2009, @Elwert2013, and @HunermundEtAl2025.

The easiest way to write a DAG in R is `ggdag::dagify()`, which uses formula syntax: each line lists a variable and its direct causes. Note that `DAGassist` requires that node names must match the column names in your data.

```{r dag}
dag <- ggdag::dagify(
  turnout  ~ income + age + parental_ses + polint + elect_comp,
  income   ~ age + parental_ses + industry,
  polint   ~ income,
  industry ~ age,
  exposure = "income",
  outcome  = "turnout"
)
```

Alternatively, you can also draw the DAG in the [DAGitty](https://dagitty.net/) web tool and paste its code into `dagitty::dagitty()`. 

```{r dag-plot, echo = FALSE, message = FALSE, warning = FALSE, eval = has_plot, fig.height = 3.5, fig.alt = "The turnout DAG. Age and parental_ses affect income and turnout; age affects industry, which affects income; income affects political interest, which affects turnout; election competitiveness affects turnout."}
library(ggdag)
library(ggplot2)

#coordinates for dag labels
label_df <- data.frame(
  name = c("turnout", "income", "parental_ses", "age", "polint", "industry", "elect_comp"),
  label_x = c(10, 0, 5, 5, 5, 0, 9),
  label_y = c(0, 0, 1, -1, 0.55, -0.75, -0.75)
)
label_df$label <- unname(attr(turnout_dag, "labels")[label_df$name])

ggdag_status(turnout_dag, text = FALSE) +
  theme_dag() +
  scale_color_manual(values = c(exposure = "black", outcome = "grey30"),
                     na.value = "grey60") +
  theme(legend.position = "none") +
  geom_label(
    data = label_df,
    aes(x = label_x, y = label_y, label = label),
    inherit.aes = FALSE,
    size = 3.4,
    label.size = 0.25
  )
```

In our example, age and parental_ses affect both income and turnout. Political interest carries part of income's effect to turnout. Industry affects income only, and election competitiveness affects turnout only.

## Step 3: Classify your controls

Now take the regression you would have run. A common instinct is to control for everything available. Pass the DAG and that regression to `DAGassist()`. The regression is written as the full model call, so `DAGassist()` can re-fit it with other controls.

```{r roles}
DAGassist(dag,
          lm(turnout ~ income + parental_ses + age + polint + industry + elect_comp,
             data = turnout_data),
          show = "roles")
```

Classifying roles needs only the DAG and the formula, not the data. For the total effect:

- `age` and `parental_ses` are **confounders**. They must be controlled for.
- `polint` is a **mediator** and a bad control. Controlling for it removes the part of income's effect that runs through political interest.
- `industry` affects only income, and `elect_comp` affects only turnout. Both are **neutral controls**: safe to include, but not required.

[Causal roles and adjustment sets](https://grahamgoff.com/DAGassist/articles/roles-and-sets.html) defines every role `DAGassist` can assign.

## Step 4: Re-estimate with DAG-derived adjustment sets

With `show = "models"`, `DAGassist()` also re-fits the model with two adjustment sets derived from the DAG. The **minimal** set contains only the controls needed to block every back-door path. The **canonical** set also adds every safe control.

```{r models, results = "asis", eval = has_tables}
DAGassist(dag,
          lm(turnout ~ income + parental_ses + age + polint + industry + elect_comp,
             data = turnout_data),
          show = "models", type = "text", verbose = FALSE)
```

Both DAG-derived models estimate income's effect at 0.49, close to the true total effect of 0.50. The original regression returns 0.28; by controlling for the mediator, the original model estimates something close to the *direct* effect (0.30). In the console, the full report also includes balance diagnostics, which flag when missing data leave the models with different samples.

To see the comparison as a plot, set `type = "dotwhisker"`:

```{r dotwhisker, warning = FALSE, eval = has_plot, fig.height = 4.5, fig.alt = "Dot-and-whisker plot of the income coefficient: about 0.28 in the original model and about 0.49 in the minimal and canonical models."}
DAGassist(dag,
          lm(turnout ~ income + parental_ses + age + polint + industry + elect_comp,
             data = turnout_data),
          type = "dotwhisker")
```

## Step 5: Target your estimand explicitly

The DAG-derived regressions recover the total effect here because the model is linear and has no interactions. The `estimand` argument targets an effect directly, whatever the model:

- `estimand = "total"` adds inverse-probability-weighted estimates of the average total effect.
- `estimand = "direct"` adds sequential g-estimates of the average controlled direct effect.

```{r estimands, results = "asis", eval = has_estimands}
DAGassist(dag,
          lm(turnout ~ income + parental_ses + age + polint + industry + elect_comp,
             data = turnout_data),
          estimand = c("total", "direct"),
          show = "models", type = "text", verbose = FALSE)
```

The weighted total-effect estimates (0.49–0.50) agree with the regression estimates, and the direct-effect estimates (0.28) are close to the true direct effect of 0.30. Each number now corresponds to an explict estimand. [Total and direct effects](https://grahamgoff.com/DAGassist/articles/estimands.html) explains how each column is estimated, how to read the weight diagnostics, and what each estimand assumes.

## Next steps

Every conclusion above assumes the DAG is correct. Thus, it is important that you use `DAGassist`'s uncertainty tests to see if model results are sensitive to uncertainty in the DAG. For example, what if political interest raises income, rather than the reverse?

```{r robustness}
pdag_robustness(dag,
                formula = turnout ~ income + parental_ses + age + polint + industry + elect_comp,
                uncertain_edges = "income -- polint")
```

If that edge is reversed, political interest becomes a confounder, changing the adjustment set and estimates. [Robustness to DAG uncertainty](https://grahamgoff.com/DAGassist/articles/robustness.html) explains uncertainty testing in greater detail.

Further documentation and tutorials:

- [Causal roles and adjustment sets](https://grahamgoff.com/DAGassist/articles/roles-and-sets.html) covers every role and both adjustment sets.
- [Total and direct effects](https://grahamgoff.com/DAGassist/articles/estimands.html) covers estimand recovery and its assumptions.
- [Robustness to DAG uncertainty](https://grahamgoff.com/DAGassist/articles/robustness.html) tests uncertain and missing arrows.
- [Exporting reports](https://grahamgoff.com/DAGassist/articles/exporting.html) writes the report to LaTeX, Word, Excel, plain text, or a plot.
- [Supported model engines](https://grahamgoff.com/DAGassist/articles/compatibility.html) lists the estimators `DAGassist` works with beyond `lm()`.

## References
