12강 / 20강

limma 의 아이디어 — 분산을 빌려온다

digamma · polygamma · brentq 가 하는 일

limma 의 아이디어 — 분산을 빌려온다
혼자 잰 값과 모두가 알려 준 값 사이 어딘가로 끌어당기기 — 그것이 수축(shrinkage)입니다.

수축(shrinkage)의 최종 공식은 놀랄 만큼 간단합니다.

s2p = (d0*s0 + d*s2) / (d0 + d)

가중 평균입니다. 그것뿐이에요.

  • s2 — 이 유전자가 직접 잰 분산, 무게는 d(=5, 우리 자유도)
  • s0 — 13,095개 전체가 알려 준 보통의 분산, 무게는 d0

즉 d0 는 "전체의 의견을 샘플 몇 개어치로 쳐 줄까" 입니다. d0 가 크면 개별 유전자의 측정을 덜 믿고 전체 쪽으로 강하게 끌어당기고, d0 가 0에 가까우면 보통의 t 검정과 같아집니다.

우리 데이터에서 계산하면 d0 ≈ 2.65, s0 ≈ 0.0285 가 나옵니다. 자유도 5짜리 개별 측정에 자유도 2.65짜리 "전체 의견"을 섞는 셈이니, 꽤 묵직한 보정입니다.

d0 와 s0 는 어떻게 구하나

여기가 코드랩에서 가장 낯선 부분입니다. 한 줄씩 뜯어봅시다.

e = np.log(s2) - digamma(d/2) + np.log(d/2) tri = np.var(e, ddof=1) - polygamma(1, d/2) d0 = brentq(lambda x: polygamma(1, x/2) - tri, 1e-3, 1e6) s0 = np.exp(e.mean() + digamma(d0/2) - np.log(d0/2))

발상은 이렇습니다. 분산 13,095개가 어떤 분포에서 나왔다고 보고, 그 분포의 중심(s0) 과 폭(d0) 을 데이터로부터 되짚어 내는 것입니다. "데이터에서 사전분포를 얻는다"고 해서 경험적(empirical) 베이즈라고 부릅니다.

📐
log(s2)
분산에 로그를 씌워 좌우대칭에 가깝게
🎯
digamma
로그를 씌우며 생긴 치우침을 보정
📊
polygamma(1, ·)
그 흩어짐의 이론적 크기

digamma와 polygamma는 어렵게 생겼지만 하는 일은 소박합니다. "자유도 d 로 잰 분산에 로그를 씌우면 평균과 분산이 이론적으로 얼마가 되는가"를 알려 주는 보정표예요. 그 이론값과 실제 관측값의 차이가 바로 유전자마다 진짜로 다른 만큼(tri)이고, 그 크기에 맞는 d0 를 brentq 가 찾아 줍니다.

limma 의 핵심을 외부 R 없이 20줄로 재현했습니다.
Python 브라우저에서 실행

🎮 수축 강도를 손으로 돌려 보기

d0 슬라이더를 움직여 보세요. 0 이면 보통의 t 검정, 크게 올리면 모든 유전자가 같은 분산을 쓰게 됩니다. 실제 데이터가 고른 값(2.65)이 그 사이 어디쯤인지 확인해 보세요.

수축 실험실 — d0 를 돌리면 순위가 어떻게 바뀔까
불러오는 중…
🤔 확인 문제
s2p = (d0*s0 + d*s2) / (d0 + d) 는 무엇인가요?
🤔 확인 문제
d0 가 0에 아주 가까워지면 결과는 어떻게 되나요?
🤔 확인 문제
brentq(lambda x: polygamma(1, x/2) - tri, 1e-3, 1e6) 가 하는 일은?