Заменить NA, когда последнее и следующее значения, отличные от NA, равны

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

> dat
   id message index
1   1    <NA>     1
2   1     foo     2
3   1     foo     3
4   1    <NA>     4
5   1     foo     5
6   1    <NA>     6
7   2    <NA>     1
8   2     baz     2
9   2    <NA>     3
10  2     baz     4
11  2     baz     5
12  2     baz     6
13  3     bar     1
14  3    <NA>     2
15  3    <NA>     3
16  3     bar     4
17  3    <NA>     5
18  3     bar     6
19  3    <NA>     7
20  3     qux     8

Моя цель - заменить значения NA, которые окружены «сообщением» такой же, используя первое появление сообщения (наименьшее значение index) и последнее появление сообщения (используя максимальное значение index) по идентификатору

Иногда последовательности NA имеют длину всего 1, в других случаях они могут быть очень длинными. Несмотря на это, все NA, которые «зажаты» между одним и тем же значением «сообщения» до и после NA, должны быть заполнены.

Результатом для приведенной выше неполной таблицы будет:

 > output
   id message index
1   1    <NA>     1
2   1     foo     2
3   1     foo     3
4   1     foo     4
5   1     foo     5
6   1    <NA>     6
7   2    <NA>     1
8   2     baz     2
9   2     baz     3
10  2     baz     4
11  2     baz     5
12  2     baz     6
13  3     bar     1
14  3     bar     2
15  3     bar     3
16  3     bar     4
17  3     bar     5
18  3     bar     6
19  3    <NA>     7
20  3     qux     8

Любое руководство с использованием data.table или dplyr здесь было бы полезно, так как я даже не знаю, с чего начать.

Насколько я мог понять, это подгруппа по уникальным сообщениям, но этот метод не учитывает id:

#get distinct messages
messages = unique(dat$message)

#remove NA
messages = messages[!is.na(messages)]

#subset dat for each message
for (i in 1:length(messages)) {print(dat[dat$message == messages[i],]) }

данные:

 dput(dat)
structure(list(id = c(1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 
3, 3, 3, 3, 3, 3, 3), message = c(NA, "foo", "foo", NA, "foo", 
NA, NA, "baz", NA, "baz", "baz", "baz", "bar", NA, NA, "bar", 
NA, "bar", NA, "qux"), index = c(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 
5, 6, 1, 2, 3, 4, 5, 6, 7, 8)), row.names = c(NA, -20L), class = "data.frame")

Можете ли вы уточнить, что вы подразумеваете под id? то есть, если в этих данных в строке 6 было «foo», а также в строке 8, строка 7, тем не менее, не была бы заполнена, а осталась бы отсутствующей? Я не думаю, что это в настоящее время проиллюстрировано в вашем примере данных

Calum You 15.02.2019 23:55

Это правильно, в этом случае 7 останется неверным, поскольку id строки 6 равно 1, а id строки 8 равно 2. И если бы строка 7 была «foo», строка 6 все равно оставалась бы NA, опять же из-за разных идентификаторов.

the_darkside 15.02.2019 23:57
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
2
159
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

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

tidyr::fill по умолчанию заполняет пропущенные значения предыдущим значением, поэтому некоторые значения будут переполнены. К сожалению, он не учитывает группировку, поэтому мы должны использовать условие if_else, чтобы исправить его ошибки.

Во-первых, мы фиксируем исходные местоположения пропущенных значений и вычисляем максимальное и минимальное index для каждого id и message. После заполнения соединяемся по этим index границам. Если совпадения нет, то id изменился; если есть совпадение, либо это была правильная замена, либо index находится за пределами границ. Поэтому мы проверяем местоположения с исходными отсутствующими значениями для этих условий и заменяем обратно на NA, если они выполняются.

Обновлено: это может быть нарушено на другом вводе, пытаясь исправить

library(tidyverse)
dat <- structure(list(id = c(1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3), message = c(NA, "foo", "foo", NA, "foo", NA, NA, "baz", NA, "baz", "baz", "baz", "bar", NA, NA, "bar", NA, "bar", NA, "qux"), index = c(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 7, 8)), row.names = c(NA, -20L), class = "data.frame")

indices <- dat %>%
  group_by(id, message) %>%
  summarise(min = min(index), max = max(index)) %>%
  drop_na

dat %>%
  mutate(orig_na = is.na(message)) %>%
  fill(message) %>%
  left_join(indices, by = c("id", "message")) %>% 
  mutate(
    message = if_else(
      condition = orig_na &
        (index < min | index > max | is.na(min)),
      true = NA_character_,
      false = message
    )
  )
