통계 분석 리포트 앱
데이터를 올리면 자동으로 통계 분석을 수행하고, 결과를 Word나 HTML 보고서로 내려받을 수 있는 앱을 만듭니다. "실전 R 통계 분석" 교재에서 배운 t-검정, 상관분석이 여기서 Shiny 앱과 만납니다.
앱 구조
stat-report/
├── app.R
└── report_template.Rmd
사용자가 CSV 파일을 업로드하면 앱이 변수를 자동으로 인식합니다. 숫자형 변수 두 개를 고르면 t-검정과 상관분석이 즉시 실행되고, 분석 결과를 담은 R Markdown 보고서를 한 번의 클릭으로 내려받을 수 있습니다.
R Markdown 보고서 템플릿
먼저 보고서 틀을 만듭니다. 이 파일은 downloadHandler가 호출할 때 Shiny가 파라미터를 주입해 렌더링합니다.
# 새 파일: stat-report/report_template.Rmd
---
title: "`r params$title`"
date: "`r Sys.Date()`"
output: html_document
params:
title: "통계 분석 보고서"
data: NULL
var1: ""
var2: ""
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE)
library(tidyverse)
```
## 데이터 요약
```{r}
summary(params$data)
```
## 독립표본 t-검정
`r params$var1`과 `r params$var2`의 평균 차이를 검정합니다.
```{r}
t_result <- t.test(params$data[[params$var1]], params$data[[params$var2]])
print(t_result)
```
## 상관분석
```{r}
cor_result <- cor.test(params$data[[params$var1]], params$data[[params$var2]])
print(cor_result)
```
## 산점도
```{r, fig.width=7, fig.height=5}
ggplot(params$data, aes(x = .data[[params$var1]], y = .data[[params$var2]])) +
geom_point(alpha = 0.6, color = "#2c7bb6") +
geom_smooth(method = "lm", se = TRUE, color = "#d7191c") +
labs(
x = params$var1,
y = params$var2,
title = paste(params$var1, "vs", params$var2)
) +
theme_minimal()
```
앱 전체 코드
# 새 파일: stat-report/app.R
library(shiny)
library(bslib)
library(tidyverse)
library(plotly)
library(DT)
# ── UI ─────────────────────────────────────────────────────
ui <- page_sidebar(
title = "통계 분석 리포트",
theme = bs_theme(bootswatch = "lumen"),
sidebar = sidebar(
width = 280,
fileInput(
inputId = "upload",
label = "CSV 파일 업로드",
accept = ".csv",
placeholder = "파일을 선택하세요"
),
uiOutput("var1_ui"),
uiOutput("var2_ui"),
hr(),
textInput(
inputId = "report_title",
label = "보고서 제목",
value = "통계 분석 보고서"
),
selectInput(
inputId = "report_format",
label = "보고서 형식",
choices = c("HTML" = "html_document",
"Word" = "word_document"),
selected = "html_document"
),
downloadButton("download_report", "보고서 내려받기", class = "btn-primary w-100")
),
navset_card_tab(
nav_panel(
"데이터 미리보기",
DTOutput("data_preview")
),
nav_panel(
"기술 통계",
tableOutput("summary_table")
),
nav_panel(
"t-검정",
verbatimTextOutput("ttest_result"),
plotlyOutput("boxplot_chart", height = "350px")
),
nav_panel(
"상관분석",
verbatimTextOutput("cor_result"),
plotlyOutput("scatter_chart", height = "350px")
)
)
)
# ── Server ─────────────────────────────────────────────────
server <- function(input, output, session) {
# 업로드된 데이터 읽기
uploaded_data <- reactive({
req(input$upload)
tryCatch(
read.csv(input$upload$datapath, stringsAsFactors = FALSE),
error = function(e) {
showNotification(paste("파일 읽기 오류:", e$message), type = "error")
NULL
}
)
})
# 숫자형 변수만 추출
numeric_vars <- reactive({
req(uploaded_data())
names(Filter(is.numeric, uploaded_data()))
})
# 변수 선택 UI — 데이터 로드 후 동적 생성
output$var1_ui <- renderUI({
req(length(numeric_vars()) >= 2)
selectInput("var1", "첫 번째 변수", choices = numeric_vars(), selected = numeric_vars()[1])
})
output$var2_ui <- renderUI({
req(length(numeric_vars()) >= 2)
selectInput("var2", "두 번째 변수", choices = numeric_vars(), selected = numeric_vars()[2])
})
# ── 탭 출력 ────────────────────────────────────────────────
output$data_preview <- renderDT({
req(uploaded_data())
datatable(uploaded_data(), options = list(pageLength = 8, scrollX = TRUE), rownames = FALSE)
})
output$summary_table <- renderTable({
req(uploaded_data())
df <- uploaded_data() |> select(where(is.numeric))
data.frame(
변수 = names(df),
평균 = round(colMeans(df, na.rm = TRUE), 3),
표준편차 = round(apply(df, 2, sd, na.rm = TRUE), 3),
최솟값 = round(apply(df, 2, min, na.rm = TRUE), 3),
최댓값 = round(apply(df, 2, max, na.rm = TRUE), 3),
결측수 = colSums(is.na(df))
)
}, striped = TRUE, hover = TRUE, bordered = TRUE)
output$ttest_result <- renderPrint({
req(input$var1, input$var2, uploaded_data())
x <- uploaded_data()[[input$var1]]
y <- uploaded_data()[[input$var2]]
t.test(x, y)
})
output$boxplot_chart <- renderPlotly({
req(input$var1, input$var2, uploaded_data())
df_long <- uploaded_data() |>
select(all_of(c(input$var1, input$var2))) |>
pivot_longer(everything(), names_to = "변수", values_to = "값")
p <- df_long |>
ggplot(aes(x = 변수, y = 값, fill = 변수)) +
geom_boxplot(alpha = 0.7, outlier.shape = 21) +
labs(x = NULL, y = "값") +
theme_minimal() +
theme(legend.position = "none")
ggplotly(p)
})
output$cor_result <- renderPrint({
req(input$var1, input$var2, uploaded_data())
x <- uploaded_data()[[input$var1]]
y <- uploaded_data()[[input$var2]]
cor.test(x, y)
})
output$scatter_chart <- renderPlotly({
req(input$var1, input$var2, uploaded_data())
df <- uploaded_data()
p <- df |>
ggplot(aes(x = .data[[input$var1]], y = .data[[input$var2]])) +
geom_point(alpha = 0.6, color = "#2c7bb6") +
geom_smooth(method = "lm", se = TRUE, color = "#d7191c", fill = "#fdae61") +
labs(
x = input$var1,
y = input$var2,
title = paste("r =", round(cor(df[[input$var1]], df[[input$var2]], use = "complete.obs"), 3))
) +
theme_minimal()
ggplotly(p)
})
# ── 보고서 다운로드 ────────────────────────────────────────
output$download_report <- downloadHandler(
filename = function() {
ext <- if (input$report_format == "html_document") ".html" else ".docx"
paste0(input$report_title, "_", Sys.Date(), ext)
},
content = function(file) {
req(uploaded_data(), input$var1, input$var2)
# 임시 디렉토리에 템플릿 복사 (샌드박스 문제 방지)
tmp_rmd <- file.path(tempdir(), "report_template.Rmd")
file.copy("report_template.Rmd", tmp_rmd, overwrite = TRUE)
# R Markdown 렌더링
rmarkdown::render(
input = tmp_rmd,
output_file = file,
output_format = input$report_format,
params = list(
title = input$report_title,
data = uploaded_data(),
var1 = input$var1,
var2 = input$var2
),
envir = new.env(parent = globalenv())
)
}
)
}
shinyApp(ui, server)
downloadHandler 작동 원리
downloadHandler는 두 함수를 받습니다.
| 함수 | 역할 |
|---|---|
filename |
내려받을 파일명 반환 |
content(file) |
file 경로에 파일을 실제로 씀 |
content 안에서 rmarkdown::render()를 호출할 때 params 인자로 현재 앱 상태(데이터, 변수명, 제목)를 템플릿에 주입합니다. 템플릿은 params$data, params$var1처럼 값을 가져다 씁니다.
테스트용 샘플 데이터
앱을 바로 테스트하고 싶다면 R 콘솔에서 CSV 파일을 만듭니다.
set.seed(1)
df <- data.frame(
height = rnorm(100, mean = 170, sd = 8),
weight = rnorm(100, mean = 65, sd = 10),
age = sample(20:60, 100, replace = TRUE)
)
write.csv(df, "sample.csv", row.names = FALSE)
이 CSV를 앱에 업로드하면 height와 weight 중 두 개를 선택해 t-검정과 상관분석 결과를 확인하고, HTML 보고서를 내려받을 수 있습니다.