Wilcoxon Signed Rank Test 통계를 웹에서 구현하기 위해서 어떤 식을 스크립트를 작성하면 좋을지 알아보겠습니다.
차근차근 하죠.
우선 Wilcoxon Signed Rank Test 에 대해서 알아볼 필요가 있습니다.
참가자 | 약물 투여 전 혈압 | 약물 투여 후 혈압
-----------------------------------------
1 | 120 | 115
2 | 130 | 128
3 | 110 | 105
4 | 140 | 135
5 | 125 | 120
6 | 130 | 125
7 | 135 | 130
8 | 115 | 110
9 | 140 | 138
10 | 125 | 122
자, 위와 같이 데이터가 있다고 가정합시다.
여기서 참가자에 대하여 쌍을 이루고 있는 2개의 데이터가 있죠?
약물 투여 전 혈압, 약물 투여 후 혈압이 각 참가자마다 쌍을 이뤄 값을 가지고 있습니다.
이때, 약물 투여 전후의 데이터가 두 종속 그룹이 됩니다.
Wilcoxon Signed-Rank Test 는 이러한 두 종속 샘플 간에 차이가 있는지를 분석하게 됩니다.
이 두 그룹 간에 통계적으로 유의미한 차이가 있는지 검사하는 것이지요.
여기서 “t 검정도 동일한 기능을 하지 않나요?” 라고 생각할 수도 있습니다.
맞습니다.
Wilcoxon Test는 종속 샘플 t검정의 비모수적 대응입니다.
Wilcoxon Test의 특별한 점은 데이터가 정규분포를 따를 필요가 없다는 것입니다.
간단히 말해, 데이터가 정규 분포를 따르는 경우에는 모수적 검정을 사용하며, t-test를 수행합니다.
그렇지 않은 경우, 비모수적 검정을 사용하며 두 종속 샘플의 경우는 Wilcoxon Test를 사용합니다.
그렇다면 왜 항상 Wilcoxon Test를 사용하지 않을까요?
정규 분포를 확인할 필요 없이 그냥 Wilcoxon Test를 사용하면 편하지 않을까요?
이건 모수적 검정 vs 비모수적 검정에 관한 이야기기도 합니다.
모수적 검정은 일반적으로 비모수적 검정보다 더 큰 검정력을 가지게 됩니다.
모수적 검정에서는 일반적으로 더 작은 차이나 더 작은 샘플로도 귀무 가설을 기각할 수 있습니다.
예컨대 데이터의 수가 적더라도 이용할 수 있으며, 검정력도 더 좋다는 뜻이지요.
반대로 비모수적 검정은 데이터의 수가 충분히 존재해야 이용할 수 있으며, 검정력은 그에 반해 떨어지는 단점을 가지게 되는 겁니다.
때문에 이용할 수 있다면, 무조건 모수적 검정은 t-test를 따르는 것이 이득이겠죠.
참가자 | 약물 투여 전 혈압 | 약물 투여 후 혈압
-----------------------------------------
1 | 134 | 145
2 | 133 | 136
3 | 141 | 135
4 | 139 | 143
위 데이터에서 두 쌍을 이루고 있는 샘플을 종속 샘플이라고 말했죠?
종속 샘플에서는 측정 값이 항상 쌍으로 제공이 됩니다.
때문에 우리는 이 두 종속 데이터를 비교할 수가 있게 되는 거죠.
만약 t-test 에서 위 두 데이터를 비교하기 위해서는 어떤 절차를 따를까요?
참가자 | 약물 투여 전 혈압 | 약물 투여 후 혈압 | 약물 투여 전 - 후 혈압
---------------------------------------------------------
1 | 134 | 145 | -11
2 | 133 | 136 | -3
3 | 141 | 135 | 6
4 | 139 | 143 | -4
t-test 에서는 위와 같이 두 종속 데이터의 “차이”를 이용하게 됩니다.
참가자 1에서 4까지의 두 데이터 차이를 추가한 것이 보이시죠?
그렇다면, Wilcoxon Test 에서는 어떨까요?
참가자 | 약물 투여 전 혈압 | 약물 투여 후 혈압 | 약물 투여 전 - 후 혈압 | 절대값 | 랭크
------------------------------------------------------------------------------------------------
1 | 134 | 145 | -11 | 11 | 4
2 | 133 | 136 | -3 | 3 | 1
3 | 141 | 135 | 6 | 6 | 3
4 | 139 | 143 | -4 | 4 | 2
Wilcoxon Test 라는 이름답게, Rank를 부여합니다.
이 Rank 는 두 종속 데이터의 차이에 절대값을 부여하고, 이 절대값이 가장 작은 값부터 1을 차례대로 붙이게 됩니다.
다만, 이 랭크 숫자에는 숨은 기호가 있습니다.
참가자 | 약물 투여 전 혈압 | 약물 투여 후 혈압 | 약물 투여 전 - 후 혈압 | 절대값 | 랭크
------------------------------------------------------------------------------------------------
1 | 134 | 145 | -11 | 11 | (-)4
2 | 133 | 136 | -3 | 3 | (-)1
3 | 141 | 135 | 6 | 6 | (+)3
4 | 139 | 143 | -4 | 4 | (-)2
두 변수의 차이가 음수였던 경우의 랭크에는 (-) 가, 양수였던 경우의 랭크에는 (+)가 부여됩니다.
양수인 순위는 3 하나 밖에 없는 반면, 음수인 순위는 4, 1, 2 가 있습니다.
(+) 랭크들의 합은 3. (-) 랭크들의 합은 7이 되는 거죠.
만약 약물 투여 전후의 차이가 거의 없었다고 가정한다면, (+) 랭크들과 (-) 랭크들의 합이 비슷하겠죠?
Wilcoxon Test 는 이러한 방식으로 두 종속 데이터의 통계를 수행합니다.
여기서 W-통계량은 (+) 랭크들의 합과 (-)랭크들의 합 중 더 작은 값이 선정됩니다.
위의 경우에서는 3이 W-통계량이 되는 것이죠.
그렇다면 W-기대값을 계산해볼까요?
이를 위해서는 순위를 매긴 값의 개수 n을 알아야 합니다.
위 데이터를 봤을 때, n은 4가 되겠죠? 이거 알았으면 땡입니다.
이제 위에 있는 n에 우리가 구한 값을 넣어 계산하면 되는 거거든요.
즉 W-기대값은 5가 되는 겁니다.
이 기대값의 의미란, 두 그룹 간에 차이가 존재하지 않을 때 관찰될 것으로 예상되는 순위입니다.
귀무 가설이 참일 때, 약물 투여 전후의 혈압 차이가 없을 때 관찰될 것으로 예상되는 순위 합의 평균입니다.
실제 관찰된 W값이 기대값과 크게 다를 경우, 이는 귀무 가설을 기각할 가능성을 높이게 됩니다.
또한 이러한 공식으로 SD 표준편차의 값도 구할 수가 있습니다.
n은 아까 4였죠? 계산하면 대략 2.74가 나타나게 됩니다.
앞거 구한 값들을 이용해서 Z-Value 를 구할 수도 있습니다.
W 에는 앞서 구한 W-통계량을, E(T) 에는 W-기대값을, SD(T) 에는 표준편차의 값을 넣어주면 됩니다.
결과는 -0.73이 나타나게 됩니다.
귀무가설이 참일 때, 관측된 순위 합이 기대 순위의 합과 비교해서 얼마나 차이가 있는지를 표준 편차 단위로 나타낸 것입니다.
Z-Value 가 음수이면 음수 순위 합이 양수 순위 합보다 크다는 것을 의미합니다.
일반적으로, Z-Value 의 절대값이 클수록 두 그룹 간의 차이가 통계적으로 유의미할 가능성이 큽니다
이 Z-Value 는 P-Value 를 구하기 위해 사용됩니다.
다만, 지금 데이터는 너무나도 적습니다.
25개 이상의 경우가 있어야지만, 정규 분포가 가정되어 위 공식으로 Z값을 계산 가능합니다.
즉, 가장 앞서 말했듯이 비모수적 검정이기에 데이터가 충분히 존재해야 한다는 소리입니다.
자, 그럼 이제 코드를 한 번 알아보겠습니다.
# 필요한 패키지 불러오기
library(ggplot2)
library(gridExtra)
library(svglite)
# 커맨드라인 인자로부터 파일 이름, 종속 변수 이름들을 가져오기
args <- commandArgs(trailingOnly = TRUE)
# 인자 파싱
args_list <- list()
for (arg in args) {
split_arg <- strsplit(arg, "=")[[1]]
args_list[[split_arg[1]]] <- split_arg[2]
}
# 인자 설정
# 실제 데이터프레임을 읽을 경로입니다.
filename <- args_list$filename
# 분석할 종속 변수의 첫 번째 값입니다. 어떤 이벤트의 이전 값들이라고 보아도 좋습니다.
dependentVar_before <- args_list$dependentVar_before
# 분석할 종속 변수의 두 번째 값입니다. 어떤 이벤트의 이후 값들이라고 보아도 좋습니다.
dependentVar_after <- args_list$dependentVar_after
# 결과적으로
goalname <- args_list$goalname
# 데이터 읽기
data <- read.csv(filename)
# 데이터 타입 확인 및 변환 (필요시)
if (!is.numeric(data[[dependentVar_before]])) {
data[[dependentVar_before]] <- as.numeric(data[[dependentVar_before]])
}
if (!is.numeric(data[[dependentVar_after]])) {
data[[dependentVar_after]] <- as.numeric(data[[dependentVar_after]])
}
# 결측치 처리: 결측치가 있는 행 제거
data <- data[!is.na(data[[dependentVar_before]]) & !is.na(data[[dependentVar_after]]), ]
# Wilcoxon Signed-Rank Test 수행
test_result <- wilcox.test(data[[dependentVar_before]], data[[dependentVar_after]], paired = TRUE)
# 결과 요약
test_summary <- data.frame(
Statistic = test_result$statistic,
P.value = test_result$p.value
)
# 결과 시각화
# 상자 그림(Boxplot)으로 시각화
boxplot_data <- data.frame(
Group = rep(c("Before", "After"), each = nrow(data)),
Value = c(data[[dependentVar_before]], data[[dependentVar_after]])
)
boxplot <- ggplot(boxplot_data, aes(x = Group, y = Value, fill = Group)) +
geom_boxplot() +
theme_minimal() +
labs(title = "Wilcoxon Signed-Rank Test Results",
x = NULL, y = "Values")
# P-value와 통계량 텍스트
text_plot <- ggplot() +
annotate("text", x = 1, y = 1, label = paste("Statistic:", round(test_result$statistic, 2), "\nP-value:", round(test_result$p.value, 4)), size = 6) +
theme_void()
# 테이블과 플롯을 결합하여 저장
lay <- matrix(c(1, 2), nrow = 1)
# SVG 파일로 저장
svglite::svglite(file = goalname, width = 10, height = 7)
grid.arrange(boxplot, text_plot, layout_matrix = lay)
dev.off()
우선 전체 R 스크립트입니다.
차근차근 확인하겠습니다.
# 필요한 패키지 불러오기
library(ggplot2)
library(gridExtra)
library(svglite)
# 커맨드라인 인자로부터 파일 이름, 종속 변수 이름들을 가져오기
args <- commandArgs(trailingOnly = TRUE)
# 인자 파싱
args_list <- list()
for (arg in args) {
split_arg <- strsplit(arg, "=")[[1]]
args_list[[split_arg[1]]] <- split_arg[2]
}
# 인자 설정
# 실제 데이터프레임을 읽을 경로입니다.
filename <- args_list$filename
# 분석할 종속 변수의 첫 번째 값입니다. 어떤 이벤트의 이전 값들이라고 보아도 좋습니다.
dependentVar_before <- args_list$dependentVar_before
# 분석할 종속 변수의 두 번째 값입니다. 어떤 이벤트의 이후 값들이라고 보아도 좋습니다.
dependentVar_after <- args_list$dependentVar_after
# 결과적으로
goalname <- args_list$goalname
필요한 라이브러리를 불러옵니다.
이 스크립트를 실행하며 넣어준 인자들을 args로 받습니다.
이 args 를 args_list 로 변경합니다.
args_list 에 알맞은 key값을 넣어주고, 그것에 해당하는 값들을 각 인자에 할당해줍시다.
# 데이터 읽기
data <- read.csv(filename)
# 데이터 타입 확인 및 변환 (필요시)
if (!is.numeric(data[[dependentVar_before]])) {
data[[dependentVar_before]] <- as.numeric(data[[dependentVar_before]])
}
if (!is.numeric(data[[dependentVar_after]])) {
data[[dependentVar_after]] <- as.numeric(data[[dependentVar_after]])
}
받아온 파일의 경로를 통해 데이터프레임을 읽고, data에 할당합시다.
만약 데이터프레임에서 종속변수의 열의 값이 연속형 변수가 아닐 경우 as.numeric 방법을 이용해 연속형 변수로 변경을 해줍시다.
이 과정을 before 열과 after 열에 둘 다 실행하는 겁니다.
# 결측치 처리: 결측치가 있는 행 제거
data <- data[!is.na(data[[dependentVar_before]]) & !is.na(data[[dependentVar_after]]), ]
data[조건, “A”] <- 값: 조건에 맞는 행의 “A” 열 값을 지정된 값으로 변경합니다.
data[조건, ]: 조건에 맞는 행을 선택합니다.
!is.na(열): 해당 열에 결측치가 아닌 값을 선택합니다.
위 방법들이 활용한 것입니다.
조건에 맞는 행들을 캐치해서 data 에 다시 할당을 하는 것이죠.
# Wilcoxon Signed-Rank Test 수행
test_result <- wilcox.test(data[[dependentVar_before]], data[[dependentVar_after]], paired = TRUE)
wilcox.test 함수입니다.
함수 내부 어떤 위치에 어떤 인자를 넣어주면 될지 정리하겠습니다.
첫번째 인자: Wilcoxon Signed-Rank Test에서 비교할 첫 번째 연속 변수의 데이터.
두번재 인자: Wilcoxon Signed-Rank Test에서 비교할 두 번째 연속 변수의 데이터.
세번째 인자 : 데이터를 쌍으로 처리할지 여부를 지정. TRUE로 설정하여 쌍으로 처리합니다. 이는 Wilcoxon Signed-Rank Test에서 필수적입니다.
# 결과 요약
test_summary <- data.frame(
Statistic = test_result$statistic,
P.value = test_result$p.value
)
# 결과 시각화
# 상자 그림(Boxplot)으로 시각화
boxplot_data <- data.frame(
Group = rep(c("Before", "After"), each = nrow(data)),
Value = c(data[[dependentVar_before]], data[[dependentVar_after]])
)
boxplot <- ggplot(boxplot_data, aes(x = Group, y = Value, fill = Group)) +
geom_boxplot() +
theme_minimal() +
labs(title = "Wilcoxon Signed-Rank Test Results",
x = NULL, y = "Values")
# P-value와 통계량 텍스트
text_plot <- ggplot() +
annotate("text", x = 1, y = 1, label = paste("Statistic:", round(test_result$statistic, 2), "\nP-value:", round(test_result$p.value, 4)), size = 6) +
theme_void()
# 테이블과 플롯을 결합하여 저장
lay <- matrix(c(1, 2), nrow = 1)
# SVG 파일로 저장
svglite::svglite(file = goalname, width = 10, height = 7)
grid.arrange(boxplot, text_plot, layout_matrix = lay)
dev.off()
이후는 시각화의 방법입니다.
시각화의 경우는 원하는 디자인으로 변경하여 사용하여도 무방합니다.
function runWilcoxonSigned(res, filename, beforeVar, afterVar, goalname) {
const scriptPath = "example.R";
// Node.js의 child_process.spawn을 사용하여 R 스크립트를 비동기적으로 실행합니다.
// 스크립트에 필요한 모든 파라미터를 명령줄 인수로 전달합니다.
const rProcess = spawn("Rscript", [
scriptPath,
//filename 에는 분석에 사용한 CSV 데이터프레임의 경로를 넣습니다. ex."/home/data/mydata.csv"
`filename=${filename}`,
//Wilcoxon Signed Rank 에서 종속변수로 사용할 첫 번째 값입니다. 어떠한 이벤트의 이전이라고 보아도 좋습니다.
`dependentVar_before=${beforeVar}`,
//Wilcoxon Signed Rank 에서 종속변수로 사용할 두 번째 값입니다. 어떠한 이벤트의 이후라고 보아도 좋습니다.
`dependentVar_after=${afterVar}`,
//결과를 어느 곳으로 저장할지 .
`goalname=${goalname}`,
]);
rProcess.stdout.on("data", (data) => {
console.log(data.toString());
});
rProcess.stderr.on("data", (data) => {
console.error(`R Error: ${data}`);
});
// R 프로세스가 종료되면 호출되는 이벤트 핸들러입니다.
rProcess.on("close", (code) => {
// R 스크립트의 종료 코드를 로깅합니다.
console.log(`R process exited with code ${code}`);
// 종료 코드가 0이 아닌 경우, 스크립트 실행이 실패했다는 것을 의미합니다.
if (code !== 0) {
console.error("R script execution failed with code:", code);
return res.status(500).send("R script execution failed");
}
try {
// 결과 이미지 파일의 경로를 지정합니다.
const imgPath = goalname;
// 파일 시스템에서 이미지 파일을 비동기적으로 읽습니다.
fs.readFile(imgPath, (err, data) => {
if (err) {
// 파일 읽기 오류가 발생한 경우, 에러를 로깅하고 클라이언트에 오류 메시지를 전송합니다.
console.error("Error reading image file:", err);
return res.status(500).send("Error reading image file");
}
// 이미지 데이터를 성공적으로 읽은 경우, HTTP 응답으로 이미지를 전송합니다.
res.writeHead(200, { "Content-Type": "image/svg+xml" });
res.end(data);
});
} catch (err) {
// 이미지 전송 중 예외가 발생한 경우, 에러를 로깅하고 오류 메시지를 전송합니다.
console.error("Failed to send image:", err);
res.status(500).send("Failed to send image");
}
});
}
위 함수를 백엔드에 저장해두면, 언제든 백엔드 코드 내부에서 함수를 사용 가능합니다.
더미 데이터를 활용한 결과가 다음과 같이 나타나는 것을 확인할 수 있습니다.