【问题标题】:R Shiny: Creating unique datatables for different datasetsR Shiny:为不同的数据集创建唯一的数据表
【发布时间】:2021-03-12 10:03:29
【问题描述】:

已更新:应用代码下方显示了一个问题示例

我正在构建一个动态 ML 应用程序,用户可以在其中上传数据集以获取数据集中第一列的预测(响应变量应位于上传数据集的第 1 列)。用户可以为上传数据集中的变量选择一个值,并获得响应变量的预测。

我目前正在尝试创建一个存储所有选定值、时间戳和预测的数据表。

假设该表存储以前保存的值,但仅适用于该特定数据集。我的意思是,如果我保存 iris 数据集中的值,该表将使用 iris 数据集中的变量作为列。这会在上传另一个数据集并保存这些值时导致问题,因为来自 iris 数据集的列仍然存在,而不是来自新数据集的变量/列。

我的问题是:如何为上传到应用程序的每个数据集创建一个唯一的数据表?

如果这听起来很混乱,请尝试运行应用程序,计算预测并保存数据。对两个不同的数据集执行此操作,然后查看“日志”选项卡下的数据表。

如果您没有两个数据集,您可以使用这两个数据集,它们默认构建在 R 中,并且响应变量已经位于第 1 列。

write_csv(attitude, "attitude.csv")
write_csv(ToothGrowth, "ToothGrowth.csv")

您将在服务器函数的“创建日志”部分下找到有关数据表的代码。

这是应用程序的代码:

library(shiny)
library(tidyverse)
library(shinythemes)
library(data.table)
library(RCurl)
library(randomForest)
library(mlbench)
library(janitor)
library(caret)
library(recipes)
library(rsconnect)



# UI -------------------------------------------------------------------------
ui <- fluidPage(
  navbarPage(title = "Dynamic ML Application",
               
    tabPanel("Calculator", 
  
            sidebarPanel(
              
              h3("Values Selected"),
              br(),
              tableOutput('show_inputs'),
              hr(),
              actionButton("submitbutton", label = "calculate", class = "btn btn-primary", icon("calculator")),
              actionButton("savebutton", label = "Save", icon("save")),
              hr(),
              tableOutput("tabledata")
              ), # End sidebarPanel
            
            mainPanel(
              
              h3("Variables"),
              uiOutput("select")
              ) # End mainPanel
            
  ), # End tabPanel Calculator
  
          
  tabPanel("Log",
           br(),
           DT::dataTableOutput("datatable18", width = 300), 
  ), # End tabPanel "Log"
  
  tabPanel("Upload file",
           br(),
        sidebarPanel(
           fileInput(inputId = "file1", label="Upload file"),
           checkboxInput(inputId ="header", label="header", value = TRUE),
           checkboxInput(inputId ="stringAsFactors", label="stringAsFactors", value = TRUE),
           radioButtons(inputId = "sep", label = "Seperator", choices = c(Comma=",",Semicolon=";",Tab="\t",Space=" "), selected = ","),
           radioButtons(inputId = "disp", "Display", choices = c(Head = "head", All = "all"), selected = "head"),

        ), # End sidebarPanel
        
        mainPanel(
           tableOutput("contents")
        )# End mainPanel
  ) # EndtabPanel "upload file"
  
  
  ) # End tabsetPanel
) # End UI bracket


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

# Upload file content table
  get_file_or_default <- reactive({
    if (is.null(input$file1)) {
      paste("No file is uploaded yet")
    } else { 
      df <- read.csv(input$file1$datapath,
                     header = input$header,
                     sep = input$sep,
                     quote = input$quote)
      
      if(input$disp == "head") {
        return(head(df))
      }
      else {
        return(df)
      }
    }
  })
  output$contents <- renderTable(get_file_or_default())
  
  
# Create input widgets from dataset  
  output$select <- renderUI({
    req(input$file1)
    if (is.null(input$file1)) {
      "No dataset is uploaded yet"
    } else {
      df <- read.csv(input$file1$datapath,
               header = input$header,
               sep = input$sep,
               quote = input$quote)
    
      tagList(map(
      names(df[-1]),
      ~ ifelse(is.numeric(df[[.]]),
               yes = tagList(sliderInput(
                 inputId = paste0(.),
                 label = .,
                 value = mean(df[[.]], na.rm = TRUE),
                 min = round(min(df[[.]], na.rm = TRUE),2),
                 max = round(max(df[[.]], na.rm = TRUE),2)
               )),
               no = tagList(selectInput(
                 inputId = paste0(.),
                 label = .,
                 choices = sort(unique(df[[.]])),
                 selected = sort(unique(df[[.]]))[1],
               ))
      ) # End ifelse
    )) # End tagList
    }
  })
  
  
# creating dataframe of selected values to be displayed
  AllInputs <- reactive({
    req(input$file1)
    if (is.null(input$file1)) {
      
    } else {
      DATA <- read.csv(input$file1$datapath,
                     header = input$header,
                     sep = input$sep,
                     quote = input$quote)
    }
    id_exclude <- c("savebutton","submitbutton","file1","header","stringAsFactors","input_file","sep","contents","head","disp")
    id_include <- setdiff(names(input), id_exclude)
    if (length(id_include) > 0) {
          myvalues <- NULL
      for(i in id_include) {
        if(!is.null(input[[i]]) & length(input[[i]] == 1)){
          myvalues <- as.data.frame(rbind(myvalues, cbind(i, input[[i]])))
        }
      }
      names(myvalues) <- c("Variable", "Selected Value")
      myvalues %>% 
        slice(match(names(DATA[,-1]), Variable))
    }
  })
  
  
# render table of selected values to be displayed
  output$show_inputs <- renderTable({
    if (is.null(input$file1)) {
    paste("No dataset is uploaded yet.")
    } else {
    AllInputs()
    }
  })
  
  
# Creating a dataframe for calculating a prediction
  datasetInput <- reactive({ 
    req(input$file1)
    DATA <- read.csv(input$file1$datapath,
                     header = input$header,
                     sep = input$sep,
                     quote = input$quote)
    
    DATA <- as.data.frame(unclass(DATA), stringsAsFactors = TRUE)
    response <- names(DATA[1])
    model <- randomForest(eval(parse(text = paste(names(DATA)[1], "~ ."))), 
                          data = DATA, ntree = 500, mtry = 3, importance = TRUE)
    
    df1 <- data.frame(AllInputs(), stringsAsFactors = FALSE)
    input <- transpose(rbind(df1, names(DATA[1])))
    
    write.table(input,"input.csv", sep=",", quote = FALSE, row.names = FALSE, col.names = FALSE)
    test <- read.csv(paste("input.csv", sep=""), header = TRUE)
    
    
# Defining factor levels for factor variables
    cnames <- colnames(DATA[sapply(DATA,class)=="factor"])
    if (length(cnames)>0){
      lapply(cnames, function(par) {
        test[par] <<- factor(test[par], levels = unique(DATA[,par]))
      })
    }
    
# Making the actual prediction and store it in a data.frame     
    Prediction <- predict(model,test)
    Output <- data.frame("Prediction"=Prediction)
    print(format(Output, nsmall=2, big.mark=","))
    
  })
  
# display the prediction when the submit button is pressed
  output$tabledata <- renderTable({
    if (input$submitbutton>0) { 
      isolate(datasetInput()) 
    } 
  })


  # -------------------------------------------------------------------------

# Create the Log 
  saveData <- function(data) {
    data <- as.data.frame(t(data))
    if (exists("datatable18")) {
      datatable18 <<- rbind(datatable18, data)
    } else {
      datatable18 <<- data
    }
  }
  
  loadData <- function() {
    if (exists("datatable18")) {
      datatable18
    }
  }
  
# Whenever a field is filled, aggregate all form data
  formData <- reactive({
    DATA <- read.csv(input$file1$datapath,
                     header = input$header,
                     sep = input$sep,
                     quote = input$quote)
    fields <- c(colnames(DATA[,-1]), "Timestamp", "Prediction")
    data <- sapply(fields, function(x) input[[x]])
    data$Timestamp <- as.character(Sys.time())
    data$Prediction <- as.character(datasetInput())
    data
  })
  
# When the Submit button is clicked, save the form data
  observeEvent(input$savebutton, {
    saveData(formData())
  })
  
# Show the previous responses
# (update with current response when Submit is clicked)
  output$datatable18 <- DT::renderDataTable({
    input$savebutton
    loadData()
  })
  


  
} # End server bracket

# ShinyApp -------------------------------------------------------------------------
shinyApp(ui, server)

在此更新

要了解问题是如何发生的,请查看以下内容:

  1. 我将虹膜数据集上传到应用程序。

  2. 然后我做出一些预测并保存它们。

  3. 现在可以在“日志”选项卡下查看预测以及选定的输入和按下保存按钮的时间戳。

  4. 我上传了一个新数据集(态度),其中当然包含不同的变量(态度数据集总共有 7 个变量,虹膜数据集有 5 个)。

  5. 我计算了一个预测,点击了保存按钮,应用程序崩溃了。发生这种情况是因为数据集中的列数现在发生了变化,所以我收到以下错误消息:

Error in rbind: numbers of columns of arguments do not match

这可以通过重命名服务器中的数据表对象来解决,因为这会创建一个没有任何指定列的新数据表。但是一旦第一次按下Save button,数据表就会锁定列,因此不能再次更改它们。

如果我将服务器函数中的数据表名称切换回原始名称,我仍然可以访问旧数据表。所以我在想,如果数据表对象的名称可以动态依赖于上传到应用程序的数据集,那么可以显示正确的数据表。

所以我认为一个更好的问题可能是:如何创建动态/反应式数据表输出对象

【问题讨论】:

  • (1) 这是对require 的错误使用,请参阅stackoverflow.com/a/51263513/3358272。 (2) 如果多人使用真实的文件名会导致这种不可预测(无用),我建议使用tempfile(fileext=".csv") 来存储它们;更好的是,可以考虑将 sqlite 或 duckdb 用于微不足道的甚至是内存数据库,它们都非常适合这种类型的使用。
  • 哦,在搞砸了潜在的解决方案后,我忘记删除 req() 了。我对创建数据库和临时文件很陌生,你有关于如何使用它们的链接吗?或者你能告诉我应该如何在我的代码中实现它?
  • 一个好的是shiny.rstudio.com/articles/persistent-data-storage.html。它讨论了几个选项:基于文件的、DBMS 和非关系“数据库”(例如 mongodb、redis)。如果这是低用户数,我会从小处着手。
  • 明确地说,我是在评论您对require(.) 的使用,而不是req(.),这是两件截然不同的事情。 (粗略一看,您使用 req(.) 看起来很合适。)
  • 我使用了require() 函数,因为它暂时修复了一个现在已经修复的问题,所以我再次全部改回library()。感谢文章的链接!我当前的数据表日志实际上已经受到那篇文章的启发,但我似乎不记得那篇文章谈到了我在帖子中描述的具体问题。我遇到的问题是该表试图将新数据集中的数据输入存储在包含旧列的旧数据集中。我正在尝试为上传到应用程序的每个数据集创建一个唯一的数据表。这篇文章有提到过吗?

标签: r shiny datatable


【解决方案1】:

这是一个简单的闪亮应用程序,演示了一种存储list 数据(和属性)的技术。我将它存储在alldata(一个反应值)中,每个数据集都有以下属性:

  • name,只是名称,与列表本身的名称冗余
  • depvar,存储因变量,允许用户选择使用哪个变量;在显示的表格中,这显示为第一列,尽管原始数据以其原始列顺序显示
  • data,原始数据 (data.frame)
  • createdmodified,时间戳;你说的是时间戳,但我不知道你是指特定的数据集/预测/模型还是其他东西,所以我这样做了

请注意,可以多次上传相同的数据:虽然我不知道是否需要这样做,但这是允许的,因为所有引用都是在 alldata 列表中的整数索引上完成的,而不是其中的名称。

library(shiny)

NA_POSIXt_ <- Sys.time()[NA] # for class-correct NA
defdata <- list(
  mtcars = list(
    name = "mtcars",
    depvar = "mpg",
    data = head(mtcars, 10),
    created = Sys.time(),
    modified = NA_POSIXt_
  ),
  CO2 = list(
    name = "CO2",
    depvar = "uptake",
    data = head(CO2, 20),
    created = Sys.time(),
    modified = NA_POSIXt_
  )
)

makelabels <- function(x) {
  out <- mapply(function(ind, y) {
    cre <- format(y$created, "%H:%M:%S")
    mod <- format(y$modified, "%H:%M:%S")
    if (is.na(mod)) mod <- "never"
    sprintf("[%d] %s (cre: %s ; mod: %s)", ind, y$name, cre, mod)
  }, seq_along(x), x)
  setNames(seq_along(out), out)
}

ui <- fluidPage(
  sidebarLayout(
    sidebarPanel(
      selectInput("seldata", label = "Selected dataset", choices = makelabels(defdata)),
      selectInput("depvar", label = "Dependent variable", choices = names(defdata[[1]]$data)),
      hr(),
      fileInput("file1", label = "Upload data"),
      textInput("filename1", label = "Data name", placeholder = "Derive from filename"),
      checkboxInput("header", label = "Header", value = TRUE),
      checkboxInput("stringsAsFactors", label = "stringsAsFactors", value = TRUE),
      radioButtons("sep", label = "Separator",
                   choices = c(Comma = ",", Semicolon = ";", Tab = "\t", Space = " "),
                   select = ","),
      radioButtons("quote", label = "Quote",
                   choices = c(None = "", "Double quote" = '"', "Single quote" = "'"),
                   selected = '"')
    ),
    mainPanel(
      tableOutput("contents")
    )
  )
)

server <- function(input, output, session) {
  alldata <- reactiveVal(defdata)

  observeEvent(input$seldata, {
    dat <- alldata()[[ as.integer(input$seldata) ]]
    choices <- names(dat$data)
    selected <- 
      if (!is.null(dat$depvar) && dat$depvar %in% names(dat$data)) {
        dat$depvar
      } else names(dat$data)[1]
    updateSelectInput(session, "depvar", choices = choices, selected = selected)
    # ...
    # other things you might want to update when the user changes dataset
  })
  observeEvent(input$depvar, {
    ind <- as.integer(input$seldata)
    alldat <- alldata()
    if (alldat[[ ind ]]$depvar != input$depvar) {
      # only update alldata() when depvar changes
      alldat[[ ind ]]$depvar <- input$depvar
      alldat[[ ind ]]$modified <- Sys.time()
      lbls <- makelabels(alldat)
      sel <- as.integer(input$seldata)
      updateSelectInput(session, "seldata", choices = lbls, selected = lbls[sel])
      alldata(alldat)
    }
  })

  observeEvent(input$file1, {
    req(input$file1)
    df <- tryCatch({
      read.csv(input$file1$datapath,
               header = input$header, sep = input$sep,
               stringsAsFactors = input$stringsAsFactors,
               quote = input$quote)
    }, error = function(e) e)
    if (!inherits(df, "error")) {
      if (!NROW(df) > 0 || !NCOL(df) > 0) {
        df <- structure(list(message = "No data found"), class = c("simpleError", "error", "condition"))
      }
    }
    if (inherits(df, "error")) {
      showModal(modalDialog(title = "Error loading data", "No data was found in the file"))
    } else {
      nm <-
        if (nzchar(input$filename1)) {
          input$filename1
        } else tools:::file_path_sans_ext(basename(input$file1$name))
      depvar <- names(df)[1]
      newdat <- setNames(list(list(name = nm, depvar = depvar, data = df,
                                   created = Sys.time(), modified = NA_POSIXt_)),
                         nm)
      alldat <- alldata()
      alldata( c(alldat, newdat) )
      # update the selectInput to add this new dataset
      lbls <- makelabels(alldata())
      sel <- length(lbls)
      updateSelectInput(session, "seldata", choices = lbls, selected = lbls[sel])
    }
  })

  output$contents <- renderTable({
    req(input$seldata)
    seldata <- alldata()[[ as.integer(input$seldata) ]]
    # character
    depvar <- seldata$depvar
    othervars <- setdiff(names(seldata$data), seldata$depvar)
    cbind(seldata$data[, depvar, drop = FALSE], seldata$data[, othervars, drop = FALSE])
  })
}

shinyApp(ui, server)

在这个闪亮的应用程序中没有机器学习,没有建模,没有其他任何东西,它只是展示了在多个数据集之间切换的一种可能方法。

对于您的 功能,您需要对input$seldata 做出反应,以了解用户何时更改数据集。请注意,(1)我返回列表索引的整数,(2)selectInput 总是返回一个字符串。由此,如果用户在下拉列表中选择第二个数据集,您将得到"2",它显然不会自己索引。您的数据必须引用为alldata()[[ as.integer(input$seldata) ]]

为了支持不明确的重复数据,我在selectInput 文本中添加了时间戳,这样您就可以看到某些数据的“时间”。也许矫枉过正,很容易被删除。

【讨论】:

  • 这是一个很好的例子,展示了如何在多个数据集之间切换,并且是一种聪明的方式!但是,这不是我要切换的数据集,而是从输入小部件创建的数据表(日志)(加上按下保存按钮时的预测和时间戳)。我可能已经更详细地解释了这个问题。问题是,如果我将 output$datatable15 的名称更改为例如 output$datatable16 以及我在代码中引用它的所有地方,则日志适用于新数据集。
  • 所以我只需要一种保存动态输出名称的方法,如果它被调用的话。我不知道这是否可能,但如果数据表可以保存为output$datatable_[insert_uploaded_dataset_name],我认为它可以工作。
  • 我认为我对output$contents 的使用展示了一种处理响应式帧列表的方法。也就是说,将您的预测(第 3 步)放入帧列表中,当上传新数据集时,切换到现在为空的帧以用于数据表输出。
  • 我想我可能错过了这一点,这可能是由于我缺乏这方面的经验,但我认为会有一种更简单的方法来动态更改 output$datatable18 &lt;- DT::renderDataTable({ input$savebutton loadData()}) 的名称基于input$file1。当再次上传相同的数据集时,我也遇到了问题,因为这将创建一个新的数据表而不是现有的数据表。我正在查看类似于我目前正在尝试的stackoverflow.com/questions/36681209/… 的内容
  • 您想一次显示多少张表格?如果它只是一个,那么更改表的名称将忽略 UI 和您的应用的 架构 的要点。由于我没有看到任何建议您一次显示多个表,因此请保持名称不变。上面的内容与DT::DTOutput 和 DT::renderDT` 一样适用于基本表变体,并且当数据更改(例如,列的数量/名称)时,数据表会毫无问题地更改。
猜你喜欢
  • 2021-03-02
  • 1970-01-01
  • 1970-01-01
  • 2018-10-12
  • 1970-01-01
  • 1970-01-01
  • 2019-10-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多