연속형 분포 적합

news
저자

이광춘

공개

2026-07-06

사용자가 선택한 데이터에 대해 연속확률분포의 적합도를 분석하고 시각화할 수 있는 도구입니다. 이 애플리케이션을 통해 통계학적 분석을 수행하고 결과를 실시간으로 탐색할 수 있습니다.

  1. Shiny 앱: 데이터가 어떤 이론적 분포를 따르는지 확인하기 위해 히스토그램과 이론적 확률밀도함수를 비교합니다. Q-Q 플롯을 통해 데이터의 분위수가 이론적 분위수와 얼마나 일치하는지 시각적으로 평가합니다. 또한, 데이터의 누적분포함수와 이론적 누적분포함수를 비교하여 데이터의 적합도를 평가합니다. 균일분포는 Kolmogorov-Smirnov 검정을 통해, 정규분포는 Shapiro-Wilk 검정을 통해, 지수분포는 Kolmogorov-Smirnov 검정을 통해 데이터의 적합도를 평가하고 검정 결과의 p-값 통해 분포의 적합성 여부를 결정합니다.

  2. 코딩:

#| label: shinylive-continous-distribution-fitting
#| viewerHeight: 700
#| standalone: true

library(ggplot2)
library(moments)
library(palmerpenguins)

library(bslib)
library(showtext)
tryCatch(sysfonts::font_add_google("Nanum Myeongjo", "kr"), error = function(e) NULL)
showtext_auto()

# ── Tufte 팔레트 · 테마 (자동 주입) ──────────────────────────
bit_cream <- "#fffff8"; bit_ink <- "#111111"; bit_rust <- "#8a1500"
bit_muted <- "#4a4a44"; bit_grid <- "#e8e6dc"; bit_axis <- "#8a8578"
theme_tufte_bit <- function(base_size = 13) {
  ggplot2::theme_minimal(base_size = base_size) +
    ggplot2::theme(
      text = ggplot2::element_text(family = "kr", colour = bit_ink),
      plot.title = ggplot2::element_text(family = "kr", size = base_size, hjust = 0),
      plot.background = ggplot2::element_rect(fill = bit_cream, colour = NA),
      panel.background = ggplot2::element_rect(fill = bit_cream, colour = NA),
      panel.grid.major = ggplot2::element_line(colour = bit_grid, linewidth = 0.3),
      panel.grid.minor = ggplot2::element_blank(),
      axis.line = ggplot2::element_line(colour = bit_axis, linewidth = 0.3),
      axis.ticks = ggplot2::element_line(colour = bit_axis, linewidth = 0.3)
    )
}
if (requireNamespace("ggplot2", quietly = TRUE)) ggplot2::theme_set(theme_tufte_bit())
bit_theme <- bs_theme(
  version = 5, bg = bit_cream, fg = bit_ink, primary = bit_rust,
  base_font = font_collection("Palatino Linotype", "Georgia", "Times New Roman", "serif"),
  "card-box-shadow" = "none"
)
# ─────────────────────────────────────────────────────────────

ui <- fluidPage(
  theme = bit_theme,
  titlePanel("연속형 분포 적합"),
  sidebarLayout(
    sidebarPanel(
      radioButtons("data_source", "데이터 출처 선택",
                   choices = c("CSV 업로드" = "Upload CSV", "penguins", "mtcars"),
                   inline = TRUE),
      conditionalPanel(
        condition = "input.data_source == 'Upload CSV'",
        fileInput("file", "CSV 파일 선택",
                  accept = c("text/csv", "text/comma-separated-values,text/plain", ".csv"))
      ),
      selectInput("variable", "변수 선택", choices = NULL),
      radioButtons("distribution", "분포 선택",
                   choices = c("균등" = "Uniform", "정규" = "Normal", "지수" = "Exponential"))
    ),
    mainPanel(
      tabsetPanel(
        tabPanel("요약", verbatimTextOutput("summary")),
        tabPanel("히스토그램", plotOutput("histogram")),
        tabPanel("Q-Q 그래프", plotOutput("qqplot")),
        tabPanel("CDF 그래프", plotOutput("cdfplot"))
      ),
      h4("사용 방법:"),
      tags$ol(
        tags$li("데이터 출처를 선택합니다(CSV 업로드, penguins, mtcars)."),
        tags$li("'CSV 업로드'를 선택하면 업로드할 CSV 파일을 고릅니다."),
        tags$li("드롭다운에서 연속형 변수를 선택합니다."),
        tags$li("적합할 분포를 고릅니다(균등, 정규, 지수)."),
        tags$li("선택에 따라 적합 결과가 자동으로 갱신됩니다."),
        tags$li("각 탭에서 요약통계·히스토그램·Q-Q 그래프·CDF 그래프를 살펴봅니다.")
      )
    )
  )
)

