¿Cómo unir una tabla a un shapefile con ID y nombres que no coinciden (cadenas similares)?

8

Tengo un problema molesto para el que intento encontrar una solución automatizada. La versión abreviada es que tengo un archivo de formas y una tabla de datos creados para las regiones dentro de los países. La tabla de datos creada NO tiene ningún tipo de GID / códigos de administrador estandarizados para que coincidan con los archivos de forma, y ​​los nombres de las regiones tampoco son coincidencias exactas. Miremos más de cerca; Aquí está mi marco de datos ficticio + archivo de forma.

library(rgdal)

#load in shapefile
arm <- readOGR("D:/Country-Shapefiles/ARM_adm_shp", layer = "ARM_adm1")

#create dummy data frame
id <- c(100:110)
name <- c("Aragatsotn", "Ararat", "Armavir", "Gaghark'unik'", "Kotayk", "Lorri", 
          "Shirak", "Syunik'", "Tavush", "Vayots' Dzor", "Yerevan City")
value <- runif(11, 0.0, 1.0)
df <- data.frame(id, name, value)

Entonces, lo que tengo es una tabla con identificaciones aparentemente aleatorias, nombres de regiones y un valor que se trazará con un mapa coroplético. Se ve como esto:

> df
    id          name     value
1  100    Aragatsotn 0.6923852
2  101        Ararat 0.5762024
3  102       Armavir 0.4688358
4  103 Gaghark'unik' 0.4702253
5  104        Kotayk 0.9347992
6  105         Lorri 0.1937813
7  106        Shirak 0.5162604
8  107       Syunik' 0.4332389
9  108        Tavush 0.9889513
10 109  Vayots' Dzor 0.2182024
11 110  Yerevan City 0.5791886

Mirando los atributos de archivo shape de interés, tenemos esto:

> arm@data[c("ID_1", "NAME_1")]

       ID_1      NAME_1
    0     1  Aragatsotn
    1     2      Ararat
    2     3     Armavir
    3     4      Erevan
    4     5 Gegharkunik
    5     6      Kotayk
    6     7        Lori
    7     8      Shirak
    8     9      Syunik
    9    10      Tavush
    10   11 Vayots Dzor

Idealmente, dfincluiría algún tipo de ID de administrador coincidentes para unirse al shapefile. Lamentablemente, quien creó los datos que estoy usando no siguió estas convenciones. Alternativamente, sería genial hacer coincidir los nombres de las regiones en sí ... pero como puede ver, hay ligeras variaciones en cada nombre.

Hacer coincidir a mano siempre es una solución de respaldo, pero ¿quién quiere tomarse el tiempo para hacerlo? ;) Pero en realidad, incluso salvo la pereza, el proyecto en el que estoy trabajando estará mapeando docenas y docenas de países diferentes, así que estoy buscando una solución automatizada que pueda hacer todo sin tener que hacer nada a mano. es posible? ¿Puedo de alguna manera hacer coincidir estos nombres de región casi con los archivos de forma?

Nota al margen: Estoy buscando greplcoincidencias parciales de cadenas por esta publicación , pero no estoy seguro de si esta es una solución potencial porque tendré que extraer de los nombres de columna en lugar de ingresar cada nombre de región a mano.

EDITAR: cuando emparejo las ID a mano, lo que he hecho es crear una nueva columna en mi marco de datos y agregar los términos de coincidencia exactos del archivo de forma. Desafortunadamente, debido a las peculiaridades de los datos, el orden de los nombres tampoco coincide, por lo que esto aún requiere una entrada manual. Espero algún tipo de solución completamente automatizada (si es posible).

Lauren
fuente
Si tiene suerte y tiene el mismo número de registros en el mismo orden, tanto en el archivo de forma como en la tabla, puede copiar y pegar los nombres en columnas contiguas en una nueva tabla, unirlo al archivo de forma usando sus nombres y unirlo a la tabla usando sus nombres. (O usando una copia de su archivo shape, pegue los nombres de la tabla directamente en su dbf en una hoja Excel o Libre / Open Office anterior a 2007). Si no tiene un número exacto de registros uno a uno, sino muchos "tramos" largos de ellos puedes mezclar un poco de trabajo manual con copias y pastas.
Johns
Esto es lo que terminé haciendo manualmente a mano, pero desafortunadamente no están en el orden correcto. Incluso si se enumera alfabéticamente, es posible que no funcione todo el tiempo (en este ejemplo, Erevan = Ciudad de Ereván, que desordena el resto de la lista).
Lauren

