Использование одного data.frame для обновления другого

#r #indexing #dataframe

#r #индексирование #фрейм данных

Вопрос:

Учитывая 2 фрейма данных, которые идентичны с точки зрения имен столбцов / типов данных, где некоторые столбцы однозначно идентифицируют строки, существует ли эффективная функция / метод для одного data.frame для «обновления» другого?

Например, в следующем, original и replacement идентифицируются с помощью 'Name' и 'Id' . goal является результатом поиска всех строк из replacement in original (по уникальным идентификаторам) и замены на Value1 и Value2

 original = data.frame( Name = c("joe","john") , Id = c( 1 , 2) , Value1 = c(1.2,NA), Value2 = c(NA,9.2) )
replacement = data.frame( Name = c("john") , Id = 2 , Value1 = 2.2 , value2 = 5.9)
goal = data.frame( Name = c("joe","john") , Id = c( 1 , 2) , Value1 = c(1.2,2.2), Value2 = c(NA,5.9) )
 

Решение должно работать для original и replacement произвольной длины (хотя replacement никогда не должно содержать больше строк, чем original ). На практике я использую 2 столбца идентификаторов.

Ответ №1:

Я бы использовал data.table объекты. Этот код, похоже, работает в вашем примере:

 library(data.table)

# set keys
original.dt <- data.table(original, key=c("Name", "Id"))        
replacement.dt <- data.table(replacement, key=c("Name", "Id"))

goal2 <- original.dt
# subset and reassign
# goal2[replacement.dt[, list(Name, Id)]] <- replacement.dt
goal2[replacement.dt] <- replacement.dt  # cleaner and faster, see Matthew's comment

goal2 <- as.data.frame(goal2)

identical(goal, goal2) # FALSE, why? See Joris's comment
all.equal(goal, goal2) # TRUE
 

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

1. Спасибо! До сих пор я избегал зависимости от data.table и предпочел бы решение с использованием базовых классов. Будет ждать других ответов.

2. 1 для таблицы данных. Что касается того, почему идентичный возвращает FALSE: в goal2 Id является вектором int, тогда как в goal это вектор num.

3. @JorisMeys … как str() показывает. Спасибо!

4. Небольшое улучшение: может отбросить [,list(Name,Id)] бит. Просто goal2[replacement.dt]<-replacement.dt короче и быстрее replacement.dt , потому что ключ уже "Name,Id" есть. i не обязательно вводить ключ, но когда это так, i объединяются ключевые столбцы, и внутри используется более быстрый алгоритм слияния, который использует тот факт, что обе таблицы отсортированы.

Ответ №2:

Просто установите уникальный идентификатор в качестве имен строк. Тогда это простая индексация:

 rownames(original) = original$Id
rownames(replacement) = replacement$Id

original[rownames(replacement), ] = replacement
 

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

1. что, если будут строки с одинаковым идентификатором? например вы не можете использовать имена строк(оригинал) <- c(1,1,1)

2. Они просто должны быть уникальными. Например, вставить(idvar1, idvar2, …)

3. моя вина, аррр. если в оригинале есть несколько мест и замена на один идентификатор, то непонятно, откуда куда следует заменять

4. но здесь есть два идентификатора — Id и Name. Я не думаю, что этот подход работает для нецелочисленных идентификаторов

5. @SFun28, почему этого не должно быть? имена строк — это строки

Ответ №3:

Используя базовый R, вы можете использовать replace.df() приведенную ниже функцию, которая в общих чертах основана на исходном коде merge.data.frame() . В отличие от некоторых других решений, это позволяет идентифицировать несколько столбцов. Я довольно часто использую его в своей работе. Не стесняйтесь копировать и использовать.

Эта функция управляет случаями, когда строки в y не найдены в x. Имейте в виду, что функция не проверяет, уникальны ли комбинации. match() заменит только первое вхождение на первое вхождение комбинации.

Функция используется следующим образом :

 > replace.df(original, replacement,by=c('Name','Id'))
  Name Id Value1 Value2
1  joe  1    1.2     NA
2 john  2    2.2    9.2
 

Обратите внимание, что это эффективно обнаруживает ошибку записи, которая у вас есть в исходном коде. replacement содержит переменную с именем ‘value2’ (маленькая v) вместо Value2 (заглавная V). После исправления этого результат становится:

 > replace.df(original, replacement,by=c('Name','Id'))
  Name Id Value1 Value2
1  joe  1    1.2     NA
2 john  2    2.2    5.9
 

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

 > replace.df(original, replacement,by=c('Name','Id'),cols='Value2')
  Name Id Value1 Value2
1  joe  1    1.2     NA
2 john  2     NA    5.9
 