#>    id message index orig_na min max
#> 1   1    <NA>     1    TRUE  NA  NA
#> 2   1     foo     2   FALSE   2   5
#> 3   1     foo     3   FALSE   2   5
#> 4   1     foo     4    TRUE   2   5
#> 5   1     foo     5   FALSE   2   5
#> 6   1    <NA>     6    TRUE   2   5
#> 7   2    <NA>     1    TRUE  NA  NA
#> 8   2     baz     2   FALSE   2   6
#> 9   2     baz     3    TRUE   2   6
#> 10  2     baz     4   FALSE   2   6
#> 11  2     baz     5   FALSE   2   6
#> 12  2     baz     6   FALSE   2   6
#> 13  3     bar     1   FALSE   1   6
#> 14  3     bar     2    TRUE   1   6
#> 15  3     bar     3    TRUE   1   6
#> 16  3     bar     4   FALSE   1   6
#> 17  3     bar     5    TRUE   1   6
#> 18  3     bar     6   FALSE   1   6
#> 19  3    <NA>     7    TRUE   1   6
#> 20  3     qux     8   FALSE   8   8

Created on 2019-02-15 by the reprex package (v0.2.1)

на каком входе он может ломаться?

the_darkside 16.02.2019 00:17

Мой предыдущий подход, я думаю, потерпит неудачу, если в строке 8 отсутствовало значение, оно заменило бы строку 8, но оставило бы строку 7 как foo

Calum You 16.02.2019 00:22

Еще одно аккуратное решение с использованием case_when. Отредактировано, чтобы избежать заполнения после окончания серии.

library(dplyr)

dfr <- data.frame(
  index =  c(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 7, 8),
  message = c(NA, "foo", "foo", NA, "foo", NA, NA, "baz", NA, "baz", "baz", "baz", "bar", NA, NA, "bar", NA, "bar", NA, "qux"),
  id =  c(1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3)
)

dfrFilled <- dfr %>% 
  group_by(id) %>% 
  mutate(
    endSeries = max( # identify end of series
      index[message == na.omit(message)[1]],
      na.rm = T
      ),
    filledValues = case_when(
      min(index) == index ~ message,
      max(index) == index ~ message,
      index < endSeries ~ na.omit(message)[1], # fill if index is before end of series.
      TRUE ~ message
    )
  )

да output$message[19] должен остаться NA, потому что последовательность сообщений bar закончилась на output$message[18]

the_darkside 16.02.2019 00:15

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

Kris Williams 16.02.2019 03:11

Если вы заполните оба способа и проверите равенство, это должно работать, если вы учитываете группировку и индекс:

аккуратный:

library(tidyverse)

dat %>%
  arrange(id, index) %>%
  mutate(msg_down = fill(group_by(., id), message, .direction = 'down')$message,
         msg_up   = fill(group_by(., id), message, .direction = 'up')$message,
         message = case_when(!is.na(message) ~ message,
                             msg_down == msg_up ~ msg_down,
                             TRUE ~ NA_character_)) %>%
  select(-msg_down, -msg_up)

   id message index
1   1    <NA>     1
2   1     foo     2
3   1     foo     3
4   1     foo     4
5   1     foo     5
6   1    <NA>     6
7   2    <NA>     1
8   2     baz     2
9   2     baz     3
10  2     baz     4
11  2     baz     5
12  2     baz     6
13  3     bar     1
14  3     bar     2
15  3     bar     3
16  3     bar     4
17  3     bar     5
18  3     bar     6
19  3    <NA>     7
20  3     qux     8

Таблица данных

library(data.table)
library(zoo)

setDT(dat)[order(index),
           message := ifelse(na.locf(message, na.rm = FALSE) == na.locf(message, na.rm = FALSE, fromLast = TRUE),
                             na.locf(message, na.rm = FALSE),
                             NA),
           by = "id"][]

    id message index
 1:  1    <NA>     1
 2:  1     foo     2
 3:  1     foo     3
 4:  1     foo     4
 5:  1     foo     5
 6:  1    <NA>     6
 7:  2    <NA>     1
 8:  2     baz     2
 9:  2     baz     3
10:  2     baz     4
11:  2     baz     5
12:  2     baz     6
13:  3     bar     1
14:  3     bar     2
15:  3     bar     3
16:  3     bar     4
17:  3     bar     5
18:  3     bar     6
19:  3    <NA>     7
20:  3     qux     8

Вариант, в котором используется na.approx из zoo.

