APIs
Muchas de las páginas que visitamos todos los días no tienen “prearmadas” las páginas web, sino que realizan consultas a otro servidor cuando nosotros nos conectamos. Lo pueden hacer de muchas maneras, y, a veces y si estamos atentos, podemos identificar una oportunidad de oro: podemos ahorrarnos la navegación por la página web e ir directamente a hacer consultas que, de otra manera, muy probablemente nos llevaría mucho más tiempo y trabajo para diseñar el scraper.
DISCLAIMER: no siempre se debe scrapear lo que se puede scrapear
La disputa legal sobre qué se puede scrapear y que no está lejos de estar cerrada. Como recomendación, siempre lean los Términos y Condiciones de los sitios que están buscando scrapear para conocer si están infringiendo algunas de las condiciones. Con todo, me gustaría hacer un punto extra: aun siendo legal, es importante entender que nuestras peticiones mediante un scraper pueden rápidamente poner en riesgo el servicio web de una página. Como recomendación general, scrapeen lo que necesiten, siempre y cuando no infrinjan normas de usos y condiciones de los servicios y no generen una carga enorme para los servidores.
Un ejemplo de API expuesta: Precios Claros
Precios Claros es un programa del gobierno que comenzó hace ya unos años que nos muestra información sobre los productos que diversos comercios reportan. Si bien el objetivo es ofrecer información sobre los precios cercanos a cada uno de nosotros, lo cierto es que es posible scrapearlo y tener una información rica sobre la distribución espacial de los precios, algo que puede llegar a ser muy interesante.
El primer paso para conocer si podemos scrapear una página mediante el uso de una API es observar la comunicación cuando nosotros le pedimos algo a la página (es decir, cuando hacemos click en algo). Entren a la página e introduzcan cualquier dirección - para este ejemplo, yo usé juan b justo al 4000. Ahora hagan lo siguiente: antes de hacer click en la primer categoría - Alimentos Congelados - , vayan a herramientas de desarrollador - como ya sabemos hacerlo -, y en lugar de ir a la pestaña “Elements” vamos a ir a la pestaña Network, donde está precisamente lo que queremos encontrar. Ahora sí, hagan click en Alimentos Congelados y veamos qué sucede.
Analizando la interacción entre la página web y una API
Apareció una línea que, a su vez, tiene otras pestañas. En Header tenemos información específica sobre a qué dirección pidió la información, en este caso es esa dirección más bien larga con distintos parámetros que iremos viendo. En Preview tenemos un pantallazo más o menos ordenado de la respuesta. En Response tenemos la respuesta completa, que es relativamente fácil de entender. Lo que hizo es devolver el precio máximo y mínimo para un conjunto de sucursales, dada la ubicación inicial que le dimos. La respuesta es un formato json, es decir, listas con las que podemos trabajar sin mayores problemas en R. Veamos cómo hacer está consulta en R y poder analizar la respuesta del JSON para entender un poco mejor cómo trabaja esta API.
Haciendo consultas a las APIs desde R
Analizando la respuesta
Con una conexión activa a internet, nosotros podemos conectarnos sin mayores problemas a cualquier página web. Sin embargo, para trabajar con respuestas en JSON, R tiene un paquete que hace todo junto: manda la consulta, espera la respuesta y la convierte a un data.frame… nada mal, no?
Para esto vamos a necesitar tener instalado el paquete jsonlite. La consulta que voy a hacer acá es exactamente la misma que hace la página cuando hicimos click en “Alimentos Congelados” anteriormente.
alimentosCongelados <- fromJSON("https://d3e6htiiul5ek9.cloudfront.net/prod/productos?&id_categoria=01&array_sucursales=15-1-5248,12-1-2,10-3-396,15-1-5232,15-1-52,15-1-24,23-1-6220,15-1-371,10-1-219,65-1-344,12-1-103,15-1-475,15-1-473,9-3-5263,10-3-420,15-1-492,15-1-302,19-1-00082,23-1-6224,10-3-687,15-1-627,10-3-539,15-1-490,15-1-540,15-1-588,10-3-561,10-3-622,15-1-219,10-3-608,10-3-526&offset=0&limit=50&sort=-cant_sucursales_disponible")
glimpse(alimentosCongelados)
## List of 7
## $ status : int 200
## $ total : int 559
## $ maxLimitPermitido : int 100
## $ maxCantSucursalesPermitido: int 50
## $ productos :'data.frame': 41 obs. of 7 variables:
## ..$ marca : chr [1:41] "GRANJA DEL SOL" "PATY" "SWIFT" "GRANJA DEL SOL" ...
## ..$ id : chr [1:41] "7790070031808" "7790670050650" "7790360026576" "7790070016300" ...
## ..$ precioMax : num [1:41] 160 270 240 254 150 ...
## ..$ precioMin : num [1:41] 143 200 228 240 150 ...
## ..$ nombre : chr [1:41] "Milanesas de Soja Flowpack Granja del Sol 330 Gr" "Hamburguesas de Carne Clasicas sin TACC Paty 4 Un 320 Gr" "Hamburguesas de Carne Vacuna Clasicas Swift 4 Un" "Bocaditos Rebozados de Pollo Granja del Sol 400 Gr" ...
## ..$ presentacion : chr [1:41] "330.0 gr" "320.0 gr" "4.0 un" "400.0 gr" ...
## ..$ cantSucursalesDisponible: int [1:41] 26 26 26 26 26 26 26 26 26 25 ...
## $ totalPagina : int 41
## $ agrupables :'data.frame': 7 obs. of 4 variables:
## ..$ precioMax : num [1:7] 240 169 469 299 249 ...
## ..$ cantSucursalesDisponible: int [1:7] 23 24 14 16 21 25 22
## ..$ id : chr [1:7] "019999006" "019999007" "010501002" "019999003" ...
## ..$ precioMin : num [1:7] 180 145 469 271 249 ...
Lo que devolvió es una lista de 7 elementos. Todas las APIs devuelven datos diferentes, por lo cual siempre que queremos scrapear usando su servicio tenemos que hacer lo mismo que haríamos revisando una página cualquiera, es decir, entender cómo está estructurada. En este casos tenemos las siguientes listas
- status: Nos da una idea de qué sucedió con la consulta que hicimos. Son códigos de estado de respuesta del protocolo HTTP, con las que se pueden haber encontrado múltiples veces cuando navegaban páginas web. 200 implica que hubo una respuesta satisfactoria, lo cual es una buena noticia.
- total: nos dice la cantidad total de productos que hay en la base de datos para los comercios que se consultan… aunque pronto veremos que puede que no todos estén disponibles !
- maxLimitPermitido: nos informa la cantidad máxima de productos que nos puede devolver en una sola consulta
- maxCantSucursalesPermitido: Nos indica la cantidad máxima de comercios que podemos consultar
- productos: es un data.frame con la información que nos muestra la página: los productos y su precio mínimo y máximo en el conjunto de comercios cercanos
- totalPagina: nos muestra la cantidad de casos que hay en la consulta que nos devolvió (pueden corroborar la cantidad de filas que hay en el data frame de productos)
- agrupables: es un data.frame un tanto oscuro en cuanto a su interpretación, no vamos a necesitar usarlo de cualquier manera.
Bien, hay mucha información para mostrar acá… Hagamos un gráfico de los 41 productos que nos pasaron y su precio mínimo y máximo:
# Seleccionamos el data frame de productos
productos <- alimentosCongelados$productos
# Orden en el cual queremos mostrar los datos
level_order <- productos %>%
arrange(precioMax) %>%
pull(nombre)
# Pasamos este data frame a formato largo para poder hacer el gráfico
productos <- pivot_longer(productos,cols = c(precioMax,precioMin))
ggplot() +
geom_point(data = productos,mapping = aes(x=value,
y=factor(nombre,levels = level_order),
group=name,color=name)) +
scale_color_brewer(palette = "Set1") +
theme_minimal() +
guides(color=FALSE) +
labs(x="",y="")
Distancia de precios entre un conjunto de productos según información de Precios Claros
Exactamente lo que queríamos ver: precios mínimos y máximos para cada uno de los productos para los comercios que le pedimos información. Ya analizamos la respuesta de la API, investiguemos un poco más sobre la consulta que le hicimos.
Analizando la consulta a la API
Ya pudimos transformar a un data frame y hasta graficar la respuesta de la consulta que le hicimos. Pero analicemos un poco más cómo funciona esta consulta y cómo poder usarla a nuestro favor para poder obtener algo todavía más interesante. Prestemos atención a la consulta:
La consulta está dividida por &, que es la que envía parámetros a la API. Piensen en la consulta a la API como una función de las que estuvimos trabajando en R: sabe hacer una tarea en particular, devolver la información sobre los precios mínimos y máximos de los productos, pero al mismo tiempo debemos decirle cuáles productos, en cuáles sucursales, entre otras variables. Veamos los parámetros:
- id_categoria: presumiblemente se trata del código de “grandes categorías” que nos muestra la página. 01 parece hacer referencia a “Alimentos Congelados”
- array_sucursales: es la lista de sucursales para los cuales queremos el precio, separadas por comas. Los números hacen referencia a comercios únicos
- offset: es un número que nos dice desde que elemento de la lista total de resultados queremos arrancar. Esto es porque existe un límite de respuestas que puede devolver la API, por lo cual nos corta la respuesta hasta ese número. Ya vamos a ver cómo resolver este problema
- limit: Cantidad de productos que queremos que nos devuelva como máximo. En la respuesta anterior vimos que solo devuelve hasta 100 productos, con lo cual cualquier número por encima de esto nos va a seguir devolviendo 100.
- sort: Un criterio para ordenar los resultados. En este caso, quiere ordenar de manera descendente a la cantidad de sucursales donde esta disponible el producto.
Por la consulta anterior, sabemos que existen 559 productos que en algún momento estuvieron en algunos de estos comercios en la categoria de Alimentos Congelados ¿Cómo podemos extraer a todos? Tenemos que usar offset !
offset <- seq(0,559,by=100)
lista <- map(offset,function(offsetNumber){
cat("offset: ",offsetNumber,"\r")
consulta <- paste("https://d3e6htiiul5ek9.cloudfront.net/prod/productos?&id_categoria=01&array_sucursales=15-1-5248,12-1-2,10-3-396,15-1-5232,15-1-52,15-1-24,23-1-6220,15-1-371,10-1-219,65-1-344,12-1-103,15-1-475,15-1-473,9-3-5263,10-3-420,15-1-492,15-1-302,19-1-00082,23-1-6224,10-3-687,15-1-627,10-3-539,15-1-490,15-1-540,15-1-588,10-3-561,10-3-622,15-1-219,10-3-608,10-3-526&offset=",offsetNumber,"&limit=100&sort=-cant_sucursales_disponible",sep="")
resultado <- fromJSON(consulta)
resultado$productos
})
Tuvimos que usar la función map(), que lo que hace es iterar por cada uno de los elementos de un objeto que le pasamos y hace lo mismo para cada uno de estos valores. En nuestro caso particular, le pasamos los valores de offset mediante los cuales podemos obtener todos los resultados disponibles en la API, y para cada uno de estos valores lo convertimos a un objeto con el que podemos trabajar y, para cada uno de estos valores, nos quedamos con el data.frame de productos. Lo único que tenemos que hacer es juntar todos estos data.frames en uno solo y veamos qué fue lo que obtuvimos:
## Rows: 408
## Columns: 7
## $ marca <chr> "PATY", "GRANJA DEL SOL", "GRANJA DEL SOL"...
## $ id <chr> "7790670050728", "7790070031822", "7790070...
## $ precioMax <dbl> 95.40, 168.90, 159.90, 150.00, 138.00, 254...
## $ precioMin <dbl> 92.00, 144.99, 143.00, 150.00, 126.99, 239...
## $ nombre <chr> "Hamburguesas de Carne Finitas Paty 2 Un 1...
## $ presentacion <chr> "110.0 gr", "330.0 gr", "330.0 gr", "276.0...
## $ cantSucursalesDisponible <int> 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, 24...
408 productos de alimentos congelados con su respectivo precio mínimo y máximo para cada una de las sucursales. Podemos hacernos distintas preguntas, cómo por ejemplo cuál es la diferencia promedio entre precio máximo y mínimo para aquellos productos que aparecen en más de una sucursal.
Su turno: buscando los precios mínimos y máximos de los productos de limpieza
Ahora les toca a ustedes. Armen un data frame que tenga todos los productos de la categoría de limpieza. Recuerden que para eso tienen que modificar un poco la consulta… En particular, “descubrir” cual es el código que corresponde a esa categoría. Luego, saber cuál es la cantidad de productos que hay y, en base a eso, armar los valores de offset a recorrer.
Armando un dataset de precios
Ya estuvimos entendiendo un poco más cómo es que funciona esto de pedir información a las APIs. Ahora bien, hasta ahora solo pudimos extraer información sobre el precio máximo y mínimo por las grandes categorías de la página y para un conjunto de comercios cercanos ¿Podemos juntar información sobre el precio de cada uno de los productos para cada uno de los comercios seleccionados? Modifiquemos levemente la consulta que hicimos anteriormente, y en lugar de pasar muchas sucursales como parámetros, enviemos solo una y veamos qué es lo que responde:
sucursalUnica <- fromJSON("https://d3e6htiiul5ek9.cloudfront.net/prod/productos?&id_categoria=01&array_sucursales=15-1-5248&offset=0&limit=100")
glimpse(sucursalUnica)
## List of 7
## $ status : int 200
## $ total : int 129
## $ maxLimitPermitido : int 100
## $ maxCantSucursalesPermitido: int 50
## $ productos :'data.frame': 76 obs. of 7 variables:
## ..$ marca : chr [1:76] "GRANJA DEL SOL" "DÍA" "DÍA" "LUCCHETTI" ...
## ..$ id : chr [1:76] "7790070032669" "8480017241320" "8480017130334" "7790070035127" ...
## ..$ precioMax : num [1:76] 190 113 365 107 110 ...
## ..$ precioMin : num [1:76] 190 113 365 107 110 ...
## ..$ nombre : chr [1:76] "Acelga Congelada Granja Del Sol 500 Gr" "Acelga Supercongelada Dia 400 Gr" "Almendrado Ban~ado en Chocolate Dia 1.2 Lt" "Arvejas Congeladas Lucchetti 300 Gr" ...
## ..$ presentacion : chr [1:76] "500.0 gr" "400.0 gr" "1.2 lt" "300.0 gr" ...
## ..$ cantSucursalesDisponible: int [1:76] 1 1 1 1 1 1 1 1 1 1 ...
## $ totalPagina : int 76
## $ agrupables :'data.frame': 9 obs. of 4 variables:
## ..$ precioMax : num [1:9] 234 145 140 469 297 ...
## ..$ cantSucursalesDisponible: int [1:9] 1 1 1 1 1 1 1 1 1
## ..$ id : chr [1:9] "019999006" "019999007" "019999005" "010501002" ...
## ..$ precioMin : num [1:9] 196 145 140 469 297 ...
Parece que nos está devolviendo lo mismo que para muchas sucursales, pero podemos aprovecharlo a nuestro favor. Al coincidir el precio mínimo con el máximo, entonces tenemos el precio de todos esos productos para la sucursal, Es decir, podemos saber exactamente el valor para la sucursal que estamos buscando, exactamente lo que queríamos. Ahora solo nos queda un cabo suelto: vincular el id de las sucursales con información sobre su ubicación y empresa.
Hagamos lo que estamos aprendiendo: “escuchar” qué pedidos hace la página cuando le pedimos que cargue cierta información. Si es necesario, abrán una ventana de incognito y prueben lo siguiente: ingresen una dirección, la que ustedes quieran. Yo elegí Av. Juan B. Justo al 4000, y vean la cantidad de consultas que se generan,
Analizando la interacción entre la página web y una API
Pueden miraralas con detenimiento, pero vamos a enfocarnos en solo una de todas estas: la consulta de sucursales, la que aparece en cuarto lugar. Veamos que es lo que sucede si ejecutamos esta consulta desde R
sucursales <- fromJSON("https://d3e6htiiul5ek9.cloudfront.net/prod/sucursales?lat=-34.6046544&lng=-58.4584745&limit=30")
glimpse(sucursales)
## List of 5
## $ status : int 200
## $ totalPagina : int 30
## $ total : int 2875
## $ sucursales :'data.frame': 30 obs. of 15 variables:
## ..$ distanciaNumero : num [1:30] 0.0572 0.1242 0.1413 0.4745 0.4854 ...
## ..$ distanciaDescripcion: chr [1:30] "0.06 kilómetros" "0.12 kilómetros" "0.14 kilómetros" "0.47 kilómetros" ...
## ..$ banderaId : int [1:30] 1 1 3 1 1 1 1 1 1 1 ...
## ..$ lat : chr [1:30] "-34.6041648" "-34.6048231" "-34.604107" "-34.6056819" ...
## ..$ lng : chr [1:30] "-58.4586699" "-58.4571361" "-58.459865" "-58.4634946" ...
## ..$ sucursalNombre : chr [1:30] "5248 - LA PATERNAL" "AV.SAN MARTIN" "San Martín 2289" "5232 - VILLA GENERAL MITRE" ...
## ..$ id : chr [1:30] "15-1-5248" "12-1-2" "10-3-396" "15-1-5232" ...
## ..$ sucursalTipo : chr [1:30] "Autoservicio" "Supermercado" "Autoservicio" "Autoservicio" ...
## ..$ provincia : chr [1:30] "AR-C" "AR-C" "AR-C" "AR-C" ...
## ..$ direccion : chr [1:30] "Paysandu 1816" "Av. San Martín 2095" "Av. San Martín 2289" "Av Donato Alvarez 1887" ...
## ..$ banderaDescripcion : chr [1:30] "Supermercados DIA" "COTO CICSA" "Express" "Supermercados DIA" ...
## ..$ localidad : chr [1:30] "CAPITAL FEDERAL" "Paternal" "Ciudad Autónoma de Buenos Aires" "CAPITAL FEDERAL" ...
## ..$ comercioRazonSocial : chr [1:30] "DIA Argentina S.A" "Coto Centro Integral de Comercialización S.A." "INC S.A." "DIA Argentina S.A" ...
## ..$ comercioId : int [1:30] 15 12 10 15 15 15 23 15 10 65 ...
## ..$ sucursalId : chr [1:30] "5248" "2" "396" "5232" ...
## $ maxLimitPermitido: int 30
Lo que está devolviendo es, de nuevo, una lista, en este caso de 5 elementos. Lo más interesante de esto es que el data frame sucursales, en la cuarta posición de esta lista, tiene información muy relevante sobre todas las sucursales más cercanas. Lo que hace esta función de la API, entonces, es exactamente lo siguiente: le pasamos un punto expresado en latitud y longitud y nos devuelve las sucursales más cercanas, con un limite de 30 sucursales. Por supuesto, podemos usar el mismo principio de offset para quedarnos con todas las sucursales que queramos y es exactamente lo que vamos a hacer: extraer información sobre todas las sucursales de la página.
offset <- seq(0,2875,by=30)
lista <- map(offset,function(offsetNumber){
cat("offset: ",offsetNumber,"\r")
consulta <- paste("https://d3e6htiiul5ek9.cloudfront.net/prod/sucursales?lat=-34.6046544&lng=-58.4584745&limit=30&offset=",offsetNumber,sep="")
resultado <- fromJSON(consulta)
resultado$sucursales
})
Parece que pudo scrapear todas las sucursales sin problemas. Como hicimos anteriormente, podemos usar bind_rows() con el objetivo de tener todo junto en un solo data.frame()
## Rows: 2,727
## Columns: 15
## $ distanciaNumero <dbl> 0.0572, 0.1242, 0.1413, 0.4745, 0.4854, 0.5132...
## $ distanciaDescripcion <chr> "0.06 kilómetros", "0.12 kilómetros", "0.14 ki...
## $ banderaId <int> 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1...
## $ lat <chr> "-34.6041648", "-34.6048231", "-34.604107", "-...
## $ lng <chr> "-58.4586699", "-58.4571361", "-58.459865", "-...
## $ sucursalNombre <chr> "5248 - LA PATERNAL", "AV.SAN MARTIN", "San Ma...
## $ id <chr> "15-1-5248", "12-1-2", "10-3-396", "15-1-5232"...
## $ sucursalTipo <chr> "Autoservicio", "Supermercado", "Autoservicio"...
## $ provincia <chr> "AR-C", "AR-C", "AR-C", "AR-C", "AR-C", "AR-C"...
## $ direccion <chr> "Paysandu 1816", "Av. San Martín 2095", "Av. S...
## $ banderaDescripcion <chr> "Supermercados DIA", "COTO CICSA", "Express", ...
## $ localidad <chr> "CAPITAL FEDERAL", "Paternal", "Ciudad Autónom...
## $ comercioRazonSocial <chr> "DIA Argentina S.A", "Coto Centro Integral de ...
## $ comercioId <int> 15, 12, 10, 15, 15, 15, 23, 15, 10, 65, 12, 15...
## $ sucursalId <chr> "5248", "2", "396", "5232", "52", "24", "6220"...
Como vemos, la cantidad de filas es menor a la cantidad total de sucursales que decía que tenía la API. Esto es probablemente porque algunas sucursales han dejado de reportar, no nos preocupamos tanto porque tenemos más del 95% del total y más de 2.700 sucursales en todo el pais. Si tienen instalados los paquetes sf y leaflet podemos hacer un mapa rápidamente
## Linking to GEOS 3.8.0, GDAL 3.0.4, PROJ 6.3.1
library(leaflet)
sucursalesSF <- sucursales %>%
mutate(lat=as.numeric(lat),lng=as.numeric(lng)) %>%
st_as_sf(coords=c("lng","lat"),crs=4326)
leaflet(sucursalesSF) %>% addTiles() %>% addCircles()
Con toda esta información ya podemos, finalmente, crear nuestro propio dataset de precios. Lo que vamos a hacer es intentar ser lo más federales posibles: vamos a samplear 5 puntos de venta por cada una de las provincias de Argentina
Ya sabemos qué es lo que hay que hacer: para cadauna de estas sucursales, buscar todos los productos. Pero recuerden que nos queda un cabo suelto para cerrar esto. Nosotros podemos usar la estrategia de offset porque sabemos cual es el límite de posiciones antes de arrancar a buscar porque lo hicimos antes. Entonces nueestro código va a hacer exactamente eso: buscar la cantidad de casos que existen para adecuar, en base a eso, la cantidad de offsets
# Tenemos que pasar una consulta para cada una de las sucursales
sucursales <- unique(muestrasSucursales %>% pull(id))
# Para cada sucursal
listaPreciosXSucursal <- map(1:length(sucursales),function(sucursal){
# Para que nos vaya marcando por cual sucursal va
cat("Sucursal: ",sucursal," / ",length(sucursales),"\r")
# Hacemos una primera consulta solo para conocer la cantidad de datos que tiene para darnos
consultaDummy <- fromJSON(paste("https://d3e6htiiul5ek9.cloudfront.net/prod/productos?&array_sucursales=",sucursales[sucursal],"&offset=0&limit=1",sep=""))
# Total de resultados
total <- consultaDummy$total
# Valores que tiene que usar de offset
offset <- seq(0,total,by=100)
map(offset,function(offsetNumber){
consultaPrecio <- fromJSON(paste("https://d3e6htiiul5ek9.cloudfront.net/prod/productos?&array_sucursales=",sucursales[sucursal],"&offset=",offsetNumber,"&limit=100",sep=""))
consultaPrecio$productos
}) %>% bind_rows()
})
Si ven que tarda mucho - efectivamente lo hace - pueden seleccionar solo 2 o 3 sucursales para ver si funciona correctamente. Si no, pueden cargar los datos, que subí a internet, mediante el siguiente link
load(url("https://github.com/martinmontane/martinmontane.github.io/raw/master/listaPreciosXSucursal.RData"))
Tenemos un objeto un poco pesado con 120 elementos… uno por cada comercio ! Usemos bind_rows() que debería colapsar toda esta información a un solo data frame
Tenemos un data.frame con 512.311 filas. Cada fila contiene el precio de un determinado producto en una sucursal el día 15 de julio de 2020. Hacemos una pequeña limpieza de estos datos, eliminando una de las columnas de precios y tambien la de columna de cantidad de sucurslaes disponibles, que siempre será 0, y al mismo tiempo le cambios el nombre a precioMin por precio.
Ejercicios
Fácil: Buscar el precio promedio de la coca-cola de 2.25 ltr entre todos los comercios de Argentina
Fácil/medio: Buscar el precio promedio de la coca-cola de 2.25 ltr para cada una de las provincias scrapeadas
Difícil: Vincular cada uno de los productos con la categoría correspondiente de Precios Claros. Pista: hay que hacer el scraper por gran categoría (01, 02, 03, etc…) para poder conocer la categoría a la que pertenece cada uno de ellos