Mini intro a Chainy (Shiny)

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.

directory <- "C:/Users/dhumb/Documents/projects/sip-alcaldias-nlp/data/SIP"  
files <- list.files(directory)

data <- map(files,
            ~ read_csv(paste(directory, .x, sep = "/"), 
                       locale = locale(encoding = "latin1"),
                       show_col_types = FALSE) %>% 
                         as_tibble() %>% 
                         mutate_all(as.character)) %>% 
  bind_rows() 
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 personales
  mutate(
    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.