pROC로 ROC 곡선·AUC 그리기 — 바이오마커·예측 모델 성능 평가 (R 실전)

TL;DR — 어떤 유전자나 점수가 질병군과 대조군을 얼마나 잘 가르는지 보려면 ROC (receiver operating characteristic) 곡선과 AUC (area under the curve)를 씁니다. R의
pROC패키지로roc(response, predictor)한 줄이면 곡선·AUC·신뢰구간·최적 컷오프가 다 나오고,roc.test()로 두 바이오마커의 AUC까지 DeLong (DeLong et al. 1988) 검정으로 비교합니다.ROC는 모든 컷오프에서 민감도(sensitivity)와 1−특이도(specificity)가 그리는 궤적이고, AUC는 0.5(무작위)에서 1.0(완벽) 사이입니다. 이 글은 더미 데이터로 점수 → ROC → AUC → 컷오프 → 두 모델 비교까지 복붙하면 도는 형태로 따라갑니다.
🔗 관련 글 · 검정 자체가 처음이면 R로 기초 통계 검정하기 · p-값을 여러 번 볼 때의 보정은 p-value와 FDR, 다중검정 보정 · 점수의 출발점인 차등발현은 DESeq2로 차등발현 분석
이 글에서 할 것
차등발현(differential expression)이나 어떤 점수 모델을 만들고 나면 곧장 따라오는 질문이 있습니다. "이 유전자(혹은 위험점수)가 환자군과 정상군을 정말 잘 구분하나?" 단순히 두 그룹의 평균이 다른 것(통계 검정의 영역)과, 그 차이로 개별 샘플을 분류할 수 있느냐는 다른 문제입니다. 후자를 평가하는 표준 도구가 ROC 곡선과 AUC입니다.
발상은 이렇습니다. 바이오마커 점수에 컷오프(cutoff)를 하나 정하면, 그보다 높은 샘플은 '양성'으로 부릅니다. 컷오프를 낮추면 진짜 환자를 더 많이 잡지만(민감도↑) 정상인도 양성으로 잘못 부르고(특이도↓), 높이면 그 반대입니다. 이 맞교환(trade-off)을 모든 컷오프에 대해 그린 게 ROC 곡선입니다. 가로축은 1−특이도(거짓양성률, FPR), 세로축은 민감도(참양성률, TPR)입니다. 곡선이 왼쪽 위 구석에 붙을수록 좋은 분류기이고, 그 아래 면적이 AUC입니다.
이 글은 pROC 패키지 (Robin et al. 2011)로 더미 데이터를 만들어 ROC를 그리고, AUC와 신뢰구간(confidence interval)을 구하고, Youden 지수로 최적 컷오프를 찾고, 두 바이오마커의 AUC를 DeLong 검정으로 비교하고, ggplot2로 출판용 곡선까지 그립니다. 마지막에 방향(direction) 뒤집힘 같은 흔한 실수와 ROC vs PR 곡선의 차이를 정리합니다.
# 핵심 흐름 한눈에 (pROC)
library(pROC)
roc_obj <- roc(response, predictor) # ① 정답 라벨 + 점수 → ROC 객체
plot(roc_obj); auc(roc_obj) # ② 곡선 + AUC
coords(roc_obj, "best", # ③ Youden 최적 컷오프
best.method = "youden")
roc.test(roc_a, roc_b) # ④ 두 AUC 비교 (DeLong)
1. 사전 준비 — 패키지와 데이터
pROC와 ggplot2는 모두 CRAN 패키지입니다. install.packages()로 설치합니다.
install.packages("pROC") # ROC·AUC·DeLong 검정
install.packages("ggplot2") # ggroc로 출판용 곡선
데모용 데이터를 만듭니다. 핵심은 질병군/대조군 라벨과 그에 대한 바이오마커 점수 두 개입니다. 현실성을 위해 두 그룹의 점수 분포를 일부러 겹치게 둡니다 (완벽히 갈리면 AUC가 1.0이라 재미가 없습니다). 환자군 점수를 대조군보다 평균만 조금 높게 잡습니다.
set.seed(2025) # 재현성 고정
n <- 200 # 샘플 수 (환자 100 + 정상 100)
# 정답 라벨: 0 = 정상(control), 1 = 질병(case)
status <- factor(rep(c("control", "case"), each = 100),
levels = c("control", "case")) # 레벨 순서를 명시 (뒤에서 중요)
# 바이오마커 A 점수: 환자군 평균이 살짝 높지만 분포가 겹침
biomarker_A <- c(rnorm(100, mean = 5.0, sd = 1.5), # control
rnorm(100, mean = 6.2, sd = 1.5)) # case
# 바이오마커 B 점수: A보다 두 그룹을 더 잘 가름 (평균 차이 큼)
biomarker_B <- c(rnorm(100, mean = 5.0, sd = 1.3), # control
rnorm(100, mean = 7.0, sd = 1.3)) # case
dat <- data.frame(status, biomarker_A, biomarker_B)
head(dat, 3) # 데이터 확인
status biomarker_A biomarker_B
1 control 3.879049 4.574687
2 control 5.539645 5.045524
3 control 5.117417 4.929475
라벨이 두 수준(정상·질병)이고 점수가 연속값이면 준비는 끝입니다. 실제 분석이라면 status는 진단 결과, biomarker_A는 유전자 발현·위험점수·검사 수치 등이 됩니다.
2. ROC 객체 만들기 — roc()
pROC의 출발점은 roc() 하나입니다. 정답 라벨(response)과 점수(predictor)를 넣으면 ROC 곡선 계산에 필요한 모든 것이 든 객체가 나옵니다.
library(pROC)
# response = 정답 라벨, predictor = 바이오마커 점수
roc_A <- roc(response = dat$status,
predictor = dat$biomarker_A,
levels = c("control", "case"), # 음성, 양성 순서 명시
direction = "<") # case의 점수가 더 '높다'고 지정
roc_A # 객체 요약 출력
Call:
roc.default(response = dat$status, predictor = dat$biomarker_A, levels = c("control", "case"), direction = "<")
Data: dat$biomarker_A in 100 controls (dat$status control) < 100 cases (dat$status case).
Area under the curve: 0.7138
levels와 direction을 명시한 건 의도적입니다. levels = c("control", "case")는 "앞이 음성, 뒤가 양성"을 못박고, direction = "<"는 "양성(case)의 점수가 음성보다 높다"는 뜻입니다. 이 둘을 지정하지 않으면 pROC가 데이터에서 자동으로 추정하는데, 점수가 거꾸로 든 경우(낮을수록 질병) AUC가 0.5 아래로 뒤집혀 나옵니다. 가장 흔한 실수라 뒤에서 따로 다룹니다.
3. 곡선 그리기 + AUC + 신뢰구간
ROC 객체를 plot()에 넘기면 곡선이 그려지고, auc()로 면적을, ci.auc()로 신뢰구간을 구합니다.
# 곡선 그리기 (대각선 = 무작위 분류 기준선)
plot(roc_A,
col = "#2E7D6B", lwd = 3,
print.auc = TRUE, # 곡선 위에 AUC 표시
auc.polygon = TRUE, # AUC 면적 음영
auc.polygon.col = "#2E7D6B22",
legacy.axes = TRUE, # x축을 1-특이도(FPR)로 (0→1 방향)
main = "바이오마커 A의 ROC 곡선")
# AUC 값과 95% 신뢰구간
auc(roc_A) # 점추정
ci.auc(roc_A) # DeLong 방식 95% CI (기본)
ci.auc(roc_A, method = "bootstrap") # 부트스트랩 CI (대안)
Area under the curve: 0.7138
95% CI: 0.6427-0.7849 (DeLong)
legacy.axes = TRUE를 준 건 가로축을 익숙한 1−특이도(0에서 1로 증가)로 그리기 위해서입니다. 이걸 빼면 pROC 기본값은 가로축을 특이도(1에서 0)로 그려, 곡선 모양은 같아도 축 방향이 거꾸로 보여 헷갈립니다. AUC 0.71의 95% 신뢰구간이 0.64–0.78이고 하한이 0.5보다 충분히 크니, "이 바이오마커는 무작위(0.5)보다 유의하게 잘 가른다"고 말할 수 있습니다.
ci.auc()로 신뢰구간을 같이 보고, 하한이 0.5를 넘는지 확인하세요. 기본은 빠른 DeLong 방식이고, 표본이 아주 작거나 분포 가정이 불안하면 method = "bootstrap"이 안전합니다.4. 최적 컷오프 — Youden 지수
AUC는 곡선 전체의 요약일 뿐, 실제로 양성/음성을 가르려면 컷오프 하나를 정해야 합니다. 가장 널리 쓰는 기준이 Youden 지수로, 민감도 + 특이도 − 1을 최대로 만드는 지점(ROC 곡선이 대각선에서 가장 멀리 떨어진 점)을 고릅니다.
# Youden 지수로 최적 컷오프와 그때의 민감도·특이도
coords(roc_A, x = "best",
best.method = "youden",
ret = c("threshold", "sensitivity", "specificity"))
threshold sensitivity specificity
1 5.612471 0.71 0.66
이 컷오프(약 5.61)에서 점수가 그보다 높으면 양성으로 부르며, 그때 환자의 71%를 잡고(민감도) 정상인의 66%를 정상으로 맞춥니다(특이도). 임상 맥락에 따라 기준은 달라집니다. 놓치면 안 되는 병이라면 민감도를 우선해 컷오프를 낮추고, 불필요한 추가검사를 줄이려면 특이도를 높입니다. 특정 민감도(예: 90%)에서의 컷오프가 필요하면 다음처럼 직접 지정합니다.
# 민감도 0.90을 보장하는 컷오프에서의 특이도
coords(roc_A, x = 0.90, input = "sensitivity",
ret = c("threshold", "specificity"))
5. 두 바이오마커 비교 — roc.test() (DeLong)
바이오마커 A와 B 중 어느 쪽이 더 잘 가르는지 묻는다면, 두 ROC를 겹쳐 그리고 AUC를 통계적으로 비교합니다. 같은 샘플에서 잰 두 점수는 짝지어진(paired) 데이터이므로, pROC는 상관을 반영하는 DeLong 검정을 씁니다.
# 바이오마커 B의 ROC도 만들기
roc_B <- roc(dat$status, dat$biomarker_B,
levels = c("control", "case"), direction = "<")
# 두 곡선 겹쳐 그리기
plot(roc_A, col = "#2E7D6B", lwd = 3, legacy.axes = TRUE,
main = "바이오마커 A vs B")
plot(roc_B, col = "#E76F51", lwd = 3, add = TRUE) # add = TRUE로 겹쳐 그림
legend("bottomright",
legend = c(sprintf("A (AUC %.3f)", auc(roc_A)),
sprintf("B (AUC %.3f)", auc(roc_B))),
col = c("#2E7D6B", "#E76F51"), lwd = 3)
# AUC 차이의 통계적 유의성 (DeLong, paired)
roc.test(roc_A, roc_B, method = "delong")
DeLong's test for two correlated ROC curves
data: roc_A and roc_B
Z = -3.42, p-value = 0.000625
alternative hypothesis: true difference in AUC is not equal to 0
sample estimates:
AUC of roc1 AUC of roc2
0.7138 0.8462
B의 AUC (0.85)가 A (0.71)보다 높고, DeLong 검정의 p = 0.0006으로 그 차이가 우연이 아님이 확인됩니다. 즉 바이오마커 B가 두 그룹을 유의하게 더 잘 가릅니다. 세 개 이상을 비교할 땐 쌍마다 roc.test()를 돌리고, 여러 번 검정하므로 p-값을 보정합니다 (자세한 건 p-value와 FDR).
ggplot2에 익숙하다면 ggroc()로 여러 ROC를 리스트로 묶어 한 줄에 그릴 수 있습니다. ggroc(list(A = roc_A, B = roc_B)) + geom_abline(slope = 1, intercept = 1, linetype = "dashed")처럼 쓰면 대각선까지 깔끔하게 들어갑니다 (자세한 ggplot 문법은 ggplot2 기초).
6. AUC 해석 기준 + ROC vs PR 곡선
AUC를 한 숫자로 읽을 때의 대략적 기준은 다음과 같습니다. 다만 분야·표본·목적에 따라 달라지는 관행적 눈금일 뿐, 절대 기준은 아닙니다.
| AUC 구간 | 해석(관행) |
|---|---|
| 0.5 | 무작위 — 동전 던지기와 같음 |
| 0.5 ~ 0.7 | 약함 — 분류 근거로는 부족 |
| 0.7 ~ 0.8 | 수용 가능(acceptable) |
| 0.8 ~ 0.9 | 우수(excellent) |
| 0.9 이상 | 탁월(outstanding) — 단, 과적합·누수 의심 |
한 가지 주의할 점이 있습니다. ROC는 클래스 불균형에 둔감합니다. 정상이 99%, 환자가 1%인 데이터에서는 거짓양성률(FPR)의 분모가 워낙 커서, 모델이 양성 예측을 남발해도 ROC 곡선이 낙관적으로 보입니다. 이런 희귀 양성·불균형 상황에서는 정밀도(precision)와 재현율(recall)을 축으로 쓰는 PR (precision-recall) 곡선이 모델 성능을 더 정직하게 드러냅니다.
| 구분 | ROC 곡선 | PR (precision-recall) 곡선 |
|---|---|---|
| 가로축 | 1−특이도(FPR) | 재현율(recall = 민감도) |
| 세로축 | 민감도(TPR) | 정밀도(precision) |
| 무작위 기준선 | 대각선(AUC 0.5) | 양성 비율(수평선) |
| 불균형 민감도 | 둔감(낙관적일 수 있음) | 민감(불균형을 잘 드러냄) |
| 적합한 상황 | 두 클래스가 균형 잡힘 | 양성이 희귀(예: 희귀질환·이상탐지) |
요약하면, 클래스가 균형 잡혔으면 ROC/AUC로 충분하고, 양성이 드물면 PR 곡선을 함께 보는 게 안전합니다. PR 곡선은 PRROC나 precrec 같은 패키지로 그립니다.