Функция:

 replace.df <- function(x,y,by,cols=NULL
           ){
    nx <- nrow(x)
    ny <- nrow(y)

    bx <- x[,by,drop=FALSE]
    by <- y[,by,drop=FALSE]
    bz <- do.call("paste", c(rbind(bx, by), sep = "r"))

    bx <- bz[seq_len(nx)]
    by <- bz[nx   seq_len(ny)]

    idx <- match(by,bx)
    idy <- match(bx,by)
    idy <- idy[!is.na(idy)]

    if(is.null(cols)) {
      cols <- intersect(names(x),names(y))
      cols <- cols[!cols %in% by]
    }

    x[idx,cols] <- y[idy,cols]
    x
  }
 

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

1. извините, я хотел сказать «вас не БЫВАЕТ». исправлено сообщение.

2. @SFun28: ах, нет, но, возможно, мне стоит начать делать это. Я также исправил опечатку (drop=FALSE в последних строках был явно ошибочным. Я не настолько хорош в воспроизведении функций с макушки моей головы …)

3. @TylerRinker: Вы видели мое исправление? Drop=FALSE во второй последней строке является мусором и должен быть удален. Это не аргумент для функции присваивания. Я ввел функцию из головы, оригинал работает. Отсюда и ошибка.

4. @Joris Теперь он работает со всем, кроме: cols='Value2' который выдает предупреждение Error in [<-.data.frame(*tmp, idx, cols, value = NULL) : replacement has length zero

Ответ №4:

Вот подход с использованием digest пакета.

 library(digest)
# generate keys for each row using the md5 checksum based on first two columns
check1 <- apply(original[,1:2], 1, digest)
check2 <- apply(replacement[,1:2], 1, digest)

# set goal to original and replace rows in replacement
goal <- original
goal[check1 %in% check2,] <- replacement
 

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

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

Ответ №5:

 # limit replacement to elements that have a correspondence in original 
existing = replacement[is.element(replacement$Id, original$Id),]
# replace original at positions where IDs from existing match   
original[match(existing$Id,original$Id),]=existing
 

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

1. Да, is.element легко обобщается (просто добавляя индексные массивы для обоих), но я не вижу простого способа для части сопоставления.

Ответ №6:

 require(plyr)
indexes_to_replace <- rownames(match_df(original,replacement,on='Id'))
indexes_from_replace<-rownames(match_df(replacement,original,on='Id'))
original[indexes_to_replace,] <- replacement[indexes_from_replace,]
 

параметр on функции match_df также может принимать векторы.

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

1. Это похоже на ответ Джона Колби, но я думаю, что немного медленнее из-за вызова match_df?

Ответ №7:

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

 update.df.with.df <- function(original, replacement, key, value) 
{
    ## PURPOSE: Update a data frame with the values in another data frame
    ## ----------------------------------------------------------------------
    ## ARGUMENT:
    ##   original: a data frame to update,
    ##   replacement: a data frame that has the updated values,
    ##   key: a character vector of variable names to form the unique key
    ##   value: a character vector of variable names to form the values that need to be updated
    ## ----------------------------------------------------------------------
    ## RETURN: The updated data frame from the old data frame "original". 
    ## ----------------------------------------------------------------------
    ## AUTHOR: Feiming Chen,  Date:  2 Dec 2015, 15:08

    n1 <- rownames(original) <- apply(original[, key, drop=F], 1, paste, collapse=".")
    n2 <- rownames(replacement) <- apply(replacement[, key, drop=F], 1, paste, collapse=".")

    n3 <- merge(data.frame(n=n1), data.frame(n=n2))[[1]] # make common keys
    n4 <- levels(n3)[n3]                # convert factor to character

    original[n4, value] <- replacement[n4, value] # update values on the common keys
    original
}
if (F) {                                # Unit Test 
    original <- data.frame(x=c(1, 2, 3), y=c(10, 20, 30))
    replacement <- data.frame(x=2, y=25)
    update.df.with.df(original, replacement, key="x", value="y") # data.frame(x=c(1, 2, 3), y=c(10, 25, 30))

    original <- data.frame(x=c(1, 2, 3), w=c("a", "b", "c"), y=c(10, 20, 30))
    replacement <- data.frame(x=2, w="b", y=25)
    update.df.with.df(original, replacement, key=c("x", "w"), value="y") # data.frame(x=c(1, 2, 3), w=c("a", "b", "c"), y=c(10, 25, 30))

    original = data.frame(Name = c("joe","john") , Id = c( 1 , 2) , Value1 = c(1.2,NA), Value2 = c(NA,9.2))
    replacement = data.frame(Name = c("john") , Id = 2 , Value1 = 2.2 , Value2 = 5.9)
    update.df.with.df(original, replacement, key="Id", value=c("Value1", "Value2"))
    ## goal = data.frame( Name = c("joe","john") , Id = c( 1 , 2) , Value1 = c(1.2,2.2), Value2 = c(NA,5.9) )
}