R — консолидация недостающих данных в нескольких столбцах

#r

Вопрос:

Предположим, что у вас есть фрейм данных с именем df из 25 столбцов, первый из которых назван ID , со второго по тринадцатый с именами от A1 до A12 и с четырнадцатого по двадцать пятый с именами от B1 до B12 . Значения в переменных A и B могут быть отсутствующими данными.

Задача, с которой я сталкиваюсь, состоит в том, чтобы объединить недостающие данные — если, скажем, в 8-й строке A4 отсутствует запись, то 8-ю строку B4 также необходимо обновить до NA, даже если в ней есть некоторые данные. Это также работает наоборот, если отсутствует запись, скажем, в 19-й строке B11, то 19-я строка A11 также должна отсутствовать.

Я могу сделать это с помощью двух for петель:

 for(i in 2:13){  for(j in 1:nrow(df)){  if(is.na(df[j,i 12])){  df[j,i] lt;- NA  }  } }  for(i in 14:25){  for(j in 1:nrow(df)){  if(is.na(df[j,i-12])){  df[j,i] lt;- NA  }  } }  

Тем не менее, я ищу решение, которое не включает for циклы и предпочтительно находится в tidyverse. Как это можно было бы сделать более эффективно?

Ответ №1:

Как насчет этого?

 #create dataset library(tidyverse) library(missForest) df lt;- data.frame(id = c(1:10)) df[paste0("a", 1:10)] lt;- lapply(1:10, function(x) rnorm(10, x)) df[paste0("b", 1:10)] lt;- lapply(1:10, function(x) rnorm(10, x)) df lt;- bind_cols(df[1], missForest::prodNA(df[-1], noNA = 0.2)) #add NAs df  

purrr::map над переменными:

 df[paste0("a", 1:10)] lt;- map2(df %gt;% select(starts_with("a")), df %gt;% select(starts_with("b")),  ~ ifelse(is.na(.y), NA, .x)) df[paste0("b", 1:10)] lt;- map2(df %gt;% select(starts_with("b")), df %gt;% select(starts_with("a")),  ~ ifelse(is.na(.y), NA, .x)) df  

Ответ №2:

Мы могли бы перейти к длинному формату, затем в данной строке , которая содержит NA , заменить все значения на NA , а затем вернуться к широкому:

 spec lt;-   df %gt;%   build_longer_spec(cols = -ID,  names_to = c(".value", "set"),  names_pattern = "(. )(\d )",  values_to = "value")  df %gt;%   pivot_longer_spec(spec) %gt;%   print() %gt;%   # Intermediary long format:  #gt; # A tibble: 6 x 4  #gt; ID set A B  #gt; lt;intgt; lt;chrgt; lt;dblgt; lt;dblgt;  #gt; 1 1 1 1 NA  #gt; 2 1 2 4 10  #gt; 3 2 1 2 8  #gt; 4 2 2 5 NA  #gt; 5 3 1 3 9  #gt; 6 3 2 NA 12  rowwise(ID, set) %gt;%   mutate(across(everything(),   ~ ifelse(any(is.na(c_across(everything()))), NA, .x))) %gt;%   pivot_wider_spec(spec)  #gt; # A tibble: 3 x 5 #gt; ID A1 A2 B1 B2 #gt; lt;intgt; lt;dblgt; lt;dblgt; lt;dblgt; lt;dblgt; #gt; 1 1 NA 4 NA 10 #gt; 2 2 2 NA 8 NA #gt; 3 3 3 NA 9 NA  

С образцами данных:

 df lt;- data.frame(  ID = 1:3,  A1 = c(1, 2, 3),  A2 = c(4, 5, NA),  B1 = c(NA, 8, 9),  B2 = c(10, NA, 12) ) df #gt; ID A1 A2 B1 B2 #gt; 1 1 1 4 NA 10 #gt; 2 2 2 5 8 NA #gt; 3 3 3 NA 9 12  

Комментарии:

1. Отличный ответ. Я думал о том же самом, но это на 100% чище, чем мой подход.

2. Единственное, что у меня получилось в моем, так это то, что мой в два раза быстрее выполняется.

3. Спасибо. Я не запускал тесты, я не ожидаю, что это будет быстро. Я думаю, что это компромисс между удобочитаемостью и производительностью, который должен решить ОП. Но если производительность важна, я бы ожидал, что мой другой ответ будет работать лучше 🙂

Ответ №3:

Мы могли бы создать маски значений NA в обеих подтаблицах, объединить их и применить их обратно к обеим подтаблицам:

 na_mask lt;- is.na(df[2:13]) | is.na(df[14:25])  df[2:13][na_mask] lt;- NA df[14:25][na_mask] lt;- NA  

Комментарии:

1. Я тоже пытался заставить это работать, но не думал об этом таким образом. Идеальное решение прямо здесь!

Ответ №4:

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

Спасибо, что проголосовали и приняли в качестве ответа, если вам это нравится.

 # Create data nrows lt;- 10 ncols lt;- 25 df1 lt;- as.data.frame(matrix(1:(nrows*ncols), nrow = nrows)) colnames(df1) lt;- c(  "ID",  paste0("A", 1:12),  paste0("B", 1:12) )  df1[1:3, c("A1")] lt;- NA df1[5:7, c("B5")] lt;- NA  # Prepare calculation cols lt;- as.list(as.data.frame(  matrix(c(paste0("A", 1:12),  paste0("B", 1:12)), nrow = 2, byrow = TRUE) ))  # Do calculation for (col in cols) {  missing lt;- is.na(rowSums(df1[col]))  df1[missing, col] lt;- NA }  

Ответ №5:

У меня есть решение, которое демонстрирует некоторые преимущества переформатирования данных.

Позвольте мне сначала сгенерировать некоторые данные (поскольку мы их не получили).

 library(tidyverse)  sample_nrows lt;- 10 full_df lt;-   tibble(  ID = rep(seq_len(sample_nrows), each = 12   12),  name = c(str_c("A", 1:12),  str_c("B", 1:12)) %gt;%  rep(sample_nrows),  value =   rgamma(  n = 12 * 2 * sample_nrows,  shape = sample.int(20, size = 10)  ) %gt;%   round())  full_df %gt;%   #' add 10% missingness  mutate(value = if_else(rbinom(n(), size = 1, prob = 0.1) %gt;% as.logical(), NA_real_, value)) %gt;%   #' reconstruct into wide-format  pivot_wider() %gt;%  print(n = Inf, width = Inf) -gt;  partial_df  

Таким образом, мы генерируем gamma распределенные данные для sample_nrows строк (то full_df есть), добавляем 10% отсутствующих данных ко всему этому и вызываем его partial_df .

Это форматирование дает нам новую идею. Работа с длинным форматом даст такой результат…

 partial_df %gt;%   pivot_longer(-ID) %gt;%   tidyr::extract(name, c("name", "var_id"), regex = "(\D )(\d )") %gt;%   pivot_wider(names_from = "name") %gt;%   mutate(  both_na = is.na(A) | is.na(B),  A = if_else(both_na, NA_real_, A),  B = if_else(both_na, NA_real_, B),  both_na = NULL  ) -gt; partial_df_with_NAs  

Чтобы вернуться к исходному формату:

 partial_df_with_NAs %gt;%  pivot_wider(names_from = var_id, values_from = c(A,B), names_sep = "") %gt;%   print(n = Inf, width = Inf)