Wilcoxon Sum Rank Test 통계를 웹에서 구현하기 위해서 어떤 식으로 스크립트를 작성하면 좋을지 알아보겠습니다.
차근차근 하죠.
우선 Wilcoxon Sum Rank Test 에 대해서 알아보겠습니다.
| ID | Group | Value |
|----|--------|-------|
| 1 | Group1 | 5 |
| 2 | Group1 | 6 |
| 3 | Group1 | 7 |
| 4 | Group1 | 8 |
| 5 | Group2 | 1 |
| 6 | Group2 | 2 |
| 7 | Group2 | 3 |
| 8 | Group2 | 4 |
예컨대 위와 같은 가상의 데이터가 있다고 가정합시다.
Sum Rank Test 를 위해서는 범주형 이진변수 열과, 그에 대응하는 연속형 변수 열이 필요합니다.
여기서 범주형 이진 변수 열이 Group, 연속형 변수 열이 Value 가 될 것입니다.
| ID | Group | Value | Rank |
|----|--------|-------|------|
| 1 | Group1 | 5 | 5 |
| 2 | Group1 | 6 | 6 |
| 3 | Group1 | 7 | 7 |
| 4 | Group1 | 8 | 8 |
| 5 | Group2 | 1 | 1 |
| 6 | Group2 | 2 | 2 |
| 7 | Group2 | 3 | 3 |
| 8 | Group2 | 4 | 4 |
이제 여기 각 데이터에 Rank를 부여할 것입니다.
이 Rank는 연속형 변수의 값이 가장 작은 데이터에 1, 그 다음으로 작은 데이터에 2가 부여되는 방식입니다.
이후, 이 Rank 값을 통해서 분석을 수행합니다.
Group은 이진 변수였죠? 즉, 2개의 값만 존재하는 데이터입니다.
그렇다면 2개의 값에 대하여 각자 할당된 Rank 들의 값을 더해줄 수 있습니다.
Group1: 5 + 6 + 7 + 8 = 26
Group2: 1 + 2 + 3 + 4 = 10
요렇게요.
이렇게 두 그룹의 각 Rank 를 Sum 한 값을 통해 분석하기에, Wilcoxon Sum Rank Test 입니다.
그렇다면 어떤 통계값을 얻을 수 있을까요?
u_statistic <- min(u1, u2)
우선 U-통계량입니다.
R 코드에서는 위와 같은 계산 방식을 가지게 됩니다.
u1 과 u2가 아까 위에서 계산한 각 그룹의 랭크 합계가 됩니다.
두 개의 랭크 합계 중에서 더 작은 값을 택하는 거죠.
제가 제시한 가상의 데이터로 U-통계량을 구하자면, 10이 되겠군요.
# 샘플 크기 계산
n1 <- sum(data$Group == "Group1")
n2 <- sum(data$Group == "Group2")
# Expected U 계산
expected_u <- (n1 * n2) / 2
이번에는 U-기대값입니다.
각 그룹의 개수를 곱하고 2로 나눈 값입니다.
만약 각 그룹의 점수가 거의 비슷하다고 할 때, 기대하게 되는 평균 순위의 합이 됩니다.
sd_u <- sqrt((n1 * n2 * (n1 + n2 + 1)) / 12)
샘플의 크기를 통해서 SD도 구할 수 있습니다.
sqrt 는 제곱근. 루트입니다.
# Z-value 계산
z_value <- (u_statistic - expected_u) / sd_u
위에서 구했던 U-통계량, U-기대값, SD를 이용하여 Z-value 도 구할 수 있습니다.
Z-value가 클수록 두 그룹 간의 차이가 크다는 것을 의미합니다.
# 필요한 패키지 불러오기
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
# 데이터프레임에서 이진 변수에 해당하는 Column
GroupVar <- args_list$GroupVar
# 데이터프레임에서 연속 변수에 해당하는 Column
ValueVar <- args_list$ValueVar
# 생성된 결과 파일을 저장할 경로
goalname <- args_list$goalname
# 데이터 불러오기
data <- read.csv(filename)
# 결측치 처리 후 data 에 다시 담기
data <- data[!is.na(data[[GroupVar]]) & !is.na(data[[ValueVar]]), ]
# 연속 변수에 해당하는 열의 데이터를 체크, 숫자로 변환
if (!is.numeric(data[[ValueVar]])) {
data[[ValueVar]] <- as.numeric(data[[ValueVar]])
}
# 범주형 이진 변수에 해당하는 열의 데이터를 체크, 범주형임을 명시
if (!is.factor(data[[GroupVar]])) {
data[[GroupVar]] <- as.factor(data[[GroupVar]])
}
# Wilcoxon Rank-Sum Test 수행
test_result <- wilcox.test(data[[ValueVar]] ~ data[[GroupVar]])
# 추가 통계량 계산
n1 <- sum(data[[GroupVar]] == levels(data[[GroupVar]])[1])
n2 <- sum(data[[GroupVar]] == levels(data[[GroupVar]])[2])
r1 <- sum(rank(data[[ValueVar]])[data[[GroupVar]] == levels(data[[GroupVar]])[1]])
r2 <- sum(rank(data[[ValueVar]])[data[[GroupVar]] == levels(data[[GroupVar]])[2]])
u1 <- r1 - (n1 * (n1 + 1) / 2)
u2 <- r2 - (n2 * (n2 + 1) / 2)
u_statistic <- min(u1, u2)
# Expected U and Standard Deviation
expected_u <- (n1 * n2) / 2
sd_u <- sqrt((n1 * n2 * (n1 + n2 + 1)) / 12)
# Z-value 계산
z_value <- (u_statistic - expected_u) / sd_u
# 결과 요약
test_summary <- data.frame(
U_Statistic = u_statistic,
Expected_U = expected_u,
SD = sd_u,
Z_value = z_value,
P_value = test_result$p.value
)
# 결과 시각화
# 상자 그림(Boxplot)으로 시각화
boxplot_data <- data.frame(
Group = data[[GroupVar]],
Value = data[[ValueVar]]
)
boxplot <- ggplot(boxplot_data, aes(x = Group, y = Value, fill = Group)) +
geom_boxplot() +
theme_minimal() +
labs(title = "Wilcoxon Rank-Sum Test Results",
x = NULL, y = "Values") +
scale_x_discrete(labels = levels(data[[GroupVar]]))
# 통계량 텍스트
stat_text <- paste(
"Sample sizes (n1, n2): ", n1, ", ", n2, "\n",
"U Statistic: ", round(u_statistic, 2), "\n",
"Expected U: ", round(expected_u, 2), "\n",
"SD: ", round(sd_u, 2), "\n",
"Z-value: ", round(z_value, 2), "\n",
"P-value: ", round(test_result$p.value, 4)
)
# P-value와 통계량 텍스트
text_plot <- ggplot() +
annotate("text", x = 1, y = 1, label = stat_text, 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()
우선 전체 코드입니다.
어떤 식으로 데이터를 구했는지 코드를 둘러보겠습니다.
# 필요한 패키지 불러오기
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
# 데이터프레임에서 이진 변수에 해당하는 Column
GroupVar <- args_list$GroupVar
# 데이터프레임에서 연속 변수에 해당하는 Column
ValueVar <- args_list$ValueVar
# 생성된 결과 파일을 저장할 경로
goalname <- args_list$goalname
해당 코드를 사용하기 위해서 필요한 인자들을 코드 앞 부분에서 파싱하여 저장합니다.
# 데이터 불러오기
data <- read.csv(filename)
# 결측치 처리 후 data 에 다시 담기
data <- data[!is.na(data[[GroupVar]]) & !is.na(data[[ValueVar]]), ]
지정된 경로를 이용하여 데이터를 읽어옵시다.
data[조건, “A”] <- 값: 조건에 맞는 행의 “A” 열 값을 지정된 값으로 변경합니다.
data[조건, ]: 조건에 맞는 행을 선택합니다.
!is.na(열): 해당 열에 결측치가 아닌 값을 선택합니다.
위 방법들이 활용한 것입니다.
조건에 맞는 행들을 캐치해서 data 에 다시 할당을 하는 것이죠.
# 연속 변수에 해당하는 열의 데이터를 체크, 숫자로 변환
if (!is.numeric(data[[ValueVar]])) {
data[[ValueVar]] <- as.numeric(data[[ValueVar]])
}
# 범주형 이진 변수에 해당하는 열의 데이터를 체크, 범주형임을 명시
if (!is.factor(data[[GroupVar]])) {
data[[GroupVar]] <- as.factor(data[[GroupVar]])
}
연속형 변수에 해당하는 열이 정말 numeric 인지 확인합시다.
is.numeric() 을 이용해서 확인합니다. 만약 아니라면, 연속형으로 바꿔주는 거죠.
as.numeric() 을 이용해서 연속형으로 변경. 변경된 데이터를 다시 data[[ValueVar]] 에 할당합니다.
마찬가지로 범주형도 같은 작업을 할 겁니다.
다만, is.numeric() 대신에 is.factor() 을 이용해서 범주형인지 확인을 합니다.
이후, as.factor() 을 이용해서 범주형임을 명시합니다.
# Wilcoxon Rank-Sum Test 수행
test_result <- wilcox.test(data[[ValueVar]] ~ data[[GroupVar]])
이제 wicoxon rank sum 테스트를 수행합니다.
반드시 ValueVar 과 GroupVar 이라는 Column 이 데이터 내에 존재하여야 합니다.
~의 왼쪽에는 비교할 연속형 데이터 (측정값)가 있어야 합니다.
~의 오른쪽에는 그룹을 나타내는 범주형 데이터 (이진 변수 또는 범주형 변수)가 있어야 합니다.
# 추가 통계량 계산
n1 <- sum(data[[GroupVar]] == levels(data[[GroupVar]])[1])
n2 <- sum(data[[GroupVar]] == levels(data[[GroupVar]])[2])
r1 <- sum(rank(data[[ValueVar]])[data[[GroupVar]] == levels(data[[GroupVar]])[1]])
r2 <- sum(rank(data[[ValueVar]])[data[[GroupVar]] == levels(data[[GroupVar]])[2]])
u1 <- r1 - (n1 * (n1 + 1) / 2)
u2 <- r2 - (n2 * (n2 + 1) / 2)
u_statistic <- min(u1, u2)
# Expected U and Standard Deviation
expected_u <- (n1 * n2) / 2
sd_u <- sqrt((n1 * n2 * (n1 + n2 + 1)) / 12)
# Z-value 계산
z_value <- (u_statistic - expected_u) / sd_u
# 결과 요약
test_summary <- data.frame(
U_Statistic = u_statistic,
Expected_U = expected_u,
SD = sd_u,
Z_value = z_value,
P_value = test_result$p.value
)
# 결과 시각화
# 상자 그림(Boxplot)으로 시각화
boxplot_data <- data.frame(
Group = data[[GroupVar]],
Value = data[[ValueVar]]
)
boxplot <- ggplot(boxplot_data, aes(x = Group, y = Value, fill = Group)) +
geom_boxplot() +
theme_minimal() +
labs(title = "Wilcoxon Rank-Sum Test Results",
x = NULL, y = "Values") +
scale_x_discrete(labels = levels(data[[GroupVar]]))
# 통계량 텍스트
stat_text <- paste(
"Sample sizes (n1, n2): ", n1, ", ", n2, "\n",
"U Statistic: ", round(u_statistic, 2), "\n",
"Expected U: ", round(expected_u, 2), "\n",
"SD: ", round(sd_u, 2), "\n",
"Z-value: ", round(z_value, 2), "\n",
"P-value: ", round(test_result$p.value, 4)
)
# P-value와 통계량 텍스트
text_plot <- ggplot() +
annotate("text", x = 1, y = 1, label = stat_text, 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()
이후 계산은 앞서 설명했던 내용과 동일하며,
생산한 계산값을 어떤 형식의 Plot으로 저장할지는 직접 디자인을 찾아 활용해도 좋습니다.
위와 같이 R 스크립트를 EC2 내의 특정 경로에 저장했다면,
이제 백엔드 내에서 해당 R 스크립트를 사용하기 위한 전용 함수를 하나 만들 겁니다.
function runWilcoxonSum(res, filename, groupVar, valueVar, goalname) {
const scriptPath = "your script.R";
// Node.js의 child_process.spawn을 사용하여 R 스크립트를 비동기적으로 실행합니다.
// 스크립트에 필요한 모든 파라미터를 명령줄 인수로 전달합니다.
const rProcess = spawn("Rscript", [
scriptPath,
//filename 에는 분석에 사용한 CSV 데이터프레임의 경로를 넣습니다. ex."/home/data/mydata.csv"
`filename=${filename}`,
//Wilcoxon Signed Rank 에서 종속변수로 사용할 첫 번째 값입니다. 어떠한 이벤트의 이전이라고 보아도 좋습니다.
`GroupVar=${groupVar}`,
//Wilcoxon Signed Rank 에서 종속변수로 사용할 두 번째 값입니다. 어떠한 이벤트의 이후라고 보아도 좋습니다.
`ValueVar=${valueVar}`,
//결과를 어느 곳으로 저장할지 .
`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");
}
});
}
위와 같은 함수를 백엔드 코드 내부에 저장해두면, 언제든지 해당 함수에 알맞은 변수를 할당하여 코드를 실행 가능합니다.
runWilcoxonSum(res, filename, groupVar, valueVar, goalname);
위와 같이 말이죠.
이제 특정한 변수를 이용하여 백엔드에서 Wilcoxon Rank Sum Test 를 돌리고, 그 결과를 저장 가능합니다.