#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)