[개발 공부] / Paired permutation test: 평균 차이를 직접 뒤섞어 보기.md

Paired permutation test: 평균 차이를 직접 뒤섞어 보기

조회

2026년 5월 13일 | 개발 공부


Paired permutation test는 같은 테스트셋 위에서 두 모델이나 두 설정의 점수 차이를 비교할 때, 샘플별 차이의 방향을 무작위로 뒤집어 “이 정도 차이가 우연히 나올 수 있는가”를 보는 검정이다. 이름만 보면 통계 패키지 안쪽 옵션처럼 느껴지는데, 내가 실제 실험 로그에 붙일 때의 감각은 꽤 직관적이었다. A와 B가 같은 query, 같은 edge, 같은 샘플을 풀었다면 각 줄에는 이미 짝이 있다. 그 짝을 유지한 채, 어느 쪽 점수라고 부를지만 계속 섞어 보는 방식이다.

최근에 McNemar 검정을 정리하면서 한 가지 경계가 계속 걸렸다. 성공/실패처럼 binary outcome으로 자를 수 있으면 2x2 불일치 표가 깔끔하다. 그런데 MRR, nDCG, AUC, latency, score margin처럼 연속값이 나오면 이야기가 조금 달라진다. 성공/실패로 억지 변환하면 정보가 많이 날아간다. 이럴 때 paired permutation test를 옆에 두면, 평균 차이는 유지하되 샘플별 짝 구조도 같이 보존할 수 있다.

독립 표본으로 풀지 않는 이유

두 설정을 비교할 때 가장 먼저 조심할 점은 “같은 테스트셋”이라는 사실을 버리지 않는 것이다. 예를 들어 profile A와 profile B가 100개 query를 똑같이 평가했고, 각 query마다 nDCG@10이 하나씩 나왔다고 하자. 이때 A의 100개 점수와 B의 100개 점수를 서로 독립인 두 묶음처럼 다루면, query 자체의 난이도가 만든 공통 변동을 놓친다. 어떤 query는 두 profile 모두 쉽게 맞히고, 어떤 query는 둘 다 거의 못 맞힌다. 우리가 보고 싶은 것은 그 공통 난이도가 아니라 같은 query에서 A와 B가 얼마나 갈렸는지다.

그래서 paired 비교에서는 먼저 줄별 차이를 만든다. query 1에서 B-A가 0.03, query 2에서 -0.01, query 3에서 0.08처럼 차이 벡터를 만든 뒤 평균을 낸다. 관찰된 평균 차이가 0.021이라고 해도, 그것만으로 바로 “B가 낫다”고 끝내지는 않는다. 귀무가설은 대략 이렇게 잡는다. 두 설정 사이에 진짜 차이가 없다면, 각 query에서 어느 쪽을 A라고 부르고 어느 쪽을 B라고 부르는지는 임의적이어야 한다.

부호를 뒤집는 작은 실험

paired permutation test의 가장 단순한 형태는 차이값의 부호를 무작위로 뒤집는 것이다. 각 샘플의 절댓값은 그대로 두고, B가 이긴 것으로 볼지 A가 이긴 것으로 볼지만 랜덤하게 바꾼다. 이렇게 만든 가짜 차이 벡터의 평균을 수천 번 계산하면, “차이가 없다는 세계”에서 평균 차이가 어느 정도까지 흔들리는지 분포가 생긴다. 관찰된 평균 차이가 이 분포의 끝쪽에 있으면, 단순한 우연 변동으로 보기 어렵다고 읽는다.

단계 하는 일 남길 기록
1 샘플별 paired score 차이 계산 unit id, A score, B score, delta
2 각 delta의 부호를 랜덤하게 뒤집기 seed, permutation count
3 뒤집힌 delta 평균을 반복 계산 null mean distribution
4 관찰 delta가 null 분포 어디에 있는지 확인 p-value, effect size, inspect queue

나는 이 표를 볼 때 p-value보다 먼저 관찰 delta와 null 분포의 폭을 같이 본다. 예를 들어 평균 차이가 0.004인데 null 분포도 -0.02에서 0.02 사이를 넓게 오간다면, 그 차이는 운영 결정을 바꾸기에는 약하다. 반대로 평균 차이가 0.012처럼 작아 보여도 null 분포가 아주 좁고 거의 대부분 0 근처에 몰려 있다면, 같은 query set에서는 꽤 일관된 개선일 수 있다.

bootstrap과 헷갈렸던 지점

처음에는 paired bootstrap과 paired permutation test가 비슷하게 느껴졌다. 둘 다 query나 sample 단위를 반복해서 다시 계산하고, 평균 차이의 분포를 본다. 차이는 질문이 조금 다르다는 데 있다. Bootstrap은 “현재 평가셋과 비슷한 샘플 구성이 다시 오면 점수가 얼마나 흔들릴까”에 가깝다. 그래서 confidence interval이나 paired delta interval을 붙이기 좋다. Permutation test는 “차이가 없다고 가정했을 때도 이 정도 방향성이 나올까”에 더 가깝다.

