Skip to contents
if(!requireNamespace("fabricatr", quietly = TRUE)) {
  install.packages("fabricatr")
}

library(CausalQueries)
library(dplyr)
library(knitr)

Make a model

Generating: To make a model you need to provide a DAG statement to make_model.
For instance

  • "X->Y"
  • "X -> M -> Y <- X" or
  • "Z -> X -> Y <-> X".
# examples of models
xy_model <- make_model("X -> Y")
iv_model <- make_model("Z -> X -> Y <-> X")

Graphing: Once you have made a model you can inspect the DAG:

plot(xy_model)

Simple summaries: You can access a simple summary using summary()

summary(xy_model)
#> 
#> Causal statement: 
#> X -> Y
#> 
#> Nodal types: 
#> $X
#> 0  1
#> 
#>   node position display interpretation
#> 1    X       NA      X0          X = 0
#> 2    X       NA      X1          X = 1
#> 
#> $Y
#> 00  10  01  11
#> 
#>   node position display interpretation
#> 1    Y        1   Y[*]*      Y | X = 0
#> 2    Y        2   Y*[*]      Y | X = 1
#> 
#> Number of types by node:
#> X Y 
#> 2 4 
#> 
#> Number of causal types:  8
#> 
#> Note: Model does not contain: posterior_distribution, stan_objects;
#> to include these objects use update_model()
#> 
#> Note: To pose causal queries of this model use query_model()

or you can examine model details using inspect().

Inspecting: The model has a set of parameters and a default distribution over these.

xy_model |> inspect("parameters_df") 
#> 
#> parameters_df
#> Mapping of model parameters to nodal types: 
#> 
#>   param_names: name of parameter
#>   node:        name of endogeneous node associated
#>                with the parameter
#>   gen:         partial causal ordering of the
#>                parameter's node
#>   param_set:   parameter groupings forming a simplex
#>   given:       if model has confounding gives
#>                conditioning nodal type
#>   param_value: parameter values
#>   priors:      hyperparameters of the prior
#>                Dirichlet distribution 
#> 
#>   param_names node gen param_set nodal_type given param_value priors
#> 1         X.0    X   1         X          0              0.50      1
#> 2         X.1    X   1         X          1              0.50      1
#> 3        Y.00    Y   2         Y         00              0.25      1
#> 4        Y.10    Y   2         Y         10              0.25      1
#> 5        Y.01    Y   2         Y         01              0.25      1
#> 6        Y.11    Y   2         Y         11              0.25      1

Tailoring: These features can be edited using set_restrictions, set_priors and set_parameters.

Here is an example of setting a monotonicity restriction (see ?set_restrictions for more):

iv_model <- 
  iv_model |> set_restrictions(decreasing('Z', 'X'))

Here is an example of setting priors (see ?set_priors for more):

iv_model <- 
  iv_model |> set_priors(distribution = "jeffreys")
#> Altering all parameters.

Simulation: Data can be drawn from a model like this:

data <- make_data(iv_model, n = 4) 

data |> kable()
Z X Y
0 1 1
1 0 0
1 0 1
1 1 1

Update the model

Updating: Update using update_model. You can pass all rstan arguments to update_model.

df <- 
  data.frame(X = rbinom(100, 1, .5)) |>
  mutate(Y = rbinom(100, 1, .25 + X*.5))

xy_model <- 
  xy_model |> 
  update_model(df, refresh = 0)

Inspecting: You can access the posterior distribution on model parameters directly thus:


xy_model |> grab("posterior_distribution") |> 
  head() |> kable()
X.0 X.1 Y.00 Y.10 Y.01 Y.11
0.4866569 0.5133431 0.1779771 0.2021202 0.4300694 0.1898334
0.5583331 0.4416669 0.0040999 0.3005823 0.5902477 0.1050701
0.5271612 0.4728388 0.0531936 0.3516975 0.5905585 0.0045504
0.5637254 0.4362746 0.0400795 0.2803068 0.5334359 0.1461778
0.6073027 0.3926973 0.2665650 0.0452801 0.3071206 0.3810344
0.5437248 0.4562752 0.1933328 0.1887049 0.5473692 0.0705931

