library(shiny) library(shinydashboard) library(ggplot2) library(dplyr) library(readr) library(tidyr) library(cluster) library(randomForest) library(DT) # データ読み込み csv_path <- "FullGamePitchData.csv" df <- read_csv(csv_path, locale = locale(encoding = "UTF-8")) %>% mutate( 球速 = as.numeric(gsub("km/h", "", 球速)), 座標top = as.numeric(座標top), 座標left = as.numeric(座標left) ) ui <- dashboardPage( dashboardHeader(title = "NPB 投球分析"), dashboardSidebar( sidebarMenu( menuItem("分析結果", tabName = "analysis", icon = icon("chart-area")), menuItem("データベース", tabName = "data", icon = icon("table")), radioButtons("role", "分析対象:", choices = c("投手", "打者")), uiOutput("name_selector"), actionButton("go", "分析実行"), br(), selectInput("filter_date", "日付で絞り込み:", choices = c("すべて", unique(df$日付))), selectInput("filter_pitcher", "投手で絞り込み:", choices = c("すべて", unique(df$投手_名前))), selectInput("filter_batter", "打者で絞り込み:", choices = c("すべて", unique(df$打者_名前))) ) ), dashboardBody( tabItems( tabItem(tabName = "analysis", fluidRow( box(title = "ヒートマップ", width = 6, plotOutput("heatmap")), box(title = "球種分布", width = 6, tableOutput("pitch_types")) ), fluidRow( box(title = "クラスタリング", width = 6, plotOutput("cluster_plot")), box(title = "次の球種予測", width = 6, textOutput("next_pitch")) ) ), tabItem(tabName = "data", fluidRow( box(title = "全投球データ", width = 12, DTOutput("full_data")) ) ) ) ) ) server <- function(input, output, session) { output$name_selector <- renderUI({ choices <- if (input$role == "投手") unique(na.omit(df$投手_名前)) else unique(na.omit(df$打者_名前)) selectInput("name", paste0(input$role, "名を選択:"), choices = choices) }) filtered_data <- eventReactive(input$go, { col <- if (input$role == "投手") "投手_名前" else "打者_名前" df %>% filter(!!sym(col) == input$name) }) output$heatmap <- renderPlot({ d <- filtered_data() if (nrow(d) < 1) return(plot.new()) ggplot(d, aes(x = 座標left, y = 座標top)) + stat_density2d(aes(fill = ..level..), geom = "polygon") + scale_fill_viridis_c() + ggtitle(paste(input$name, "のヒートマップ")) + scale_y_reverse() + theme_minimal() }) output$pitch_types <- renderTable({ d <- filtered_data() if (nrow(d) < 1) return(NULL) d %>% count(球種, name = "投球数") }) output$cluster_plot <- renderPlot({ d <- filtered_data() %>% drop_na(球速, 座標top, 座標left) if (nrow(d) < 3) return(plot.new()) km <- kmeans(d[, c("球速", "座標top", "座標left")], centers = 3) d$cluster <- factor(km$cluster) ggplot(d, aes(x = 座標left, y = 座標top, color = cluster)) + geom_point(size = 3) + scale_y_reverse() + ggtitle(paste(input$name, "のクラスタリング")) + theme_minimal() }) output$next_pitch <- renderText({ d <- filtered_data() %>% drop_na(球速, 座標top, 座標left, 球種) if (nrow(d) < 5) return("データ不足") d$球種 <- as.factor(d$球種) model <- randomForest(球種 ~ 球速 + 座標top + 座標left, data = d) newdata <- d[nrow(d), c("球速", "座標top", "座標left")] pred <- predict(model, newdata) paste("次の球種予測:", as.character(pred)) }) filtered_table_data <- reactive({ d <- df if (input$filter_date != "すべて") d <- d[d$日付 == input$filter_date, ] if (input$filter_pitcher != "すべて") d <- d[d$投手_名前 == input$filter_pitcher, ] if (input$filter_batter != "すべて") d <- d[d$打者_名前 == input$filter_batter, ] d }) output$full_data <- renderDT({ datatable(filtered_table_data(), options = list(pageLength = 25, scrollX = TRUE)) }) } shinyApp(ui, server)