회귀분석

회귀분석
결정계수
모형요약
회귀분석 가정
회귀 계수
공개

2024-05-17

회귀분석은 독립 변수와 종속 변수 간의 관계를 모델링하는 통계 기법입니다. 기본적으로 선형 회귀분석에서는 독립 변수 \(x\)가 종속 변수 \(y\)에 미치는 영향을 설명하기 위해 직선 방정식을 사용합니다. 이 방정식은 \(y = \beta_0 + \beta_1 x + \epsilon\)의 형태를 가지며, \(\beta_0\)는 절편(intercept), \(\beta_1\)는 기울기(coefficient), \(\epsilon\)은 오차항(residual)입니다. 회귀분석을 통해 추정된 회귀계수 \(\beta_0\)\(\beta_1\)는 독립 변수가 한 단위 증가할 때 종속 변수의 변화량을 나타내며, 모형 적합도를 평가하기 위해 결정계수 \(R^2\)와 같은 지표를 사용합니다.

1 Shiny 앱

#| label: shinylive-linear-reg
#| viewerWidth: 800
#| viewerHeight: 700
#| standalone: true

library(shiny)
library(ggplot2)
library(plotly)
library(DT)
library(broom)
library(showtext)
showtext_auto()

# UI 정의
ui <- shiny::tagList(
  withMathJax(),
  titlePanel(
    title = "선형 회귀분석",
    windowTitle = "단순 선형 회귀분석"
  ),
  fluidPage(
    theme = shinythemes::shinytheme("flatly"),
    sidebarLayout(
      sidebarPanel(
        radioButtons("data_type", "데이터 입력 방식:",
                     c("직접 입력" = "manual",
                       "CSV 업로드" = "csv"),
                     selected = "manual"),
        conditionalPanel(
          condition = "input.data_type == 'manual'",
          tags$b("데이터:"),
          textInput("x", "x", value = "90, 100, 90, 80, 87, 75, 85, 95, 78, 88", placeholder = "쉼표로 구분된 값을 입력하세요. 소수점은 점(.)을 사용하세요. 예: 4.2, 4.4, 5, 5.03 등"),
          textInput("y", "y", value = "950, 1000, 850, 750, 950, 775, 800, 970, 770, 920", placeholder = "쉼표로 구분된 값을 입력하세요. 소수점은 점(.)을 사용하세요. 예: 4.2, 4.4, 5, 5.03 등")
        ),
        conditionalPanel(
          condition = "input.data_type == 'csv'",
          fileInput("file", "CSV 파일 업로드", accept = c("text/csv", "text/comma-separated-values,text/plain", ".csv")),
          selectInput("x_var", "X 변수 선택:", ""),
          selectInput("y_var", "Y 변수 선택:", "")
        ),
        hr(),
        tags$b("그래프:"),
        checkboxInput("se", "회귀선 주변에 신뢰구간 추가", TRUE),
        textInput("xlab", label = "축 레이블:", value = "x", placeholder = "x 레이블"),
        textInput("ylab", label = NULL, value = "y", placeholder = "y 레이블"),
        hr(),
        tags$div(
          tags$span(property = "cc:attributionName", "RShiny@UCLouvain"), " 코드를 참조하여 개발되었습니다.",
          tags$a(href = "http://creativecommons.org/licenses/by/2.0/be/", target = "_blank", "Creative Commons Attribution 2.0 Belgium license"),
          tags$img(alt = "Licence Creative Commons", style = "border-width:0", src = "http://i.creativecommons.org/l/by/2.0/be/80x15.png")
        ),
      ),
      mainPanel(
        tabsetPanel(
          tabPanel("데이터",
                   br(),
                   DT::dataTableOutput("tbl"),
                   br(),
                   uiOutput("data")
          ),
          tabPanel("손으로 계산",
                   br(),
                   uiOutput("by_hand")
          ),
          tabPanel("R 계산 및 해석",
                   br(),
                   h4("회귀 모형 요약"),
                   verbatimTextOutput("summary"),
                   br(),
                   h4("모형 요약"),
                   DT::dataTableOutput("model_table"),
                   h4("회귀 계수"),
                   DT::dataTableOutput("coef_table"),
                   h4("해석"),
                   uiOutput("interpretation")
          ),
          tabPanel("회귀 그래프",
                   br(),
                   uiOutput("results"),
                   plotlyOutput("plot")
          ),
          tabPanel("가정",
                   br(),
                   plotOutput("assumptions")
          )
        )
      )
    )
  )
)

