Обработка фрейма данных в r подгруппой: возможно ли избавиться от цикла for? - PullRequest
1 голос
/ 08 октября 2019

Я часто работаю с фреймами данных, и мне приходится выполнять некоторые сложные преобразования / манипуляции с данными по подгруппам, определенным в одном из столбцов. Я знаю о dplyr и group_by и знаю, что многие вещи можно решить с помощью group_by. Однако часто мне приходится делать довольно сложные вычисления и в конечном итоге просто использовать цикл for.

Мне было интересно узнать о существовании какого-то другого общего подхода или парадигмы, которая была бы более быстрой / более элегантной. Может быть, карта (с которой я не очень знаком)?

Ниже приведен пример. Обратите внимание - это подделка и бессмысленность. Итак, давайте проигнорируем, почему мне нужно делать эти вещи или тот факт, что в столбце может быть 2 последовательных NA и т. Д. Это не главное в моем вопросе. Дело в том, что часто мне приходится работать «в рамках ограничений подгруппы», а затем - внутри этой подгруппы - я должен выполнять операции по столбцам, по строкам, а иногда даже по клеткам.

Я также понимаю, что мог бы, вероятно,поместите большую часть этого кода в функцию, разбейте мой фрейм данных на список, основанный на 'group', примените эту функцию к каждому элементу этого списка и затем сделайте do.call (rbind ...) в конце. Но так ли это?

Большое спасибо за любые подсказки!

library(dplyr)
library(forcats)
set.seed(123)
x <- tibble(group = c(rep('a', 10), rep('b', 10), rep('c', 10)),
                attrib = c(sample(c("one", "two", "three", "four"), 10, replace = T),
                           sample(c("one", "two", "three"), 10, replace = T),
                           sample(c("one", "three", "four"), 10, replace = T)),
                v1 = sample(c(1:5, NA), 30, replace = T),
                v2 = sample(c(1:5, NA), 30, replace = T),
                v3 = sample(c(1:5, NA), 30, replace = T),
                n1 = abs(rnorm(30)), n2 = abs(rnorm(30)), n3 = abs(rnorm(30)))

v_vars = paste0("v", 1:3)
n_vars = paste0("n", 1:3)

results <- NULL  # Placeholder for final results

for(i in seq(length(unique(x$group)))) { # loop through groups
  mygroup <- unique(x$group)[i]
  mysubtable <- x %>% filter(group == mygroup)

  # IMPUTE NAs in v columns
  # Replace every NA with a mean of values above and below it; and if it's the first or 
  # the last value, with the mean of 2 values below or above it.
  for (v in v_vars){  # loop through v columns
    which_nas <- which(is.na(mysubtable[[v]])) # create index of NAs for column v
    if (length(which_nas) == 0) next else {
      for (na in which_nas) { # loop through indexes of column values that are NAs
        if (na == 1) {
          mysubtable[[v]][na] <- mean(c(mysubtable[[v]][na + 1], 
                                      mysubtable[[v]][na + 2]), na.rm = TRUE)
        } else if (na == nrow(mysubtable)) {
          mysubtable[[v]][na] <- mean(c(mysubtable[[v]][na - 2],
                                      mysubtable[[v]][na - 1]), na.rm = TRUE)
        } else {
          mysubtable[[v]][na] <- mean(c(mysubtable[[v]][na - 1], 
                                      mysubtable[[v]][na + 1]), na.rm = TRUE)
        }
      } # end of loop through NA indexes
    } # end of else
  } # end of loop through v vars

  # Aggregate v columns (mean) for each value of column 'attrib'
  result1 <- mysubtable %>% group_by(attrib) %>% 
    summarize_at(v_vars, mean)
  # Aggregate n columns (sum) for each value of column 'attrib'
  result2 <- mysubtable %>% group_by(attrib) %>% 
    summarize_at(n_vars, sum)
  # final result should contain the name of the group
  results[[i]] <- cbind(mygroup, result1, result2[-1])
}
results <- do.call(rbind, results)

Ответы [ 2 ]

2 голосов
/ 09 октября 2019

Может быть, этот пример слишком прост, но в этом случае единственное, что вам нужно вывести - это вменение.

my_impute <- function(x) {
  which_nas <- which(is.na(x))
  for (na in which_nas) {
    if (na == 1) {
      x[na] <- mean(c(x[na + 1], x[na + 2]), na.rm = TRUE)
    } else if (na == length(x)) {
      x[na] <- mean(c(x[na - 2], x[na - 1]), na.rm = TRUE)
    } else {
      x[na] <- mean(c(x[na - 1], x[na + 1]), na.rm = TRUE)
    }
  }
  x
}

