군집분석

다변량 분석
군집분석
Clustering
K-평균
PCA
공개

2026-07-06

군집분석(Clustering Analysis)은 데이터를 유사한 그룹으로 나누는 비지도 학습 기법입니다. 데이터의 숨겨진 구조를 발견하고, 각 군집 내의 데이터 관측점들이 최대한 유사하도록 합니다. 대표적인 기법으로는 k-평균(K-means) 클러스터링이 있으며, 사전에 정해진 k개의 중심을 기준으로 데이터를 반복적으로 군집화합니다.

#| label: shinylive-lln-coin
#| viewerHeight: 700
#| standalone: true

library(shiny)
library(ggplot2)
library(dplyr)
library(DT)
library(RColorBrewer)
library(showtext)
showtext_auto()

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('K-평균 군집분석과 PCA 시각화'),

  sidebarLayout(
    sidebarPanel(
      radioButtons('dataset', '데이터셋 선택',
                   choices = c('USArrests', 'iris', '업로드된 데이터셋')),
      conditionalPanel(
        condition = "input.dataset == '업로드된 데이터셋'",
        fileInput('file1', 'CSV 파일 선택',
                  accept = c('text/csv',
                             'text/comma-separated-values,text/plain',
                             '.csv'))
      ),
      uiOutput("varselect_ui"),
      sliderInput('clusters', '군집 개수', 3, min = 1, max = 10)
    ),

    mainPanel(
      tabsetPanel(
        tabPanel("그래프", plotOutput('plot')),
        tabPanel("군집별 관측점수", tableOutput('table1')),
        tabPanel("군집별 요약통계", tableOutput('table2'))
      )
    )
  )
)

server <- function(input, output, session) {

  dataInput <- reactive({
    switch(input$dataset,
           'iris' = iris,
           'USArrests' = USArrests,
           '업로드된 데이터셋' = {
             req(input$file1)
             read.csv(input$file1$datapath)
           })
  })

  observe({
    req(dataInput())
    num_vars <- names(dataInput())[sapply(dataInput(), is.numeric)]
    updateSelectInput(session, 'xcol', choices = num_vars)
    updateSelectInput(session, 'ycol', choices = num_vars, selected = num_vars[2])
  })

  output$varselect_ui <- renderUI({
    req(dataInput())
    num_vars <- names(dataInput())[sapply(dataInput(), is.numeric)]
    list(
      selectInput('xcol', 'X 변수', num_vars),
      selectInput('ycol', 'Y 변수', num_vars, selected = num_vars[2])
    )
  })

  selectedData <- reactive({
    req(input$xcol, input$ycol)
    dataInput()[, c(input$xcol, input$ycol)]
  })

  pca <- reactive({
    req(selectedData())
    prcomp(selectedData(), scale. = TRUE)
  })

  cluster_results <- reactive({
    req(pca())
    kmeans(pca()$x[, 1:2], input$clusters)
  })

  output$plot <- renderPlot({
    req(cluster_results(), pca())

    df <- data.frame(pca()$x[, 1:2])

    ggplot(df, aes(x = PC1, y = PC2, color = factor(cluster_results()$cluster))) +
      geom_point(size = 3, show.legend = TRUE) +
      geom_text(data = data.frame(cluster_results()$centers),
                aes(label = "X"), size = 8, color = "black") +
      scale_color_brewer(type = "qual", palette = "Set2") +
      scale_shape_manual("클러스터", values = c(1:input$clusters)) +
      theme_tufte_bit() +
      labs(title = paste("K =", input$clusters, "인 K-평균 군집분석"),
           color = "군집")
  })

  output$table1 <- renderTable({
    req(cluster_results(), dataInput())
    df_res <- dataInput()
    df_res$cluster <- cluster_results()$cluster
    df_res %>%
      group_by(cluster) %>%
      tally(name = "개수") # 각 클러스터의 샘플 수 계산
  })

  output$table2 <- renderTable({
    req(cluster_results(), dataInput())
    df_res <- dataInput()
    df_res$cluster <- cluster_results()$cluster
    df_res %>% group_by(cluster) %>%
      summarise(관측점수 = n(),
                X_평균 = mean(get(input$xcol)),
                Y_평균 = mean(get(input$ycol)))
  })
}

shinyApp(ui = ui, server = server)

라이센스