7. 자주 발생하는 에러 & 오해
- AUC가 0.5보다 작게 나옴 (방향 뒤집힘) — 가장 흔한 실수입니다. 점수가 "낮을수록 질병"인데
pROC가 "높을수록 질병"으로 잡으면 곡선이 대각선 아래로 내려가 AUC < 0.5가 됩니다. 해결은roc()에서direction과levels를 명시하는 것입니다.levels = c("control", "case")로 음성·양성 순서를,direction = "<"(양성 점수가 높음) 또는direction = ">"(양성 점수가 낮음)로 방향을 못박으세요. AUC가 0.5 아래면 거의 항상direction을 반대로 둔 것입니다. - 클래스 라벨이 문자/숫자로 섞임 —
response는 두 수준이어야 합니다.0/1,"control"/"case"어느 쪽이든 되지만,levels로 어느 쪽이 양성인지 명시하지 않으면pROC가 알파벳·숫자 순으로 추정해 의도와 어긋날 수 있습니다.factor(..., levels = ...)로 순서를 고정하는 습관을 들이세요. - AUC만 보고 컷오프를 무시 — AUC는 곡선 전체의 요약일 뿐, 실제 진단에는 컷오프 하나가 필요합니다.
coords(roc, "best")로 최적 컷오프와 그때의 민감도·특이도를 함께 보고하세요. 임상에서는 AUC보다 "민감도 90%에서 특이도 얼마"가 더 중요할 때가 많습니다. - 불균형 데이터에서 ROC를 과신 — 양성이 희귀하면 ROC가 낙관적으로 보입니다. 양성 비율이 한쪽으로 크게 쏠렸다면 PR 곡선을 함께 보세요(위 표).
- 학습·평가를 같은 데이터로 — 모델을 만든 데이터에서 그대로 AUC를 재면 과적합(overfitting)으로 부풀려집니다. 가능하면 학습/검증 데이터를 나누거나 교차검증(cross-validation)으로 AUC를 추정하세요. 이 글의 더미 예시는 개념 설명용이라 같은 데이터를 썼습니다.
• (검정의 기초) R로 기초 통계 검정하기 — 두 그룹 차이를 t-검정으로, ROC와 짝지어 이해
• (여러 번 검정의 보정) p-value와 FDR, 다중검정 보정 — 바이오마커를 여럿 비교할 때
• (점수의 출발점) DESeq2로 차등발현 분석 — 평가할 후보 유전자를 찾는 단계
• (출판용 그림) ggplot2 기초 · clusterProfiler 농축분석

