iBetter Books
수정

통계 분석 리포트 앱

데이터를 올리면 자동으로 통계 분석을 수행하고, 결과를 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를 앱에 업로드하면 heightweight 중 두 개를 선택해 t-검정과 상관분석 결과를 확인하고, HTML 보고서를 내려받을 수 있습니다.

Ch 03. 통계 분석 리포트 앱 — 실전 R과 Shiny | iBetter Books