QBA quantitative bias analysis

Author

SEOYEON CHOI

Published

November 12, 2025

QBA

  • real world data 같은 현실의 데이터에서 발생할 수 있는 문제
    • 미측정 교란 unmeasured confounding
      • 진짜 관계보다 과대 또는 과소 추정
    • 오분류 Misclassification
      • 데이터가 잘못 구분
    • 선택 평향 Selection bias
      • 표본이 왜곡
  • E-value = 이 결과를 교란으로 설명하려면, 교란자가 얼마나 강해야 할까?

\(RR_{adjusted} = \frac{RR_{observed}}{Bias Factor}\)

Basic Factor는 U가 얼마나 노출과 결과에 영향을 주는지 계산한 값

0. Import

import math
import numpy as np

1. E-value function

def e_value(rr):
    rr = float(rr)
    if rr < 1:
        rr = 1/rr
    return rr + math.sqrt(rr*(rr - 1))
  • 관찰된 효과크기인 RR이 교란으로만 설명되려면, 교란-노출과 교란-결과 간 연관인 RR이 최소 어느정도여야 하는지 제시

  • RR이 1보다 작다면, 역변환하여 1보다 크게 만든 후 공식 적용

\(E-value = RR = \sqrt{RR(RR-1)}\)

  • 단, RR이 1보다 커야 함

2. Unmeasured confounding

def adjust_rr_unmeasured_confounding(rr_crude, rr_ud, rr_ue, p_u0):
    odds_u0 = p_u0/(1 - p_u0)
    odds_u1 = rr_ue * odds_u0
    p_u1 = odds_u1/(1 + odds_u1)
    bf = (p_u1*rr_ud + (1 - p_u1)) / (p_u0*rr_ud + (1 - p_u0))

    rr_adj = rr_crude / bf
    return rr_adj, bf, {"p_u1": p_u1}
  • rr_crude=observed RR

  • rr_ud=confounding교란자- 결과 간 위험비RR

  • rr_ue=confounding교란자-노출 간 위험비RR

  • p_u0=비노출군에서 U의 유병률, \(P(U=1|E=0)\)

  • 노출군에서 U 유병률 \(p_{u1}\) 역산

    • 가정 \(rr_{UE} = \frac{odds(U|E=1)}{odds(U|E=0)}\)
    • 먼저 \(odds_{u0} = \frac{P_{u0}}{1-p_{u0}}\)
    • \(odds_{u1} = rr_{UE} \times odds_{U0} \rightarrow p_{u1} = \frac{odds_{u1}}{1+odds_{u1}}\)
  • Bias factor 계산

    • \(Bias Factor = \frac{p_{u1} \times rr_{UD} + (1-p_{u1})}{p_{u0} \times rr_{UD} + (1-p_{u0})}\)
    • U가 노출군에 더 많고, U의 결과위험이\(rr_{UD}\) 올라가면, 커짐
  • adjusted RR

    • \(RR_{adj} = \frac{RR_{obaer}}{Bias factor}\)
    • Bias facotr가 1보다 크면 보정 후 RR이 내려가고, 과대 추정을 보정하고,
    • Bias facotr가 1보다 크면 보정 후 RR이 올라가고, 과소 추정을 보정한다.

3. Adjust Misclassification

def correct_or_misclassification_exposure(a, b, c, d, SeE, SpE):
    A = np.array([[SeE, 1-SpE],
                  [1-SeE, SpE]])
    y1 = np.array([a, c])  # D=1
    y0 = np.array([b, d])  # D=0
    try:
        A_inv = np.linalg.inv(A)
    except np.linalg.LinAlgError:
        raise ValueError("Se/Sp 설정으로 역행렬이 존재하지 않습니다.")

    x1 = A_inv @ y1  # [E=1,D=1], [E=0,D=1]
    x0 = A_inv @ y0  # [E=1,D=0], [E=0,D=0]

    x1 = np.clip(x1, 0, None)
    x0 = np.clip(x0, 0, None)

    a_t, c_t = x1
    b_t, d_t = x0
    or_corr = (a_t * d_t) / (b_t * c_t) if (b_t>0 and c_t>0) else np.inf
    return or_corr, {"a_true": a_t, "b_true": b_t, "c_true": c_t, "d_true": d_t}
D=1     D=0
E*=1       a       b
E*=0       c       d
  • SeE = 민감도 \(P(E^*=1|E=1)\)
  • spE = 특이도 \(P(E^*=0|E=0)\)
  • 분류행렬로 진짜분포가 관찰 분포로 변환됨

4. Adjust Selection bias

def adjust_or_selection_bias(OR_obs, s11, s10, s01, s00):
    bf_sel = (s11 * s00) / (s10 * s01)
    OR_adj = OR_obs / bf_sel
    return OR_adj, bf_sel
  • 선택확률 \(P(S=1|E,D)\)가 E,D에 따라 다름
    • s11=P(S=1|E=1,D=1)
    • s10=P(S=1|E=1,D=0)
    • s01=P(S=1|E=0,D=1)
    • s00=P(S=1|E=0,D=0)
  • \(Bias factor = \frac{s_{11}s_{00}}{s_{10}s_{01}}\)
  • \(OR_{adj} = \frac{OR_{obs}}{Bias Factor_{sel}}\)
  • E=1, D=1(s11)이랑 E=0,D=0(s00)이 선택 많이되면 관찰한 OR이 커질 수 있오 Bias Facotr_sel이 1보다 커질 수 있음 이떄 보정하면 OR_adj가 작아짐
  1. E-value 예시
rr_est = 1.80
print(f"  RR={rr_est:.2f} 의 E-value:", round(e_value(rr_est), 3))
  RR=1.80 의 E-value: 3.0
  1. 미측정 교란 보정 예시
rr_crude = 2.5
rr_ud = 1.8   
rr_ue = 3.0    
p_u0  = 0.30     
rr_adj, bf, extra = adjust_rr_unmeasured_confounding(rr_crude, rr_ud, rr_ue, p_u0)
print(f"  Crude RR={rr_crude:.2f} -> Adjusted RR={rr_adj:.3f} (Bias factor={bf:.3f}, p_u1={extra['p_u1']:.3f})")
  Crude RR=2.50 -> Adjusted RR=2.138 (Bias factor=1.169, p_u1=0.563)
  1. 노출 오분류 보정 예시
a,b,c,d = 120, 80, 60, 140
SeE, SpE = 0.85, 0.90
or_corr, cells = correct_or_misclassification_exposure(a,b,c,d,SeE,SpE)
or_obs = (a*d)/(b*c)
print(f"  관찰 OR={or_obs:.3f} -> 보정 OR={or_corr:.3f}")
print("  추정 진짜 셀:", {k: round(v,2) for k,v in cells.items()})
  관찰 OR=3.500 -> 보정 OR=5.702
  추정 진짜 셀: {'a_true': 136.0, 'b_true': 77.33, 'c_true': 44.0, 'd_true': 142.67}
  1. 선택편향 보정 예시
OR_obs = 1.9
s11, s10, s01, s00 = 0.9, 0.6, 0.7, 0.5
or_adj, bf_sel = adjust_or_selection_bias(OR_obs, s11, s10, s01, s00)
print(f"  관찰 OR={OR_obs:.2f} -> 보정 OR={or_adj:.3f} (Selection bias factor={bf_sel:.3f})")
  관찰 OR=1.90 -> 보정 OR=1.773 (Selection bias factor=1.071)