server <- function(input, output, session) {
  data <- reactive({
    if (input$data_source == "Upload CSV") {
      req(input$file)
      read.csv(input$file$datapath)
    } else if (input$data_source == "penguins") {
      penguins[, c("bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g")]
    } else if (input$data_source == "mtcars") {
      mtcars[, c("mpg", "disp", "hp", "drat", "wt", "qsec")]
    }
  })

  observe({
    req(data())
    updateSelectInput(session, "variable", choices = names(data()))
  })

  variable <- reactive({
    req(input$variable)
    data()[[input$variable]]
  })

  fit_result <- reactive({
    req(input$distribution, variable())
    distribution <- tolower(input$distribution)

    if (distribution == "uniform") {
      min_val <- min(variable(), na.rm = TRUE)
      max_val <- max(variable(), na.rm = TRUE)
      test_result <- ks.test(variable(), "punif", min = min_val, max = max_val)
    } else if (distribution == "normal") {
      test_result <- shapiro.test(variable())
    } else if (distribution == "exponential") {
      rate_val <- 1/mean(variable(), na.rm = TRUE)
      test_result <- ks.test(variable(), "pexp", rate = rate_val)
    }

    list(
      distribution = distribution,
      test_result = test_result,
      params = switch(distribution,
                      "uniform" = list(min = min_val, max = max_val),
                      "normal" = list(mean = mean(variable(), na.rm = TRUE), sd = sd(variable(), na.rm = TRUE)),
                      "exponential" = list(rate = rate_val))
    )
  })

  output$summary <- renderPrint({
    req(fit_result())
    cat("선택한 분포:", fit_result()$distribution, "\n\n")
    cat("기술통계:\n")
    print(summary(variable()))
    cat("\n왜도:", skewness(variable()), "\n")
    cat("첨도:", kurtosis(variable()), "\n\n")

    if (fit_result()$distribution == "uniform") {
      cat("균등분포에 대한 Kolmogorov-Smirnov 검정\n")
    } else if (fit_result()$distribution == "normal") {
      cat("Shapiro-Wilk 정규성 검정\n")
    } else if (fit_result()$distribution == "exponential") {
      cat("지수분포에 대한 Kolmogorov-Smirnov 검정\n")
    }

    cat("p-값:", fit_result()$test_result$p.value, "\n")
  })

  output$histogram <- renderPlot({
    req(fit_result())
    distribution <- fit_result()$distribution
    params <- fit_result()$params
    ggplot(data.frame(x = variable()), aes(x)) +
      geom_histogram(aes(y = ..density..), bins = 30, color = "black", fill = "#c9c6b8") +
      stat_function(fun = switch(distribution,
                                 "uniform" = dunif,
                                 "normal" = dnorm,
                                 "exponential" = dexp),
                    args = params,
                    color = "#8a1500", size = 1) +
      labs(title = "적합된 분포와 히스토그램",
           x = input, y = "밀도")
  })

  output$qqplot <- renderPlot({
    req(fit_result())
    distribution <- fit_result()$distribution
    params <- fit_result()$params
    ggplot(data.frame(x = variable()), aes(sample = x)) +
      stat_qq(distribution = switch(distribution,
                                    "uniform" = qunif,
                                    "normal" = qnorm,
                                    "exponential" = qexp),
              dparams = unlist(params),
              color = "#4a4a44") +
      stat_qq_line(distribution = switch(distribution,
                                         "uniform" = qunif,
                                         "normal" = qnorm,
                                         "exponential" = qexp),
                   dparams = unlist(params),
                   color = "#8a1500") +
      labs(title = "Q-Q 그래프",
           x = "이론적 분위수", y = "표본 분위수")
  })

  output$cdfplot <- renderPlot({
    req(fit_result())
    distribution <- fit_result()$distribution
    params <- fit_result()$params
    ggplot(data.frame(x = variable()), aes(x)) +
      stat_ecdf(geom = "step", color = "#4a4a44", size = 1) +
      stat_function(fun = switch(distribution,
                                 "uniform" = punif,
                                 "normal" = pnorm,
                                 "exponential" = pexp),
                    args = unlist(params),
                    color = "#8a1500", size = 1) +
      labs(title = "경험적·이론적 CDF",
           x = input$variable, y = "Cumulative Probability")
  })


}

shinyApp(ui, server)

라이센스