Во-первых, мы извлекаем уникальные элементы из столбца message, которые не являются NA, и находим их позиции в dat$message

x <- unique(na.omit(dat$message))
(y <- match(dat$message, x))
# [1] NA  1  1 NA  1 NA NA  2 NA  2  2  2  3 NA NA  3 NA  3 NA  4

library(zoo)
library(dplyr)
out <- do.call(coalesce, 
               lapply(seq_along(x), function(i) as.double(na.approx(match(y, i) * i, na.rm = FALSE))))
dat$new <- x[out]
dat
#    id message index  new
#1   1    <NA>     1 <NA>
#2   1     foo     2  foo
#3   1     foo     3  foo
#4   1    <NA>     4  foo
#5   1     foo     5  foo
#6   1    <NA>     6 <NA>
#7   2    <NA>     1 <NA>
#8   2     baz     2  baz
#9   2    <NA>     3  baz
#10  2     baz     4  baz
#11  2     baz     5  baz
#12  2     baz     6  baz
#13  3     bar     1  bar
#14  3    <NA>     2  bar
#15  3    <NA>     3  bar
#16  3     bar     4  bar
#17  3    <NA>     5  bar
#18  3     bar     6  bar
#19  3    <NA>     7 <NA>
#20  3     qux     8  qux

тл;др

Когда мы звоним

match(y, 1) * 1
# [1] NA  1  1 NA  1 NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA

мы получаем элементы только там, где есть 1s в y. Соответственно, когда мы делаем

match(y, 2) * 2
# [1] NA NA NA NA NA NA NA  2 NA  2  2  2 NA NA NA NA NA NA NA NA

результат тот же для 2s.

Думайте о 1 и 2 как о первом и втором элементах в

x
# [1] "foo" "baz" "bar" "qux"

то есть "foo" и "baz".

Теперь для каждого match(y, i) * i мы можем вызвать na.approx из zoo, чтобы заполнить промежуточные NA (позже i станет seq_along(x)).

na.approx(match(y, 2) * 2, na.rm = FALSE)
# [1] NA NA NA NA NA NA NA  2  2  2  2  2 NA NA NA NA NA NA NA NA

Мы делаем то же самое для каждого элемента в seq_along(x), то есть 1:4, используя lapply. Результат - список

lapply(seq_along(x), function(i) as.double(na.approx(match(y, i) * i, na.rm = FALSE)))
#[[1]]
# [1] NA  1  1  1  1 NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA
#
#[[2]]
# [1] NA NA NA NA NA NA NA  2  2  2  2  2 NA NA NA NA NA NA NA NA
#
#[[3]]
# [1] NA NA NA NA NA NA NA NA NA NA NA NA  3  3  3  3  3  3 NA NA
#
#[[4]]
# [1] NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA  4

(as.double был нужен здесь, потому что иначе coalesce жаловался бы, что "Аргумент 4 должен быть типа double, а не integer")

Мы почти на месте. Далее нам нужно найти первое непропущенное значение в каждой позиции, здесь в игру вступает coalesce из dplyr, и результат

out <- do.call(coalesce, 
               lapply(seq_along(x), function(i) as.integer(na.approx(match(y, i) * i, na.rm = FALSE))))
out
# [1] NA  1  1  1  1 NA NA  2  2  2  2  2  3  3  3  3  3  3 NA  4

Мы можем использовать этот вектор для извлечения желаемых значений из x как

x[out]
# [1] NA    "foo" "foo" "foo" "foo" NA    NA    "baz" "baz" "baz" "baz" "baz" "bar" "bar" "bar" "bar" "bar" "bar" NA    "qux"

Надеюсь это поможет.

Ответ принят как подходящий

Выполните na.locf0 как вперед, так и назад, и если они одинаковы, используйте общее значение; в противном случае используйте IN. Группировка выполняется с помощью ave.

library(zoo)

filler <- function(x) {
  forward <- na.locf0(x)
  backward <- na.locf0(x, fromLast = TRUE)
  ifelse(forward == backward, forward, NA)
}
transform(dat, message = ave(message, id, FUN = filler))

давая:

   id message index
1   1    <NA>     1
2   1     foo     2
3   1     foo     3
4   1     foo     4
5   1     foo     5
6   1    <NA>     6
7   2    <NA>     1
8   2     baz     2
9   2     baz     3
10  2     baz     4
11  2     baz     5
12  2     baz     6
13  3     bar     1
14  3     bar     2
15  3     bar     3
16  3     bar     4
17  3     bar     5
18  3     bar     6
19  3    <NA>     7
20  3     qux     8

Другие вопросы по теме