En este tutorial vamos a utilizar la tećnica conocida como web scraping para extraer información de la página de lobby del Senado de Chile. Específicamente, veremos cómo extraer con R la tabla en que aparece el registro de las reuniones que sostiene cada senador/a y cómo transformarla para que quede en un formato analizable.

Actualmente, cuando accedemos a la página de audiencias nos encontramos con una sola tabla que contiene todos los registros de reuniones desde 2014 a la fecha. Si queremos encontrar algo, solo podemos hacerlo con la opción de buscar de nuestro navegador. No es posible filtrar por senador/a para ver todas sus reuniones. Tampoco es posible buscar patrones complejos (por ejemplo, varias palabras al mismo tiempo) y menos explorar quiénes han solicitado más reuniones 😑.

El objetivo de este tutorial es mostrar paso a paso cómo convetir esa tabla en un set de datos analizable en R. Es un ejemplo sencillo de web scraping porque solo trabajaremos con un elemento del sitio web. El principal desafío será limpiar los datos para poder utilizarlos.

En esta primera parte mostraremos el proceso de obtención y limpieza de los datos. En la segunda, exploraremos dos aspectos: veremos cómo hacer búsquedas por palabras clave en el contenido de las reuniones y averiguaremos quiénes solicitan más audiencias, con qué senadores/as se han reunido y los intereses de quién representan.

Algunas de las cosas que mostraremos cómo hacer a lo largo de esta primera parte:

  • escrapear datos que se encuentran en una tabla usando el paquete {rvest}
  • separar variables que se encuentran dentro de una misma columna usando el paquete {tidyr}
  • crear nombres de archivos que incluyan la fecha en que se crearon usando los paquetes {lubridate} y {glue}.

Los paquetes que utilizaremos 📦

install.packages("tidyverse")
install.packages("janitor")
library(rvest) 
library(dplyr)
library(janitor) 
library(tidyr) 
library(lubridate)
library(stringr)
library(glue)
library(readr)

Importar los datos 📥

Lo primero que tenemos que hacer es leer los datos desde el sitio web del Senado1. Con la función read_html() importaremos todo el código html de la página de audiencias y lo guardaremos en un objeto de R al que llamaremos reuniones_html.

reuniones_html <- read_html("https://www.senado.cl/appsenado/index.php?mo=lobby&ac=GetReuniones")

Para poder extraer la información de la tabla necesitamos saber cuál es su etiqueta html. Usualmente las tablas tienen la etiqueta table, pero lo vamos a corroborar utilizando Selector Gadget. Con esta extensión de Google Chrome podemos hacer clic sobre un elemento de la página para ver su etiqueta:

Ahora, extraeremos la información de la etiqueta html que nos interesa y la guardaremos en un objeto al que llamaremos reuniones. Para seleccionar la etiqueta usamos la función html_nodes() y luego con html_table() indicamos que queremos leer la tabla como tal (es decir, que queremos mantener ese formato):

reuniones <- reuniones_html %>% 
  html_nodes("table") %>% 
  html_table()

En nuestro ambiente global (Global Environment) tenemos ahora el objeto reuniones, que corresponde a una lista de 2 elementos. Si lo revisamos usando View(reuniones), veremos que el primero de ellos corresponde a la tabla pequeña que está encima de la tabla con las audiencias. La tabla que realmente nos interesa es la segunda ([[2]])2.

Lo que haremos a continuación será quedarnos solo con el segundo elemento de la lista (es decir, reuniones[[2]]) y convertir esta tabla en un tibble, un tipo especial de data frame. Convertirlo a un tibble no es un paso obligatorio, pero tiene ventajas como, por ejemplo, la forma en que se imprime en la consola. Si quieres conocer más detalles sobre los tibbles, puedes revisar el capítulo correspondiente de “R para Ciencia de Datos”

Reescribiremos entonces nuestro objeto para guardar estos cambios:

reuniones <- tibble(reuniones[[2]])

head(reuniones)
## # A tibble: 6 x 5
##   `Sujeto pasivo`   `Fecha - Duración… materia     Asistentes    Representado   
##   <chr>             <chr>              <chr>       <chr>         <chr>          
## 1 Pugh Olavarría, … 2020-11-19  30 Mi… Ley de fin… Clement, Art… Asocacion de l…
## 2 Huenchumilla Jar… 2020-03-26  30 Mi… Suspende r… Gallo Delgad… Centro de Estu…
## 3 Huenchumilla Jar… 2020-03-26  30 Mi… Se suspend… Rimbaud, Axel MEL - Movimien…
## 4 Huenchumilla Jar… 2020-03-23  30 Mi… Suspende r… Reinoso Cifu… Sindicato Núme…
## 5 Huenchumilla Jar… 2020-03-23  30 Mi… Audiencia … Munoz, Victo… VisibLES       
## 6 Huenchumilla Jar… 2020-03-23  30 Mi… Por contin… Espinosa Ába… Federación Nac…

