En muchas ocasiones nos enfrentamos a casos donde queremos unir dos conjunto de datos (dataframes) por ejemplo las encuestas de hogares tipicamente separan la información de los miembros del hogar (edad, sexo, escolaridad, etc.) de la información de la vivienda (cuartos, equipamiento, etc.). Tipicamente tenemos algun tipo de identificador que funcione como una llave y que conecta a ambos objetos:
| DF | Registros | Variables |
|---|---|---|
| Hogares | 2 | 3 |
| Miembros | 6 | 3 |
Las llaves pueden ser numeros, caractéres, factores, etc. R nos permite unir objetos siempre y cuando las llaves sean del mismo “tipo”.
Hogares<-tibble(
ID_Hogar = 1:2,
Ingreso = c(1000,5000),
Color = c("Azul","Rojo")
)
Miembros <-tibble(
ID_Hogar = c(1,1,1,2,2,3),
ID_Persona = c("01","02","03","01","02","01"),
Edad = seq(10, 60, by = 10)
)
En este caso supongamos que estamos interesados en conocer cuantas personas viven en casas azules. Podriamos utilizar la libreria dplyr y la llave “ID Hogar” para asignarle a cada individuo (ID Persona). Lo que buscamos es usar la variable “ID Hogar” que se encuentra en el df Hogares, para asignarle a los miembros del hogar la información de la vivienda.
knitr::kable(Hogares, col.names = gsub("[.]", " ", names(Hogares)),caption = "Hogares")
| ID_Hogar | Ingreso | Color |
|---|---|---|
| 1 | 1000 | Azul |
| 2 | 5000 | Rojo |
El df Miembros también cuenta con una variable llamada ID_Hogar y ésta será la que nos ayude a unir la información de Hogares en Miembros.
knitr::kable(Miembros, col.names = gsub("[.]", " ", names(Miembros)),caption = "Miembros")
| ID_Hogar | ID_Persona | Edad |
|---|---|---|
| 1 | 01 | 10 |
| 1 | 02 | 20 |
| 1 | 03 | 30 |
| 2 | 01 | 40 |
| 2 | 02 | 50 |
| 3 | 01 | 60 |
El comando que usaremos es left_join (hay otras variantes), pero en general lo que nos indica es que se unira utilizando la información del objeto a la izquierda de la función, para “empatar”.
Miembros %>% left_join(Hogares,by="ID_Hogar")
## # A tibble: 6 x 5
## ID_Hogar ID_Persona Edad Ingreso Color
## <dbl> <chr> <dbl> <dbl> <chr>
## 1 1 01 10 1000 Azul
## 2 1 02 20 1000 Azul
## 3 1 03 30 1000 Azul
## 4 2 01 40 5000 Rojo
## 5 2 02 50 5000 Rojo
## 6 3 01 60 NA <NA>
Como podemos ver, el individuo que habita en el hogar numero 3 no cuenta con su correspondiente registro en el df Hogares. Si hubieramos realizado un right_join, éste ultimo registro se hubiera eliminado, ya que lo que queremos preservar son los registros de Hogares.
El ejemplo anterior mostró como se pueden unir objetos cuando sabemos que la llave es la misma para los diferentes elementos que queremos unir. Sin embargo ¿qué sucede cuando esto no es el caso?. Vamos a ver un ejemplo muy práctico de estadisticas de homicidio en México. El INEGI publica de manera regular esta información, perso al descargarla solamente nos indica el nombre de los municipios y estados donde ocurrieron los eventos. No facilita su clave geostadística (Muy mal!) que nos permita concatenar esta información con otros indicadores.
Lo primero que vamos a hacer es cargar nuestra base de datos de homicidios. Removemos los casos donde no sabemos en donde ocurrieron los eventos y excluimos las repeticiones (esta es una serie de tiempo municipal de homicidios registrados mensualmente en cada municipio para el periodo de 1990-2018).
data <- haven::read_dta("INEGI_hom_byage.dta")%>%
rename(ent=state, mun=municipio) %>% filter(mun!="No especificado")
data %>% group_by(ent) %>% tally()
## # A tibble: 32 x 2
## ent n
## <chr> <int>
## 1 Aguascalientes 4176
## 2 Baja California 2088
## 3 Baja California Sur 2088
## 4 Campeche 4176
## 5 Chiapas 42804
## 6 Chihuahua 23664
## 7 Ciudad de México 5916
## 8 Coahuila de Zaragoza 13572
## 9 Colima 3828
## 10 Durango 13920
## # … with 22 more rows
homicides<- data %>% select(mun,ent) %>% unique()
Como podemos observar, el df cuenta con cinco variables, en donde un municipio se presenta en más de una ocasión. De manera más precisa hay 843,552 registros o alrededor de 340 observaciones por municipio.
knitr::kable(head(data), "markdown")
| mun | ent | total | anos0 | anos1_4 | anos10_14 | anos15_19 | anos5_9 | year | month |
|---|---|---|---|---|---|---|---|---|---|
| Aguascalientes | Aguascalientes | 1 | NA | NA | NA | NA | NA | 1990 | 1 |
| Aguascalientes | Aguascalientes | 1 | NA | NA | 1 | NA | NA | 1990 | 2 |
| Aguascalientes | Aguascalientes | 3 | NA | NA | NA | NA | NA | 1990 | 3 |
| Aguascalientes | Aguascalientes | 4 | NA | NA | NA | 1 | NA | 1990 | 4 |
| Aguascalientes | Aguascalientes | 3 | NA | NA | NA | NA | NA | 1990 | 5 |
| Aguascalientes | Aguascalientes | 5 | NA | NA | NA | NA | NA | 1990 | 6 |
De igual manera cargamos una base (también del INEGI), donde tenemos ya cargados los catalogos con las claves geoestadísticas, así como sus nombres respectivos. Vemos la distribución de los municipios por entidad:
inegi<-read.dta13("INEGI_entmun.dta", generate.factors = T)
inegi %>% group_by(NOM_ENT) %>% tally()
## # A tibble: 32 x 2
## NOM_ENT n
## <chr> <int>
## 1 Aguascalientes 11
## 2 Baja California 5
## 3 Baja California Sur 5
## 4 Campeche 11
## 5 Chiapas 123
## 6 Chihuahua 67
## 7 Ciudad de México 16
## 8 Coahuila 38
## 9 Colima 10
## 10 Durango 39
## # … with 22 more rows
Como podemos observar, existen entidades con nombres diferentes. Tal es el caso de Coahuila, en el df data tenemos al estado como “Coahuila”, en tanto que en inegi se tiene guardado como “Coahuila de Zaragoza”. Una posible solución es reemplazar el nombre de las entidades de acuerdo a un formato en especial:
data %>% group_by(ent) %>% tally()
## # A tibble: 32 x 2
## ent n
## <chr> <int>
## 1 Aguascalientes 4176
## 2 Baja California 2088
## 3 Baja California Sur 2088
## 4 Campeche 4176
## 5 Chiapas 42804
## 6 Chihuahua 23664
## 7 Ciudad de México 5916
## 8 Coahuila de Zaragoza 13572
## 9 Colima 3828
## 10 Durango 13920
## # … with 22 more rows
data$ent[data$ent %in% "Michoacán De Ocampo" ]<- "Michoacán"
data$ent[data$ent %in% "Coahuila de Zaragoza"] <- "Coahuila"
data$ent[data$ent %in% "Veracruz De Ignacio De La Llave" ]<- "Veracruz"
Facil ¿verdad?, pero ahora tenemos que “homologar” de igual forma los nombres de municipios. De forma más específica, tendríamos que revisar más de 2400 registros “a mano”, lo cual podria incrementar el riesgo de errores de captura o recodificación. Una solución a esto es el paquete de fuzzyjoin que utiliza el motor de Mark P.J. van der Loo, 2014. Su paper es muy recomendable, ya que ayuda a comprender las diferentes métricas del “approximate string matching”.
Primero vamos a entender que es lo que hace el motor stringdist.
Lo primero que vamos a hacer es un matching o union exacta. Utilizaremos para ello el nombre de las entidades y luego de entidades y municipios de manera simultánea.
homicides %>% left_join(inegi %>% select(NOM_ENT,CVE_ENT) %>%
unique(),by=c("ent"="NOM_ENT")) %>% group_by(CVE_ENT) %>% tally()
## # A tibble: 30 x 2
## CVE_ENT n
## <chr> <int>
## 1 01 12
## 2 02 6
## 3 03 6
## 4 04 12
## 5 06 11
## 6 07 123
## 7 08 68
## 8 09 17
## 9 10 40
## 10 11 47
## # … with 20 more rows
En este caso podemos ver que hay 30 filas, 29 que se codificaron correctamente y una fila con NA.
homicides %>% left_join(inegi %>% select(NOMGEO,CVE_MUN,NOM_ENT,CVE_ENT) %>%
unique(),by=c("mun"="NOMGEO","ent"="NOM_ENT" )) %>%
mutate(entmun=paste0(CVE_ENT,CVE_MUN)) %>% group_by(entmun) %>% tally()
## # A tibble: 2,019 x 2
## entmun n
## <chr> <int>
## 1 01001 1
## 2 01002 1
## 3 01003 1
## 4 01004 1
## 5 01005 1
## 6 01006 1
## 7 01007 1
## 8 01008 1
## 9 01009 1
## 10 01010 1
## # … with 2,009 more rows
En este caso, aunque hay más de 2400 municipios solamente tenemos 2019 uniones exactas. Es decir cerca de 450 nombres de municipios no son exactamente iguales en ambos df. ¿Cuál será nuestra salida? stringdist_join. Veamos el caso más sencillo con la información a nivel estatal.
inegi %>% select(NOM_ENT,CVE_ENT) %>% unique()
## NOM_ENT CVE_ENT
## 1 Aguascalientes 01
## 12 Baja California 02
## 17 Baja California Sur 03
## 22 Campeche 04
## 33 Coahuila 05
## 71 Colima 06
## 81 Chiapas 07
## 204 Chihuahua 08
## 271 Ciudad de México 09
## 287 Durango 10
## 326 Guanajuato 11
## 372 Guerrero 12
## 453 Hidalgo 13
## 537 Jalisco 14
## 662 México 15
## 787 Michoacán 16
## 900 Morelos 17
## 933 Nayarit 18
## 953 Nuevo León 19
## 1004 Oaxaca 20
## 1574 Puebla 21
## 1791 Querétaro 22
## 1809 Quintana Roo 23
## 1820 San Luis Potosí 24
## 1878 Sinaloa 25
## 1896 Sonora 26
## 1968 Tabasco 27
## 1985 Tamaulipas 28
## 2028 Tlaxcala 29
## 2088 Veracruz 30
## 2300 Yucatán 31
## 2406 Zacatecas 32
stringdist_left_join(
data %>% select(ent) %>% rename(NOM_ENT=ent) %>% unique(),
inegi %>% select(NOM_ENT,CVE_ENT) %>% unique(),
method = "soundex", by ="NOM_ENT", distance_col = NULL)
## Warning in do_dist(a = b, b = a, method = method, weight = weight, q = q, : Non-
## printable ascii or non-ascii characters in soundex. Results may be unreliable.
## See ?printable_ascii.
## # A tibble: 34 x 3
## NOM_ENT.x NOM_ENT.y CVE_ENT
## <chr> <chr> <chr>
## 1 Aguascalientes Aguascalientes 01
## 2 Campeche Campeche 04
## 3 Baja California Sur Baja California 02
## 4 Baja California Sur Baja California Sur 03
## 5 Baja California Baja California 02
## 6 Baja California Baja California Sur 03
## 7 Coahuila Coahuila 05
## 8 Chiapas Chiapas 07
## 9 Chihuahua Chihuahua 08
## 10 Colima Colima 06
## # … with 24 more rows
Veamos que seguimos teniendo problemas en la unión. Una forma más sistémica es separar las uniones exactas de las no exactas. Posteriormente procedemos a la parte medular de esta nota.
El problema anterior es abordado a partir de las librearias que Primero y dado que esto busca ser un ejemplo escalable, cambiemos los nombres de las variables siguiendo una estructura anidada:
De igual manera hacemos una ligera limpieza para contabilizar solamente las observaciones en municipios validos. Posteriormente separamos el df principal en dos, Coded que contiene aquellas observaciones donde se realizó un matching exacto a dos niveles. Esto es, hubo una correspondencia de entidad y municipio en ambos df. El df con el que vamos a trabajar es Uncoded y corresponde a los casos donde no hubo un matching exacto, por lo que procederemos a realizar un “fuzzy matching”.
Coded<-inegi %>% rename(C1ID=CVE_ENT,C2ID=CVE_MUN,C1Name=NOM_ENT,C2Name=NOMGEO) %>%
select(C2ID,C1ID,C1Name,C2Name)
Uncoded<-data %>% filter(mun!="Total" & is.na(mun)!=T) %>%rename(U1Name=ent,U2Name=mun) %>% select(U1Name,U2Name) %>% unique()
as_tibble(Coded)->Coded
as_tibble(Uncoded)->Uncoded
Matched<-inner_join(Coded,Uncoded,by=c("C1Name"="U1Name","C2Name"="U2Name"))
Unmatched<-anti_join(Uncoded,Coded,by=c("U1Name"="C1Name","U2Name"="C2Name"))
Una parte importante de este ejercicio es desarrollar el emparejamiento de manera escalonada. Esto es, para cada nivel se debe realizar primero un matching exacto y posteriormente un fuzzy matching. De esta manera, del df de Unmatched se analiza primero en su nivel más alto de agregaciñon. Esto es, previo a realizar el fuzzy matching en el nivel de anidamiento más bajo, lo que buscamos separar con el comando regex_inner_join.
Unmatched<-regex_inner_join(Unmatched,Coded %>% select("C1Name") %>% unique(),by =c("U1Name"="C1Name"))
Matched<-bind_rows(Matched,
inner_join(Coded,Unmatched,by=c("C1Name"="C1Name","C2Name"="U2Name"))
)
Matched<-Matched%>% mutate(U1Name=if_else(is.na(U1Name)==T,C1Name,U1Name))
Unmatched<-anti_join(Unmatched,Coded,by=c("C1Name"="C1Name","U2Name"="C2Name"))
X<-Unmatched # Uncoded data
Y<-Coded # Coded data
varsX <- c(Name = "C1Name")
varsY <- c(Name = "C1Name", ID ="C1ID")
X %>% select(!!varsX) %>% distinct() %>% mutate(Name=as.character(Name)) ->x
Y %>% select(!!varsY) %>% distinct() %>% mutate(Name=as.character(Name)) ->y
Z<-stringdist_join(x, y, by = "Name") %>%
select(-Name.y) %>%
rename(U1Name=Name.x)
Z<-left_join(X,Z, by="U1Name")
rm(varsX,varsY,x,y)
varsX2 <- c(Name = "U2Name", "U1Name")
varsY2 <- c(Name = "C2Name", ID ="C1ID", "C2ID")
z<-Z %>% pull(ID) %>% unique()
df <- data.frame()
for (x in 1:length(z)){
Z %>% filter(ID==z[[x]])->W
Y %>% filter(C1ID==z[[x]])->w
W %>% select(!!varsX2) %>% distinct() %>% mutate(Name=as.character(Name)) ->W
w %>% select(!!varsY2) %>% distinct() %>% mutate(Name=as.character(Name)) ->w
Ww<-stringdist_join(W,w, mode="inner",method = "soundex", by ="Name")
df <- bind_rows(df, Ww)
rm(w,W,Ww)
}
df %>%select(Name.x,ID,C2ID,U1Name) %>% distinct() %>% rename(U2Name=Name.x) ->df
`%notin%` <- Negate(`%in%`)
df %>% group_by(U2Name,ID,U1Name) %>% tally() %>% filter(n>1) %>% pull(U2Name)->df2
df %>% group_by(U2Name,ID,U1Name,C2ID) %>% filter(U2Name%notin%df2) %>% rename (C1ID=ID)->Unmatched
Al final no identificamos los siguientes muncipios:
Match_Unmatch<-bind_rows(Unmatched,Matched %>% rename(U2Name=C2Name) %>% select(-C1Name))
Match_Unmatch %>% rename(ent=U1Name,mun=U2Name,CVE_MUN=C2ID,CVE_ENT=C1ID)->Match_Unmatch
df<-full_join(data,Match_Unmatch)
## Joining, by = c("mun", "ent")
## Warning: Column `mun` has different attributes on LHS and RHS of join
## Warning: Column `ent` has different attributes on LHS and RHS of join
df %>% mutate(CVE_MUN=if_else(mun=="Briseńas","011",CVE_MUN),
CVE_MUN=if_else(mun=="San Martín de Bolańos","076",CVE_MUN),
CVE_MUN=if_else(mun=="San Pedro Mixtepec","319",CVE_MUN),
CVE_MUN=if_else(mun=="Tlajomulco de Zúńiga","097",CVE_MUN)) %>%
mutate(CVE_ENT=if_else(mun=="Briseńas","016",CVE_ENT),
CVE_ENT=if_else(mun=="San Martín de Bolańos","014",CVE_ENT),
CVE_ENT=if_else(mun=="San Pedro Mixtepec","020",CVE_ENT),
CVE_ENT=if_else(mun=="Tlajomulco de Zúńiga","014",CVE_ENT))->df
library(foreign)
write.dta(df, "homicides_minors.dta")