DAG와 인과 다이어그램

Directed Acyclic Graphs — 인과 구조를 시각적으로 추론하다

인과 다이어그램(DAG)의 정의와 기본 규칙, 경로(path)의 유형, d-분리(d-separation), 충돌자(collider)의 역할, 그리고 DAG를 활용한 교란·선택편향의 구조적 분류를 다룬다. Hernán & Robins (2020) Ch.6을 기반으로 작성하였다.

Experimentation
Causal Inference
저자

Kwangmin Kim

공개

2026년 03월 20일

1 정의

정의: 인과 방향 비순환 그래프 (Causal DAG)

인과 DAG(Directed Acyclic Graph)는 변수를 노드(node)로, 직접 인과 효과를 방향 화살표(edge)로 나타내는 그래프이다.

  1. Directed: 화살표에 방향이 있다 (\(L \to A\)\(L\)\(A\)에 인과 효과를 가짐)
  2. Acyclic: 순환이 없다 (변수가 직접적·간접적으로 자기 자신을 유발하지 않음)
  3. 공통 원인(common cause)이 측정되지 않았더라도 그래프에 포함되어야 한다
  • 역학: Causal Diagram, Causal DAG
  • IT: Causal Graph (주로 인과 발견(causal discovery)에서 사용)
역학 용어 IT 용어 비고
Causal DAG Causal Graph 인과 다이어그램
Confounder Path Backdoor Path 교란 경로
Collider Collider 충돌자
d-separation d-separation 조건부 독립 판별
Backdoor Criterion Backdoor Criterion 교란 보정 충분 조건
Mediator Mediator 매개 변수

2 개념 및 원리

2.1 DAG의 기본 요소

노드: 변수 (처리 \(A\), 결과 \(Y\), 교란 \(L\), 충돌자 \(C\) 등)

화살표: \(V \to W\)\(V\)\(W\)에 직접 인과 효과를 가진다는 의미. 화살표가 없으면 직접 효과가 없다고 가정한다.

경로 (Path): 두 변수 사이를 화살표를 따라 (방향 무시하고) 연결하는 일련의 간선.

2.2 세 가지 기본 구조

(1) 인과 사슬 (Causal Chain)       A → B → Y
(2) 분기 (Fork / Common Cause)     A ← L → Y
(3) 충돌자 (Collider)               A → L ← Y
구조 주변 연관 \(L\)로 조건부 시
(1) 인과 사슬 \(A \to B \to Y\) \(A\)\(Y\) 연관 \(B\)로 조건부 → 연관 차단
(2) 분기 \(A \leftarrow L \to Y\) \(A\)\(Y\) 연관 \(L\)로 조건부 → 연관 차단
(3) 충돌자 \(A \to L \leftarrow Y\) \(A\)\(Y\) 독립 \(L\)로 조건부 → 연관 개방

핵심 규칙: 충돌자를 제외한 변수를 조건부하면 경로를 차단한다. 충돌자를 조건부하면 오히려 경로를 개방한다.

2.3 d-분리 (d-separation)

두 변수가 d-분리되면, 해당 조건부 하에서 통계적으로 독립이다.

d-분리 규칙

경로가 차단(blocked)되는 조건:

  1. 경로에 비충돌자(non-collider)가 있고, 그 변수를 조건부한 경우
  2. 경로에 충돌자가 있고, 그 충돌자(또는 그 후손)를 조건부하지 않은 경우

모든 경로가 차단되면 두 변수는 d-분리 → 조건부 독립

예시 (Figure 6.1: \(L \to A \to Y\), \(L \to Y\)):

  • 경로 1: \(A \to Y\) (인과 경로) — 항상 개방
  • 경로 2: \(A \leftarrow L \to Y\) (교란 경로) — \(L\)로 조건부 시 차단

\(L\)로 조건부하면 교란 경로가 차단되고 인과 경로만 남는다 → 교환가능성 달성.

2.4 교란의 그래프적 정의

DAG에서 \(A\)\(Y\) 사이에 인과 경로가 아닌 개방된 경로(backdoor path)가 존재하면 교란이 있다.

Backdoor Criterion (Pearl, 1993):