where each row is a draw of parameters.

Query the model

Arbitrary queries

Querying: You ask arbitrary causal queries of the model.

Examples of unconditional queries:

xy_model |> 
  query_model("Y[X=1] > Y[X=0]", 
              using = c("priors", "posteriors")) 
#> 
#> Causal queries generated by query_model (all at population level)
#> 
#> |label           |using      |  mean|    sd| cred.low| cred.high|
#> |:---------------|:----------|-----:|-----:|--------:|---------:|
#> |Y[X=1] > Y[X=0] |priors     | 0.254| 0.196|    0.007|     0.722|
#> |Y[X=1] > Y[X=0] |posteriors | 0.446| 0.119|    0.196|     0.649|

This query asks the probability that Y(1)>Y(0)Y(1)> Y(0).

Examples of conditional queries:

xy_model |> 
  query_model("Y[X=1] > Y[X=0] :|: X == 1 & Y == 1", using = c("priors", "posteriors")) 
#> 
#> Causal queries generated by query_model (all at population level)
#> 
#> |label                                 |using      |  mean|    sd| cred.low| cred.high|
#> |:-------------------------------------|:----------|-----:|-----:|--------:|---------:|
#> |Y[X=1] > Y[X=0] given X == 1 & Y == 1 |priors     | 0.499| 0.287|    0.023|     0.978|
#> |Y[X=1] > Y[X=0] given X == 1 & Y == 1 |posteriors | 0.677| 0.173|    0.335|     0.972|

This query asks the probability that Y(1)>Y(0)Y(1) > Y(0)given X=1X=1 and Y=1Y=1; it is a type of “causes of effects” query. Note that “:|:” is used to separate the main query element from the conditional statement to avoid ambiguity, since “|” is reserved for the “or” operator.

Queries can even be conditional on counterfactual quantities. Here the probability of a positive effect given some effect:

xy_model |> 
  query_model("Y[X=1] > Y[X=0] :|: Y[X=1] != Y[X=0]",
              using = c("priors", "posteriors")) 
#> 
#> Causal queries generated by query_model (all at population level)
#> 
#> |label                                  |using      |  mean|    sd| cred.low| cred.high|
#> |:--------------------------------------|:----------|-----:|-----:|--------:|---------:|
#> |Y[X=1] > Y[X=0] given Y[X=1] != Y[X=0] |priors     | 0.503| 0.290|    0.024|     0.975|
#> |Y[X=1] > Y[X=0] given Y[X=1] != Y[X=0] |posteriors | 0.748| 0.106|    0.574|     0.971|

Note that we use “:” to separate the base query from the condition rather than “|” to avoid confusion with logical operators.

Output

Query output is ready for printing as tables, but can also be plotted, which is especially useful with batch requests:

batch_queries <- xy_model |> 
  query_model(queries = list(ATE = "Y[X=1] - Y[X=0]", 
                             `Positive effect given any effect` = "Y[X=1] > Y[X=0] :|: Y[X=1] != Y[X=0]"), 
              using = c("priors", "posteriors"), 
              expand_grid = TRUE) 

batch_queries |> kable(digits = 2, caption = "tabular output")
tabular output
label query given using case_level mean sd cred.low cred.high
ATE Y[X=1] - Y[X=0] - priors FALSE 0.00 0.32 -0.62 0.63
ATE Y[X=1] - Y[X=0] - posteriors FALSE 0.28 0.09 0.08 0.45
Positive effect given any effect Y[X=1] > Y[X=0] Y[X=1] != Y[X=0] priors FALSE 0.50 0.29 0.02 0.98
Positive effect given any effect Y[X=1] > Y[X=0] Y[X=1] != Y[X=0] posteriors FALSE 0.75 0.11 0.57 0.97
batch_queries |> plot()