Spaces:
Sleeping
Sleeping
| 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) | |