Cómo unir (fusionar) marcos de datos (interno, externo, izquierdo, derecho)

1233

Dados dos marcos de datos:

df1 = data.frame(CustomerId = c(1:6), Product = c(rep("Toaster", 3), rep("Radio", 3)))
df2 = data.frame(CustomerId = c(2, 4, 6), State = c(rep("Alabama", 2), rep("Ohio", 1)))

df1
#  CustomerId Product
#           1 Toaster
#           2 Toaster
#           3 Toaster
#           4   Radio
#           5   Radio
#           6   Radio

df2
#  CustomerId   State
#           2 Alabama
#           4 Alabama
#           6    Ohio

¿Cómo puedo hacer un estilo de base de datos, es decir, estilo sql, une ? Es decir, cómo obtengo:


Crédito adicional:

¿Cómo puedo hacer una declaración de selección de estilo SQL?

Dan Goldstein
fuente
44
stat545-ubc.github.io/bit001_dplyr-cheatsheet.html ← mi respuesta favorita a esta pregunta
isomorphismes
La hoja de trucos Transformación de datos con dplyr creada y mantenida por RStudio también tiene buenas infografías sobre cómo funcionan las uniones en dplyr rstudio.com/resources/cheatsheets
Arthur Yip
2
Si vino aquí en lugar de querer saber sobre la fusión de marcos de datos de pandas , ese recurso se puede encontrar aquí .
cs95

Respuestas:

1350

Al usar la mergefunción y sus parámetros opcionales:

Unión interna: merge(df1, df2) funcionará para estos ejemplos porque R une automáticamente los marcos por nombres de variables comunes, pero lo más probable es que desee especificarmerge(df1, df2, by = "CustomerId")para asegurarse de que coincida solo en los campos que desea. También puede usar losparámetrosby.xyby.ysi las variables coincidentes tienen nombres diferentes en los diferentes marcos de datos.

Unión externa: merge(x = df1, y = df2, by = "CustomerId", all = TRUE)

Exterior izquierdo: merge(x = df1, y = df2, by = "CustomerId", all.x = TRUE)

Derecho exterior: merge(x = df1, y = df2, by = "CustomerId", all.y = TRUE)

Cruz de unión: merge(x = df1, y = df2, by = NULL)

Al igual que con la combinación interna, es probable que desee pasar explícitamente "CustomerId" a R como la variable coincidente. Creo que casi siempre es mejor indicar explícitamente los identificadores en los que desea fusionarse; es más seguro si los marcos de datos de entrada cambian inesperadamente y son más fáciles de leer más adelante.

Puede combinar en varias columnas, dando byun vector, por ejemplo, by = c("CustomerId", "OrderId").

Si los nombres de columna para fusionar no son los mismos, puede especificar, por ejemplo, by.x = "CustomerId_in_df1", by.y = "CustomerId_in_df2"dónde CustomerId_in_df1está el nombre de la columna en el primer marco de datos y CustomerId_in_df2es el nombre de la columna en el segundo marco de datos. (Estos también pueden ser vectores si necesita fusionarse en varias columnas).

Matt Parker
fuente
2
@MattParker He estado usando el paquete sqldf para una gran cantidad de consultas complejas contra marcos de datos, realmente lo necesitaba para hacer una unión cruzada (es decir, la unión cruzada de data.frame) Me pregunto cómo se compara desde una perspectiva de rendimiento ... . ???
Nicholas Hamilton
99
@ADP Nunca he usado realmente sqldf, así que no estoy seguro de la velocidad. Si el rendimiento es un problema importante para usted, también debe consultar el data.tablepaquete: es un conjunto completamente nuevo de sintaxis de combinación, pero es radicalmente más rápido que cualquier cosa de la que estamos hablando aquí.
Matt Parker
55
Con más claridad y explicación ..... mkmanu.wordpress.com/2016/04/08/…
Manoj Kumar
42
Una adición menor que fue útil para mí: cuando desea fusionar el uso de más de una columna:merge(x=df1,y=df2, by.x=c("x_col1","x_col2"), by.y=c("y_col1","y_col2"))
Dileep Kumar Patchigolla el
8
Esto funciona data.tableahora, la misma función solo que más rápido.
marbel
222

Recomendaría consultar el paquete sqldf de Gabor Grothendieck , que le permite expresar estas operaciones en SQL.

library(sqldf)