둘 중 하나만 써야 하는 것은 아니다. 작은 실험에서는 평균 delta, bootstrap interval, permutation p-value를 나란히 두면 꽤 읽기 편하다. 다만 숫자가 세 개로 늘었다고 결론이 자동으로 세 배 단단해지는 것은 아니다. 오히려 내가 보고 싶은 건 세 숫자가 서로 다른 이야기를 하는 순간이다. 평균은 좋아졌는데 interval이 넓거나, permutation p-value는 작지만 몇몇 outlier query가 대부분의 차이를 만든다면, 그건 “배포 후보”보다 “다시 열 query 목록”에 가깝다.

GraphRAG와 GNN 실험에서 쓰기 좋은 모양

GraphRAG profile 비교에서는 query를 paired unit으로 두는 편이 자연스럽다. 각 query마다 A/B의 MRR, nDCG, coverage score, answer rubric score를 남긴 뒤 delta를 계산한다. 그 다음 permutation test를 돌리면 profile B가 전체 평균에서 이겼는지보다, 같은 query 묶음 안에서 B 우세가 얼마나 일관적인지 볼 수 있다. 중요한 건 query set을 중간에 바꾸지 않는 것이다. query fingerprint가 달라진 run끼리 섞으면 검정이 아니라 다른 시험지를 비교하는 일이 된다.

GNN 링크 예측에서는 edge를 unit으로 둘 수도 있고, seed별 metric을 unit으로 둘 수도 있다. edge-level score를 비교할 때는 test edge가 같은지, negative sampling이 고정됐는지, threshold를 validation에서 고른 뒤 test에 그대로 적용했는지부터 확인해야 한다. seed-level AUC나 average precision을 비교한다면 unit 수가 너무 적어질 수 있으니, p-value를 크게 믿기보다 방향성 확인과 실패 seed inspection queue를 만드는 정도로 쓰는 편이 낫다.

내가 실험 로그에 붙이는 최소 필드

실무적으로는 긴 통계 보고서보다 작은 JSON이나 CSV 한 장이 더 오래 남는다. 내가 남기고 싶은 최소 필드는 다섯 가지다. paired unit의 이름, 관찰 평균 delta, permutation 횟수와 seed, 양측 p-value, 그리고 delta 절댓값이 큰 샘플 목록이다. 특히 마지막 샘플 목록이 중요하다. 검정 결과가 유의하다는 말은 원인 설명이 아니다. 어느 query, 어느 edge, 어느 cluster에서 차이가 났는지를 열어 봐야 다음 수정이 보인다.

  • unit: query, edge, user, seed처럼 짝을 유지할 단위
  • metric: MRR, nDCG, AUC, latency처럼 연속 차이를 남길 지표
  • delta: B-A 방향을 명확히 고정한 샘플별 차이
  • null distribution: 부호 뒤집기나 label 교환으로 만든 가짜 평균 차이 분포
  • inspection queue: 큰 양수/음수 delta가 나온 샘플 묶음

이렇게 남겨 두면 평균표 하나보다 실험을 다시 열기가 쉬워진다. “B가 0.7점 높았다”가 아니라 “B는 평균 0.7점 높았고, 부호 뒤집기 기준으로는 우연 변동 끝쪽에 있었으며, 차이의 절반은 multi-hop query 12개에서 나왔다”처럼 말할 수 있기 때문이다. 이 정도면 다음 행동도 자연스럽게 정해진다. B를 바로 기본값으로 올릴지, multi-hop query만 따로 볼지, 아니면 metric 단위부터 다시 의심할지 선택지가 생긴다.

주의할 점

Permutation test도 만능은 아니다. paired unit이 독립이라는 가정이 너무 깨져 있으면 결과가 과하게 자신감 있어 보일 수 있다. 같은 사용자의 여러 질문, 같은 템플릿에서 파생된 query, 같은 graph component 안의 edge를 모두 독립 샘플처럼 놓으면 분포가 실제보다 좁아질 수 있다. 이런 경우에는 unit을 더 크게 묶거나, cluster 단위 평균을 만든 뒤 비교하는 편이 안전하다.

또 하나는 반복 횟수와 seed를 반드시 남기는 것이다. 작은 p-value 하나만 복사해 두면 나중에 재현이 안 된다. permutation count가 1,000인지 100,000인지, 양측 검정인지 단측 검정인지, ties와 zero delta를 어떻게 처리했는지까지 메모해 둬야 같은 숫자로 돌아갈 수 있다. 나는 이런 세부 기록이 통계식 자체보다 더 실용적이라고 느낀다. 평가 로그의 목적은 멋진 검정 이름을 붙이는 게 아니라, 다음 실험에서 같은 판단을 다시 열 수 있게 만드는 것이기 때문이다.

정리하면 McNemar 표는 binary success/failure의 엇갈림을 보여 주고, bootstrap은 평균 metric의 흔들림 폭을 보여 주고, paired permutation test는 관찰된 평균 차이가 짝 구조 안에서 얼마나 우연처럼 보이는지 묻는다. 이 셋을 구분해 두면 평가표를 보는 속도가 조금 느려진다. 대신 평균 하나에 너무 빨리 끌려가지 않는다. 개인 프로젝트 실험에서는 그 느려짐이 꽤 큰 안전장치가 된다.

댓글

홈으로 돌아가기

검색 결과

"" 검색 결과입니다.