Causal

Causal reasoning with DAGs, Pearl’s do-calculus, back-door adjustment, CLP-validated adjustment sets, and counterfactual estimation.

Run with:

etai cookbook/causal/causal_demo.eta

Or compile first for better performance:

etac -O cookbook/causal/causal_demo.eta
etai causal_demo.etac

Causal DAG Example

cookbook/do-calculus/dag.eta · source

Building and querying a DAG with std.causal.

(module dag-demo
  (import std.io std.causal)
  (begin

    ;; Define a DAG over market variables
    ;; sentiment -> macro, sentiment -> return
    ;; macro -> return, beta -> return, sector -> return
    (define g
      (causal:dag
        '((sentiment macro)
          (sentiment return)
          (macro     return)
          (beta      return)
          (sector    return))))

    ;; Query the DAG structure
    (println (causal:parents   g 'return))
    ;; => (macro beta sector sentiment)

    (println (causal:ancestors g 'return))
    ;; => (sentiment macro beta sector)

    ;; Does X have a causal path to Y?
    (println (causal:connected? g 'beta    'return))  ; => #t
    (println (causal:connected? g 'return  'beta))    ; => #f

    ;; d-separation: is 'beta d-separated from 'sector given 'return?
    (println (causal:d-separated? g 'beta 'sector '(return)))  ; => ...
  ))

Do-Calculus Rules

cookbook/do-calculus/do-rules.eta · source

Applying Pearl’s three rules of do-calculus symbolically to manipulate interventional distributions.

(module do-rules
  (import std.io std.causal)
  (begin

    ;; Backdoor criterion
    ;; A set Z satisfies the back-door criterion relative to (X, Y) if:
    ;;   1. No node in Z is a descendant of X
    ;;   2. Z blocks every back-door path from X to Y
    (define g
      (causal:dag
        '((confounder x) (confounder y) (x y))))

    ;; Find all valid back-door adjustment sets
    (define adj-sets
      (causal:backdoor-sets g 'x 'y))

    (display "Back-door adjustment sets: ")
    (println adj-sets)
    ;; => ((confounder))

    ;; Identify the interventional distribution P(y | do(x))
    ;; using the back-door adjustment formula:
    ;;   P(y | do(x)) = sum_z P(y | x, z) * P(z)
    (define formula
      (causal:identify g 'y 'x (car adj-sets)))

    (println formula)
    ;; => (backdoor-adjust y x (confounder))
  ))

Causal — Neural Pipeline

cookbook/causal/causal_demo.eta · source

A four-stage pipeline that chains symbolic processing, causal reasoning, constraint logic, and neural computation into a single program.

Data-generating process (known, for validation):

return = 1.5·beta + 0.8·sector_code + 0.3·beta·sector_code + noise
True ATE(beta: 1.2→0.9) ≈ 0.447

Stage 1 — Symbolic Factor Model

Define a linear factor model as an S-expression, symbolically differentiate it with respect to each factor, and simplify with fixed-point algebraic rewriting.

;; Factor model: return = a*beta + b*macro + noise
(define factor-model
  '(+ (* alpha beta) (* gamma macro)))

;; Symbolic gradient w.r.t. beta
(define d-beta  (symbolic-diff factor-model 'beta))
(define d-macro (symbolic-diff factor-model 'macro))

(println d-beta)    ; => alpha
(println d-macro)   ; => gamma

Stage 2 — Causal Reasoning

Encode the DAG of market variables and use the back-door adjustment formula to derive P(return | do(beta)).

(define g
  (causal:dag
    '((sentiment macro) (sentiment return)
      (macro return) (beta return)
      (sector return))))

;; Back-door adjustment for P(return | do(beta))
;; Adjustment set: {macro, sector}  (blocks all back-door paths)
(define adj-sets (causal:backdoor-sets g 'beta 'return))
(println (car adj-sets))    ; => (macro sector)

Stage 3 — CLP Validation

Use findall with backtracking to discover every valid back-door adjustment set, then validate each candidate with CLP(Z) domain constraints.

(define valid-sets
  (filter
    (lambda (s)
      ;; Verify subset size constraint via CLP(Z)
      (let ((n (logic-var)))
        (clp:domain n 1 3)
        (clp:= n (length s))
        (clp:labeling (list n) 'strategy 'ff)))
    adj-sets))

(println (length valid-sets))   ; number of valid adjustment sets

Stage 4 — Neural ATE Estimation

Train a small neural network (nn/sequential + nn/linear) to estimate E[return | beta, sector], then plug predictions into the causal formula to compute the Average Treatment Effect.

(import std.torch)

;; Two-layer MLP: input(2) -> hidden(16) -> output(1)
(define model
  (nn/sequential
    (nn/linear 2 16)
    (nn/relu)
    (nn/linear 16 1)))

;; Mini-batch SGD training loop
(defun train-step (model xs ys lr)
  (let* ((pred (nn/forward model xs))
         (loss (mse-loss pred ys)))
    (zero-grad! model)
    (backward! loss)
    (sgd-step! model lr)
    (item loss)))

;; Training loop (50 epochs)
(let loop ((epoch 0))
  (when (< epoch 50)
    (let ((loss (train-step model X-train y-train 0.01)))
      (when (= (modulo epoch 10) 0)
        (display "epoch ") (display epoch)
        (display "  loss = ") (println loss)))
    (loop (+ epoch 1))))

;; ATE via back-door adjustment:
;;   ATE = E[Y|do(beta=1.2)] - E[Y|do(beta=0.9)]
;;       = sum_s [ E_nn[return|1.2,s] - E_nn[return|0.9,s] ] * P(s)
(display "Estimated ATE: ")
(println (compute-ate model sector-probs 1.2 0.9))
;; => ~0.447  (matches ground truth)