# 서버 로직 정의
server <- function(input, output, session) {
  data <- reactive({
    if (input$data_type == "manual") {
      x <- extract(input$x)
      y <- extract(input$y)
      data.frame(x, y)
    } else {
      req(input$file)
      read.csv(input$file$datapath, header = TRUE)
    }
  })

  observe({
    if (input$data_type == "csv") {
      updateSelectInput(session, "x_var", choices = names(data()))
      updateSelectInput(session, "y_var", choices = names(data()))
    }
  })

  extract <- function(text) {
    text <- gsub(" ", "", text)
    split <- strsplit(text, ",", fixed = FALSE)[[1]]
    as.numeric(split)
  }

  # 데이터 출력
  output$tbl <- DT::renderDataTable({
    DT::datatable(data(),
                  extensions = "Buttons",
                  options = list(
                    lengthChange = FALSE,
                    dom = "Blfrtip",
                    buttons = c("copy", "csv", "excel", "pdf", "print")
                  )
    )
  })

  output$data <- renderUI({
    req(data())
    x <- data()[[ifelse(input$data_type == "manual", "x", input$x_var)]]
    y <- data()[[ifelse(input$data_type == "manual", "y", input$y_var)]]
    if (anyNA(x) | length(x) < 2 | anyNA(y) | length(y) < 2) {
      "잘못된 입력이거나 관측치가 충분하지 않습니다."
    } else if (length(x) != length(y)) {
      "x와 y의 관측치 수는 동일해야 합니다."
    } else {
      withMathJax(
        paste0("\\(\\bar{x} =\\) ", round(mean(x), 3)),
        br(),
        paste0("\\(\\bar{y} =\\) ", round(mean(y), 3)),
        br(),
        paste0("\\(n =\\) ", length(x))
      )
    }
  })

  output$by_hand <- renderUI({
    req(data())
    x <- data()[[ifelse(input$data_type == "manual", "x", input$x_var)]]
    y <- data()[[ifelse(input$data_type == "manual", "y", input$y_var)]]
    fit <- lm(y ~ x)
    withMathJax(
      paste0("\\(\\hat{\\beta}_1 = \\dfrac{\\big(\\sum^n_{i = 1} x_i y_i \\big) - n \\bar{x} \\bar{y}}{\\sum^n_{i = 1} (x_i - \\bar{x})^2} = \\) ", round(fit$coef[[2]], 3)),
      br(),
      paste0("\\(\\hat{\\beta}_0 = \\bar{y} - \\hat{\\beta}_1 \\bar{x} = \\) ", round(fit$coef[[1]], 3)),
      br(),
      br(),
      paste0("\\( \\Rightarrow y = \\hat{\\beta}_0 + \\hat{\\beta}_1 x = \\) ", round(fit$coef[[1]], 3), " + ", round(fit$coef[[2]], 3), "\\( x \\)")
    )
  })

  output$summary <- renderPrint({
    req(data())
    x <- data()[[ifelse(input$data_type == "manual", "x", input$x_var)]]
    y <- data()[[ifelse(input$data_type == "manual", "y", input$y_var)]]
    fit <- lm(y ~ x)
    summary(fit)
  })

  output$model_table <- DT::renderDataTable({
    req(data())
    x <- data()[[ifelse(input$data_type == "manual", "x", input$x_var)]]
    y <- data()[[ifelse(input$data_type == "manual", "y", input$y_var)]]
    fit <- lm(y ~ x)
    ## 모형요약
    model_df <- broom::glance(fit) |>
      mutate(across(where(is.numeric), ~ round(.x, digits = 1)))
    DT::datatable(model_df,
                  options = list(
                    lengthChange = FALSE,
                    dom = "t",
                    scrollX = TRUE
                  ),
                  rownames = FALSE)
  })

  output$coef_table <- DT::renderDataTable({
    req(data())
    x <- data()[[ifelse(input$data_type == "manual", "x", input$x_var)]]
    y <- data()[[ifelse(input$data_type == "manual", "y", input$y_var)]]
    fit <- lm(y ~ x)
    ## 계수
    coef_df <- broom::tidy(fit, conf.int = TRUE) |>
      mutate(across(where(is.numeric), ~ round(.x, digits = 1)))
    names(coef_df) <- c("항", "추정치", "표준오차", "t 값", "P-값", "2.5% 신뢰구간", "97.5% 신뢰구간")
    DT::datatable(coef_df,
                  options = list(
                    lengthChange = FALSE,
                    dom = "t",
                    scrollX = TRUE
                  ),
                  rownames = FALSE)
  })

  output$results <- renderUI({
    req(data())
    x <- data()[[ifelse(input$data_type == "manual", "x", input$x_var)]]
    y <- data()[[ifelse(input$data_type == "manual", "y", input$y_var)]]
    fit <- lm(y ~ x)
    withMathJax(
      paste0(
        "수정된 \\( R^2 = \\) ", round(summary(fit)$adj.r.squared, 3),
        ", \\( \\beta_0 = \\) ", round(fit$coef[[1]], 3),
        ", \\( \\beta_1 = \\) ", round(fit$coef[[2]], 3),
        ", P-값 ", "\\( = \\) ", signif(summary(fit)$coef[2, 4], 3)
      )
    )
  })

  output$interpretation <- renderUI({
    req(data())
    x <- data()[[ifelse(input$data_type == "manual", "x", input$x_var)]]
    y <- data()[[ifelse(input$data_type == "manual", "y", input$y_var)]]
    fit <- lm(y ~ x)
    if (summary(fit)$coefficients[1, 4] < 0.05 & summary(fit)$coefficients[2, 4] < 0.05) {
      withMathJax(
        paste0("해석: (계수를 해석하기 전에 선형 회귀분석의 가정(독립성, 선형성, 동분산성, 이상치 및 정규성)이 충족되는지 확인하세요.)"),
        br(),
        br(),
        paste0(input$xlab, "의 (가상의) 값이 0일 때, ", input$ylab, "의 평균은 ", round(fit$coef[[1]], 3), "입니다."),
        br(),
        br(),
        paste0(input$xlab, "이 한 단위 증가할 때, ", input$ylab, ifelse(round(fit$coef[[2]], 3) >= 0, "은 (평균적으로) ", "은 (평균적으로) "), abs(round(fit$coef[[2]], 3)), ifelse(round(fit$coef[[2]], 3) >= 0, " 단위 증가합니다.", " 단위 감소합니다.")),
        br(),
        br(),
        paste0("회귀모형의 적합도(수정된 \\(R^2\\))는 ", round(summary(fit)$adj.r.squared, 3), "으로, ", input$xlab, "에 의해 ", input$ylab, "의 변동이 ", round(summary(fit)$adj.r.squared * 100, 1), "%만큼 설명됩니다.")
      )
    } else if (summary(fit)$coefficients[1, 4] < 0.05 & summary(fit)$coefficients[2, 4] >= 0.05) {
      withMathJax(
        paste0("해석: (계수를 해석하기 전에 선형 회귀분석의 가정(독립성, 선형성, 동분산성, 이상치 및 정규성)이 충족되는지 확인하세요.)"),
        br(),
        br(),
        paste0(input$xlab, "의 (가상의) 값이 0일 때, ", input$ylab, "의 평균은 ", round(fit$coef[[1]], 3), "입니다."),
        br(),
        br(),
        paste0("\\( \\beta_1 \\)", "은 0과 유의미하게 다르지 않습니다 (p-값 = ", round(summary(fit)$coefficients[2, 4], 3), "). 따라서 ", input$xlab, "과 ", input$ylab, " 사이에 유의미한 관계가 없습니다.")
      )
    } else if (summary(fit)$coefficients[1, 4] >= 0.05 & summary(fit)$coefficients[2, 4] < 0.05) {
      withMathJax(
        paste0("해석: (계수를 해석하기 전에 선형 회귀분석의 가정(독립성, 선형성, 동분산성, 이상치 및 정규성)이 충족되는지 확인하세요.)"),
        br(),
        br(),
        paste0("\\( \\beta_0 \\)", "은 0과 유의미하게 다르지 않습니다 (p-값 = ", round(summary(fit)$coefficients[1, 4], 3), "). 따라서 ", input$xlab, "이 0일 때, ", input$ylab, "의 평균은 0과 유의미하게 다르지 않습니다."),
        br(),
        br(),
        paste0(input$xlab, "이 한 단위 증가할 때, ", input$ylab, ifelse(round(fit$coef[[2]], 3) >= 0, "은 (평균적으로) ", "은 (평균적으로) "), abs(round(fit$coef[[2]], 3)), ifelse(round(fit$coef[[2]], 3) >= 0, " 단위 증가합니다.", " 단위 감소합니다.")),
        br(),
        br(),
        paste0("회귀모형의 적합도(수정된 \\(R^2\\))는 ", round(summary(fit)$adj.r.squared, 3), "으로, ", input$xlab, "에 의해 ", input$ylab, "의 변동이 ", round(summary(fit)$adj.r.squared * 100, 1), "%만큼 설명됩니다.")
      )
    } else {
      withMathJax(
        paste0("해석: (계수를 해석하기 전에 선형 회귀분석의 가정(독립성, 선형성, 동분산성, 이상치 및 정규성)이 충족되는지 확인하세요.)"),
        br(),
        br(),
        paste0("\\( \\beta_0 \\)", "과 ", "\\( \\beta_1 \\)", "은 0과 유의미하게 다르지 않습니다 (p-값 = ", round(summary(fit)$coefficients[1, 4], 3), "와 ", round(summary(fit)$coefficients[2, 4], 3), "). 따라서 ", input$ylab, "의 평균은 0과 유의미하게 다르지 않습니다."),
        br(),
        br(),
        paste0("회귀모형의 적합도(수정된 \\(R^2\\))는 ", round(summary(fit)$adj.r.squared, 3), "으로, ", input$xlab, "과 ", input$ylab, " 사이에 유의미한 관계가 없습니다.")
      )
    }
  })

  output$assumptions <- renderPlot({
    req(data())
    x <- data()[[ifelse(input$data_type == "manual", "x", input$x_var)]]
    y <- data()[[ifelse(input$data_type == "manual", "y", input$y_var)]]
    fit <- lm(y ~ x)
    par(mfrow = c(2, 2))
    plot(fit, which = c(1:3, 5))
  })

  output$plot <- renderPlotly({
    req(data())
    x <- data()[[ifelse(input$data_type == "manual", "x", input$x_var)]]
    y <- data()[[ifelse(input$data_type == "manual", "y", input$y_var)]]
    fit <- lm(y ~ x)
    dat <- data.frame(x, y)
    p <- ggplot(dat, aes(x = x, y = y)) +
      geom_point() +
      stat_smooth(method = "lm", se = input$se) +
      ylab(input$ylab) +
      xlab(input$xlab) +
      theme_minimal()
    ggplotly(p)
  })

}

# 애플리케이션 실행
shinyApp(ui = ui, server = server)

2 코드

라이센스

CC BY-SA-NC & GPL-3