Тогда вам просто нужно сгруппировать соответствующим образом, вменять и суммировать.

x2 <- x %>% group_by(group) %>% mutate_at(v_vars, my_impute) %>%
  group_by(group, attrib) 
full_join(x2 %>% summarize_at(v_vars, mean),
          x2 %>% summarize_at(n_vars, sum))

Мой обычный метод для таких вещей, когда подобные расчеты должны выполняться для нескольких столбцов, - это поместить его в длинный формат. Здесь это немного похоже на длинный путь, но, возможно, это было бы полезно увидеть.

x %>% mutate(row=1:n()) %>% gather("variable", "value", c(v_vars, n_vars)) %>%
  separate(variable, c("var", "x"), sep=1) %>% spread(var, value) %>%
  arrange(group, x, row) %>% group_by(group, x) %>%
  mutate(v=my_impute(v)) %>% group_by(group, attrib, x) %>%
  summarize(v=mean(v), n=sum(n)) %>%
  gather("var", "value", v, n) %>% mutate(X=paste0(var, x)) %>%
  select(-x, -var) %>% spread(X, value)

В более общем случае, сплит-применение-комбинирование - это, вероятно, путь, как вы предлагаете в своем вопросе. ;вот способ использования tidyverse.

doX <- function(x) {
  x2 <- x %>% mutate_at(v_vars, my_impute) %>% group_by(attrib)
  full_join(x2 %>% summarize_at(v_vars, mean),
            x2 %>% summarize_at(n_vars, sum))
}
x %>% group_by(group) %>% nest() %>%
  mutate(result=map(data, doX)) %>% select(-data) %>% unnest()

Более традиционный метод - do.call, split и rbind;здесь я не прилагаю усилий, чтобы сохранить информацию о группе.

do.call(rbind, lapply(split(x, x$group), doX))
1 голос
/ 09 октября 2019

Первое, что нужно сделать, это изменить вмененные данные в функцию. Я сделал несколько простых модификаций, чтобы он принимал вектор, и упростил вызов до mean.

fx_na_rm <- function(z) {
  which_nas <- which(is.na(z))

  if (length(which_nas) > 0) {
    for (na in which_nas) { # loop through indexes of column values that are NAs
      if (na == 1) {
        z[na] <- mean(z[na + (1:2)], na.rm = TRUE)
      } else if (na == nrow(mysubtable)) {
        z[na] <- mean(z[na - (1:2)], na.rm = TRUE)
      } else {
        z[na] <- mean(z[c(na - 1, na + 1)], na.rm = TRUE)
      }
    } # end of loop through NA indexes
  }
  return(z)
}

Мне нравится data.table, так что вот решение, которое использует его. Теперь, поскольку вы используете разные функции для групп переменных n и v, большинство purrr или любых других решений также будут немного забавными.

library(data.table)
dt <- copy(as.data.table(x))

v_vars = paste0("v", 1:3)
n_vars = paste0("n", 1:3)

dt[, (v_vars) := lapply(.SD, as.numeric), .SDcols = v_vars]
dt[, (v_vars) := lapply(.SD, fx_na_rm), by = group, .SDcols = v_vars]

# see /11716192/r-data-table-primenit-funktsiy-a-k-nekotorym-stolbtsam-i-funktsiy-b-k-drugim
scols <- list(v_vars, n_vars)
funs <- rep(c(mean, sum), lengths(scols))

dt[, setNames(Map(function(f, x) f(x), funs, .SD), unlist(scols))
   , by = .(group,attrib)
   , .SDcols = unlist(scols)]

Сам цикл for трудно векторизоватьпотому что результаты могут зависеть от самого себя. Вот моя попытка, которая не совпадает с вашей:

# not identical
fx_na_rm2 <- function(z) {
  which_nas <- which(is.na(z))

  if (length(which_nas) > 0) {
    ind <- c(rbind(which_nas - 1 + 2 * (which_nas == 1) + -1 * (which_nas == length(z)),
                   which_nas + 1 + 1 * (which_nas == 1) + -2 * (which_nas == length(z)))) 

    z[which_nas] <- colMeans(matrix(z[ind], nrow = 2), na.rm = T)
  }
  return(z)
}
...