변수 집합 \(\mathbf{Z}\)가 다음을 만족하면, \(\mathbf{Z}\)로 조건부하여 \(A\)\(Y\)에 대한 인과 효과를 식별할 수 있다:

  1. \(\mathbf{Z}\)의 어떤 변수도 \(A\)의 후손이 아니다
  2. \(\mathbf{Z}\)\(A\)에서 \(Y\)로의 모든 backdoor path를 차단한다

2.5 충돌자 편향 (Collider Bias)

A → L ← Y

\(A\)\(Y\)\(L\)의 공통 원인이면, \(L\)을 조건부하면 \(A\)\(Y\) 사이에 가짜 연관(spurious association)이 생긴다.

예시: 유전형 \(A\)와 흡연 \(Y\)가 심장병 \(L\)의 원인이다. 심장병 환자(\(L=1\))만 분석하면, 유전형이 없는 사람 중 흡연자 비율이 높아진다 — \(A\)\(Y\)는 원래 독립인데, \(L\)로 조건부하면 연관이 생긴다.

충돌자의 후손을 조건부해도 같은 문제가 발생한다 (Hernán & Robins, 2020, Ch.6).


3 직관적 설명

3.1 “파이프” 비유: 연관의 흐름

DAG의 경로를 파이프라고 생각하자. 연관(association)은 파이프를 통해 “흐른다.”

  • 인과 사슬 (\(A \to B \to Y\)): 물이 \(A\)에서 \(Y\)로 흐른다. \(B\)를 잠그면(조건부) 흐름이 멈춘다.
  • 분기 (\(A \leftarrow L \to Y\)): \(L\)에서 양쪽으로 흐른다. \(L\)을 잠그면 흐름이 멈춘다.
  • 충돌자 (\(A \to L \leftarrow Y\)): \(L\)이 벽이다 — 양쪽에서 온 물이 \(L\)에서 만나지만 통과하지 않는다. \(L\)을 “열면”(조건부) 오히려 물이 역류한다.

3.2 “공통 결과의 함정”: IT에서의 충돌자 편향

대학 입시(\(L\))에서 운동 능력(\(A\))과 학업 성적(\(Y\))이 모두 합격에 기여한다고 하자. 대학 내부(합격자)만 분석하면, 운동 잘하는 학생일수록 성적이 낮은 것처럼 보인다 — Berkson’s paradox.

IT 예시: “활성 사용자”(\(L\))가 되려면 사용 빈도(\(A\))가 높거나 고가 구매(\(Y\))가 있어야 한다. 활성 사용자만 분석하면, 사용 빈도와 구매 금액 사이에 음의 상관이 생긴다 — 원래 독립인데도.

3.3 왜 DAG를 그려야 하는가?

DAG 없이 인과 추론을 시도하면:

  • 어떤 변수를 보정해야 하는지 직관에 의존 → 과도한 보정(충돌자 보정)의 위험
  • 어떤 경로가 교란 경로인지, 인과 경로인지 구분 불가
  • 매개 변수를 보정하면 직접 효과만 추정하게 되는 문제를 인식 못함

“그래프를 그리기 전에 결론을 내리지 마라.” — Hernán & Robins (2020, Ch.6)


4 왜 필요한가

4.1 변수 선택의 실패 사례

실수 DAG에서의 구조 결과
교란 변수를 보정하지 않음 Backdoor path 개방 인과 효과 편향 추정
충돌자를 보정함 Collider path 개방 가짜 연관 생성 (선택 편향)
매개 변수를 보정함 인과 경로 차단 직접 효과만 추정 (총 효과 상실)
처리의 후손을 보정함 인과 경로 일부 차단 과소추정

4.2 비즈니스에서의 DAG 활용

상황 DAG 활용 결과
추천 알고리즘 효과 분석 사용자 활동 → 추천 노출 → 구매 다이어그램 어떤 변수를 보정해야 하는지 명확
마케팅 채널 기여도 채널 노출 → 인지 → 방문 → 구매 매개 vs. 교란 구분
이탈 원인 분석 기능 사용 → 만족도 → 이탈 직접 효과 vs. 간접 효과 분리