Respuestas:

6

Iría por el stringdistpaquete que ha implementado muchos algoritmos para calcular la similitud parcial (distancia) de las cadenas incluidas Jaro-winkler. Aquí hay una solución rápida para usted:

  #df to be joined
  id <- c(100:111)
  name <- c("Aragatsotn", "Ararat", "Armavir", "Gaghark'unik'", "Kotayk", "Lorri", 
            "Shirak", "Syunik'", "Tavush", "Vayots' Dzor", "Yerevan City","Aragatsotn")
  value <- runif(12, 0.0, 1.0)
  df <- data.frame(id, name, value)

  #create shape data df
  shpNames <- c("Aragatsotn",
               "Ararat",
               "Armavir",
               "Erevan",
               "Gegharkunik",
               "Kotayk",
               "Lori",
               "Shirak",
               "Syunik",
               "Tavush",
               "VayotsDzor")
  arm.data  <- data.frame(ID_1=1:11,NAME_1=shpNames)

  #simple match (only testing)
  match(df$name,arm.data$NAME_1)
  #simple merge (testing)
  merge(arm.data,df,by.x="NAME_1",by.y="name",all.x=TRUE)

  #partial match using stringdist package
  library("stringdist")
  am<-amatch(arm.data$NAME_1,df$name,maxDist = 3)
  b<-data.frame()
  for (i in 1:dim(arm.data)[1]) {
      b<-rbind(b,data.frame(arm.data[i,],df[am[i],]))
  }
  b

produce:

ID_1      NAME_1  id          name     value
1     1  Aragatsotn 100    Aragatsotn 0.8510984
2     2      Ararat 101        Ararat 0.3004329
3     3     Armavir 102       Armavir 0.9258740
4     4      Erevan  NA          <NA>        NA
5     5 Gegharkunik 103 Gaghark'unik' 0.9935353
6     6      Kotayk 104        Kotayk 0.6025050
7     7        Lori 105         Lorri 0.9577662
8     8      Shirak 106        Shirak 0.6346550
9     9      Syunik 107       Syunik' 0.6531175
10   10      Tavush 108        Tavush 0.9726032
11   11  VayotsDzor 109  Vayots' Dzor 0.3457315

Puedes jugar con el parámetro maxDist del método amatch. ¡Aunque 3 funciona mejor con sus datos de muestra!

Farid Cheraghi
fuente
Sí, esto funcionó para mi ejemplo! ¡Ahora para probar algunos más! Pregunta relacionada: ¿cómo puedo lograr esta misma unión mientras mantengo el shapefile espacial? Parece que este bit de código acaba de crear un marco de datos con los datos unidos, pero aún así tendré que poder mapearlo.
Lauren
He creado el marco de datos manualmente para que su problema pueda ser reproducible. Cuando lee un archivo de forma a través de readOGR, la clase de salida sería una de las clases de derivados "sp", como "SpatialPointsDataFrame". Y todos tienen un atributo "data" que contiene todos los datos de atributo que son de tipo dataframe. En mi ejemplo, me estoy uniendo al marco de datos y la información geométrica está intacta. Así que para su ejemplo, simplemente cambie arm.dataa arm@data, y que funcionaría bien.
Farid Cheraghi
No lo use arm@data, eso crearía un gran desastre (los registros no coinciden con sus geometrías correctas)
Robert Hijmans
6

Quiero agregar algunos detalles a la respuesta de Farid Cher ya que este es un problema muy común. Usando amatchpuede hacer maravillas, pero con estos Spatialobjetos se debe no utilizar base::mergey no tener acceso a la @dataranura. Eso conduciría inevitablemente a un terrible desastre ( base::mergecambia el orden de los registros y ya no coincidirían con las geometrías).

