Ejemplo de visualizacion interactiva sobre solicitudes de informacion
spanish
interactive
shiny
Author
David Humberto Jiménez S.
Published
March 3, 2024
Bienvenides!
A ustedes no les pasa que sienten que pueden hacer algo mejor, ¿pero no saben exactamente qué falta? A mí me pasa seguido, pero afortunadamente el internet tiene cosas preciosas que te invitan a intentarlo. Y luego de ver todas las cosas bonitas que se pueden hacer en Shiny decidí seguir este tutorial, el cheatsheet y las recomendaciones de R ladies
Ahora, para no perder la bonita costumbre de trabajar con datos de solicitudes, también he estado leyendo sobre los modelos de word embedding para Procesamiento de Lenguaje Natural (PLN) word2vec y doc2vec. En resumen, estos modelos de machine learning lo que hacen es encontrar palabras por contextos similares (palabras adyacentes). Por ejemplo, nuestros teclados en los celulares de autocorrector, o cuando buscas algo en Google y completa el texto. Me he inspirado de diversos trabajos en español: como este análisis de rap, un ejercicio para detectar noticias falsas y un artículo sobre un centro de emergencias de la Universidad del Azuay, Ecuador.
Entonces, en esta entrada haré lo siguiente: de un conjunto de datos de solicitudes de información pública realizaré un clasificador temático con doc2vec, y los resultados serán presentados en una web app interactiva para que la exploren por ustedes mismas, para que no vean solo lo que les presento.
Las SIP utilizadas corresponden a las dieciséis alcaldías de la Ciudad de México, mi fuente de información es el Instituto Nacional de Transparencia, Acceso a la Información y Proteccion de Datos Personales (INAI)
Datos
Cuando buscas y descargas los “datos abiertos” de la Plataforma Nacional de Transparencia (PNT), te devuelve un archivo de tipo zip en tu correo. Desde que empiezas la búsqueda hasta recibir el correo pasan entre 15 y 20 minutos.
Para conservar los datos de la manera en que se cargaron a la PNT, voy a reutilizar los csv de este proyecto pero que estoy haciendo en python. Posteriormente, voy a unir todos los archivos en una sola base de datos para utilizar tidyverse para limpiar los datos.
library(dplyr)
Attaching package: 'dplyr'
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
library(purrr)library(readr)
Warning: package 'readr' was built under R version 4.3.3
library(shiny)
Warning: package 'shiny' was built under R version 4.3.3
library(udpipe)
Warning: package 'udpipe' was built under R version 4.3.3
library(stringr)library(ggplot2)
Warning: package 'ggplot2' was built under R version 4.3.3
library(doc2vec)
Warning: package 'doc2vec' was built under R version 4.3.3
library(word2vec)
Warning: package 'word2vec' was built under R version 4.3.3
library(lubridate)
Attaching package: 'lubridate'
The following objects are masked from 'package:base':
date, intersect, setdiff, union
Como siempre, a mí me gusta ‘llamar’ las librerías con las que voy a trabajar en el primer código. Luego ‘leo’ todos los csv. En esta ocasión utilicé el paquete reader en lugar de la función base de R read.csv porque me salían mensajes de error por el encoding.
Warning: One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
dat <- vroom(...)
problems(dat)
One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
dat <- vroom(...)
problems(dat)
One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
dat <- vroom(...)
problems(dat)
One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
dat <- vroom(...)
problems(dat)
One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
dat <- vroom(...)
problems(dat)
One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
dat <- vroom(...)
problems(dat)
summary(data)
FOLIO FECHASOLICITUD DEPENDENCIA ESTATUS
Length:212235 Length:212235 Length:212235 Length:212235
Class :character Class :character Class :character Class :character
Mode :character Mode :character Mode :character Mode :character
MEDIOENTRADA TIPOSOLICITUD DESCRIPCIONSOLICITUD OTROSDATOS
Length:212235 Length:212235 Length:212235 Length:212235
Class :character Class :character Class :character Class :character
Mode :character Mode :character Mode :character Mode :character
ARCHIVOADJUNTOSOLICITUD MEDIOENTREGA FECHALIMITE
Length:212235 Length:212235 Length:212235
Class :character Class :character Class :character
Mode :character Mode :character Mode :character
RESPUESTA TEXTORESPUESTA FECHARESPUESTA FECHASOLICITUDTERMINO
Length:212235 Length:212235 Length:212235 Length:212235
Class :character Class :character Class :character Class :character
Mode :character Mode :character Mode :character Mode :character
PAIS ESTADO MUNICIPIO CODIGOPOSTAL
Length:212235 Length:212235 Length:212235 Length:212235
Class :character Class :character Class :character Class :character
Mode :character Mode :character Mode :character Mode :character
SECTOR PRORROGA PREVENCION DISPONIBILIDAD
Length:212235 Length:212235 Length:212235 Length:212235
Class :character Class :character Class :character Class :character
Mode :character Mode :character Mode :character Mode :character
TIPODERECHOARCOP QUEJA
Length:212235 Length:212235
Class :character Class :character
Mode :character Mode :character
Posteriormente, hice una breve limpieza de datos. Había hecho una para reemplazar nombres de alcaldías, pero coinciden con aquellas solicitudes sobre datos personales.
data <- data %>%filter(TIPOSOLICITUD =='Información pública') %>%#Porque solo nos interesan las SIP, y habia 16 de datos personalesmutate(FECHASOLICITUD =dmy(FECHASOLICITUD),FECHALIMITE =dmy(FECHALIMITE),FECHARESPUESTA =dmy(FECHARESPUESTA),FECHASOLICITUDTERMINO =dmy(FECHASOLICITUDTERMINO),DEPENDENCIA =str_remove_all(DEPENDENCIA, "Alcaldía"),DEPENDENCIA =str_trim(DEPENDENCIA, 'both'),FOLIO =str_remove_all(FOLIO, "[:punct:]"),FOLIO =str_remove_all(FOLIO, "=") )summary(data)
FOLIO FECHASOLICITUD DEPENDENCIA ESTATUS
Length:208436 Min. :2019-02-22 Length:208436 Length:208436
Class :character 1st Qu.:2020-03-08 Class :character Class :character
Mode :character Median :2021-12-10 Mode :character Mode :character
Mean :2021-09-26
3rd Qu.:2023-03-08
Max. :2024-02-25
MEDIOENTRADA TIPOSOLICITUD DESCRIPCIONSOLICITUD OTROSDATOS
Length:208436 Length:208436 Length:208436 Length:208436
Class :character Class :character Class :character Class :character
Mode :character Mode :character Mode :character Mode :character
ARCHIVOADJUNTOSOLICITUD MEDIOENTREGA FECHALIMITE
Length:208436 Length:208436 Min. :2019-03-08
Class :character Class :character 1st Qu.:2020-04-20
Mode :character Mode :character Median :2022-01-13
Mean :2021-11-11
3rd Qu.:2023-03-24
Max. :2024-03-15
NA's :380
RESPUESTA TEXTORESPUESTA FECHARESPUESTA
Length:208436 Length:208436 Min. :2019-02-26
Class :character Class :character 1st Qu.:2020-04-20
Mode :character Mode :character Median :2021-12-16
Mean :2021-10-30
3rd Qu.:2023-03-10
Max. :2024-02-25
NA's :1747
FECHASOLICITUDTERMINO PAIS ESTADO MUNICIPIO
Min. :2019-02-26 Length:208436 Length:208436 Length:208436
1st Qu.:2020-04-20 Class :character Class :character Class :character
Median :2021-12-16 Mode :character Mode :character Mode :character
Mean :2021-10-29
3rd Qu.:2023-03-09
Max. :2024-02-25
NA's :2291
CODIGOPOSTAL SECTOR PRORROGA PREVENCION
Length:208436 Length:208436 Length:208436 Length:208436
Class :character Class :character Class :character Class :character
Mode :character Mode :character Mode :character Mode :character
DISPONIBILIDAD TIPODERECHOARCOP QUEJA
Length:208436 Length:208436 Length:208436
Class :character Class :character Class :character
Mode :character Mode :character Mode :character
Como pueden observar, hay un total de 208,436 solicitudes para el periodo 2019-2024 en las 16 alcaldías de la CDMX. Luego, doc2vec tiene la restricción de que los textos no deben superar las mil palabras. Por eso cree la variable y aplique un filtro. Para terminar trabajando con 206,566 solicitudes.
data <- data %>%mutate(txt_clean_word2vec(DESCRIPCIONSOLICITUD),nword =txt_count_words(DESCRIPCIONSOLICITUD) ) %>%filter(nword >0& nword <=1000)summary(data)
FOLIO FECHASOLICITUD DEPENDENCIA ESTATUS
Length:206566 Min. :2019-02-22 Length:206566 Length:206566
Class :character 1st Qu.:2020-03-08 Class :character Class :character
Mode :character Median :2021-12-01 Mode :character Mode :character
Mean :2021-09-26
3rd Qu.:2023-03-10
Max. :2024-02-25
MEDIOENTRADA TIPOSOLICITUD DESCRIPCIONSOLICITUD OTROSDATOS
Length:206566 Length:206566 Length:206566 Length:206566
Class :character Class :character Class :character Class :character
Mode :character Mode :character Mode :character Mode :character
ARCHIVOADJUNTOSOLICITUD MEDIOENTREGA FECHALIMITE
Length:206566 Length:206566 Min. :2019-03-08
Class :character Class :character 1st Qu.:2020-04-20
Mode :character Mode :character Median :2021-12-29
Mean :2021-11-11
3rd Qu.:2023-03-29
Max. :2024-03-15
NA's :380
RESPUESTA TEXTORESPUESTA FECHARESPUESTA
Length:206566 Length:206566 Min. :2019-02-26
Class :character Class :character 1st Qu.:2020-04-20
Mode :character Mode :character Median :2021-12-16
Mean :2021-10-30
3rd Qu.:2023-03-15
Max. :2024-02-25
NA's :1747
FECHASOLICITUDTERMINO PAIS ESTADO MUNICIPIO
Min. :2019-02-26 Length:206566 Length:206566 Length:206566
1st Qu.:2020-04-20 Class :character Class :character Class :character
Median :2021-12-14 Mode :character Mode :character Mode :character
Mean :2021-10-28
3rd Qu.:2023-03-14
Max. :2024-02-25
NA's :2291
CODIGOPOSTAL SECTOR PRORROGA PREVENCION
Length:206566 Length:206566 Length:206566 Length:206566
Class :character Class :character Class :character Class :character
Mode :character Mode :character Mode :character Mode :character
DISPONIBILIDAD TIPODERECHOARCOP QUEJA
Length:206566 Length:206566 Length:206566
Class :character Class :character Class :character
Mode :character Mode :character Mode :character
txt_clean_word2vec(DESCRIPCIONSOLICITUD) nword
Length:206566 Min. : 1.00
Class :character 1st Qu.: 29.00
Mode :character Median : 55.00
Mean : 90.47
3rd Qu.: 109.00
Max. :1000.00
Word2vec
Y ahora es momento de utilizar doc2vec. Para entenderlo, es necesario explicar que este es una extensión de la arquitectura de machine learning word2vec. Esta detecta el contexto de las palabras de acuerdo a sus palabras vecinas. Hay dos modelos: Continous Bag of Words (CBOW) que predice la palabra objetivo de acuerdo con las palabras de alrededor, y se recomienda para datos pequeños. Y el otro, Continous Skip-Gram Model que funciona al revés: a partir de una palabra objetivo, predice las palabras de contexto. Este se recomienda para conjuntos de datos más grandes.
Ahora bien, doc2vec, como una extensión, permite al modelo “entender” frases, párrafos o documentos. Si leen el artículo sobre emergencias en Ecuador vienen varios artículos discutiendo las ventajas de uno sobre otro para determinados conjuntos de datos. Así que por facilidad, y porque además es solo el primer paso, se entrenará un modelo doc2vec. Para esto, utilizo los mismos parámetros del blog de bnosac.be.
sip <- data %>%select(FOLIO, DESCRIPCIONSOLICITUD) %>%rename(doc_id = FOLIO,text = DESCRIPCIONSOLICITUD)model <-paragraph2vec(x=sip, type='PV-DBOW',dim =150, iter =10, min_count =3, lr =0.05, threads =2)
Y ya, con eso se genera el modelo. Sencillo, ¿no? Ahora, aún no descubro por qué, pero el modelo tiene menos renglones que los datos. Y justo por esta razón, mi idea original de utilizar un clasificador automático se fue al traste, pero no importa. Porque aún se puede hacer algo interesante con chainy.
Shiny
Shiny es un entorno de trabajo basado en web para visualizaciones interactivas. En otras palabras, es una herramienta para que otras personas puedan interactuar con los análisis, gráficas, tablas y demás cosas que hagamos en R y Python. Y esto nos permite, entre muchas cosas, que las personas que no necesitan saber de datos puedan interactuar y ver lo que nosotras consideramos importante.
Antes de entrar de lleno a chainy es necesario entender que se puede hacer todo lo que ya sabemos hacer en R habitualmente. Con la diferencia de que hay que adaptarlo para que sea interactivo, es decir, que otra persona pueda ajustar cualquier valor, variable o parámetro sin necesidad de tocar el código. Por eso, uno de los posibles usos para el modelo que doc2vec que se entrenó antes, es encontrar documentos que tengan similitud con nuevas palabras. Como pueden ver en el siguiente ejemplo:
similar <-predict(model, newdata =c("agua"), type ='nearest', which ='word2doc', top_n =5)similar
[[1]]
term1 term2 similarity rank
1 agua 0424000219620 0.6598905 1
2 agua 0429000043820 0.6564962 2
3 agua 0424000219720 0.6475419 3
4 agua -Construcción 0.6281804 4
5 agua Espejos 0.6231290 5
Y no es perfecto, pero sí nos muestra solicitudes que contengan la palabra, o palabras, que estemos buscando.
data %>%filter(FOLIO=="0424000219720") %>%select(DESCRIPCIONSOLICITUD)
# A tibble: 1 × 1
DESCRIPCIONSOLICITUD
<chr>
1 "¿Cuántas pipas tiene la Alcaldía para reparto de agua?\n¿De dónde se surten?…
Shiny necesita dos secciones para funcionar: la UI (User Interface o interfaz de usuario) que es lo que la persona va a ver: una gráfica, una tabla, un botón, sombra aquí, sombra allá, etc. Y el Server que es donde van las funciones o la “lógica” de lo que queremos que haga nuestra aplicación de chainy.
ui <-fluidPage(titlePanel("Buscador de SIP por palabra"),sidebarLayout(sidebarPanel(textInput("newdata", "Escribe una palabra:", value =""),numericInput("top_n", "Máximo de solicitudes similares:", value =5, min =1),actionButton("submit", "Puchale aquí") ),mainPanel(tableOutput("similar_docs") ) ))
En este código podemos observar lo siguiente:
La funcion fluidpage() sin la cual nada funciona, es como en ggplot.
titlePanel, que nos permite poner un título al chainy.
sidebarLayout que va a configurar lo que aparece a un ladito, en este caso, los parámetros que puede cambiar la persona
textInput (la palabra)
numericInput (el máximo de solicitudes que puede aparecer, como vimos en el ejemplo aparecen otros términos que no son el folio y así no nos sirve)
actionButton (el botón que va a activar nuestra app)
mainPanel en donde se va a mostrar lo que queremos, en este caso la tabla con las solicitudes.
En general, esta parte me parece que es autoexplicativa y tiene lógica sobre lo que queremos que la persona vea. Además, los nombres en inglés ‘newdata’, ‘top_n’ y ‘submit’ están relacionados con la parte del server porque de nada sirve que lo que se ve este muy bonito si no hace lo que queremos.
server <-function(input, output) {observeEvent(input$submit, {req(input$newdata) similar <-predict(model, newdata = input$newdata, type ="nearest", which ="word2doc", top_n = input$top_n) similar <- similar[[1]] #como es una lista, solo queremos el contenido similar_folio <- data %>%filter(FOLIO %in% similar$term2) %>%select(FOLIO, DESCRIPCIONSOLICITUD) output$similar_docs <-renderTable({ similar_folio }) })}
En este caso, todo funciona como cuando se define una función. Ya sé que yo no soy muy fan de las funciones porque #Tidyverse, pero la idea es que todo vaya dentro de las llaves { } paso a pasito. En este caso, todo empieza cuando la persona presiona el botón.
server <-function(input, output) {observeEvent(input$submit, {req(input$newdata) similar <-predict(model, newdata = input$newdata, type ="nearest", which ="word2doc", top_n = input$top_n) similar <- similar[[1]] #como es una lista, solo queremos el contenido similar_folio <- data %>%filter(FOLIO %in% similar$term2) %>%select(FOLIO, DESCRIPCIONSOLICITUD, FECHASOLICITUD) output$similar_docs <-renderTable({ similar_folio }) })}
Y por último podemos ver que de verdad nuestra app funcione. Pero antes, un disclaimer: esta shiny app es muuuuy sencilla, en este caso solo admite buscar una palabra y no hay mucho de interesante. Hay muchos ejemplos en internet, especialmente en la galería de shiny y no quise hacer un refrito de lo mismo. Por eso preferí frustrarme un rato con doc2vec.
It’s alive!!
shinyApp(ui = ui, server = server)
Shiny applications not supported in static R Markdown documents
Entonces, para finalizar esta entrada, un shiny app necesita 3 elementos (que no son flores, azúcar y muchos colores):
Una UI
Un server (o lógica, o lo que queramos mostrar)
La función shinyApp(ui = ui, server = server)
Por último, dado que esto es un markdown (o un documento plano) no hay chance de que se vea la app de chainy. Entonces, trate de montarla en shinyapps.io Y es más difícil de lo que imagine. Así que, algunas consideraciones: el csv final (solo con folio, descripción y fecha) pesa 122 mb, así que lo convertí en un dataframe de R (rds) y pesa 22 mb. El modelo fue ajustado, porque como estaba originalmente era casi de 500 mb. Así que en la app final tiene 50 dimensiones y fue entrenado en 6 “hilos” en paralelo. Para revisar la app puchale aquí.
Como siempre, mis DM están abiertos en @dhumbertojs y espero que pronto pueda poner comentarios en el blog. Si tienen alguna recomendación o sugerencia, siempre es bienvenida.