5 응용 분야

분야 역학/의학 IT/비즈니스 DAG 역할
교란 식별 흡연-커피-폐암 사용 패턴-추천-구매 Backdoor path 식별
선택 편향 분석 입원 환자 편향 활성 사용자 편향 Collider 구조 발견
매개 분석 약물 → 바이오마커 → 질병 기능 → 행동 → 전환 Direct vs. indirect effect
인과 발견 유전체학 추천 시스템 피처 관계 PC, FCI 알고리즘
도구변수 타당성 멘델리안 무작위화 쿠폰 무작위 노출 IV 가정 검증

6 예시

6.1 역학: 심장 이식 연구의 DAG

설정:
- L: 질병 중증도
- A: 심장 이식
- Y: 사망
- U: 흡연 여부 (미관측)

DAG:
  L → A
  L → Y
  U → A
  U → Y
  A → Y

Backdoor paths (A에서 Y로):
  (1) A ← L → Y     → L로 보정하면 차단
  (2) A ← U → Y     → U가 미관측이면 차단 불가

결론: U가 미관측이면 L만으로 교환가능성 달성 불가

6.2 IT: 추천 알고리즘 효과 분석의 DAG

설정:
- L: 사용자 과거 행동 (관측)
- A: 추천 알고리즘 노출
- Y: 구매
- M: 클릭 (매개 변수)
- C: "활성 사용자" 라벨 (충돌자)

DAG:
  L → A (과거 행동이 추천에 영향)
  L → Y (과거 행동이 구매에 영향)
  A → M → Y (추천 → 클릭 → 구매)
  A → Y (추천 → 직접 구매)
  A → C, Y → C (활성 사용자는 추천 노출과 구매 모두의 결과)

분석 시 주의:
✅ L로 보정: backdoor path 차단
❌ M으로 보정: 인과 경로 차단 (총 효과 아닌 직접 효과만 추정)
❌ C로 보정: 충돌자 편향 발생

7 코드 예시

7.1 DAG 구조에 따른 편향 시뮬레이션

import numpy as np
import pandas as pd

np.random.seed(42)
n = 10000

# === 구조 1: 교란 (Fork) ===
# L → A, L → Y, A → Y
L = np.random.binomial(1, 0.5, n)
A_fork = np.random.binomial(1, 0.3 + 0.4 * L)
true_ate = 0.10
Y_fork = np.random.binomial(1, np.clip(0.2 + 0.3 * L + true_ate * A_fork, 0, 1))

naive_fork = Y_fork[A_fork == 1].mean() - Y_fork[A_fork == 0].mean()

adj_fork = 0
for l in [0, 1]:
    p_l = (L == l).mean()
    y1 = Y_fork[(L == l) & (A_fork == 1)].mean()
    y0 = Y_fork[(L == l) & (A_fork == 0)].mean()
    adj_fork += (y1 - y0) * p_l

print("=== 구조 1: 교란 (Fork) L → A, L → Y ===")
print(f"  True ATE:    {true_ate:.3f}")
print(f"  Naive:       {naive_fork:.3f}  (교란 포함)")
print(f"  L 보정 후:   {adj_fork:.3f}  (교란 제거)")

# === 구조 2: 충돌자 편향 (Collider) ===
# A → C ← Y, A ⊥⊥ Y
A_coll = np.random.binomial(1, 0.5, n)
Y_coll = np.random.binomial(1, 0.3, n)  # A와 Y는 독립
C = np.random.binomial(1, np.clip(0.1 + 0.3 * A_coll + 0.3 * Y_coll, 0, 1))

naive_coll = Y_coll[A_coll == 1].mean() - Y_coll[A_coll == 0].mean()
# C=1로 조건부 (충돌자 보정 — 잘못된 보정)
c1 = (C == 1)
biased_coll = Y_coll[c1 & (A_coll == 1)].mean() - Y_coll[c1 & (A_coll == 0)].mean()

print("\n=== 구조 2: 충돌자 (Collider) A → C ← Y ===")
print(f"  True ATE:    0.000 (A ⊥⊥ Y)")
print(f"  Naive:       {naive_coll:.3f}  (올바름 — A⊥Y)")
print(f"  C=1 보정 후: {biased_coll:.3f}  (충돌자 편향!)")