## inner join
df3 <- sqldf("SELECT CustomerId, Product, State 
              FROM df1
              JOIN df2 USING(CustomerID)")

## left join (substitute 'right' for right join)
df4 <- sqldf("SELECT CustomerId, Product, State 
              FROM df1
              LEFT JOIN df2 USING(CustomerID)")

Creo que la sintaxis de SQL es más simple y más natural que su equivalente R (pero esto puede reflejar mi sesgo RDBMS).

Consulte sqldf GitHub de Gabor para obtener más información sobre combinaciones.

medriscoll
fuente
198

Existe el enfoque data.table para una unión interna, que es muy eficiente en cuanto a tiempo y memoria (y necesario para algunos data.frames más grandes):

library(data.table)

dt1 <- data.table(df1, key = "CustomerId") 
dt2 <- data.table(df2, key = "CustomerId")

joined.dt1.dt.2 <- dt1[dt2]

mergetambién funciona en data.tables (ya que es genérico y llama merge.data.table)

merge(dt1, dt2)

data.table documentado en stackoverflow:
Cómo hacer una operación de combinación data.table
Traducción de uniones SQL en claves foráneas a R sintaxis data.table
Alternativas eficientes para combinar para data.frames más grandes R
Cómo hacer una unión externa izquierda básica con data.table en R?

Otra opción es la joinfunción que se encuentra en el paquete plyr

library(plyr)

join(df1, df2,
     type = "inner")

#   CustomerId Product   State
# 1          2 Toaster Alabama
# 2          4   Radio Alabama
# 3          6   Radio    Ohio

Las opciones para type: inner, left, right, full.

De ?join: A diferencia merge, [ join] conserva el orden de x sin importar qué tipo de combinación se use.

Etienne Low-Décarie
fuente
8
+1 por mencionar plyr::join. Microbenchmarking indica que se realiza aproximadamente 3 veces más rápido que merge.
Beasterfield
20
Sin embargo, data.tablees mucho más rápido que ambos. También hay un gran soporte en SO, no veo a muchos escritores de paquetes respondiendo preguntas aquí tan a menudo como el data.tableescritor o los contribuyentes.
marbel
1
¿Cuál es la data.tablesintaxis para fusionar una lista de marcos de datos ?
Aleksandr Blekh
55
Tenga en cuenta: dt1 [dt2] es una unión externa derecha (no una unión interna "pura") de modo que TODAS las filas de dt2 serán parte del resultado, incluso si no hay una fila coincidente en dt1. Impacto: el resultado tiene filas potencialmente no deseadas si tiene valores clave en dt2 que no coinciden con los valores clave de dt1.
R Yoda
8
@RYoda puede simplemente especificar nomatch = 0Len ese caso.
David Arenburg
181

También puedes hacer combinaciones usando el increíble paquete dplyr de Hadley Wickham .

library(dplyr)

#make sure that CustomerId cols are both type numeric
#they ARE not using the provided code in question and dplyr will complain
df1$CustomerId <- as.numeric(df1$CustomerId)
df2$CustomerId <- as.numeric(df2$CustomerId)

Mutación de combinaciones: agregue columnas a df1 utilizando coincidencias en df2

#inner
inner_join(df1, df2)

#left outer
left_join(df1, df2)

#right outer
right_join(df1, df2)

#alternate right outer
left_join(df2, df1)

#full join
full_join(df1, df2)

Filtrado de combinaciones: filtrar filas en df1, no modificar columnas

semi_join(df1, df2) #keep only observations in df1 that match in df2.
anti_join(df1, df2) #drops all observations in df1 that match in df2.
Andrew Barr
fuente
16
¿Por qué necesitas convertir CustomerIda numérico? No veo ninguna mención en la documentación (para ambos plyry dplyr) sobre este tipo de restricción. ¿Funcionaría incorrectamente su código si la columna de combinación fuera del charactertipo (especialmente interesado plyr)? ¿Me estoy perdiendo de algo?
Aleksandr Blekh
¿Se podría usar semi_join (df1, df2, df3, df4) para mantener solo observaciones en df1 que coincidan con el resto de las columnas?
Ghose Bishwajit
@GhoseBishwajit Suponiendo que se refiere al resto de los marcos de datos en lugar de columnas, puede usar rbind en df2, df3 y df4 si tienen la misma estructura, por ejemplo, semi_join (df1, rbind (df2, df3, df4))
abhy3
Sí, me refería al marco de datos. Pero no tienen la misma estructura que faltan algunos en ciertas filas. Para cuatro marcos de datos, tengo datos sobre cuatro indicadores diferentes (PIB, PNB GINI, MMR) para diferentes países. Quiero unirme a los marcos de datos de una manera que mantenga solo aquellos países presentes para los cuatro indicadores.
Ghose Bishwajit
86

Hay algunos buenos ejemplos de cómo hacer esto en R Wiki . Robaré un par aquí:

Método de fusión

Dado que sus claves tienen el mismo nombre, la forma corta de hacer una unión interna es merge ():

merge(df1,df2)

se puede crear una unión interna completa (todos los registros de ambas tablas) con la palabra clave "all":

merge(df1,df2, all=TRUE)

una unión externa izquierda de df1 y df2:

merge(df1,df2, all.x=TRUE)

una unión externa derecha de df1 y df2:

merge(df1,df2, all.y=TRUE)

puedes voltearlos, abofetearlos y frotarlos para obtener las otras dos uniones externas que preguntaste :)

Método de subíndice

Una combinación externa izquierda con df1 a la izquierda usando un método de subíndice sería:

df1[,"State"]<-df2[df1[ ,"Product"], "State"]

La otra combinación de combinaciones externas se puede crear mezclando el ejemplo de subíndice de combinación externa izquierda. (Sí, sé que es el equivalente a decir "Lo dejaré como un ejercicio para el lector ...")

JD Long
fuente
44
El enlace "R Wiki" está roto.
zx8754
79

Nuevo en 2014:

Especialmente si también está interesado en la manipulación de datos en general (incluida la clasificación, filtrado, subconjunto, resumen, etc.), definitivamente debe echar un vistazo dplyr, que viene con una variedad de funciones, todas diseñadas para facilitar su trabajo específicamente con marcos de datos y ciertos otros tipos de bases de datos. Incluso ofrece una interfaz SQL bastante elaborada, e incluso una función para convertir (la mayoría) del código SQL directamente en R.

Las cuatro funciones relacionadas con la unión en el paquete dplyr son (entre comillas):

  • inner_join(x, y, by = NULL, copy = FALSE, ...): devuelve todas las filas de x donde hay valores coincidentes en y, y todas las columnas de x e y
  • left_join(x, y, by = NULL, copy = FALSE, ...): devuelve todas las filas de x, y todas las columnas de x e y
  • semi_join(x, y, by = NULL, copy = FALSE, ...): devuelve todas las filas de x donde hay valores coincidentes en y, manteniendo solo columnas de x.
  • anti_join(x, y, by = NULL, copy = FALSE, ...): devuelve todas las filas de x donde no hay valores coincidentes en y, manteniendo solo columnas de x

Todo está aquí con gran detalle.

La selección de columnas se puede hacer por select(df,"column"). Si eso no es lo suficientemente SQL para usted, entonces está la sql()función, en la que puede ingresar el código SQL tal cual, y realizará la operación que especificó tal como estaba escribiendo en R todo el tiempo (para obtener más información, consulte a la viñeta dplyr / bases de datos ). Por ejemplo, si se aplica correctamente, sql("SELECT * FROM hflights")seleccionará todas las columnas de la tabla dplyr "hflights" (un "tbl").

comandante
fuente
Definitivamente la mejor solución dada la importancia que el paquete dplyr ha ganado en los últimos dos años.
Marco Fumagalli
72

Actualización sobre métodos data.table para unir conjuntos de datos. Vea los ejemplos a continuación para cada tipo de unión. Hay dos métodos, uno desde el [.data.tablemomento en que se pasa la segunda tabla de datos como el primer argumento para el subconjunto, otra forma es usar la mergefunción que despacha al método rápido de tabla de datos.

df1 = data.frame(CustomerId = c(1:6), Product = c(rep("Toaster", 3), rep("Radio", 3)))
df2 = data.frame(CustomerId = c(2L, 4L, 7L), State = c(rep("Alabama", 2), rep("Ohio", 1))) # one value changed to show full outer join

library(data.table)

dt1 = as.data.table(df1)
dt2 = as.data.table(df2)
setkey(dt1, CustomerId)
setkey(dt2, CustomerId)
# right outer join keyed data.tables
dt1[dt2]

setkey(dt1, NULL)
setkey(dt2, NULL)
# right outer join unkeyed data.tables - use `on` argument
dt1[dt2, on = "CustomerId"]

# left outer join - swap dt1 with dt2
dt2[dt1, on = "CustomerId"]

# inner join - use `nomatch` argument
dt1[dt2, nomatch=NULL, on = "CustomerId"]

# anti join - use `!` operator
dt1[!dt2, on = "CustomerId"]

# inner join - using merge method
merge(dt1, dt2, by = "CustomerId")

# full outer join
merge(dt1, dt2, by = "CustomerId", all = TRUE)

# see ?merge.data.table arguments for other cases

Por debajo de las pruebas de referencia base R, sqldf, dplyr y data.table.
Las pruebas de referencia prueban conjuntos de datos no codificados / no indexados. El punto de referencia se realiza en conjuntos de datos de filas de 50M-1, hay valores comunes de 50M-2 en la columna de unión, por lo que cada escenario (interno, izquierdo, derecho, completo) se puede probar y la unión aún no es trivial de realizar. Es un tipo de unión que también enfatiza los algoritmos de unión. Sincronizaciones son con fecha de sqldf:0.4.11, dplyr:0.7.8, data.table:1.12.0.

# inner
Unit: seconds
   expr       min        lq      mean    median        uq       max neval
   base 111.66266 111.66266 111.66266 111.66266 111.66266 111.66266     1
  sqldf 624.88388 624.88388 624.88388 624.88388 624.88388 624.88388     1
  dplyr  51.91233  51.91233  51.91233  51.91233  51.91233  51.91233     1
     DT  10.40552  10.40552  10.40552  10.40552  10.40552  10.40552     1
# left
Unit: seconds
   expr        min         lq       mean     median         uq        max 
   base 142.782030 142.782030 142.782030 142.782030 142.782030 142.782030     
  sqldf 613.917109 613.917109 613.917109 613.917109 613.917109 613.917109     
  dplyr  49.711912  49.711912  49.711912  49.711912  49.711912  49.711912     
     DT   9.674348   9.674348   9.674348   9.674348   9.674348   9.674348       
# right
Unit: seconds
   expr        min         lq       mean     median         uq        max
   base 122.366301 122.366301 122.366301 122.366301 122.366301 122.366301     
  sqldf 611.119157 611.119157 611.119157 611.119157 611.119157 611.119157     
  dplyr  50.384841  50.384841  50.384841  50.384841  50.384841  50.384841     
     DT   9.899145   9.899145   9.899145   9.899145   9.899145   9.899145     
# full
Unit: seconds
  expr       min        lq      mean    median        uq       max neval
  base 141.79464 141.79464 141.79464 141.79464 141.79464 141.79464     1
 dplyr  94.66436  94.66436  94.66436  94.66436  94.66436  94.66436     1
    DT  21.62573  21.62573  21.62573  21.62573  21.62573  21.62573     1

Tenga en cuenta que hay otros tipos de combinaciones que puede realizar usando data.table:
- actualizar en combinación - si desea buscar valores de otra tabla a su tabla principal
- agregar en combinación - si desea agregar en clave que está uniendo, no tiene para materializar todos se unen resultados
- la superposición de unirse a - si desea combinar por cadenas
- rodando unirse a - si quieres fusión para poder coincidir con los valores de precedente / siguiente filas haciendo rodar hacia delante o hacia atrás
- no equi unen - si su la condición de unión no es igual

Código para reproducir:

library(microbenchmark)
library(sqldf)
library(dplyr)
library(data.table)
sapply(c("sqldf","dplyr","data.table"), packageVersion, simplify=FALSE)

n = 5e7
set.seed(108)
df1 = data.frame(x=sample(n,n-1L), y1=rnorm(n-1L))
df2 = data.frame(x=sample(n,n-1L), y2=rnorm(n-1L))
dt1 = as.data.table(df1)
dt2 = as.data.table(df2)

mb = list()
# inner join
microbenchmark(times = 1L,
               base = merge(df1, df2, by = "x"),
               sqldf = sqldf("SELECT * FROM df1 INNER JOIN df2 ON df1.x = df2.x"),
               dplyr = inner_join(df1, df2, by = "x"),
               DT = dt1[dt2, nomatch=NULL, on = "x"]) -> mb$inner

# left outer join
microbenchmark(times = 1L,
               base = merge(df1, df2, by = "x", all.x = TRUE),
               sqldf = sqldf("SELECT * FROM df1 LEFT OUTER JOIN df2 ON df1.x = df2.x"),
               dplyr = left_join(df1, df2, by = c("x"="x")),
               DT = dt2[dt1, on = "x"]) -> mb$left

# right outer join
microbenchmark(times = 1L,
               base = merge(df1, df2, by = "x", all.y = TRUE),
               sqldf = sqldf("SELECT * FROM df2 LEFT OUTER JOIN df1 ON df2.x = df1.x"),
               dplyr = right_join(df1, df2, by = "x"),
               DT = dt1[dt2, on = "x"]) -> mb$right

# full outer join
microbenchmark(times = 1L,
               base = merge(df1, df2, by = "x", all = TRUE),
               dplyr = full_join(df1, df2, by = "x"),
               DT = merge(dt1, dt2, by = "x", all = TRUE)) -> mb$full

lapply(mb, print) -> nul
jangorecki
fuente
¿Vale la pena agregar un ejemplo que muestre cómo usar diferentes nombres de columna en el on = también?
SymbolixAU
1
@Symbolix podemos esperar el lanzamiento de 1.9.8 ya que agregará operadores no equi a onarg
jangorecki
Otro pensamiento; ¿Vale la pena agregar una nota con que merge.data.tableexiste el sort = TRUEargumento predeterminado , que agrega una clave durante la fusión y la deja allí en el resultado? Esto es algo a tener en cuenta, especialmente si está tratando de evitar configurar las teclas.
SymbolixAU
1
Me sorprende que nadie haya mencionado que la mayoría de ellos no funcionan si hay dups ...
statquant
@statquant Puedes hacer una unión cartesiana data.table, ¿qué quieres decir? Puede ser más específico, por favor.
David Arenburg
32

dplyr desde 0.4 implementó todas esas uniones incluidas outer_join, pero valió la pena señalar que durante las primeras versiones anteriores a 0.4 solía no ofrecer outer_join, y como resultado había un montón de código de usuario de solución alternativa muy malo flotando durante bastante tiempo después (todavía puede encontrar dicho código en SO, respuestas de Kaggle, github de ese período. Por lo tanto, esta respuesta todavía tiene un propósito útil).

Destacados de lanzamiento relacionados con Join :

v0.5 (6/2016)

  • Manejo para el tipo POSIXct, zonas horarias, duplicados, diferentes niveles de factores. Mejores errores y advertencias.
  • Nuevo argumento de sufijo para controlar qué sufijo reciben los nombres de variables duplicadas (# 1296)

v0.4.0 (1/2015)

  • Implementar unión derecha y unión externa (# 96)
  • Unir mutaciones, que agregan nuevas variables a una tabla a partir de filas coincidentes en otra. Uniones de filtrado, que filtran las observaciones de una tabla en función de si coinciden o no con una observación en la otra tabla.

v0.3 (10/2014)

  • Ahora puede left_join por diferentes variables en cada tabla: df1%>% left_join (df2, c ("var1" = "var2"))

v0.2 (5/2014)

  • * _join () ya no reordena los nombres de columna (# 324)

v0.1.3 (4/2014)

Soluciones para los comentarios de hadley en ese tema:

  • right_join (x, y) es lo mismo que left_join (y, x) en términos de las filas, solo las columnas tendrán diferentes órdenes. Trabaja fácilmente con select (new_column_order)
  • external_join es básicamente union (left_join (x, y), right_join (x, y)), es decir, preservar todas las filas en ambos marcos de datos.
smci
fuente
1
@ Gregor: no, no se debe eliminar. Es importante que los usuarios de R sepan que las capacidades de unión estuvieron ausentes durante muchos años, ya que la mayoría del código contiene soluciones alternativas o implementaciones manuales ad-hoc, o anuncios publicitarios con vectores de índices, o peor aún, evita usar estos paquetes o operaciones en absoluto. Cada semana veo esas preguntas en SO. Estaremos deshaciendo la confusión durante muchos años por venir.
smci
@Gregor y otros que preguntaron: actualizado, resumiendo los cambios históricos y lo que faltaba durante varios años cuando se hizo esta pregunta. Esto ilustra por qué el código de ese período fue mayormente hacky, o evitó el uso de uniones dplyr y volvió a fusionarse. Si verifica las bases de códigos históricas en SO y Kaggle, aún puede ver el retraso de la adopción y el código de usuario gravemente confuso que esto provocó. Avíseme si aún encuentra que esta respuesta es insuficiente.
smci
@ Gregor: Aquellos de nosotros que lo adoptamos a mediados de 2014 no elegimos el mejor momento. (Pensé que había versiones anteriores (0.0.x) en 2013, pero no, mi error.) Independientemente, todavía había mucho código basura en 2015, eso fue lo que me motivó a publicar esto, estaba tratando de desmitificar la porquería que encontré en Kaggle, github, SO.
smci
2
Sí, lo entiendo, y creo que haces un buen trabajo al respecto. (Yo era uno de los primeros también, y mientras todavía me gusta la dplyrsintaxis, el cambio de lazyevala rlangbackends rompió un montón de código para mí, que me volvía a aprender más data.table, y ahora sobre todo utilizar data.table.)
Gregor Thomas
@ Gregor: interesante, ¿puedes señalarme cualquier Q&A (tuya o de alguien más) que cubra eso? Parece que cada una de nuestras adopciones de plyr/ dplyr/ data.table/ tidyverse depende en gran medida de qué año comenzamos, y de qué estado (embrionario) estaban los paquetes en ese entonces, a diferencia de ahora ...
smci
25

Al unir dos marcos de datos con ~ 1 millón de filas cada uno, uno con 2 columnas y el otro con ~ 20, sorprendentemente he encontrado merge(..., all.x = TRUE, all.y = TRUE)que es más rápido entonces dplyr::full_join(). Esto es con dplyr v0.4

La fusión tarda ~ 17 segundos, full_join tarda ~ 65 segundos.

Sin embargo, algo de comida, ya que generalmente prefiero dplyr para tareas de manipulación.

BradP
fuente
24

Para el caso de una unión izquierda con una 0..*:0..1cardinalidad o una unión derecha con una 0..1:0..*cardinalidad, es posible asignar en el lugar las columnas unilaterales de la unión (la 0..1tabla) directamente sobre la unión (la 0..*tabla), y así evitar la creación de Una tabla de datos completamente nueva. Esto requiere hacer coincidir las columnas clave del participante en el ensamblador e indexar + ordenar las filas del ensamblador en consecuencia para la asignación.

Si la clave es una sola columna, entonces podemos usar una sola llamada a match()para hacer la coincidencia. Este es el caso que cubriré en esta respuesta.

Aquí hay un ejemplo basado en el OP, excepto que agregué una fila adicional df2con una identificación de 7 para probar el caso de una clave que no coincide en la combinación. Esto se df1deja efectivamente unirse df2:

df1 <- data.frame(CustomerId=1:6,Product=c(rep('Toaster',3L),rep('Radio',3L)));
df2 <- data.frame(CustomerId=c(2L,4L,6L,7L),State=c(rep('Alabama',2L),'Ohio','Texas'));
df1[names(df2)[-1L]] <- df2[match(df1[,1L],df2[,1L]),-1L];
df1;
##   CustomerId Product   State
## 1          1 Toaster    <NA>
## 2          2 Toaster Alabama
## 3          3 Toaster    <NA>
## 4          4   Radio Alabama
## 5          5   Radio    <NA>
## 6          6   Radio    Ohio

En lo anterior codifiqué la suposición de que la columna clave es la primera columna de ambas tablas de entrada. Yo diría que, en general, esto no es una suposición irrazonable, ya que, si tiene un data.frame con una columna clave, sería extraño si no se hubiera configurado como la primera columna del data.frame desde El principio. Y siempre puede reordenar las columnas para que sea así. Una consecuencia ventajosa de esta suposición es que el nombre de la columna clave no tiene que estar codificado, aunque supongo que solo está reemplazando una suposición por otra. La concisión es otra ventaja de la indexación de enteros, así como la velocidad. En los puntos de referencia a continuación, cambiaré la implementación para usar la indexación de nombre de cadena para que coincida con las implementaciones de la competencia.

Creo que esta es una solución particularmente apropiada si tiene varias tablas que desea unir en una sola tabla grande. La reconstrucción repetida de la tabla completa para cada fusión sería innecesaria e ineficiente.

Por otro lado, si necesita que el participante permanezca inalterado a través de esta operación por cualquier razón, entonces esta solución no se puede usar, ya que modifica al participante directamente. Aunque en ese caso, simplemente podría hacer una copia y realizar las asignaciones en el lugar en la copia.


Como nota al margen, analicé brevemente las posibles soluciones de coincidencia para las claves de varias columnas. Desafortunadamente, las únicas soluciones coincidentes que encontré fueron:

  • concatenaciones ineficientes. por ejemplo match(interaction(df1$a,df1$b),interaction(df2$a,df2$b)), o la misma idea con paste().
  • conjunciones cartesianas ineficientes, por ejemplo outer(df1$a,df2$a,`==`) & outer(df1$b,df2$b,`==`).
  • base R merge()y funciones de fusión basadas en paquetes equivalentes, que siempre asignan una nueva tabla para devolver el resultado combinado, y por lo tanto no son adecuadas para una solución basada en asignación en el lugar.

Por ejemplo, vea Hacer coincidir varias columnas en diferentes marcos de datos y obtener otra columna como resultado , hacer coincidir dos columnas con otras dos columnas , Hacer coincidir en varias columnas y el engaño de esta pregunta donde originalmente se me ocurrió la solución in situ, Combinar dos tramas de datos con diferente número de filas de R .


Benchmarking

Decidí hacer mi propia evaluación comparativa para ver cómo el enfoque de asignación en el lugar se compara con las otras soluciones que se han ofrecido en esta pregunta.

Código de prueba:

library(microbenchmark);
library(data.table);
library(sqldf);
library(plyr);
library(dplyr);

solSpecs <- list(
    merge=list(testFuncs=list(
        inner=function(df1,df2,key) merge(df1,df2,key),
        left =function(df1,df2,key) merge(df1,df2,key,all.x=T),
        right=function(df1,df2,key) merge(df1,df2,key,all.y=T),
        full =function(df1,df2,key) merge(df1,df2,key,all=T)
    )),
    data.table.unkeyed=list(argSpec='data.table.unkeyed',testFuncs=list(
        inner=function(dt1,dt2,key) dt1[dt2,on=key,nomatch=0L,allow.cartesian=T],
        left =function(dt1,dt2,key) dt2[dt1,on=key,allow.cartesian=T],
        right=function(dt1,dt2,key) dt1[dt2,on=key,allow.cartesian=T],
        full =function(dt1,dt2,key) merge(dt1,dt2,key,all=T,allow.cartesian=T) ## calls merge.data.table()
    )),
    data.table.keyed=list(argSpec='data.table.keyed',testFuncs=list(
        inner=function(dt1,dt2) dt1[dt2,nomatch=0L,allow.cartesian=T],
        left =function(dt1,dt2) dt2[dt1,allow.cartesian=T],
        right=function(dt1,dt2) dt1[dt2,allow.cartesian=T],
        full =function(dt1,dt2) merge(dt1,dt2,all=T,allow.cartesian=T) ## calls merge.data.table()
    )),
    sqldf.unindexed=list(testFuncs=list( ## note: must pass connection=NULL to avoid running against the live DB connection, which would result in collisions with the residual tables from the last query upload
        inner=function(df1,df2,key) sqldf(paste0('select * from df1 inner join df2 using(',paste(collapse=',',key),')'),connection=NULL),
        left =function(df1,df2,key) sqldf(paste0('select * from df1 left join df2 using(',paste(collapse=',',key),')'),connection=NULL),
        right=function(df1,df2,key) sqldf(paste0('select * from df2 left join df1 using(',paste(collapse=',',key),')'),connection=NULL) ## can't do right join proper, not yet supported; inverted left join is equivalent
        ##full =function(df1,df2,key) sqldf(paste0('select * from df1 full join df2 using(',paste(collapse=',',key),')'),connection=NULL) ## can't do full join proper, not yet supported; possible to hack it with a union of left joins, but too unreasonable to include in testing
    )),
    sqldf.indexed=list(testFuncs=list( ## important: requires an active DB connection with preindexed main.df1 and main.df2 ready to go; arguments are actually ignored
        inner=function(df1,df2,key) sqldf(paste0('select * from main.df1 inner join main.df2 using(',paste(collapse=',',key),')')),
        left =function(df1,df2,key) sqldf(paste0('select * from main.df1 left join main.df2 using(',paste(collapse=',',key),')')),
        right=function(df1,df2,key) sqldf(paste0('select * from main.df2 left join main.df1 using(',paste(collapse=',',key),')')) ## can't do right join proper, not yet supported; inverted left join is equivalent
        ##full =function(df1,df2,key) sqldf(paste0('select * from main.df1 full join main.df2 using(',paste(collapse=',',key),')')) ## can't do full join proper, not yet supported; possible to hack it with a union of left joins, but too unreasonable to include in testing
    )),
    plyr=list(testFuncs=list(
        inner=function(df1,df2,key) join(df1,df2,key,'inner'),
        left =function(df1,df2,key) join(df1,df2,key,'left'),
        right=function(df1,df2,key) join(df1,df2,key,'right'),
        full =function(df1,df2,key) join(df1,df2,key,'full')
    )),
    dplyr=list(testFuncs=list(
        inner=function(df1,df2,key) inner_join(df1,df2,key),
        left =function(df1,df2,key) left_join(df1,df2,key),
        right=function(df1,df2,key) right_join(df1,df2,key),
        full =function(df1,df2,key) full_join(df1,df2,key)
    )),
    in.place=list(testFuncs=list(
        left =function(df1,df2,key) { cns <- setdiff(names(df2),key); df1[cns] <- df2[match(df1[,key],df2[,key]),cns]; df1; },
        right=function(df1,df2,key) { cns <- setdiff(names(df1),key); df2[cns] <- df1[match(df2[,key],df1[,key]),cns]; df2; }
    ))
);

getSolTypes <- function() names(solSpecs);
getJoinTypes <- function() unique(unlist(lapply(solSpecs,function(x) names(x$testFuncs))));
getArgSpec <- function(argSpecs,key=NULL) if (is.null(key)) argSpecs$default else argSpecs[[key]];

initSqldf <- function() {
    sqldf(); ## creates sqlite connection on first run, cleans up and closes existing connection otherwise
    if (exists('sqldfInitFlag',envir=globalenv(),inherits=F) && sqldfInitFlag) { ## false only on first run
        sqldf(); ## creates a new connection
    } else {
        assign('sqldfInitFlag',T,envir=globalenv()); ## set to true for the one and only time
    }; ## end if
    invisible();
}; ## end initSqldf()

setUpBenchmarkCall <- function(argSpecs,joinType,solTypes=getSolTypes(),env=parent.frame()) {
    ## builds and returns a list of expressions suitable for passing to the list argument of microbenchmark(), and assigns variables to resolve symbol references in those expressions
    callExpressions <- list();
    nms <- character();
    for (solType in solTypes) {
        testFunc <- solSpecs[[solType]]$testFuncs[[joinType]];
        if (is.null(testFunc)) next; ## this join type is not defined for this solution type
        testFuncName <- paste0('tf.',solType);
        assign(testFuncName,testFunc,envir=env);
        argSpecKey <- solSpecs[[solType]]$argSpec;
        argSpec <- getArgSpec(argSpecs,argSpecKey);
        argList <- setNames(nm=names(argSpec$args),vector('list',length(argSpec$args)));
        for (i in seq_along(argSpec$args)) {
            argName <- paste0('tfa.',argSpecKey,i);
            assign(argName,argSpec$args[[i]],envir=env);
            argList[[i]] <- if (i%in%argSpec$copySpec) call('copy',as.symbol(argName)) else as.symbol(argName);
        }; ## end for
        callExpressions[[length(callExpressions)+1L]] <- do.call(call,c(list(testFuncName),argList),quote=T);
        nms[length(nms)+1L] <- solType;
    }; ## end for
    names(callExpressions) <- nms;
    callExpressions;
}; ## end setUpBenchmarkCall()

harmonize <- function(res) {
    res <- as.data.frame(res); ## coerce to data.frame
    for (ci in which(sapply(res,is.factor))) res[[ci]] <- as.character(res[[ci]]); ## coerce factor columns to character
    for (ci in which(sapply(res,is.logical))) res[[ci]] <- as.integer(res[[ci]]); ## coerce logical columns to integer (works around sqldf quirk of munging logicals to integers)
    ##for (ci in which(sapply(res,inherits,'POSIXct'))) res[[ci]] <- as.double(res[[ci]]); ## coerce POSIXct columns to double (works around sqldf quirk of losing POSIXct class) ----- POSIXct doesn't work at all in sqldf.indexed
    res <- res[order(names(res))]; ## order columns
    res <- res[do.call(order,res),]; ## order rows
    res;
}; ## end harmonize()

checkIdentical <- function(argSpecs,solTypes=getSolTypes()) {
    for (joinType in getJoinTypes()) {
        callExpressions <- setUpBenchmarkCall(argSpecs,joinType,solTypes);
        if (length(callExpressions)<2L) next;
        ex <- harmonize(eval(callExpressions[[1L]]));
        for (i in seq(2L,len=length(callExpressions)-1L)) {
            y <- harmonize(eval(callExpressions[[i]]));
            if (!isTRUE(all.equal(ex,y,check.attributes=F))) {
                ex <<- ex;
                y <<- y;
                solType <- names(callExpressions)[i];
                stop(paste0('non-identical: ',solType,' ',joinType,'.'));
            }; ## end if
        }; ## end for
    }; ## end for
    invisible();
}; ## end checkIdentical()

testJoinType <- function(argSpecs,joinType,solTypes=getSolTypes(),metric=NULL,times=100L) {
    callExpressions <- setUpBenchmarkCall(argSpecs,joinType,solTypes);
    bm <- microbenchmark(list=callExpressions,times=times);
    if (is.null(metric)) return(bm);
    bm <- summary(bm);
    res <- setNames(nm=names(callExpressions),bm[[metric]]);
    attr(res,'unit') <- attr(bm,'unit');
    res;
}; ## end testJoinType()

testAllJoinTypes <- function(argSpecs,solTypes=getSolTypes(),metric=NULL,times=100L) {
    joinTypes <- getJoinTypes();
    resList <- setNames(nm=joinTypes,lapply(joinTypes,function(joinType) testJoinType(argSpecs,joinType,solTypes,metric,times)));
    if (is.null(metric)) return(resList);
    units <- unname(unlist(lapply(resList,attr,'unit')));
    res <- do.call(data.frame,c(list(join=joinTypes),setNames(nm=solTypes,rep(list(rep(NA_real_,length(joinTypes))),length(solTypes))),list(unit=units,stringsAsFactors=F)));
    for (i in seq_along(resList)) res[i,match(names(resList[[i]]),names(res))] <- resList[[i]];
    res;
}; ## end testAllJoinTypes()

testGrid <- function(makeArgSpecsFunc,sizes,overlaps,solTypes=getSolTypes(),joinTypes=getJoinTypes(),metric='median',times=100L) {

    res <- expand.grid(size=sizes,overlap=overlaps,joinType=joinTypes,stringsAsFactors=F);
    res[solTypes] <- NA_real_;
    res$unit <- NA_character_;
    for (ri in seq_len(nrow(res))) {

        size <- res$size[ri];
        overlap <- res$overlap[ri];
        joinType <- res$joinType[ri];

        argSpecs <- makeArgSpecsFunc(size,overlap);

        checkIdentical(argSpecs,solTypes);

        cur <- testJoinType(argSpecs,joinType,solTypes,metric,times);
        res[ri,match(names(cur),names(res))] <- cur;
        res$unit[ri] <- attr(cur,'unit');

    }; ## end for

    res;

}; ## end testGrid()

Aquí hay un punto de referencia del ejemplo basado en el OP que demostré anteriormente:

## OP's example, supplemented with a non-matching row in df2
argSpecs <- list(
    default=list(copySpec=1:2,args=list(
        df1 <- data.frame(CustomerId=1:6,Product=c(rep('Toaster',3L),rep('Radio',3L))),
        df2 <- data.frame(CustomerId=c(2L,4L,6L,7L),State=c(rep('Alabama',2L),'Ohio','Texas')),
        'CustomerId'
    )),
    data.table.unkeyed=list(copySpec=1:2,args=list(
        as.data.table(df1),
        as.data.table(df2),
        'CustomerId'
    )),
    data.table.keyed=list(copySpec=1:2,args=list(
        setkey(as.data.table(df1),CustomerId),
        setkey(as.data.table(df2),CustomerId)
    ))
);
## prepare sqldf
initSqldf();
sqldf('create index df1_key on df1(CustomerId);'); ## upload and create an sqlite index on df1
sqldf('create index df2_key on df2(CustomerId);'); ## upload and create an sqlite index on df2

checkIdentical(argSpecs);

testAllJoinTypes(argSpecs,metric='median');
##    join    merge data.table.unkeyed data.table.keyed sqldf.unindexed sqldf.indexed      plyr    dplyr in.place         unit
## 1 inner  644.259           861.9345          923.516        9157.752      1580.390  959.2250 270.9190       NA microseconds
## 2  left  713.539           888.0205          910.045        8820.334      1529.714  968.4195 270.9185 224.3045 microseconds
## 3 right 1221.804           909.1900          923.944        8930.668      1533.135 1063.7860 269.8495 218.1035 microseconds
## 4  full 1302.203          3107.5380         3184.729              NA            NA 1593.6475 270.7055       NA microseconds

Aquí comparo datos de entrada aleatorios, probando diferentes escalas y diferentes patrones de superposición de teclas entre las dos tablas de entrada. Este punto de referencia todavía está restringido al caso de una clave entera de una sola columna. Además, para garantizar que la solución en el lugar funcione para las uniones izquierda y derecha de las mismas tablas, todos los datos de prueba aleatorios usan 0..1:0..1cardinalidad. Esto se implementa mediante el muestreo sin reemplazo de la columna clave del primer data.frame al generar la columna clave del segundo data.frame.

makeArgSpecs.singleIntegerKey.optionalOneToOne <- function(size,overlap) {

    com <- as.integer(size*overlap);

    argSpecs <- list(
        default=list(copySpec=1:2,args=list(
            df1 <- data.frame(id=sample(size),y1=rnorm(size),y2=rnorm(size)),
            df2 <- data.frame(id=sample(c(if (com>0L) sample(df1$id,com) else integer(),seq(size+1L,len=size-com))),y3=rnorm(size),y4=rnorm(size)),
            'id'
        )),
        data.table.unkeyed=list(copySpec=1:2,args=list(
            as.data.table(df1),
            as.data.table(df2),
            'id'
        )),
        data.table.keyed=list(copySpec=1:2,args=list(
            setkey(as.data.table(df1),id),
            setkey(as.data.table(df2),id)
        ))
    );
    ## prepare sqldf
    initSqldf();
    sqldf('create index df1_key on df1(id);'); ## upload and create an sqlite index on df1
    sqldf('create index df2_key on df2(id);'); ## upload and create an sqlite index on df2

    argSpecs;

}; ## end makeArgSpecs.singleIntegerKey.optionalOneToOne()

## cross of various input sizes and key overlaps
sizes <- c(1e1L,1e3L,1e6L);
overlaps <- c(0.99,0.5,0.01);
system.time({ res <- testGrid(makeArgSpecs.singleIntegerKey.optionalOneToOne,sizes,overlaps); });
##     user   system  elapsed
## 22024.65 12308.63 34493.19

Escribí un código para crear diagramas de log-log de los resultados anteriores. Genere un gráfico separado para cada porcentaje de superposición. Está un poco desordenado, pero me gusta tener todos los tipos de soluciones y tipos de unión representados en la misma trama.

Usé la interpolación de spline para mostrar una curva suave para cada combinación de solución / tipo de unión, dibujada con símbolos pch individuales. El tipo de unión es capturado por el símbolo pch, usando un punto para los corchetes angulares internos, izquierdo y derecho para izquierda y derecha, y un diamante para completo. El tipo de solución es capturado por el color como se muestra en la leyenda.

plotRes <- function(res,titleFunc,useFloor=F) {
    solTypes <- setdiff(names(res),c('size','overlap','joinType','unit')); ## derive from res
    normMult <- c(microseconds=1e-3,milliseconds=1); ## normalize to milliseconds
    joinTypes <- getJoinTypes();
    cols <- c(merge='purple',data.table.unkeyed='blue',data.table.keyed='#00DDDD',sqldf.unindexed='brown',sqldf.indexed='orange',plyr='red',dplyr='#00BB00',in.place='magenta');
    pchs <- list(inner=20L,left='<',right='>',full=23L);
    cexs <- c(inner=0.7,left=1,right=1,full=0.7);
    NP <- 60L;
    ord <- order(decreasing=T,colMeans(res[res$size==max(res$size),solTypes],na.rm=T));
    ymajors <- data.frame(y=c(1,1e3),label=c('1ms','1s'),stringsAsFactors=F);
    for (overlap in unique(res$overlap)) {
        x1 <- res[res$overlap==overlap,];
        x1[solTypes] <- x1[solTypes]*normMult[x1$unit]; x1$unit <- NULL;
        xlim <- c(1e1,max(x1$size));
        xticks <- 10^seq(log10(xlim[1L]),log10(xlim[2L]));
        ylim <- c(1e-1,10^((if (useFloor) floor else ceiling)(log10(max(x1[solTypes],na.rm=T))))); ## use floor() to zoom in a little more, only sqldf.unindexed will break above, but xpd=NA will keep it visible
        yticks <- 10^seq(log10(ylim[1L]),log10(ylim[2L]));
        yticks.minor <- rep(yticks[-length(yticks)],each=9L)*1:9;
        plot(NA,xlim=xlim,ylim=ylim,xaxs='i',yaxs='i',axes=F,xlab='size (rows)',ylab='time (ms)',log='xy');
        abline(v=xticks,col='lightgrey');
        abline(h=yticks.minor,col='lightgrey',lty=3L);
        abline(h=yticks,col='lightgrey');
        axis(1L,xticks,parse(text=sprintf('10^%d',as.integer(log10(xticks)))));
        axis(2L,yticks,parse(text=sprintf('10^%d',as.integer(log10(yticks)))),las=1L);
        axis(4L,ymajors$y,ymajors$label,las=1L,tick=F,cex.axis=0.7,hadj=0.5);
        for (joinType in rev(joinTypes)) { ## reverse to draw full first, since it's larger and would be more obtrusive if drawn last
            x2 <- x1[x1$joinType==joinType,];
            for (solType in solTypes) {
                if (any(!is.na(x2[[solType]]))) {
                    xy <- spline(x2$size,x2[[solType]],xout=10^(seq(log10(x2$size[1L]),log10(x2$size[nrow(x2)]),len=NP)));
                    points(xy$x,xy$y,pch=pchs[[joinType]],col=cols[solType],cex=cexs[joinType],xpd=NA);
                }; ## end if
            }; ## end for
        }; ## end for
        ## custom legend
        ## due to logarithmic skew, must do all distance calcs in inches, and convert to user coords afterward
        ## the bottom-left corner of the legend will be defined in normalized figure coords, although we can convert to inches immediately
        leg.cex <- 0.7;
        leg.x.in <- grconvertX(0.275,'nfc','in');
        leg.y.in <- grconvertY(0.6,'nfc','in');
        leg.x.user <- grconvertX(leg.x.in,'in');
        leg.y.user <- grconvertY(leg.y.in,'in');
        leg.outpad.w.in <- 0.1;
        leg.outpad.h.in <- 0.1;
        leg.midpad.w.in <- 0.1;
        leg.midpad.h.in <- 0.1;
        leg.sol.w.in <- max(strwidth(solTypes,'in',leg.cex));
        leg.sol.h.in <- max(strheight(solTypes,'in',leg.cex))*1.5; ## multiplication factor for greater line height
        leg.join.w.in <- max(strheight(joinTypes,'in',leg.cex))*1.5; ## ditto
        leg.join.h.in <- max(strwidth(joinTypes,'in',leg.cex));
        leg.main.w.in <- leg.join.w.in*length(joinTypes);
        leg.main.h.in <- leg.sol.h.in*length(solTypes);
        leg.x2.user <- grconvertX(leg.x.in+leg.outpad.w.in*2+leg.main.w.in+leg.midpad.w.in+leg.sol.w.in,'in');
        leg.y2.user <- grconvertY(leg.y.in+leg.outpad.h.in*2+leg.main.h.in+leg.midpad.h.in+leg.join.h.in,'in');
        leg.cols.x.user <- grconvertX(leg.x.in+leg.outpad.w.in+leg.join.w.in*(0.5+seq(0L,length(joinTypes)-1L)),'in');
        leg.lines.y.user <- grconvertY(leg.y.in+leg.outpad.h.in+leg.main.h.in-leg.sol.h.in*(0.5+seq(0L,length(solTypes)-1L)),'in');
        leg.sol.x.user <- grconvertX(leg.x.in+leg.outpad.w.in+leg.main.w.in+leg.midpad.w.in,'in');
        leg.join.y.user <- grconvertY(leg.y.in+leg.outpad.h.in+leg.main.h.in+leg.midpad.h.in,'in');
        rect(leg.x.user,leg.y.user,leg.x2.user,leg.y2.user,col='white');
        text(leg.sol.x.user,leg.lines.y.user,solTypes[ord],cex=leg.cex,pos=4L,offset=0);
        text(leg.cols.x.user,leg.join.y.user,joinTypes,cex=leg.cex,pos=4L,offset=0,srt=90); ## srt rotation applies *after* pos/offset positioning
        for (i in seq_along(joinTypes)) {
            joinType <- joinTypes[i];
            points(rep(leg.cols.x.user[i],length(solTypes)),ifelse(colSums(!is.na(x1[x1$joinType==joinType,solTypes[ord]]))==0L,NA,leg.lines.y.user),pch=pchs[[joinType]],col=cols[solTypes[ord]]);
        }; ## end for
        title(titleFunc(overlap));
        readline(sprintf('overlap %.02f',overlap));
    }; ## end for
}; ## end plotRes()

titleFunc <- function(overlap) sprintf('R merge solutions: single-column integer key, 0..1:0..1 cardinality, %d%% overlap',as.integer(overlap*100));
plotRes(res,titleFunc,T);

R-merge-benchmark-single-column-integer-key-optional-one-to-one-99

R-merge-benchmark-single-column-integer-key-optional-one-to-one-50

R-merge-benchmark-single-column-integer-key-optional-one-to-one-1


Aquí hay un segundo punto de referencia a gran escala que es más resistente, con respecto al número y los tipos de columnas clave, así como a la cardinalidad. Para este punto de referencia utilizo tres columnas clave: un carácter, un número entero y uno lógico, sin restricciones de cardinalidad (es decir, 0..*:0..*). (En general, no es aconsejable definir columnas clave con valores dobles o complejos debido a complicaciones de comparación de punto flotante, y básicamente nadie usa el tipo sin formato, mucho menos para columnas clave, por lo que no he incluido esos tipos en la clave Además, por el bien de la información, inicialmente intenté usar cuatro columnas clave al incluir una columna de clave POSIXct, pero el tipo POSIXct no funcionó bien con la sqldf.indexedsolución por alguna razón, posiblemente debido a anomalías de comparación de punto flotante, así que eliminado)

makeArgSpecs.assortedKey.optionalManyToMany <- function(size,overlap,uniquePct=75) {

    ## number of unique keys in df1
    u1Size <- as.integer(size*uniquePct/100);

    ## (roughly) divide u1Size into bases, so we can use expand.grid() to produce the required number of unique key values with repetitions within individual key columns
    ## use ceiling() to ensure we cover u1Size; will truncate afterward
    u1SizePerKeyColumn <- as.integer(ceiling(u1Size^(1/3)));

    ## generate the unique key values for df1
    keys1 <- expand.grid(stringsAsFactors=F,
        idCharacter=replicate(u1SizePerKeyColumn,paste(collapse='',sample(letters,sample(4:12,1L),T))),
        idInteger=sample(u1SizePerKeyColumn),
        idLogical=sample(c(F,T),u1SizePerKeyColumn,T)
        ##idPOSIXct=as.POSIXct('2016-01-01 00:00:00','UTC')+sample(u1SizePerKeyColumn)
    )[seq_len(u1Size),];

    ## rbind some repetitions of the unique keys; this will prepare one side of the many-to-many relationship
    ## also scramble the order afterward
    keys1 <- rbind(keys1,keys1[sample(nrow(keys1),size-u1Size,T),])[sample(size),];

    ## common and unilateral key counts
    com <- as.integer(size*overlap);
    uni <- size-com;

    ## generate some unilateral keys for df2 by synthesizing outside of the idInteger range of df1
    keys2 <- data.frame(stringsAsFactors=F,
        idCharacter=replicate(uni,paste(collapse='',sample(letters,sample(4:12,1L),T))),
        idInteger=u1SizePerKeyColumn+sample(uni),
        idLogical=sample(c(F,T),uni,T)
        ##idPOSIXct=as.POSIXct('2016-01-01 00:00:00','UTC')+u1SizePerKeyColumn+sample(uni)
    );

    ## rbind random keys from df1; this will complete the many-to-many relationship
    ## also scramble the order afterward
    keys2 <- rbind(keys2,keys1[sample(nrow(keys1),com,T),])[sample(size),];

    ##keyNames <- c('idCharacter','idInteger','idLogical','idPOSIXct');
    keyNames <- c('idCharacter','idInteger','idLogical');
    ## note: was going to use raw and complex type for two of the non-key columns, but data.table doesn't seem to fully support them
    argSpecs <- list(
        default=list(copySpec=1:2,args=list(
            df1 <- cbind(stringsAsFactors=F,keys1,y1=sample(c(F,T),size,T),y2=sample(size),y3=rnorm(size),y4=replicate(size,paste(collapse='',sample(letters,sample(4:12,1L),T)))),
            df2 <- cbind(stringsAsFactors=F,keys2,y5=sample(c(F,T),size,T),y6=sample(size),y7=rnorm(size),y8=replicate(size,paste(collapse='',sample(letters,sample(4:12,1L),T)))),
            keyNames
        )),
        data.table.unkeyed=list(copySpec=1:2,args=list(
            as.data.table(df1),
            as.data.table(df2),
            keyNames
        )),
        data.table.keyed=list(copySpec=1:2,args=list(
            setkeyv(as.data.table(df1),keyNames),
            setkeyv(as.data.table(df2),keyNames)
        ))
    );
    ## prepare sqldf
    initSqldf();
    sqldf(paste0('create index df1_key on df1(',paste(collapse=',',keyNames),');')); ## upload and create an sqlite index on df1
    sqldf(paste0('create index df2_key on df2(',paste(collapse=',',keyNames),');')); ## upload and create an sqlite index on df2

    argSpecs;

}; ## end makeArgSpecs.assortedKey.optionalManyToMany()

sizes <- c(1e1L,1e3L,1e5L); ## 1e5L instead of 1e6L to respect more heavy-duty inputs
overlaps <- c(0.99,0.5,0.01);
solTypes <- setdiff(getSolTypes(),'in.place');
system.time({ res <- testGrid(makeArgSpecs.assortedKey.optionalManyToMany,sizes,overlaps,solTypes); });
##     user   system  elapsed
## 38895.50   784.19 39745.53

Las parcelas resultantes, utilizando el mismo código de trazado dado anteriormente:

titleFunc <- function(overlap) sprintf('R merge solutions: character/integer/logical key, 0..*:0..* cardinality, %d%% overlap',as.integer(overlap*100));
plotRes(res,titleFunc,F);

R-merge-benchmark-assorted-key-optional-many-to-many-99

R-merge-benchmark-assorted-key-optional-many-to-many-50

R-merge-benchmark-assorted-key-optional-many-to-many-1

bgoldst
fuente
muy buen análisis, pero es una pena que establezca una escala de 10 ^ 1 a 10 ^ 6, esos son conjuntos tan pequeños que la diferencia de velocidad es casi irrelevante. ¡10 ^ 6 a 10 ^ 8 sería interesante de ver!
jangorecki
1
También vi que incluyes el tiempo de coerción de clase en el punto de referencia, lo que lo hace inválido para la operación de unión.
Jangorecki
8
  1. Usando la mergefunción podemos seleccionar la variable de la tabla izquierda o la tabla derecha, de la misma manera que todos estamos familiarizados con la instrucción select en SQL (EX: Seleccione a. * ... o Seleccione b. * De .....)
  2. Tenemos que agregar código adicional que se subconjunto de la tabla recién unida.

    • SQL: - select a.* from df1 a inner join df2 b on a.CustomerId=b.CustomerId

    • R: - merge(df1, df2, by.x = "CustomerId", by.y = "CustomerId")[,names(df1)]

Mismo camino

  • SQL: - select b.* from df1 a inner join df2 b on a.CustomerId=b.CustomerId

  • R: - merge(df1, df2, by.x = "CustomerId", by.y = "CustomerId")[,names(df2)]

sanjeeb
fuente
7

Para una unión interna en todas las columnas, también puede usar fintersectdesde data.table -package o intersectdesde dplyr -package como una alternativa mergesin especificar las by-columnas. esto dará las filas que son iguales entre dos marcos de datos:

merge(df1, df2)
#   V1 V2
# 1  B  2
# 2  C  3
dplyr::intersect(df1, df2)
#   V1 V2
# 1  B  2
# 2  C  3
data.table::fintersect(setDT(df1), setDT(df2))
#    V1 V2
# 1:  B  2
# 2:  C  3

Datos de ejemplo:

df1 <- data.frame(V1 = LETTERS[1:4], V2 = 1:4)
df2 <- data.frame(V1 = LETTERS[2:3], V2 = 2:3)
Jaap
fuente
5

Actualizar unirse. Otra combinación importante de estilo SQL es una " combinación de actualización " donde las columnas de una tabla se actualizan (o crean) utilizando otra tabla.

Modificando las tablas de ejemplo del OP ...

sales = data.frame(
  CustomerId = c(1, 1, 1, 3, 4, 6), 
  Year = 2000:2005,
  Product = c(rep("Toaster", 3), rep("Radio", 3))
)
cust = data.frame(
  CustomerId = c(1, 1, 4, 6), 
  Year = c(2001L, 2002L, 2002L, 2002L),
  State = state.name[1:4]
)

sales
# CustomerId Year Product
#          1 2000 Toaster
#          1 2001 Toaster
#          1 2002 Toaster
#          3 2003   Radio
#          4 2004   Radio
#          6 2005   Radio

cust
# CustomerId Year    State
#          1 2001  Alabama
#          1 2002   Alaska
#          4 2002  Arizona
#          6 2002 Arkansas

Supongamos que queremos agregar el estado del cliente custa la tabla de compras sales, ignorando la columna del año. Con la base R, podemos identificar filas coincidentes y luego copiar valores sobre:

sales$State <- cust$State[ match(sales$CustomerId, cust$CustomerId) ]

# CustomerId Year Product    State
#          1 2000 Toaster  Alabama
#          1 2001 Toaster  Alabama
#          1 2002 Toaster  Alabama
#          3 2003   Radio     <NA>
#          4 2004   Radio  Arizona
#          6 2005   Radio Arkansas

# cleanup for the next example
sales$State <- NULL

Como se puede ver aquí, matchselecciona la primera fila coincidente de la tabla de clientes.


Actualización de unirse con múltiples columnas. El enfoque anterior funciona bien cuando nos unimos en una sola columna y estamos satisfechos con la primera coincidencia. Supongamos que queremos que el año de medición en la tabla del cliente coincida con el año de venta.

Como respuesta @ de bgoldst menciona, matchcon interactionpodría ser una opción para este caso. Más directamente, uno podría usar data.table:

library(data.table)
setDT(sales); setDT(cust)

sales[, State := cust[sales, on=.(CustomerId, Year), x.State]]

#    CustomerId Year Product   State
# 1:          1 2000 Toaster    <NA>
# 2:          1 2001 Toaster Alabama
# 3:          1 2002 Toaster  Alaska
# 4:          3 2003   Radio    <NA>
# 5:          4 2004   Radio    <NA>
# 6:          6 2005   Radio    <NA>

# cleanup for next example
sales[, State := NULL]

Actualización continua unirse. Alternativamente, podemos querer tomar el último estado en el que se encontró al cliente:

sales[, State := cust[sales, on=.(CustomerId, Year), roll=TRUE, x.State]]

#    CustomerId Year Product    State
# 1:          1 2000 Toaster     <NA>
# 2:          1 2001 Toaster  Alabama
# 3:          1 2002 Toaster   Alaska
# 4:          3 2003   Radio     <NA>
# 5:          4 2004   Radio  Arizona
# 6:          6 2005   Radio Arkansas

Los tres ejemplos anteriores se centran en crear / agregar una nueva columna. Consulte las preguntas frecuentes relacionadas con R para ver un ejemplo de actualización / modificación de una columna existente.

Frank
fuente