library(shiny) library(ggplot2) library(dplyr) library(tidyr) library(corrplot) library(plotly) library(gridExtra) ui <- fluidPage( # タイトルと概要 titlePanel("経営指標分析ダッシュボード"), # スタイルの設定 tags$head( tags$style(HTML(" body { font-family: 'Arial', sans-serif; } .nav-tabs > li > a { color: #333; } .tab-pane { padding: 20px; } h3 { color: #2c3e50; margin-top: 20px; } .well { background-color: #ecf0f1; } ")) ), # タブの構成 tabsetPanel( # タブ1: 相関分析 tabPanel("相関分析", fluidRow( column(12, h3("指標間の相関係数(ヒートマップ)"), p("各ビジネス部門の売上、原価、粗利、営業利益、KPIなど全指標間の相関を表示します。") ) ), fluidRow( column(8, plotOutput("correlation_heatmap", height = "600px")), column(4, wellPanel( h4("相関分析の概要"), verbatimTextOutput("correlation_summary") ) ) ) ), # タブ2: 回帰分析 tabPanel("回帰分析", fluidRow( column(12, h3("KPIと売上・利益の関係分析")) ), fluidRow( column(6, wellPanel( h4("回帰分析の設定"), selectInput("x_var", "説明変数(X軸)", c("整備_車検件数" = "x25", "整備_整備・板金数" = "x31", "運送_自社ドライバー数" = "x67", "運送_他社ドライバー数" = "x73", "レンタル_レンタル台数" = "x91")), selectInput("y_var", "被説明変数(Y軸)", c("整備_売上" = "x2", "整備_営業利益" = "x9", "商品_売上" = "x37", "商品_営業利益" = "x41", "運送_売上" = "x52", "運送_営業利益" = "x56", "レンタル_売上" = "x76", "レンタル_営業利益" = "x80")), actionButton("update_regression", "更新", class = "btn-primary") ) ) ), fluidRow( column(6, plotlyOutput("regression_scatter")), column(6, wellPanel( h4("回帰分析結果"), verbatimTextOutput("regression_results") ) ) ) ), # タブ3: 時系列分析 tabPanel("時系列分析", fluidRow( column(12, h3("各部門の営業利益推移")) ), fluidRow( column(12, plotlyOutput("timeseries_profit", height = "500px")) ), fluidRow( column(12, h3("売上高の推移")) ), fluidRow( column(12, plotlyOutput("timeseries_sales", height = "500px")) ), fluidRow( column(12, h3("KPI推移")) ), fluidRow( column(12, plotlyOutput("timeseries_kpi", height = "500px")) ) ), # タブ4: 統計サマリー tabPanel("統計サマリー", fluidRow( column(12, h3("基本統計量")) ), fluidRow( column(6, wellPanel( h4("整備部門"), verbatimTextOutput("summary_seisetsubi") ) ), column(6, wellPanel( h4("商品部門"), verbatimTextOutput("summary_shohin") ) ) ), fluidRow( column(6, wellPanel( h4("運送部門"), verbatimTextOutput("summary_unsou") ) ), column(6, wellPanel( h4("レンタル部門"), verbatimTextOutput("summary_rental") ) ) ) ), # タブ5: 詳細な散布図分析 tabPanel("詳細分析", fluidRow( column(12, h3("主要指標の散布図行列")) ), fluidRow( column(12, wellPanel( h4("分析対象指標の選択(複数選択可)"), checkboxGroupInput("scatter_vars", "変数を選択", c("整備_売上" = "x2", "整備_粗利" = "x7", "整備_営業利益" = "x9", "商品_売上" = "x37", "商品_粗利" = "x39", "商品_営業利益" = "x41", "運送_売上" = "x52", "運送_粗利" = "x54", "運送_営業利益" = "x56", "レンタル_売上" = "x76", "レンタル_粗利" = "x78", "レンタル_営業利益" = "x80"), selected = c("x2", "x9", "x37", "x52")) ) ) ), fluidRow( column(12, plotOutput("scatter_matrix", height = "800px")) ) ) ) ) server <- function(input, output, session) { # データの読み込み df <- reactive({ # ファイルパスを設定(Docker対応) possible_paths <- c( "DummyData.csv", # Docker環境 "SampleData.csv", # ローカル環境 file.path(getwd(), "DummyData.csv"), file.path(getwd(), "SampleData.csv") ) for (path in possible_paths) { if (file.exists(path)) { # CSVを読み込み data <- read.csv(path, row.names = 1, check.names = FALSE) # 年月を列として追加 data <- data %>% mutate(年月 = rownames(.)) # すべての数値列を明示的に数値型に変換 numeric_cols <- colnames(data)[colnames(data) != "年月"] for (col in numeric_cols) { data[[col]] <- as.numeric(as.character(data[[col]])) } return(data) } } # ファイルが見つからない場合は警告 showNotification("DummyData.csv または SampleData.csv が見つかりません", type = "error") return(NULL) }) # 列名のマッピング(全28列) get_column_label <- function(col_name) { labels <- list( "x2" = "整備_売上", "x3" = "整備_一般車検", "x4" = "整備_整備・板金", "x5" = "整備_その他", "x6" = "整備_原価", "x7" = "整備_粗利", "x8" = "整備_販管費", "x9" = "整備_営業利益", "x25" = "整備KPI_車検件数", "x31" = "整備KPI_整備・板金数", "x37" = "商品_売上", "x38" = "商品_原価", "x39" = "商品_粗利", "x40" = "商品_販管費", "x41" = "商品_営業利益", "x52" = "運送_売上", "x53" = "運送_原価", "x54" = "運送_粗利", "x55" = "運送_販管費", "x56" = "運送_営業利益", "x67" = "運送KPI_自社ドライバー数", "x73" = "運送KPI_他社ドライバー数", "x76" = "レンタル_売上", "x77" = "レンタル_原価", "x78" = "レンタル_粗利", "x79" = "レンタル_販管費", "x80" = "レンタル_営業利益", "x91" = "レンタルKPI_レンタル台数" ) return(labels[[col_name]] %||% col_name) } # タブ1: 相関ヒートマップ output$correlation_heatmap <- renderPlot({ if (is.null(df())) return(NULL) tryCatch({ # 数値列だけを選択 numeric_data <- df() %>% select(-年月) %>% select_if(is.numeric) # 完全なケースのみを使用(欠損値やNaN、Infを除外) numeric_data_clean <- numeric_data[complete.cases(numeric_data), ] if (nrow(numeric_data_clean) < 3) { plot(1, 1, type="n", xlab="", ylab="", main="エラー:分析可能なデータが不足しています") text(1, 1, "欠損値やNaN値が多すぎます", cex=1.5) return() } # 相関行列を計算 corr_matrix <- cor(numeric_data_clean, use = "complete.obs", method = "pearson") # NaN値を0に置き換え corr_matrix[is.na(corr_matrix)] <- 0 # ヒートマップを作成 corrplot(corr_matrix, method = "color", type = "full", tl.cex = 0.7, tl.col = "black", addCoef.col = "white", number.cex = 0.6, title = "相関係数ヒートマップ", mar = c(0, 0, 2, 0)) }, error = function(e) { plot(1, 1, type="n", xlab="", ylab="", main="エラー") text(1, 1, paste("エラー:", e$message), cex=1.2, col="red") }) }) # 相関分析の概要 output$correlation_summary <- renderPrint({ if (is.null(df())) return(NULL) tryCatch({ numeric_data <- df() %>% select(-年月) %>% select_if(is.numeric) numeric_data_clean <- numeric_data[complete.cases(numeric_data), ] corr_matrix <- cor(numeric_data_clean, use = "complete.obs") corr_matrix[is.na(corr_matrix)] <- 0 # 相関が強い組み合わせを抽出 upper_tri <- upper.tri(corr_matrix) corr_pairs <- data.frame( var1_code = rownames(corr_matrix)[row(corr_matrix)[upper_tri]], var2_code = colnames(corr_matrix)[col(corr_matrix)[upper_tri]], correlation = corr_matrix[upper_tri] ) %>% arrange(desc(abs(correlation))) %>% head(10) # 日本語ラベルを追加 corr_pairs$var1_label <- sapply(corr_pairs$var1_code, get_column_label) corr_pairs$var2_label <- sapply(corr_pairs$var2_code, get_column_label) cat("相関係数が強い上位10の組み合わせ:\n\n") for (i in 1:nrow(corr_pairs)) { cat(sprintf("%s (%s) と %s (%s): %.3f\n", corr_pairs$var1_label[i], corr_pairs$var1_code[i], corr_pairs$var2_label[i], corr_pairs$var2_code[i], corr_pairs$correlation[i])) } }, error = function(e) { cat("エラーが発生しました:", e$message) }) }) # タブ2: 回帰分析 regression_data <- reactive({ if (is.null(df())) return(NULL) x_var <- input$x_var y_var <- input$y_var data_for_reg <- df() %>% select(年月, !!x_var, !!y_var) %>% rename(X = !!x_var, Y = !!y_var) %>% filter(!is.na(X), !is.na(Y), X != 0, is.finite(X), is.finite(Y)) if (nrow(data_for_reg) < 3) { return(NULL) } return(data_for_reg) }) %>% debounce(500) output$regression_scatter <- renderPlotly({ if (is.null(regression_data())) return(NULL) data <- regression_data() model <- lm(Y ~ X, data = data) x_label <- get_column_label(input$x_var) y_label <- get_column_label(input$y_var) p <- ggplot(data, aes(x = X, y = Y)) + geom_point(size = 3, color = "steelblue", alpha = 0.6) + geom_smooth(method = "lm", se = TRUE, color = "red", fill = "red", alpha = 0.2) + labs(title = paste(y_label, "vs", x_label), subtitle = sprintf("(%s) vs (%s)", input$y_var, input$x_var), x = x_label, y = y_label) + theme_minimal() + theme(plot.title = element_text(hjust = 0.5, size = 14, face = "bold")) ggplotly(p) }) output$regression_results <- renderPrint({ if (is.null(regression_data())) return(NULL) data <- regression_data() model <- lm(Y ~ X, data = data) x_label <- get_column_label(input$x_var) y_label <- get_column_label(input$y_var) cat(sprintf("回帰分析: %s = β₀ + β₁ × %s\n", y_label, x_label)) cat(sprintf(" (%s) = β₀ + β₁ × (%s)\n\n", input$y_var, input$x_var)) print(summary(model)) cat("\n相関係数 (r):", format(cor(data$X, data$Y), digits = 4)) cat("\n決定係数 (R²):", format(summary(model)$r.squared, digits = 4)) cat("\n観測数:", nrow(data), "\n") }) # タブ3: 時系列分析 output$timeseries_profit <- renderPlotly({ if (is.null(df())) return(NULL) data <- df() %>% select(年月, x9, x41, x56, x80) %>% rename("整備営業利益" = x9, "商品営業利益" = x41, "運送営業利益" = x56, "レンタル営業利益" = x80) %>% gather(key = "部門", value = "営業利益", -年月) p <- ggplot(data, aes(x = factor(年月, levels = unique(年月)), y = 営業利益, color = 部門, group = 部門)) + geom_line(size = 1) + geom_point(size = 2) + labs(title = "各部門の営業利益推移", x = "年月", y = "営業利益(円)", color = "部門") + theme_minimal() + theme(axis.text.x = element_text(angle = 45, hjust = 1), plot.title = element_text(hjust = 0.5, size = 14, face = "bold")) ggplotly(p) }) output$timeseries_sales <- renderPlotly({ if (is.null(df())) return(NULL) data <- df() %>% select(年月, x2, x37, x52, x76) %>% rename("整備売上" = x2, "商品売上" = x37, "運送売上" = x52, "レンタル売上" = x76) %>% gather(key = "部門", value = "売上", -年月) p <- ggplot(data, aes(x = factor(年月, levels = unique(年月)), y = 売上, color = 部門, group = 部門)) + geom_line(size = 1) + geom_point(size = 2) + labs(title = "各部門の売上高推移", x = "年月", y = "売上(円)", color = "部門") + theme_minimal() + theme(axis.text.x = element_text(angle = 45, hjust = 1), plot.title = element_text(hjust = 0.5, size = 14, face = "bold")) ggplotly(p) }) output$timeseries_kpi <- renderPlotly({ if (is.null(df())) return(NULL) data <- df() %>% select(年月, x25, x31, x67, x73, x91) %>% rename("車検件数" = x25, "整備・板金数" = x31, "自社ドライバー数" = x67, "他社ドライバー数" = x73, "レンタル台数" = x91) %>% gather(key = "KPI", value = "件数", -年月) p <- ggplot(data, aes(x = factor(年月, levels = unique(年月)), y = 件数, color = KPI, group = KPI)) + geom_line(size = 1) + geom_point(size = 2) + labs(title = "KPI推移", x = "年月", y = "件数・台数", color = "KPI") + theme_minimal() + theme(axis.text.x = element_text(angle = 45, hjust = 1), plot.title = element_text(hjust = 0.5, size = 14, face = "bold")) ggplotly(p) }) # タブ4: 統計サマリー output$summary_seisetsubi <- renderPrint({ if (is.null(df())) return(NULL) cat("整備部門の基本統計量\n") cat("==================\n\n") cat("売上:\n") print(summary(df()$x2)) cat("\n営業利益:\n") print(summary(df()$x9)) cat("\n車検件数:\n") print(summary(df()$x25)) cat("\n整備・板金数:\n") print(summary(df()$x31)) }) output$summary_shohin <- renderPrint({ if (is.null(df())) return(NULL) cat("商品部門の基本統計量\n") cat("==================\n\n") cat("売上:\n") print(summary(df()$x37)) cat("\n原価:\n") print(summary(df()$x38)) cat("\n粗利:\n") print(summary(df()$x39)) cat("\n営業利益:\n") print(summary(df()$x41)) }) output$summary_unsou <- renderPrint({ if (is.null(df())) return(NULL) cat("運送部門の基本統計量\n") cat("==================\n\n") cat("売上:\n") print(summary(df()$x52)) cat("\n営業利益:\n") print(summary(df()$x56)) cat("\n自社ドライバー数:\n") print(summary(df()$x67)) cat("\n他社ドライバー数:\n") print(summary(df()$x73)) }) output$summary_rental <- renderPrint({ if (is.null(df())) return(NULL) cat("レンタル部門の基本統計量\n") cat("======================\n\n") cat("売上:\n") print(summary(df()$x76)) cat("\n原価:\n") print(summary(df()$x77)) cat("\n粗利:\n") print(summary(df()$x78)) cat("\n営業利益:\n") print(summary(df()$x80)) cat("\nレンタル台数:\n") print(summary(df()$x91)) }) # タブ5: 散布図行列 output$scatter_matrix <- renderPlot({ if (is.null(df())) return(NULL) tryCatch({ selected_vars <- input$scatter_vars if (length(selected_vars) == 0) { plot(1, 1, type="n", xlab="", ylab="", main="変数を選択してください") text(1, 1, "左側のチェックボックスから分析対象の指標を選択してください", cex=1.2) return() } data <- df() %>% select(all_of(selected_vars)) %>% select_if(is.numeric) # 完全なケースのみを使用 data_clean <- data[complete.cases(data), ] if (nrow(data_clean) < 3) { plot(1, 1, type="n", xlab="", ylab="") text(1, 1, "データが不足しています", cex=1.5) return() } # 列名を日本語に変更 colnames(data_clean) <- sapply(colnames(data_clean), get_column_label) # 散布図行列を作成 pairs(data_clean, panel = function(x, y, ...) { points(x, y, col = "steelblue", cex = 0.7) abline(lm(y ~ x), col = "red", lty = 2) }, main = "変数間の散布図行列") }, error = function(e) { plot(1, 1, type="n", xlab="", ylab="") text(1, 1, paste("エラー:", e$message), cex=1.2, col="red") }) }) } # アプリの実行 shinyApp(ui = ui, server = server)