¡Listo! Tenemos toda la tabla en nuestro computador :) Antes de analizarla, tenemos que hacer un par de arreglos.

Unificar el nombre de las variables ✏️

Si miramos la tabla anterior, veremos que no hay consistencia en el uso de mayúsculas y minúsculas en los nombres de las variables. Además, algunas de ellas tienen espacios en el nombre, lo que nos forzaría a rodearlas de tildes graves cuando queramos mencionarlas en nuestro código (así: `Sujeto pasivo`).

Para resolver esto, usaremos la función clean_names() del paquete {janitor} que, como su nombre lo indica, sirve precisamente para limpiar nombres. La opción por defecto es snake case, es decir, todo en minúsculas y las palabras separadas por un guión bajo (así: sujeto_pasivo). Rescribiremos nuestro objeto y luego chequearemos que quedó todo bien con la función names(), que imprime los nombres de las variables de un set de datos.

reuniones <- clean_names(reuniones)
names(reuniones)
## [1] "sujeto_pasivo"        "fecha_duracion_lugar" "materia"             
## [4] "asistentes"           "representado"

¡Listo!

Separar las variables de la segunda columna ✂️

La segunda columna de nuestro data frame tiene tres variables juntas: la fecha de la reunión, su duración en minutos y el lugar en que se llevó a cabo. Tal como está no nos sirve de mucho. Por ejemplo, no podemos filtrar con facilidad por fecha, ni tampoco podemos explorar la duración de las reuniones.

Para resolver todo esto, lo primero que tendríamos que hacer es separar las variables, para que cada una tenga su propia columna. Para ello, podemos utilizar la función separate() del paquete {tidyr}. En esta función tenemos que indicar la columna que queremos separar, cómo queremos que se llamen las columnas nuevas y cuál es el patrón que separa los datos correspondientes a cada variable. Afortunadamente, hay un patrón. En principio podría parecernos que solo hay un espacio entre cada uno de estos datos (lo que no sería útil, porque no sería un patrón exclusivo para separar cada variable). Sin embargo, si miramos con atención, veremos que son dos espacios:

Probemos, entonces, separar las columnas con ese patrón:

reuniones %>% 
  separate(fecha_duracion_lugar, c("fecha", "duracion", "lugar"), "  ")
## # A tibble: 3,381 x 7
##    sujeto_pasivo  fecha  duracion lugar   materia    asistentes   representado  
##    <chr>          <chr>  <chr>    <chr>   <chr>      <chr>        <chr>         
##  1 Pugh Olavarrí… 2020-… 30 Min.  oficin… "Ley de f… Clement, Ar… Asocacion de …
##  2 Huenchumilla … 2020-… 30 Min.  Congre… "Suspende… Gallo Delga… Centro de Est…
##  3 Huenchumilla … 2020-… 30 Min.  Congre… "Se suspe… Rimbaud, Ax… MEL - Movimie…
##  4 Huenchumilla … 2020-… 30 Min.  Congre… "Suspende… Reinoso Cif… Sindicato Núm…
##  5 Huenchumilla … 2020-… 30 Min.  Congre… "Audienci… Munoz, Vict… VisibLES      
##  6 Huenchumilla … 2020-… 30 Min.  Congre… "Por cont… Espinosa Áb… Federación Na…
##  7 Pugh Olavarrí… 2020-… 30 Min.  Argent… "Reunión … Sallorenzo … Facebook      
##  8 Galilea Vial,… 2020-… 20 Min.  Oficin… "Alcances… Bssaber, Go… DIDI MOBILITY…
##  9 Coloma Correa… 2020-… 15 Min.  Oficin… "El Centr… SILVA ARANE… Centro de Est…
## 10 Bianchi Chele… 2020-… 30 Min.  piso 9… "artículo… Guzmán ponc… Asociacion de…
## # … with 3,371 more rows

¡Funciona! Sin embargo, obtuvimos una ⚠️advertencia⚠️:

## Warning: Expected 3 pieces. Additional pieces discarded in 48 rows [56, 63, 74,
## 87, 224, 234, 242, 464, 474, 502, 529, 553, 580, 642, 651, 721, 765, 775, 875,
## 893, ...].

