Tengo dos data.frame
s con múltiples columnas comunes (en este caso date
, city
, ctry
, y ( other_
) number
).
Ahora me gustaría fusionarlos en las columnas anteriores, pero tolero algún nivel de diferencia:
threshold.numbers <- 3
threshold.date <- 5 # in days
Si la diferencia entre las date
entradas es > threshold.date
(en días) o > threshold.numbers
, no quiero que las líneas se fusionen. Del mismo modo, si la entrada en city
es una subcadena de la df
entrada del otro en la city
columna, quiero que las líneas se fusionen. [Si alguien tiene una mejor idea para probar la similitud de los nombres de ciudades reales, me alegraría saberlo.] (Y mantenga las primeras df
entradas de date
, city
y country
ambas ( other_
) number
columnas y todas las demás columnas en df
.
Considere el siguiente ejemplo:
df1 <- data.frame(date = c("2003-08-29", "1999-06-12", "2000-08-29", "1999-02-24", "2001-04-17",
"1999-06-30", "1999-03-16", "1999-07-16", "2001-08-29", "2002-07-30"),
city = c("Berlin", "Paris", "London", "Rome", "Bern",
"Copenhagen", "Warsaw", "Moscow", "Tunis", "Vienna"),
ctry = c("Germany", "France", "UK", "Italy", "Switzerland",
"Denmark", "Poland", "Russia", "Tunisia", "Austria"),
number = c(10, 20, 30, 40, 50, 60, 70, 80, 90, 100),
col = c("apple", "banana", "pear", "banana", "lemon", "cucumber", "apple", "peach", "cherry", "cherry"))
df2 <- data.frame(date = c("2003-08-29", "1999-06-12", "2000-08-29", "1999-02-24", "2001-04-17", # all identical to df1
"1999-06-29", "1999-03-14", "1999-07-17", # all 1-2 days different
"2000-01-29", "2002-07-01"), # all very different (> 2 weeks)
city = c("Berlin", "East-Paris", "near London", "Rome", # same or slight differences
"Zurich", # completely different
"Copenhagen", "Warsaw", "Moscow", "Tunis", "Vienna"), # same
ctry = c("Germany", "France", "UK", "Italy", "Switzerland", # all the same
"Denmark", "Poland", "Russia", "Tunisia", "Austria"),
other_number = c(13, 17, 3100, 45, 51, 61, 780, 85, 90, 101), # slightly different to very different
other_col = c("yellow", "green", "blue", "red", "purple", "orange", "blue", "red", "black", "beige"))
Ahora, me gustaría fusionar data.frames
y recibir un df
donde las líneas se fusionan si se cumplen las condiciones anteriores.
(La primera columna es solo para su conveniencia: detrás del primer dígito, que indica el caso original, muestra si las líneas se fusionaron ( .
) o si las líneas son de df1
( 1
) o df2
( 2
).
date city ctry number other_col other_number other_col2 #comment
1. 2003-08-29 Berlin Germany 10 apple 13 yellow # matched on date, city, number
2. 1999-06-12 Paris France 20 banana 17 green # matched on date, city similar, number - other_number == threshold.numbers
31 2000-08-29 London UK 30 pear <NA> <NA> # not matched: number - other_number > threshold.numbers
32 2000-08-29 near London UK <NA> <NA> 3100 blue #
41 1999-02-24 Rome Italy 40 banana <NA> <NA> # not matched: number - other_number > threshold.numbers
42 1999-02-24 Rome Italy <NA> <NA> 45 red #
51 2001-04-17 Bern Switzerland 50 lemon <NA> <NA> # not matched: cities different (dates okay, numbers okay)
52 2001-04-17 Zurich Switzerland <NA> <NA> 51 purple #
6. 1999-06-30 Copenhagen Denmark 60 cucumber 61 orange # matched: date difference < threshold.date (cities okay, dates okay)
71 1999-03-16 Warsaw Poland 70 apple <NA> <NA> # not matched: number - other_number > threshold.numbers (dates okay)
72 1999-03-14 Warsaw Poland <NA> <NA> 780 blue #
81 1999-07-16 Moscow Russia 80 peach <NA> <NA> # not matched: number - other_number > threshold.numbers (dates okay)
82 1999-07-17 Moscow Russia <NA> <NA> 85 red #
91 2001-08-29 Tunis Tunisia 90 cherry <NA> <NA> # not matched: date difference < threshold.date (cities okay, dates okay)
92 2000-01-29 Tunis Tunisia <NA> <NA> 90 black #
101 2002-07-30 Vienna Austria 100 cherry <NA> <NA> # not matched: date difference < threshold.date (cities okay, dates okay)
102 2002-07-01 Vienna Austria <NA> <NA> 101 beige #
Intenté diferentes implementaciones para fusionarlas, pero no puedo implementar el umbral.
EDITAR Disculpas por una formulación poco clara: me gustaría conservar todas las filas y recibir un indicador de si la fila coincide, no coincide y de df1 o no coincide y de df2.
el pseudocódigo es:
if there is a case where abs("date_df2" - "date_df1") <= threshold.date:
if "ctry_df2" == "ctry_df1":
if "city_df2" ~ "city_df1":
if abs("number_df2" - "number_df1") <= threshold.numbers:
merge and go to next row in df2
else:
add row to df1```
.
?Respuestas:
Aquí hay una solución que utiliza mi paquete safejoin , envolviendo en este caso el paquete fuzzyjoin .
Podemos usar el
by
argumento para especificar una condición compleja, usando la funciónX()
para obtener el valordf1
yY()
obtener el valordf2
.Si sus tablas reales son grandes, esto podría ser lento o imposible como lo hace un producto cartesiano, pero aquí funciona bien.
Lo que queremos es una unión completa (mantener todas las filas y unir lo que se puede unir), y queremos mantener el primer valor cuando se unen, y tomar el siguiente de otra manera, esto significa que queremos lidiar con el conflicto de columnas nombradas idénticamente por fusión, por lo que usamos el argumento
conflict = dplyr::coalesce
salida:
Creado el 13/11/2019 por el paquete reprex (v0.3.0)
Desafortunadamente, fuzzyjoin obliga a todas las columnas de una matriz al hacer una unión múltiple, y safejoin ajusta fuzzyjoin, por lo que debemos convertir las variables al tipo apropiado dentro del argumento by, esto explica las primeras líneas del
by
argumento.Más sobre safejoin : https://github.com/moodymudskipper/safejoin
fuente
Primero convertí los nombres de las ciudades en vectores de caracteres, ya que (si entendí correctamente) desea incluir los nombres de ciudades que figuran en df2.
Luego fusionarlos por país:
La biblioteca
stringr
le permitirá ver si city.x está dentro de city.y aquí (vea la última columna):Entonces puede obtener la diferencia en días entre fechas:
y la diferencia en números:
Así es como se ve el marco de datos resultante:
Pero queremos descartar cosas donde city.x no se encontró dentro de city.y, donde la diferencia de días es mayor que 5 o la diferencia numérica es mayor que 3:
Lo que queda son las tres filas que tenía arriba (que contenían puntos en la columna 1).
Ahora podemos soltar las tres columnas que creamos, y la fecha y la ciudad de df2:
fuente
Paso 1: fusionar los datos en función de "ciudad" y "ctry":
Paso 2: elimine las filas si la diferencia entre las entradas de fecha es> umbral.fecha (en días):
Paso 3: elimine las filas si la diferencia entre los números es> threshhold.number:
Los datos deben fusionarse antes de aplicar condiciones, en caso de que las filas no coincidan.
fuente
Una opción usando
data.table
(explicaciones en línea):salida:
fuente
Puede probar el
city
partido congrepl
yctry
simple con==
. Para aquellos que coinciden hasta aquí, puede calcular la diferencia de fecha convirtiéndola aldate
usoas.Date
y comparándola con adifftime
. Lanumber
diferencia se hace de la misma manera.fuente
Aquí hay un enfoque flexible que le permite especificar cualquier colección de criterios de fusión que elija.
Trabajo de preparación
Me aseguré de que todas las cuerdas en
df1
ydf2
fuera cadenas, no los factores (como se ha señalado en varias de las otras respuestas). También envolví las fechasas.Date
para convertirlas en fechas reales.Especificar los criterios de fusión
Crea una lista de listas. Cada elemento de la lista principal es un criterio; los miembros de un criterio son
final.col.name
: el nombre de la columna que queremos en la tabla finalcol.name.1
: el nombre de la columna endf1
col.name.2
: el nombre de la columna endf2
exact
: booleano; ¿Deberíamos hacer una coincidencia exacta en esta columna?threshold
: umbral (si no estamos haciendo coincidencia exacta)match.function
: una función que devuelve si las filas coinciden o no (para casos especiales como el usogrepl
para la coincidencia de cadenas; tenga en cuenta que esta función debe ser vectorizada)Función para fusionar
Esta función toma tres argumentos: los dos marcos de datos que queremos fusionar y la lista de criterios de coincidencia. Se procede de la siguiente manera:
Aplica la función, y terminamos
fuente