En su lugar, use el sp::mergemétodo, usando el SpatialPolygonsDataFrameprimer argumento en merge. También tenga en cuenta el problema potencial de tener registros duplicados. Y agregué datos para que el ejemplo sea autónomo y reproducible.

library(raster)
#example data.frame
name <- c("Aragatsotn", "Ararat", "Armavir", "Gaghark'unik'", "Kotayk", "Lorri", "Shirak", "Syunik'", "Tavush", "Vayots' Dzor", "Yerevan City","Aragatsotn")
value <- runif(12, 0.0, 1.0)
df <- data.frame(name, value)

# example SpatialPolygonsDataFrame
arm <- getData('GADM', country='ARM', level=1)[, c('NAME_1')]

Esta

merge(arm, df, by.x='NAME_1', by.y='name')

falla con el mensaje

#Error in .local(x, y, ...) : non-unique matches detected

Porque hay dos registros para "Aragatsotn" en df. Podrías hacerlo

merge(arm, df, by.x='NAME_1', by.y='name', duplicateGeoms=TRUE)

Pero normalmente el enfoque sensato es usar algo como

df <- aggregate(df[, 'value', drop=FALSE], df[, 'name', drop=FALSE], mean)
m <- merge(arm, df, by.x='NAME_1', by.y='name')
data.frame(m)

data.frame(m)
#        NAME_1       value
#1   Aragatsotn 0.421576186
#2       Ararat 0.003138734
#3      Armavir 0.703402672
#4       Erevan          NA
#5  Gegharkunik          NA
#6       Kotayk 0.926883799
#7         Lori          NA
#8       Shirak 0.430585540
#9       Syunik          NA
#10      Tavush 0.121784395
#11 Vayots Dzor          NA

Ahora, fusionar no funciona bien en este caso porque los nombres no coinciden. Entonces puedes usar

i <- amatch(df$name, arm$NAME_1, maxDist = 3)
df$match[!is.na(i)] <- arm$NAME_1[i[!is.na(i)]]
df
#            name       value       match
#1     Aragatsotn 0.421576186  Aragatsotn
#2         Ararat 0.003138734      Ararat
#3        Armavir 0.703402672     Armavir
#4  Gaghark'unik' 0.682169824 Gegharkunik
#5         Kotayk 0.926883799      Kotayk
#6          Lorri 0.128894086        Lori
#7         Shirak 0.430585540      Shirak
#8        Syunik' 0.163562936      Syunik
#9         Tavush 0.121784395      Tavush
#10  Vayots' Dzor 0.383439033 Vayots Dzor
#11  Yerevan City 0.168033419        <NA>

Casi allí, pero "Ciudad de Ereván" no coincidía con "Erevan". En este caso puedes aumentarmaxDist

i <- amatch(df$name, arm$NAME_1, maxDist = 10)
df$match[!is.na(i)] <- arm$NAME_1[i[!is.na(i)]]

Pero aumentar maxDistno siempre funcionará o dará coincidencias incorrectas porque los nombres de las variantes pueden ser muy distintos. Entonces, en muchos casos, terminará haciendo algunos reemplazos manuales como:

df[df$name=="Yerevan City", 'match'] <- "Erevan"

En ambos casos seguido de

m <- merge(arm, df, by.x='NAME_1', by.y='match')

En cualquier caso, querrá verificar si sum(table(i) > 1) == 0; aunque mergedebería fallar de todos modos si hay coincidencias duplicadas.

Robert Hijmans
fuente
Buenos detalles! Por eso llamé a mi respuesta rápido . Sin embargo, el marco de datos coincidente (df) no contendría los datos de geometría. ¿verdad? El OP quiere mapear el df unido. El agregado espacial en lugar del agregado de atributos sería otra alternativa para múltiples casos de unión.
Farid Cheraghi
df no tiene geometrías, de ahí el paso final con merge. El agregado espacial es útil para diferentes casos (si, en este ejemplo, NAME_1tenía duplicados)
Robert Hijmans