Expected 3 pieces. Additional pieces discarded in 48 rows. R esperaba poder dividir nuestra columna en tres, es decir, encontrar dos veces en cada celda nuestro patrón de dos espacios; sin embargo, en 48 ocasiones volvió a encontrar otros dos espacios. En esos casos, separó una cuarta columna y la descartó. ¿De dónde salieron esos espacios extra? Probablemente, errores de tipeo al ingresar los datos. ¿Cómo podemos resolverlo? La función separate() tiene un argumento adicional: extra. Con extra le indicamos qué queremos que haga en caso de que al dividir según nuestro patrón encuentre más piezas de las esperadas. La opción que usaremos será "merge": queremos que fusione todo en la última columna. Es decir, que luego de separar fecha y duración, todo lo que quede lo deje como parte de la columna lugar:

reuniones %>% 
  separate(fecha_duracion_lugar, c("fecha", "duracion", "lugar"), "  ", extra = "merge")
## # A tibble: 3,381 x 7
##    sujeto_pasivo  fecha  duracion lugar   materia    asistentes   representado  
##    <chr>          <chr>  <chr>    <chr>   <chr>      <chr>        <chr>         
##  1 Pugh Olavarrí… 2020-… 30 Min.  oficin… "Ley de f… Clement, Ar… Asocacion de …
##  2 Huenchumilla … 2020-… 30 Min.  Congre… "Suspende… Gallo Delga… Centro de Est…
##  3 Huenchumilla … 2020-… 30 Min.  Congre… "Se suspe… Rimbaud, Ax… MEL - Movimie…
##  4 Huenchumilla … 2020-… 30 Min.  Congre… "Suspende… Reinoso Cif… Sindicato Núm…
##  5 Huenchumilla … 2020-… 30 Min.  Congre… "Audienci… Munoz, Vict… VisibLES      
##  6 Huenchumilla … 2020-… 30 Min.  Congre… "Por cont… Espinosa Áb… Federación Na…
##  7 Pugh Olavarrí… 2020-… 30 Min.  Argent… "Reunión … Sallorenzo … Facebook      
##  8 Galilea Vial,… 2020-… 20 Min.  Oficin… "Alcances… Bssaber, Go… DIDI MOBILITY…
##  9 Coloma Correa… 2020-… 15 Min.  Oficin… "El Centr… SILVA ARANE… Centro de Est…
## 10 Bianchi Chele… 2020-… 30 Min.  piso 9… "artículo… Guzmán ponc… Asociacion de…
## # … with 3,371 more rows

Ahora sí funciona tal como esperábamos :)

Reescribiremos nuestro objeto para guardar los cambios:

reuniones <- reuniones %>% 
  separate(fecha_duracion_lugar, c("fecha", "duracion", "lugar"), "  ", extra = "merge")

La fecha como fecha 📅

Por fin tenemos la variable fecha como una columna independiente. Sin embargo, actualmente es de tipo caracter.

str(reuniones)
## tibble [3,381 × 7] (S3: tbl_df/tbl/data.frame)
##  $ sujeto_pasivo: chr [1:3381] "Pugh Olavarría, Kenneth" "Huenchumilla Jaramillo, Francisco" "Huenchumilla Jaramillo, Francisco" "Huenchumilla Jaramillo, Francisco" ...
##  $ fecha        : chr [1:3381] "2020-11-19" "2020-03-26" "2020-03-26" "2020-03-23" ...
##  $ duracion     : chr [1:3381] "30 Min." "30 Min." "30 Min." "30 Min." ...
##  $ lugar        : chr [1:3381] "oficina valparaiso" "Congreso Nacional - Morande 441" "Congreso Nacional - Morande 441" "Congreso Nacional - Morande 441" ...
##  $ materia      : chr [1:3381] "Ley de financiamiento estrategia de las FFAA" "Suspende reunión, por contingencia sanitaria que enfrenta el país relacionada al Covid-19" "Se suspende, por contingencia sanitaria que enfrenta el país relacionada al Covid-19" "Suspende reunión, por contingencia sanitaria que enfrenta el país relacionada al Covid-19" ...
##  $ asistentes   : chr [1:3381] "Clement, Arturo; Picón Gutiérrez, Francisco Roberto; Del Solar Agüero, Felipe; Valdes Saavedra, José Joaquin Valdes" "Gallo Delgado, Fatima Patricia; Puelle, María Jimena" "Rimbaud, Axel" "Reinoso Cifuentes, Mauricio; Bustos Munizaga, Maggie; Bielefeldt Herrería, Sebastián" ...
##  $ representado : chr [1:3381] "Asocacion de la Industria del Salmón de Chile AG" "Centro de Estudios de Conflicto y Cohesión Social (COES)" "MEL - Movimiento contra el Exceso de velocidad Letal" "Sindicato Número 2 Empresa Conservador de Bienes Raíces de Santiago" ...

La transformaremos en una variables de tipo fecha () usando la función as_date() del paquete {lubridate}.