# === 구조 3: 매개 변수 보정 (Mediator) ===
# A → M → Y
A_med = np.random.binomial(1, 0.5, n)
M = np.random.binomial(1, np.clip(0.3 + 0.3 * A_med, 0, 1))
Y_med = np.random.binomial(1, np.clip(0.2 + 0.4 * M, 0, 1))

total_effect = Y_med[A_med == 1].mean() - Y_med[A_med == 0].mean()
# M으로 보정하면 총 효과가 아닌 직접 효과만 남음
adj_med = 0
for m in [0, 1]:
    p_m = (M == m).mean()
    y1 = Y_med[(M == m) & (A_med == 1)].mean()
    y0 = Y_med[(M == m) & (A_med == 0)].mean()
    adj_med += (y1 - y0) * p_m

print("\n=== 구조 3: 매개 변수 보정 (Mediator) A → M → Y ===")
print(f"  총 효과 (total): {total_effect:.3f}")
print(f"  M 보정 후:       {adj_med:.3f}  (직접 효과만 — 총 효과 상실)")

7.2 d-분리 판별 함수

def check_d_separation_simple(dag_edges, path, conditioned_on):
    """
    간단한 d-분리 판별 (교육 목적).
    path: 변수 목록 (e.g., ['A', 'L', 'Y'])
    dag_edges: 방향 간선 집합 (e.g., {('L', 'A'), ('L', 'Y')})
    conditioned_on: 조건부할 변수 집합

    Returns: (blocked, reason)
    """
    for i in range(1, len(path) - 1):
        prev_node = path[i - 1]
        curr_node = path[i]
        next_node = path[i + 1]

        # 화살표 방향 확인
        into_curr = (prev_node, curr_node) in dag_edges or (next_node, curr_node) in dag_edges
        arrows_in = sum([
            (prev_node, curr_node) in dag_edges,
            (next_node, curr_node) in dag_edges
        ])

        is_collider = arrows_in == 2  # 양쪽에서 화살표가 들어옴

        if is_collider:
            if curr_node not in conditioned_on:
                return True, f"충돌자 {curr_node}가 조건부되지 않음 → 경로 차단"
        else:  # non-collider
            if curr_node in conditioned_on:
                return True, f"비충돌자 {curr_node}가 조건부됨 → 경로 차단"

    return False, "경로 개방 (차단되지 않음)"

# 테스트
edges = {('L', 'A'), ('L', 'Y'), ('A', 'Y')}

# Backdoor path: A ← L → Y
blocked, reason = check_d_separation_simple(edges, ['A', 'L', 'Y'], set())
print(f"A-L-Y (조건부 없음): blocked={blocked}, {reason}")

blocked, reason = check_d_separation_simple(edges, ['A', 'L', 'Y'], {'L'})
print(f"A-L-Y (L 조건부):   blocked={blocked}, {reason}")

# Collider: A → Y ← L
edges2 = {('A', 'Y'), ('L', 'Y')}
blocked, reason = check_d_separation_simple(edges2, ['A', 'Y', 'L'], set())
print(f"A-Y-L (조건부 없음): blocked={blocked}, {reason}")

blocked, reason = check_d_separation_simple(edges2, ['A', 'Y', 'L'], {'Y'})
print(f"A-Y-L (Y 조건부):   blocked={blocked}, {reason}")

8 관련 주제

이전/다음

같은 카테고리

다른 카테고리


9 참고 문헌

  • Hernán, M. A. & Robins, J. M. (2020). Causal Inference: What If, Ch.6. Chapman & Hall/CRC.
  • Pearl, J. (1995). Causal diagrams for empirical research. Biometrika, 82(4), 669-688.
  • Pearl, J. (2009). Causality: Models, Reasoning, and Inference (2nd ed.). Cambridge University Press.
  • Greenland, S., Pearl, J. & Robins, J. M. (1999). Causal diagrams for epidemiologic research. Epidemiology, 10(1), 37-48.

Subscribe

Enjoy this blog? Get notified of new posts by email: