Разбить в контексте временного ряда - PullRequest
0 голосов
/ 10 сентября 2018

У меня есть набор данных, который я хочу визуализировать в целом и дезагрегировать по нескольким различным переменным. Я создал flexdashboard с игрушечным блестящим приложением, чтобы выбрать тип дезагрегации, и рабочий код для построения правильного подмножества.

Мой подход повторяется, и это намек на то, что я упускаю лучший способ сделать это. Кусок, который сбивает меня с толку - это необходимость считать по дате и расширять матрицу. Я не уверен, как получить количество групп по неделям в одной трубе. Я делаю это в несколько шагов и объединяю.

Мысли

(ps. Я задал этот вопрос на RStudio Community , но я думаю, что это скорее " SO вопрос ". У меня нет прав на его удаление из RSC так что извиняюсь за кросс-пост.)

---
title: "test"
output: 
  flexdashboard::flex_dashboard:
    theme: bootstrap
runtime: shiny
---

```{r setup, include=FALSE}
  library(flexdashboard)
  library(tidyverse)
  library(tibbletime)
  library(dygraphs)
  library(magrittr)
  library(xts)
```

```{r global, include=FALSE}
  set.seed(1)
  dat <- data.frame(date = seq(as.Date("2018-01-01"), 
                               as.Date("2018-06-30"), 
                               "days"),
                    sex = sample(c("male", "female"), 181, replace=TRUE),
                    lang = sample(c("english", "spanish"), 181, replace=TRUE),
                    age = sample(20:35, 181, replace=TRUE))
  dat <- sample_n(dat, 80)
```

Sidebar {.sidebar}
=====================================

```{r}
  radioButtons("diss", label = "Disaggregation",
    choices = list("All" = 1, "By Sex" = 2, "By Language" = 3), 
    selected = 1)
```

Page 1
=====================================

```{r}
# all
  all <- reactive(
  dat %>%  
    mutate(new = 1) %>%
    arrange(date) %>%
  # time series analysis
    as_tbl_time(index = date) %>% # convert to tibble time object
    select(date, new) %>%
    collapse_by('1 week', side="start", clean=TRUE) %>%
    group_by(date) %>%
    mutate(total = sum(new, na.rm=TRUE)) %>% 
    distinct(date, .keep_all = TRUE) %>% 
    ungroup() %>%
  # expand matrix to include weeks without data
    complete(date = seq(date[1],
                        date[length(date)],
                        by = "1 week"),
             fill = list(total = 0)) 
  )

# males only
  males <- reactive(
  dat %>%  
    filter(sex=="male") %>%
    mutate(new = 1) %>%
    arrange(date) %>%
  # time series analysis
    as_tbl_time(index = date) %>%
    select(date, new) %>%
    collapse_by('1 week', side="start", clean=TRUE) %>%
    group_by(date) %>%
    mutate(total_m = sum(new, na.rm=TRUE)) %>% 
    distinct(date, .keep_all = TRUE) %>% 
    ungroup() %>%
  # expand matrix to include weeks without data
    complete(date = seq(date[1],
                        date[length(date)],
                        by = "1 week"),
             fill = list(total_m = 0)) 
  )

# females only
  females <- reactive(
  dat %>%  
    filter(sex=="female") %>%
    mutate(new = 1) %>%
    arrange(date) %>%
  # time series analysis
    as_tbl_time(index = date) %>%
    select(date, new) %>%
    collapse_by('1 week', side="start", clean=TRUE) %>%
    group_by(date) %>%
    mutate(total_f = sum(new, na.rm=TRUE)) %>% 
    distinct(date, .keep_all = TRUE) %>% 
    ungroup() %>%
  # expand matrix to include weeks without data
    complete(date = seq(date[1],
                        date[length(date)],
                        by = "1 week"),
             fill = list(total_f = 0)) 
  )

# english only
  english <- reactive(
  dat %>%  
    filter(lang=="english") %>%
    mutate(new = 1) %>%
    arrange(date) %>%
  # time series analysis
    as_tbl_time(index = date) %>%
    select(date, new) %>%
    collapse_by('1 week', side="start", clean=TRUE) %>%
    group_by(date) %>%
    mutate(total_e = sum(new, na.rm=TRUE)) %>% 
    distinct(date, .keep_all = TRUE) %>% 
    ungroup() %>%
  # expand matrix to include weeks without data
    complete(date = seq(date[1],
                        date[length(date)],
                        by = "1 week"),
             fill = list(total_e = 0)) 
  )

# spanish only
  spanish <- reactive(
  dat %>%  
    filter(lang=="spanish") %>%
    mutate(new = 1) %>%
    arrange(date) %>%
  # time series analysis
    as_tbl_time(index = date) %>%
    select(date, new) %>%
    collapse_by('1 week', side="start", clean=TRUE) %>%
    group_by(date) %>%
    mutate(total_s = sum(new, na.rm=TRUE)) %>% 
    distinct(date, .keep_all = TRUE) %>% 
    ungroup() %>%
  # expand matrix to include weeks without data
    complete(date = seq(date[1],
                        date[length(date)],
                        by = "1 week"),
             fill = list(total_s = 0)) 
  )

# combine

  totals <- reactive({

  all <- all()
  females <- females()
  males <- males()
  english <- english()
  spanish <- spanish()

  all %>%
    select(date, total) %>%
    full_join(select(females, date, total_f), by = "date") %>%
    full_join(select(males, date, total_m), by = "date") %>%
    full_join(select(english, date, total_e), by = "date") %>%
    full_join(select(spanish, date, total_s), by = "date") 
  })

# convert to xts
  totals_ <- reactive({
    totals <- totals()
    xts(totals, order.by = totals$date)
  })

# plot
  renderDygraph({

  totals_ <- totals_()

  if (input$diss == 1) {
  dygraph(totals_[, "total"],
          main= "All") %>%
    dySeries("total", label = "All") %>%
    dyRangeSelector() %>%
    dyOptions(useDataTimezone = FALSE,
              stepPlot = TRUE,
              drawGrid = FALSE,
              fillGraph = TRUE) 
  } else if (input$diss == 2) {
    dygraph(totals_[, c("total_f", "total_m")],
            main = "By sex") %>%
    dyRangeSelector() %>%
    dySeries("total_f", label = "Female") %>%
    dySeries("total_m", label = "Male") %>%
    dyOptions(useDataTimezone = FALSE,
              stepPlot = TRUE,
              drawGrid = FALSE,
              fillGraph = TRUE) 
  } else {
    dygraph(totals_[, c("total_e", "total_s")],
            main = "By language") %>%
    dyRangeSelector() %>%
    dySeries("total_e", label = "English") %>%
    dySeries("total_s", label = "Spanish") %>%
    dyOptions(useDataTimezone = FALSE,
              stepPlot = TRUE,
              drawGrid = FALSE,
              fillGraph = TRUE)
  }
  })
```

Обновление:

@ Джон Спринг предложил написать функцию, чтобы уменьшить количество повторений (применяется ниже), что является хорошим улучшением. Однако основной подход тот же. Сегмент, рассчитать, объединить, построить. Есть ли способ сделать это без разрыва и соединения?

---
title: "test"
output: 
  flexdashboard::flex_dashboard:
    theme: bootstrap
runtime: shiny
---

```{r setup, include=FALSE}
  library(flexdashboard)
  library(tidyverse)
  library(tibbletime)
  library(dygraphs)
  library(magrittr)
  library(xts)
```

```{r global, include=FALSE}
# generate data
  set.seed(1)
  dat <- data.frame(date = seq(as.Date("2018-01-01"), 
                               as.Date("2018-06-30"), 
                               "days"),
                    sex = sample(c("male", "female"), 181, replace=TRUE),
                    lang = sample(c("english", "spanish"), 181, replace=TRUE),
                    age = sample(20:35, 181, replace=TRUE))
  dat <- sample_n(dat, 80)

# Jon Spring's function
  prep_dat <- function(filtered_dat, col_name = "total") {
  filtered_dat %>%
    mutate(new = 1) %>%
    arrange(date) %>%
  # time series analysis
    tibbletime::as_tbl_time(index = date) %>% # convert to tibble time object
    select(date, new) %>%
    tibbletime::collapse_by("1 week", side = "start", clean = TRUE) %>%
    group_by(date) %>%
    mutate(total = sum(new, na.rm = TRUE)) %>%
    distinct(date, .keep_all = TRUE) %>%
    ungroup() %>%
    # expand matrix to include weeks without data
    complete(
      date = seq(date[1], date[length(date)], by = "1 week"),
      fill = list(total = 0)
    )
  }
```

Sidebar {.sidebar}
=====================================

```{r}
  radioButtons("diss", label = "Disaggregation",
    choices = list("All" = 1, "By Sex" = 2, "By Language" = 3), 
    selected = 1)
```

Page 1
=====================================

```{r}
# all
  all <- reactive(
  prep_dat(dat) 
  )

# males only
  males <- reactive(
  prep_dat(
    dat %>% 
    filter(sex == "male")
  ) %>% 
    rename("total_m" = "total")
  )

# females only
  females <- reactive(
  prep_dat(
    dat %>% 
    filter(sex == "female")
  ) %>% 
    rename("total_f" = "total")
  )

# english only
  english <- reactive(
  prep_dat(
    dat %>% 
    filter(lang == "english")
  ) %>% 
    rename("total_e" = "total")
  )

# spanish only
  spanish <- reactive(
  prep_dat(
    dat %>% 
    filter(lang == "spanish")
  ) %>% 
    rename("total_s" = "total")
  )

# combine

  totals <- reactive({

  all <- all()
  females <- females()
  males <- males()
  english <- english()
  spanish <- spanish()

  all %>%
    select(date, total) %>%
    full_join(select(females, date, total_f), by = "date") %>%
    full_join(select(males, date, total_m), by = "date") %>%
    full_join(select(english, date, total_e), by = "date") %>%
    full_join(select(spanish, date, total_s), by = "date") 
  })

# convert to xts
  totals_ <- reactive({
    totals <- totals()
    xts(totals, order.by = totals$date)
  })

# plot
  renderDygraph({

  totals_ <- totals_()

  if (input$diss == 1) {
  dygraph(totals_[, "total"],
          main= "All") %>%
    dySeries("total", label = "All") %>%
    dyRangeSelector() %>%
    dyOptions(useDataTimezone = FALSE,
              stepPlot = TRUE,
              drawGrid = FALSE,
              fillGraph = TRUE) 
  } else if (input$diss == 2) {
    dygraph(totals_[, c("total_f", "total_m")],
            main = "By sex") %>%
    dyRangeSelector() %>%
    dySeries("total_f", label = "Female") %>%
    dySeries("total_m", label = "Male") %>%
    dyOptions(useDataTimezone = FALSE,
              stepPlot = TRUE,
              drawGrid = FALSE,
              fillGraph = TRUE) 
  } else {
    dygraph(totals_[, c("total_e", "total_s")],
            main = "By language") %>%
    dyRangeSelector() %>%
    dySeries("total_e", label = "English") %>%
    dySeries("total_s", label = "Spanish") %>%
    dyOptions(useDataTimezone = FALSE,
              stepPlot = TRUE,
              drawGrid = FALSE,
              fillGraph = TRUE)
  }
  })
```

Ответы [ 3 ]

0 голосов
/ 14 сентября 2018

Я думаю, что вы можете добиться определенных успехов, изменив порядок подготовки. Прямо сейчас поток вашего приложения составляет примерно:

Данные => подготовить все комбинации => выбрать нужную визуализацию => составить сюжет

Рассмотрим вместо этого:

Данные => выбрать нужную визуализацию => подготовить требуемую комбинацию => составить сюжет

Это позволит использовать реактивность Shiny для (повторной) подготовки данных, необходимых для запрошенного участка, в ответ на изменения в выборе пользователя.

С помощью фрагментов кода (извините, я недостаточно знаком с flexdashboard и tibbletime, чтобы обеспечить выполнение этого кода, но я надеюсь, что этого достаточно, чтобы выделить подход):

Ваш элемент управления выбирает столбец, на котором вы хотите сфокусироваться (обратите внимание, что мы используем "All" = "'1'", так что это дает оценку константе в группе, иначе она должна обрабатываться отдельно):

radioButtons("diss", label = "Disaggregation",
             choices = list("All" = "'1'",
                            "By Sex" = "sex",
                            "By Language" = "lang",
                            "By other" = "column_name_of_'other'"), 
             selected = 1)

И затем используйте это в своей группе, чтобы подготовить только данные, необходимые для настоящей визуализации (вам нужно настроить функцию, предложенную @Jon_Spring в ответ на эту более раннюю группировку):

preped_dat = reactive({
  dat %>%
    group_by_(input$diss) %>%
    # etc
})

Перед построением графика (вам необходимо настроить функцию построения графика в ответ на возможное изменение формата данных):

renderDygraph({
  totals = preped_data()
  dygraph(totals) %>%
      dySeries("total", label = ) %>%
      dyRangeSelector()
})

Что касается group_by, вы можете использовать group_by_, если все ваши аргументы являются текстовыми строками, или group_by(!! sym(input$diss), other_column_name), если вы хотите смешать ввод текстовой строки из вашего элемента управления с другими именами столбцов.

Одним из возможных недостатков этого изменения в подходе является снижение скорости отклика во время интерактивности, если ваш набор данных большой. В настоящем подходе все вычисления выполняются заранее, а затем выполняются минимальные вычисления при каждом выборе - это может быть предпочтительным, если у вас большой объем обработки. Мой предложенный подход будет иметь минимальную предварительную обработку и умеренные вычисления для каждого выбора.

0 голосов
/ 14 сентября 2018

Спасибо за разъяснение ваших целей.Я думаю, что подход @ simon-sa позволит упростить ситуацию.Если мы сможем динамически запустить группирование и структурировать его так, чтобы нам не нужно было заранее знать возможные компоненты в этих группах, это будет намного проще в обслуживании.

Вот минимальный жизнеспособный продукт, который перестраиваетфункция построения для включения в нее логики группировки.

  1. После группировки по дате и независимо от того, какая у нас переменная группировки, она подсчитывает, сколько строк имеет каждая группа, а затем распределяет их, чтобы каждая группа получила столбец.

  2. Затем я использую padr::pad, чтобы заполнить все пропущенные временные ряды между ними и заменить все NA на нули.

  3. Наконец, этот фрейм данных преобразуется в объект xts и подается в граф, который, кажется, автоматически обрабатывает несколько столбцов.

Здесь:

---
title: "test"
output: 
  flexdashboard::flex_dashboard:
    theme: bootstrap
runtime: shiny
---

```{r setup, include=FALSE}
library(flexdashboard)
library(tidyverse)
library(tibbletime)
library(dygraphs)
library(magrittr)
library(xts)
```

```{r global, include=FALSE}
# generate data
set.seed(1)
dat <- data.frame(date = seq(as.Date("2018-01-01"), 
                             as.Date("2018-06-30"), 
                             "days"),
                  sex = sample(c("male", "female"), 181, replace=TRUE),
                  lang = sample(c("english", "spanish"), 181, replace=TRUE),
                  age = sample(20:35, 181, replace=TRUE))
dat <- dplyr::sample_n(dat, 80)
```

Sidebar {.sidebar}
=====================================

```{r}

radioButtons("diss", label = "Disaggregation",
             choices = list("All" = "Total",
                            "By Sex" = "sex",
                            "By Language" = "lang"), 
             selected = "Total")
```

Page 1
=====================================

```{r plot}

renderDygraph({
  grp_col <- rlang::sym(input$diss) # This converts the input selection to a symbol

  dat %>%
    mutate(Total = 1) %>% # This is a hack to let us "group" by Total -- all one group

    # Here's where we unquote the symbol so that dplyr can use it 
    #   to refer to a column. In this case I make a dummy column 
    #   that's a copy of whatever column we want to group
    mutate(my_group = !!grp_col) %>%

    # Now we make a group for every existing combination of week 
    #   (using lubridate::floor_date) and level of our grouping column,
    #   count how many rows in each group, and spread that to wide format.
    group_by(date = lubridate::floor_date(date, "1 week"), my_group) %>%
    count() %>% spread(my_group, n) %>% ungroup() %>%

    # padr:pad() fills in any missing weeks in the sequence with new rows
    #   Then we replace all the NA's with zeroes.
    padr::pad() %>% replace(is.na(.), 0) %>%

    # Finally we can convert to xts and feed the wide table into digraph.
    xts::xts(order.by = .$date) %>%
    dygraph() %>%
    dyRangeSelector() %>%
    dyOptions(
      useDataTimezone = FALSE, stepPlot = TRUE,
      drawGrid = FALSE, fillGraph = TRUE
    )
})
```
0 голосов
/ 11 сентября 2018

Это хорошее место, чтобы создать функцию, сократить код и сделать его менее подверженным ошибкам.

http://r4ds.had.co.nz/functions.html

Сложность в том, что программирование на dplyr часто требует вхождения в структуру, называемую tidyeval, которая является очень мощной, но может быть пугающей. https://dplyr.tidyverse.org/articles/programming.html

(Вот альтернативный подход, который обходит стороной: https://cran.r -project.org / web / packages / seplyr / vignettes / using_seplyr.html )

В вашем сценарии можно полностью избежать этих проблем, выполнив некоторые манипуляции до и после выполнения вашей функции. Это не так элегантно, но работает.

Кстати, я не могу гарантировать, что это сработает, так как вы не предоставили проверяемое представительство (например, включая образец данных той же формы, что и у вас), но оно работало с поддельными данными, которые я сделал вверх. (См. Внизу.) Извините, я пропустил кусок, где были предоставлены ваши данные образца.

prep_dat <- function(filtered_dat, col_name = "total") {
  filtered_dat %>%
    mutate(new = 1) %>%
    arrange(date) %>%
  # time series analysis
  tibbletime::as_tbl_time(index = date) %>% # convert to tibble time object
    select(date, new) %>%
    tibbletime::collapse_by("1 week", side = "start", clean = TRUE) %>%
    group_by(date) %>%
    mutate(total = sum(new, na.rm = TRUE)) %>%
    distinct(date, .keep_all = TRUE) %>%
    ungroup() %>%
    # expand matrix to include weeks without data
    complete(
      date = seq(date[1], date[length(date)], by = "1 week"),
      fill = list(total = 0)
    )
}

Тогда вы могли бы позвонить с вашими отфильтрованными данными и именем общего столбца. Этот фрагмент должен заменить ~ 20 строк, которые вы используете в настоящее время:

males <- prep_dat(dat_fake %>% 
  filter(sex == "male")) %>% 
  rename("total_m" = "total")

Поддельные данные, на которых я тестировал:

dat_fake <- tibble(
  date = as.Date("2018-01-01") + runif(500, 0, 100),
  new  = runif(500, 0, 100),
  sex  = sample(c("male", "female"), 
                500, replace = TRUE),
  lang = sample(c("english", "french", "spanish", "portuguese", "tagalog"), 
                500, replace = TRUE)
)
...