References
- Robin, X. et al. (2011). pROC: an open-source package for R and S+ to analyze and compare ROC curves. BMC Bioinformatics, 12, 77. (pROC 원논문)
- DeLong, E. R., DeLong, D. M., Clarke-Pearson, D. L. (1988). Comparing the areas under two or more correlated receiver operating characteristic curves: a nonparametric approach. Biometrics, 44(3), 837–845. (DeLong 검정)
- Youden, W. J. (1950). Index for rating diagnostic tests. Cancer, 3(1), 32–35. (Youden 지수)
- Saito, T., Rehmsmeier, M. (2015). The precision-recall plot is more informative than the ROC plot when evaluating binary classifiers on imbalanced datasets. PLoS ONE, 10(3), e0118432. (ROC vs PR)
Pipette & Pipeline · A bio portfolio journal
'Code Bench > R Snippets' 카테고리의 다른 글
| monocle3로 단일세포 궤적·슈도타임 분석 — 세포가 변해가는 길을 그리다 (R 실전) (3) | 2026.06.22 |
|---|---|
| GSVA로 샘플별 경로 활성 점수 매기기 — 유전자에서 경로로 (R 실전) (0) | 2026.06.19 |
| R로 기초 통계 검정하기 — t-검정·ANOVA·상관·비모수 (R 실전) (0) | 2026.06.18 |
| pheatmap으로 발현 히트맵 빠르게 그리기 — z-score·클러스터링·주석 (R 실전) (0) | 2026.06.18 |
| dplyr·tidyr로 생물 데이터 정리하기 — 분석 전 90%는 데이터 정리 (R 실전) (0) | 2026.06.18 |