Como associar uma tabela a um shapefile com IDs e nomes não correspondentes (sequências semelhantes)?

8

Estou tendo um problema irritante para o qual estou tentando encontrar uma solução automatizada. A versão abreviada é que eu tenho um shapefile e uma tabela de dados criados para regiões dentro de países. A tabela de dados criada NÃO possui nenhum tipo de código GIDs / administrador padronizado para corresponder aos arquivos de forma, e os nomes das regiões também não são correspondências exatas. Vamos olhar mais de perto; aqui está o meu quadro de dados fictício + shapefile.

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)

Então, o que tenho é uma tabela com IDs aparentemente aleatórios, nomes de regiões e um valor a ser plotado com um mapa de coropletas. Se parece com isso:

> 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

Observando os atributos shapefile de interesse, temos o seguinte:

> 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, dfincluiria algum tipo de ID de administrador correspondente para ingressar no shapefile. Quem criou os dados que estou usando não seguiu essas convenções, infelizmente. Como alternativa, seria ótimo corresponder os nomes das regiões ... mas, como você pode ver, há pequenas variações em cada nome.

Fazer a correspondência manualmente é sempre uma solução de backup, mas quem quer dedicar algum tempo para fazer isso? ;) Mas, realmente, apesar da preguiça, o projeto em que estou trabalhando mapeará dezenas e dezenas de países diferentes, por isso estou procurando uma solução automatizada que possa fazer tudo sem ter que fazer nada manualmente. Isso é possível? De alguma forma, posso combinar esses nomes de quase região com os shapefiles?

Nota: Estou procurando greplpor correspondências parciais de strings por esta postagem , mas não tenho certeza se essa é uma solução em potencial, pois precisarei desenhar os nomes das colunas em vez de inserir manualmente o nome de cada região.

EDIT: Quando eu correspondo os IDs manualmente, o que eu fiz foi criar uma nova coluna no meu quadro de dados e adicionar os termos correspondentes exatamente do shapefile. Infelizmente, devido às peculiaridades dos dados, a ordem dos nomes também não corresponde, portanto, isso ainda requer alguma entrada manual. Estou esperando por algum tipo de solução completamente automatizada (se é que é possível).

Lauren
fonte
Se você tiver sorte e tiver o mesmo número de registros na mesma ordem no shapefile e na tabela, poderá copiar e colar os nomes nas colunas adjacentes em uma nova tabela, associá-lo ao shapefile usando seus nomes e associá-lo a a tabela usando seus nomes. (Ou, usando uma cópia do seu shapefile, cole os nomes das tabelas diretamente em seu dbf em uma planilha do Excel ou do Libre / Open Office anterior a 2007.) Se você não tiver um número exato de registros um para um, mas muitos "trechos" longos deles, você pode misturar um pouco de trabalho manual com cópia e pastas.
Jans
Foi o que acabei fazendo manualmente à mão, mas infelizmente eles não estão na ordem correta. Mesmo se listado em ordem alfabética, ainda assim pode não funcionar o tempo todo (neste exemplo, Erevan = Yerevan City, que joga o restante da lista fora de ordem).
Lauren

Respostas:

6

Eu iria para o stringdistpacote que implementou muitos algoritmos para calcular a similaridade parcial (distância) de strings, incluindo Jaro-winkler. Aqui está uma solução rápida para você:

  #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

produz:

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

Você pode jogar com o parâmetro maxDist do método amatch. Embora 3 funcione melhor com seus dados de amostra!

Farid Cheraghi
fonte
Sim, isso funcionou para o meu exemplo! Agora, para testar mais alguns! Pergunta relacionada: como posso obter essa mesma junção, mantendo o arquivo shapefile espacial? Parece que esse pedaço de código acabou de criar um quadro de dados com os dados associados, mas ainda será necessário mapear.
Lauren
Eu criei o quadro de dados manualmente para que seu problema possa ser reproduzido. Quando você lê um shapefile via readOGR, a classe de saída seria uma das classes de derivadas "sp", como "SpatialPointsDataFrame". E todos eles têm um atributo "data" que contém todos os dados de atributo do tipo dataframe. No meu exemplo, estou ingressando no quadro de dados e as informações geométricas são intocadas. Portanto, para o seu exemplo, basta mudar arm.datapara arm@datae funcionaria perfeitamente.
Farid Cheraghi
Não use arm@data, que iria criar uma grande confusão (registros não corresponde suas geometrias correctas)
Robert Hijmans
6

Quero acrescentar alguns detalhes à resposta de Farid Cher, pois esse é um problema muito comum. Usando amatchpode fazer maravilhas, mas com estes Spatialobjetos que você deve não usar base::mergee não acessar o @dataslot. Isso inevitavelmente levaria a uma bagunça terrível ( base::mergemuda a ordem dos registros e eles não combinariam mais com as geometrias).

Em vez disso, use o sp::mergemétodo, usando o SpatialPolygonsDataFrameprimeiro argumento em merge. Observe também o possível problema de ter registros duplicados. E adicionei dados para que o exemplo seja independente e reproduzível.

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')]

este

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

falha com a mensagem

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

Porque existem dois registros para "Aragatsotn" em df. Você poderia fazer

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

Mas normalmente a abordagem sensata é 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

Agora, a mesclagem não funciona bem nesse caso, porque os nomes não correspondem. Então você pode 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>

Quase lá, mas "Yerevan City" não combina com "Erevan". Nesse caso, você pode aumentarmaxDist

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

Mas aumentar maxDistnem sempre funcionará ou fornecerá correspondências erradas, pois os nomes das variantes podem ser muito distintos. Portanto, em muitos casos, você fará algumas substituições manuais, como:

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

Nos dois casos seguidos por

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

Em qualquer caso, você desejará verificar se sum(table(i) > 1) == 0; embora mergedeva falhar mesmo assim, se houver correspondências duplicadas.

Robert Hijmans
fonte
Detalhes agradáveis! Foi por isso que chamei minha resposta rapidamente . No entanto, o quadro de dados correspondente (df) não conteria os dados geométricos. seria? O OP deseja mapear o df unido. Agregar espacial em vez de agregar atributo seria outra alternativa para vários casos de junção.
Farid Cheraghi
df não tem geometrias, daí o passo final com merge. Agregada espacial é útil para casos diferentes (se, neste exemplo, NAME_1tiveram duplicatas.)
Robert Hijmans