전체는 무승부인데 왜 모바일만 올랐을까
들어가며
실험 결과 회의를 떠올려 보자. 화면에 막대 두 개가 나란히 서 있다.
팀원: “이번 온보딩 실험, 전체 전환율은 무승부다. 대조군 5.5%, 처치군 5.5%. 차이 없음.” 디자이너: “근데 모바일은 올랐잖아요.” 팀원: “어… 그러네. 디바이스별로 다시 봐야겠다.”
전체 숫자는 완벽한 무승부인데, 특정 세그먼트(segment)에서는 분명히 올라 보인다. 이 감각은 착각이 아닐 때가 많다. 전체 평균이 0인 이유가 효과가 없어서가 아니라, 한쪽에서 오른 만큼 다른 쪽에서 내려서 서로 지워졌기 때문일 수 있다.
이 글은 그 상황을 다룬다. 전체 평균이 무승부라고 적힌 보고서 안에 숨어 있는 세그먼트별 효과, 그걸 이질적 처치 효과(heterogeneous treatment effects)라 부른다. 숨은 효과를 찾는 건 값어치가 크다. 그런데 그걸 찾겠다고 데이터를 사후에 마구 쪼개면 이번엔 정반대 함정, 다중비교(multiple comparisons)에 걸려 헛것을 본다. 이 두 함정 사이를 어떻게 지나가는지가 본론이다.
이질적 처치 효과란
용어부터 쉽게 풀자. 처치(treatment)는 실험에서 새로 준 변화다. 새 온보딩 화면을 처치군에 보여주고, 기존 화면을 대조군에 보여준다. 처치 효과는 그 변화가 만든 전환율 차이다.
평균 처치 효과(average treatment effect, ATE)는 전체 사용자를 통째로 묶어 잰 효과다. 처치군 전체 전환율에서 대조군 전체 전환율을 뺀 값. 들어가며의 보고서에 적힌 무승부가 바로 이 값이다.
이질적이라는 말은 이 효과가 사용자마다, 집단마다 다르다는 뜻이다. 새 온보딩이 모바일 사용자에게는 도움이 되고 데스크톱 사용자에게는 방해가 될 수 있다. 쉽게 말해, 하나의 디자인이 모두에게 같은 크기로 작동하지 않는다. 이게 자연스러운 이유는 단순하다. 모바일과 데스크톱은 화면 크기도, 사용 맥락도, 사용자 구성도 다르니까. 같은 화면이 두 집단에 같은 효과를 낼 이유가 오히려 없다.
핵심은 여기다. 세그먼트별 효과가 서로 반대 방향이면, 평균을 낼 때 상쇄된다. 실제 숫자로 보자.
| 세그먼트 | 대조군(기존 화면) | 처치군(새 온보딩) | 처치 효과 |
|---|---|---|---|
| 데스크톱 | 6.0% | 3.0% | -3.0%p |
| 모바일 | 5.0% | 8.0% | +3.0%p |
| 전체 | 5.5% | 5.5% | 0.0%p |
각 칸은 1000명씩 무작위로 배정했다고 하자. 데스크톱에서는 새 화면이 전환율을 3%p 떨어뜨렸고, 모바일에서는 3%p 올렸다. 크기가 같고 방향이 반대라 전체 평균은 정확히 0이 된다.
R로 그대로 재현해 보자.
# 세그먼트별 전환수 (각 칸 노출은 1000명, 무작위 배정)
desk_ctrl <- 60 # 데스크톱 대조군 6.0%
desk_treat <- 30 # 데스크톱 처치군 3.0%
mob_ctrl <- 50 # 모바일 대조군 5.0%
mob_treat <- 80 # 모바일 처치군 8.0%
n <- 1000
# 세그먼트별 처치 효과 = 처치 전환율 - 대조 전환율
desk_diff <- desk_treat/n - desk_ctrl/n # 데스크톱
mob_diff <- mob_treat/n - mob_ctrl/n # 모바일
c(데스크톱 = desk_diff, 모바일 = mob_diff)
데스크톱 모바일
-0.03 0.03
이제 전체 평균 처치 효과를 재보자.
# 전체 평균 처치 효과 (ATE)
ate <- (desk_treat + mob_treat)/(2*n) - (desk_ctrl + mob_ctrl)/(2*n)
ate
[1] 0
ATE는 딱 0이다. 그런데 이 0 안에는 -3%p와 +3%p라는 큰 효과 두 개가 숨어 있다. 전체 평균만 보고서에 옮기면, 이 두 효과가 존재했다는 사실 자체가 지워진다.
여기서 심슨의 역설(Simpson’s paradox)이 떠오른다면 감이 좋다. 심슨의 역설은 세그먼트를 합칠 때 방향이 뒤집히는 이야기였다. 가중치가 다르면 부분에서 A가 이겨도 전체에서 B가 이긴다. 이질적 효과는 그 짝이다. 합칠 때 방향이 뒤집히는 게 아니라, 서로 상쇄돼 0이 되는 쪽이다. 뿌리는 하나다. 전체 평균 숫자 하나로는 그 안의 구성을 알 수 없다는 것.
숨은 효과를 찾는 값어치
전체가 무승부라는 한 줄로 실험을 접으면 무엇을 잃을까. 방금 표에서 잃는 건 두 가지 의사결정이다.
하나, 모바일에서 3%p 올랐다는 사실은 그 자체로 릴리스 근거가 된다. 새 온보딩을 모든 사용자에게 밀지, 모바일에만 밀지가 갈린다. 전체 무승부라는 결론은 이 선택지를 아예 지운다. 모바일 비중이 커지고 있는 서비스라면, 이 세그먼트의 +3%p는 시간이 갈수록 전체 ATE를 플러스로 끌어올린다. 지금 무승부라는 이유로 접으면, 곧 이길 카드를 버리는 셈이다.
둘, 데스크톱에서 3%p 떨어졌다는 사실은 경고등이다. 새 화면의 어떤 요소가 큰 화면에서 역효과를 냈다는 신호다. 왜 떨어졌는지 파고들면 다음 디자인이 나아진다. 전체 무승부라는 결론은 이 경고등도 함께 꺼버린다.
그래서 이질적 효과를 보는 건 실험 하나에서 훨씬 많은 정보를 뽑아내는 일이다. 전체 평균은 “이겼나 졌나"에만 답하고, 세그먼트별 효과는 “누구에게, 왜"에 답한다. 디자인 결정은 뒤쪽 질문에서 나온다.
사후에 마구 쪼개면 헛것을 본다
여기까지만 읽으면 결론이 단순해 보인다. 무조건 잘게 쪼개서 보면 되지 않나. 그런데 여기서 정반대 함정이 기다린다.
세그먼트를 나눌 방법은 끝이 없다. 디바이스로 나누고, 국가로 나누고, 신규와 기존으로 나누고, 시간대로 나누고, 그걸 다시 교차해서 나눈다. 데스크톱 × 미국 × 신규 × 주말 같은 칸까지 내려가면 세그먼트가 수십 개로 불어난다. 그렇게 다 검정한 다음, 유의하게 나온 칸만 골라 보고서에 적는다. “북미 신규 모바일 사용자에서 새 온보딩이 유의하게 좋았다.”
문제는 검정을 많이 할수록 우연히 유의한 칸이 저절로 생긴다는 데 있다. 진짜 효과가 어디에도 없어도 그렇다. 각 검정이 진짜 효과가 0인데도 우연히 유의할 확률이 5%라면, 세그먼트를 여러 개 볼 때 적어도 하나가 우연히 걸릴 확률은 빠르게 커진다.
# 사후에 세그먼트를 m개로 쪼개 각각 유의성 검정을 했다고 하자.
# 모든 세그먼트에서 진짜 효과가 0이어도, 적어도 하나가 우연히 유의할 확률:
m <- 20
p_any <- 1 - (1 - 0.05)^m
round(p_any, 3)
[1] 0.642
세그먼트 20개를 사후에 훑으면, 전부 진짜 효과가 없어도 적어도 하나가 유의하게 나올 확률이 64%다. 절반이 훌쩍 넘는다. 그렇게 걸린 한 칸을 골라 “이 세그먼트에서 효과가 있다"고 적으면, 그건 신호가 아니라 우연을 주운 것이다. 이 문제가 p-value를 다룬 글에서 나온 다중비교다. 20번 검정하면 귀무가설이 다 참이어도 평균 한 번은 유의하게 나온다는 그 이야기가, 세그먼트를 쪼갤 때 그대로 재현된다.
그리고 이렇게 우연히 걸린 세그먼트는 데이터를 새로 모아 다시 재보면 사라진다. 재현되지 않는다는 게 우연의 결정적 특징이다. 진짜 이질적 효과라면 다음 실험에서도 같은 방향으로 나오지만, 사후에 주운 세그먼트는 다음번엔 다른 칸으로 옮겨가 있다.
그러니 앞 절의 “숨은 효과를 찾아라"와 이 절의 “마구 쪼개지 마라"는 정면으로 부딪치는 것처럼 보인다. 실제로는 부딪치지 않는다. 둘을 가르는 건 하나다. 언제 세그먼트를 정했는가.
제대로 하는 법
숨은 효과는 찾되 헛것은 줍지 않는 방법은 세 가지로 정리된다.
첫째, 세그먼트를 사전에 등록한다. 실험을 시작하기 전에, 어떤 세그먼트로 나눠 볼지 미리 적어둔다. 예를 들어 “디바이스(모바일/데스크톱)와 신규/기존, 이 두 축만 본다"고 실험 설계 문서에 못 박는다. 데이터를 열어 본 다음에 세그먼트를 고르는 게 아니라, 열기 전에 정한다. 이걸 사전 등록(pre-registration)이라 한다. 사전에 정한 세그먼트가 적을수록 검정 수가 줄고, 우연히 걸릴 확률도 함께 줄어든다. 들어가며의 모바일이 미리 등록한 세그먼트였다면, 그 +3%p는 신뢰할 만한 신호다. 반대로 데이터를 다 훑다가 눈에 띈 칸이라면, 같은 +3%p라도 무게가 다르다.
둘째, 여러 세그먼트를 볼 거라면 문턱을 높인다. 검정을 여러 번 하는 만큼 각 검정의 기준을 더 깐깐하게 잡는 게 다중비교 보정이다. 가장 단순한 방식이 본페로니 보정(Bonferroni correction)이다. 세그먼트가 20개면, 유의 기준 0.05를 세그먼트 수로 나눠 훨씬 낮은 값을 문턱으로 쓴다.
0.05 / 20 # 본페로니 보정: 세그먼트 20개면 문턱을 이 값으로 낮춘다
[1] 0.0025
기준이 0.05에서 0.0025로 내려간다. 이렇게 하면 우연히 걸린 칸이 문턱을 넘기 훨씬 어려워진다. 물론 이 방식은 보수적이라 진짜 효과도 놓치기 쉽다. 그래서 애초에 사전 등록으로 세그먼트 수 자체를 줄여두는 게 먼저고, 보정은 그다음 안전장치다.
셋째, 재현으로 확인한다. 한 실험에서 어떤 세그먼트가 유의하게 나왔다면, 그걸 최종 결론으로 굳히기 전에 데이터를 새로 모아 다시 잰다. 진짜 효과는 두 번째에도 같은 방향으로 나오고, 우연은 두 번째에서 무너진다. 특히 사전 등록에 없던 세그먼트에서 뭔가 흥미로운 게 보였다면, 그건 결론이 아니라 다음 실험의 가설로 넘긴다. 사후 탐색은 가설을 만드는 데 쓰고, 확정은 새 데이터로 한다. 이 순서를 지키면 탐색의 자유와 결론의 엄격함을 둘 다 가질 수 있다.
마무리
들어가며로 돌아가자.
디자이너: “근데 모바일은 올랐잖아요.”
이 한마디는 맞을 수도, 헛것일 수도 있다. 갈림길은 그 모바일이라는 세그먼트를 언제 정했느냐에 있다. 실험을 설계하며 미리 등록해 둔 세그먼트라면, 전체 무승부 뒤에 숨은 +3%p는 붙잡아야 할 진짜 신호다. 결과를 다 열어 본 뒤 수십 개 칸 중에서 눈에 띈 칸을 집어 든 거라면, 같은 숫자라도 우연히 주운 것일 수 있으니 새 데이터로 다시 재야 한다.
전체 평균은 여기서도 출발점이지 결론이 아니다. ATE가 0이라는 건 효과가 없다는 뜻이 아니라, 효과들이 서로 지워졌을 수도 있다는 뜻이다. 그 안을 열어 보되, 사전에 정한 문으로만 연다. 미리 정한 세그먼트로 열면 숨은 효과가 보이고, 아무 문이나 열면 헛것이 나온다. 그 차이 하나가 실험 하나의 결론을 정직하게 만든다.