Comment joindre une table à un fichier de formes avec des ID et des noms non correspondants (chaînes similaires)?

8

J'ai un problème ennuyeux pour lequel j'essaie de trouver une solution automatisée. La version abrégée est que j'ai un fichier de formes et un tableau de données créées pour les régions au sein des pays. La table de données créée n'a AUCUNE sorte de GID / codes d'administration standardisés à faire correspondre aux fichiers de formes, et les noms de région ne sont pas non plus des correspondances exactes. Regardons de plus près; voici mon faux cadre de données + 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)

Donc, ce que j'ai, c'est un tableau avec des identifiants apparemment aléatoires, des noms de régions et une valeur à tracer avec une carte choroplèth. Ressemble à ça:

> 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

En regardant les attributs de fichiers de formes d'intérêt, nous avons ceci:

> 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

Idéalement, il dffaudrait inclure une sorte d'ID administrateur correspondant à joindre au fichier de formes. Celui qui a créé les données que j'utilise n'a malheureusement pas respecté ces conventions. Alternativement, ce serait bien de faire correspondre les noms de région eux-mêmes ... mais comme vous pouvez le voir, il y a de légères variations dans chaque nom.

L'appariement à la main est toujours une solution de sauvegarde, mais qui veut prendre le temps de le faire? ;) Mais vraiment, même en dehors de la paresse, le projet sur lequel je travaille sera de cartographier des dizaines et des dizaines de pays différents, donc je recherche une solution automatisée qui peut tout faire sans rien faire à la main. Est-ce possible? Puis-je d'une manière ou d'une autre faire correspondre ces noms de région-presque aux fichiers de formes?

Sidenote: Je cherche grepldes correspondances de chaînes partielles dans cet article , mais je ne suis pas sûr que ce soit une solution potentielle car je devrai tirer des noms de colonnes plutôt que d'entrer chaque nom de région à la main.

EDIT: Lorsque je fais correspondre les ID à la main, ce que j'ai fait est de créer une nouvelle colonne dans mon bloc de données et d'ajouter les termes correspondants exacts à partir du fichier de formes. Malheureusement, en raison des particularités des données, l'ordre des noms ne correspond pas non plus, donc cela nécessite encore une saisie manuelle. J'espère une sorte de solution complètement automatisée (si c'est même possible).

Lauren
la source
Si vous avez de la chance et que vous avez le même nombre d'enregistrements dans le même ordre à la fois dans le fichier de formes et dans le tableau, vous pouvez copier et coller les noms dans les colonnes adjacentes dans un nouveau tableau, les joindre au fichier de formes à l'aide de leurs noms et les joindre à la table en utilisant ses noms. (Ou en utilisant une copie de votre fichier de formes, collez les noms de table directement dans son dbf dans une feuille Excel ou Libre / Open Office antérieure à 2007.) Si vous n'avez pas un nombre exact d'enregistrements un à un mais de nombreux "étirements" longs vous pouvez mélanger un peu de travail manuel avec des copies et des pâtes.
johns
C'est ce que j'ai fini par faire manuellement à la main, mais malheureusement, ils ne sont pas dans le bon ordre. Même s'il est répertorié par ordre alphabétique, il peut ne pas fonctionner tout le temps (dans cet exemple, Erevan = Yerevan City, ce qui met le reste de la liste hors service).
Lauren

Réponses:

6

J'irais pour un stringdistpackage qui a implémenté de nombreux algorithmes pour calculer la similitude partielle (distance) des chaînes, y compris Jaro-winkler. Voici une solution rapide pour vous:

  #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

il génère:

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

Vous pouvez jouer avec le paramètre maxDist de la méthode amatch. Bien que 3 fonctionne mieux avec vos données d'échantillon!

Farid Cheraghi
la source
Oui, cela a fonctionné pour mon exemple! Maintenant pour en tester quelques autres! Question connexe: comment puis-je obtenir cette même jointure tout en conservant l'espace du fichier de formes? Il semblerait que ce morceau de code vient de créer une trame de données avec les données jointes, mais j'aurai encore besoin de pouvoir la mapper.
Lauren
J'ai créé la trame de données manuellement afin que votre problème puisse être reproductible. Lorsque vous lisez un fichier de formes via readOGR, la classe de sortie est l'une des classes de dérivés "sp" telles que "SpatialPointsDataFrame". Et ils ont tous un attribut "data" qui contient toutes les données d'attribut qui sont de type dataframe. Dans mon exemple, je me joins au cadre de données et les informations géométriques sont intactes. Donc, pour votre exemple, changez simplement arm.dataen arm@dataet cela fonctionnerait très bien.
Farid Cheraghi
Ne pas utiliser arm@data, cela créerait un gros gâchis (les enregistrements ne correspondant pas à leurs géométries correctes)
Robert Hijmans
6

Je veux ajouter quelques détails à la réponse de Farid Cher car il s'agit d'un problème très courant. L' utilisation amatchpeut faire des merveilles, mais avec ces Spatialobjets que vous ne devriez pas utiliser base::mergeet ne pas accéder à la @datafente. Cela conduirait inévitablement à un terrible gâchis ( base::mergechange l'ordre des enregistrements, et ils ne correspondraient plus aux géométries).

À la place, utilisez la sp::mergeméthode en utilisant le SpatialPolygonsDataFramecomme premier argument dans merge. Notez également le problème potentiel d'avoir des enregistrements en double. Et j'ai ajouté des données pour que l'exemple soit autonome et reproductible.

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

Cette

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

échoue avec le message

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

Parce qu'il y a deux enregistrements pour "Aragatsotn" dans df. Vous pourriez faire

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

Mais normalement, l'approche saine consiste à utiliser quelque chose comme

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

Maintenant, la fusion ne fonctionne pas bien dans ce cas car les noms ne correspondent pas. Vous pouvez donc utiliser

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>

Presque là, mais "Erevan City" ne correspondait pas à "Erevan". Dans ce cas, vous pouvez augmentermaxDist

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

Mais augmenter maxDistne fonctionnera pas toujours ou ne donnera pas les bonnes correspondances car les noms de variantes peuvent être très distincts. Donc, dans de nombreux cas, vous finirez par faire des remplacements manuels comme:

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

Dans les deux cas, suivi de

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

Dans tous les cas, vous voudrez vérifier si sum(table(i) > 1) == 0; mais mergedevrait échouer de toute façon s'il y a des correspondances en double.

Robert Hijmans
la source
Jolis détails! C'est pourquoi j'ai appelé ma réponse rapidement . Cependant, le cadre de données correspondant (df) ne contiendrait pas les données de géométrie. Est-ce que cela serait? L'OP veut cartographier le df joint. L'agrégat spatial au lieu de l'agrégat d'attribut serait une autre alternative pour plusieurs cas de jointure.
Farid Cheraghi
df n'a pas de géométrie, d'où l'étape finale avec merge. L'agrégat spatial est utile pour différents cas (si, dans cet exemple, il y NAME_1avait des doublons.)
Robert Hijmans