reuniones <- reuniones %>% 
  mutate(fecha = as_date(fecha))

La duración como variable numérica 🔢

La variable duración también es de tipo caracter. No podemos convertirla aún en numérica con as.numeric(), ya que el hecho de que además del número de minutos diga Min. haría que todos los valores queden como NA. Por lo tanto, primero tenemos que eliminar todo lo que no sea la cantidad de minutos. Para eso, vamos a utilizar la función str_remove() del paquete {stringr} para remover " Min.".

reuniones %>% 
  mutate(duracion = str_remove(duracion, " Min."), 
         duracion = as.numeric(duracion))
## # A tibble: 3,381 x 7
##    sujeto_pasivo  fecha      duracion lugar  materia   asistentes  representado 
##    <chr>          <date>        <dbl> <chr>  <chr>     <chr>       <chr>        
##  1 Pugh Olavarrí… 2020-11-19       30 ofici… "Ley de … Clement, A… Asocacion de…
##  2 Huenchumilla … 2020-03-26       30 Congr… "Suspend… Gallo Delg… Centro de Es…
##  3 Huenchumilla … 2020-03-26       30 Congr… "Se susp… Rimbaud, A… MEL - Movimi…
##  4 Huenchumilla … 2020-03-23       30 Congr… "Suspend… Reinoso Ci… Sindicato Nú…
##  5 Huenchumilla … 2020-03-23       30 Congr… "Audienc… Munoz, Vic… VisibLES     
##  6 Huenchumilla … 2020-03-23       30 Congr… "Por con… Espinosa Á… Federación N…
##  7 Pugh Olavarrí… 2020-03-13       30 Argen… "Reunión… Sallorenzo… Facebook     
##  8 Galilea Vial,… 2020-03-11       20 Ofici… "Alcance… Bssaber, G… DIDI MOBILIT…
##  9 Coloma Correa… 2020-03-11       15 Ofici… "El Cent… SILVA ARAN… Centro de Es…
## 10 Bianchi Chele… 2020-03-11       30 piso … "artícul… Guzmán pon… Asociacion d…
## # … with 3,371 more rows

Como el resultado es el esperado, reescribiremos el objeto para guardar los cambios.

reuniones <- reuniones %>% 
  mutate(duracion = str_remove(duracion, " Min."), 
         duracion = as.numeric(duracion))

¡Listo! Nuestros datos ya están lo suficientemente limpios y ordenados como para poder analizarlos.

Guardar los datos 💾

Ahora que tenemos la tabla de audiencias en un solo data frame ordenado, podríamos querer guardar nuestro objeto como archivo .csv. Hay dos cosas que tenemos que considerar:

  • No podemos guardar el archivo usando como delimitador comas o puntos y comas, ya que algunas columnas contienen esos signos de puntuación.
  • Estos datos cambian con el tiempo, así que sería importante que el nombre del archivo tuviera la fecha de extracción.

Para resolver el primer punto, usaremos como delimitador una barra |. Es un signo que no aparece en ninguna columna. Para resolver el segundo, crearemos un objeto que contenga la fecha de hoy con la función today() del paquete {lubridate}. Luego, usaremos el paquete {glue} para componer nuestro nombre de archivo:

hoy <- today()
write_delim(reuniones, glue("reuniones_senadores_{hoy}.csv"), delim = "|")

Como hoy es “2020-05-03”, el archivo creado tiene como nombre: reuniones_senadores_2020-05-03.csv. Si lo guardara mañana, el nombre cambiaría.

Segunda parte: temas de las reuniones y principales lobistas 🔍

En la segunda parte de este tutorial:

  • revisaremos cómo hacer búsquedas por palabras clave en el contenido de las reuniones

  • averiguaremos quiénes solicitan más audiencias, con qué senadores/as se han reunido y los intereses de quién representan.

Ambas cosas implicarán trabajar con expresiones regulares y funciones del paquete {stringr}.

Notas


  1. Lo primero-primero que debería hacer es seguir ciertas normas de “etiqueta” y averiguar si tengo autorización para escrapear la página. Sin embargo, desde mi punto de vista (que puedes o no compartir), estos son datos de interés público y están disponibles en un sitio web que es público, por lo que no considero que sea necesario “pedir permiso”. En otro post más adelante mostraré cómo ser cortés a la hora de escrapear contenido.↩︎

  2. Cuando tenemos más de un elemento con la misma etiqueta, como en este caso, podemos seleccionarlo por la posición cuando leemos la etiqueta. El código que tendríamos que haber utilizado es:

    reuniones_html %>% 
      html_nodes("table:nth-child(2)") %>% 
      html_table()

    Para este post preferí mostrar el procedimiento que uno haría cuando recién empieza a aplicar este tipo de técnicas.↩︎