9 min read

Scrap de memes en R

Una idea que siempre he tenido en mente es que R (#rstats) no es un lenguaje solo para la estadística. R es un lenguaje de programación que, además de ayudarnos en hacer estadística y econometría, nos sirve para hacer muchas otras cositas.

Para este tutorial/laboratorio, vamos a usar las capacidades de R para entrar a Twitter para descargar memes en español publicados en las últimas horas, y de paso, practicar el proceso para elaborar gifs.

1. Requerimientos:

Para iniciar, requerimos tener lo siguiente:

  1. R y RStudio instalados.

  2. La librería rtweet y una cuenta de Twitter.

  3. El tidyverse (o el sabor de R que más te guste, aunque en este tutorial utilizaremos ese)

y ya.

2. Iniciamos a escribir código.

Iniciaremos llamando a las librerías que vamos a utilizar, las cuales serían las siguientes:

# Librerias: 
library(tidyverse) # Para manejo de datos
library(rtweet)    # Para descargar datos de Twitter
library(magick)    # Para hacer gifs
library(jpeg)      # Para utilizar funciones para manipular imagenes

3. Obtenemos los datos de Twitter.

Para obtener datos sobre lo que la gente publica en Twitter, necesitamos acceder a su API. Para acceder a su API de forma sencilla, la comunidad de #Rstats ha creado el paquete rtweet, el cual se encarga de simplificar la interacción con este servicio a la utilización de funciones de R de las de toda la vida (si has interactuado con APIs en R antes, recordarás que suele ser un tema algo complejo, por lo menos al principio).

En este caso, lo que vamos a hacer será buscar todos los tweets que contengan la palabra "meme" y que esten en español. Para esto, nos podemos ayudar de la herramienta de busqueda avanzada de Twitter, la cual simplifica la formulación de busquedas avanzadas dentro de la Red Social.

La consulta quedaría entonces como memes lang:es. La guardamos en un objeto llamado termino.

# Termino de busqueda: 
termino <- "memes lang:es"

Y metemos el término dentro de nuestra función para descargar Tweets:

bd <- rtweet::search_tweets(q = termino,       # Término de busquyeda
                      n = 10000,               # Numero de Tweets a descargar
                      include_rts = FALSE,     # Incluir Retweets? 
                      retryonratelimit = TRUE) # Reintentar cuando se sobrepase el límite de consulta. 

Hay que mencionar que el API de Twitter nos permite descargar aproximadamente 15,000 tweets cada 15 minutos, por lo que, si de repente quisieramos buscar algún tema más complejo, hay que tener en cuenta que nuestra descarga puede quedarse trabajando un buen rato. Igualmente, el API gratuito solamente nos permite descargar tweets de los últimos 7 días, por lo que también hay que tener eso en cuenta.

La descarga toma un rato, y la barra de descarga suele verse así cuando está trabajando:

Downloading [==============>-----]  45%

Una vez que tenemos los datos, mas vale guardarlos (por cualquier cosa). Para esto, los vamos a guardar en un excel con la siguiente función:

openxlsx::write.xlsx(bd, str_c("memes_de_twitter.xlsx"))

Lo que guardará los datos en un archivo en nuestro directorio de trabajo (el cual tienes que tener bien ubicado).

Y ya. Tenemos los datos.

4. Explorando los datos:

Ahora exploramos la información que tenemos:

# Primeros renglones
head(bd)
## # A tibble: 6 × 90
##   user_id             status_id     created_at          screen_name text  source
##   <chr>               <chr>         <dttm>              <chr>       <chr> <chr> 
## 1 1469135822540754946 150207327484… 2022-03-11 00:05:50 saikiwik    "mi … Twitt…
## 2 1024505790864994304 150207322147… 2022-03-11 00:05:37 CasmarSaulo "@Lu… Twitt…
## 3 972969430010482688  150207311818… 2022-03-11 00:05:13 RapsXXX     "Eso… Twitt…
## 4 276662928           150207310988… 2022-03-11 00:05:11 folklorivy  "\"s… Twitt…
## 5 1372272345524674560 150207310166… 2022-03-11 00:05:09 miiguelmrtz "mi … Twitt…
## 6 364908458           150207310035… 2022-03-11 00:05:08 DieBatsuDie "@u_… Twitt…
## # … with 84 more variables: display_text_width <dbl>, reply_to_status_id <chr>,
## #   reply_to_user_id <chr>, reply_to_screen_name <chr>, is_quote <lgl>,
## #   is_retweet <lgl>, favorite_count <dbl>, retweet_count <dbl>,
## #   quote_count <lgl>, reply_count <lgl>, hashtags <chr>, symbols <lgl>,
## #   urls_url <chr>, urls_t.co <chr>, urls_expanded_url <chr>, media_url <chr>,
## #   media_t.co <chr>, media_expanded_url <chr>, media_type <chr>,
## #   ext_media_url <chr>, ext_media_t.co <chr>, ext_media_expanded_url <chr>, …
# Nombres de las variables: 
names(bd)
##  [1] "user_id"                 "status_id"              
##  [3] "created_at"              "screen_name"            
##  [5] "text"                    "source"                 
##  [7] "display_text_width"      "reply_to_status_id"     
##  [9] "reply_to_user_id"        "reply_to_screen_name"   
## [11] "is_quote"                "is_retweet"             
## [13] "favorite_count"          "retweet_count"          
## [15] "quote_count"             "reply_count"            
## [17] "hashtags"                "symbols"                
## [19] "urls_url"                "urls_t.co"              
## [21] "urls_expanded_url"       "media_url"              
## [23] "media_t.co"              "media_expanded_url"     
## [25] "media_type"              "ext_media_url"          
## [27] "ext_media_t.co"          "ext_media_expanded_url" 
## [29] "ext_media_type"          "mentions_user_id"       
## [31] "mentions_screen_name"    "lang"                   
## [33] "quoted_status_id"        "quoted_text"            
## [35] "quoted_created_at"       "quoted_source"          
## [37] "quoted_favorite_count"   "quoted_retweet_count"   
## [39] "quoted_user_id"          "quoted_screen_name"     
## [41] "quoted_name"             "quoted_followers_count" 
## [43] "quoted_friends_count"    "quoted_statuses_count"  
## [45] "quoted_location"         "quoted_description"     
## [47] "quoted_verified"         "retweet_status_id"      
## [49] "retweet_text"            "retweet_created_at"     
## [51] "retweet_source"          "retweet_favorite_count" 
## [53] "retweet_retweet_count"   "retweet_user_id"        
## [55] "retweet_screen_name"     "retweet_name"           
## [57] "retweet_followers_count" "retweet_friends_count"  
## [59] "retweet_statuses_count"  "retweet_location"       
## [61] "retweet_description"     "retweet_verified"       
## [63] "place_url"               "place_name"             
## [65] "place_full_name"         "place_type"             
## [67] "country"                 "country_code"           
## [69] "geo_coords"              "coords_coords"          
## [71] "bbox_coords"             "status_url"             
## [73] "name"                    "location"               
## [75] "description"             "url"                    
## [77] "protected"               "followers_count"        
## [79] "friends_count"           "listed_count"           
## [81] "statuses_count"          "favourites_count"       
## [83] "account_created_at"      "verified"               
## [85] "profile_url"             "profile_expanded_url"   
## [87] "account_lang"            "profile_banner_url"     
## [89] "profile_background_url"  "profile_image_url"
# Dimensión de la tabla: 
dim(bd)
## [1] 18000    90

Como podemos observar, la base tiene 18,000 tweets (eso son muchos memes) por lo que lo más conveniente va a ser ordenarlos por el numero de likes (la variable favorite count) y quedarnos con los 200 memes más likeados (y aún así siguen siendo muchos). Los memes se pueden visitar con los datos de la columna media_url, la cual contiene una liga hacia la dirección donde se almacena el contenido multimedia de cada post.

# Nos quedamos con los 200 memes más likeados: 
doscientos_memes <- bd %>% 
  arrange(-favorite_count) %>% # Ordenar de mayor a menor
  head(n = 200) # Nos quedamos con los primeros 200

5. Descarga:

Una vez que tenemos estos 200 memes mas likeados, vamos a descargarlos a una carpeta.

Para crear carpetas en R, utilizamos la función dir.create().

dir.create("200 memes")

Extraemos la dirección de los memes como vector de texto:

direccion_memes <- doscientos_memes$media_url %>% 
  unique() %>%  # Para no duplicar
  unlist() # Los deslistamos. 
length(direccion_memes) # Ya se hicieron menos 
## [1] 107

Y los descargamos:

# Bucle elegante para la descarga de los memes
map(.x = seq_along(direccion_memes), # Genera una serie de numeros que va del 1 a la longitud del vector de interés
    .f = function(i){
      curl::curl_download(url = direccion_memes[i], # El link de descarga
                          destfile = str_glue("200 memes/{i}.jpg") # Cada imagen va a llevar un numero, de acuerdo a su turno de descarga
                          )
    })

Si ejecutaste esta función, probablemente te salió un error. Para tratar de solucionarlo (o darla la vuelta) tenemos dos opciones: o corregimos lo que esta mal o lo ignoramos y nos pasamos a lo que sigue ( tan como la vida). Como este ejercicio no es tan riguroso, vamos a tratar de darle la vuelta. Para esto, utilizaremos la función tryCatch, que nos permite evaluar funciones en las que todo sale bien y avisar en los casos en los que algo sale mal.

# Mejoramos el código del chunk anterior: 
map(.x = seq_along(direccion_memes), # Genera una serie de numeros que va del 1 a la longitud del vector de interés
    .f = function(i){
      tryCatch({
        # Lo que debe hacer si todo sale bien: 
          curl::curl_download(url = direccion_memes[i], # El link de descarga
                              destfile = str_glue("200 memes/{i}.jpg") # Cada imagen va a llevar un numero, de acuerdo a su turno de descarga
                              )  
      }, error = function(e){
        # Para que nos avise si hubo algun error, y en donde: 
        print(str_c("Hubo un error en la descarga del meme ", i, " T_T"))
      }
    )
  })

Y así ya tenemos nuestra carpeta llena de imagenes.

6. Limpieza manual

Como los memes provienen de Twitter, es obvio que no se espera que sean grandes memes. Para esto, vamos a quitar, manualmente, aquellos memes que no den gracia, o que sean políticamente incorrectos, o que no cumplan con nuestros altos estándares de humor.

Después de revisar manualmente las mas de 100 imagenes (y de ver memes malísimos) llegué a una selección de 31 imagenes medianamente graciosas ¯\_(ツ)_/¯ para compartir en Redes sociales. Con estas imagenes vamos a armar un GIF para compartirlas todas juntas.

7. Armado del GIF:

Para armar un gif se requiere que todas las imagenes que lo van a conformar sean de dimensiones similares; si hacemos un GIF con imagenes de distintos tamaños nos va a quedar un gif feo de ver. Para ver las dimensiones de las imagenes de nuestra carpeta, hacemos lo siguiente:

# Creamos un vector con la ubicación de las imagenes: 
imagenes <- str_c("200 memes/", list.files("200 memes//"))

# Generamos la tabla de dimensiones: 
dimensiones <- lapply(seq_along(imagenes),
       function(x){
         tryCatch({readJPEG(imagenes[x]) %>%
             dim() %>%
             append(imagenes[x])},
                  error = function(e){
                    print(str_c("error en", x))
                  })
         }) %>%
  do.call(rbind.data.frame, .)

# Cambiamos los nombres de la tabla: 
names(dimensiones) <- c("altura", "anchura", ".", "imagen")

# Visualizamos las dimensiones: 
dimensiones %>% as_tibble()

Como queda claro de arriba, las imagenes tienen dimensiones muy variadas, y eso nos dificultará generar un gif de calidad. Para homogeneizar los tamaños, vamos a rehacerlas con unas dimensiones de 900x900 píxeles (sugerencia, pero se puede usar cualquier otra dimensión).

# Creamos una nueva carpeta para las imagenes que estan bien: 
dir.create("imagenes_bien")

# Bucle elegante para reformatear nuestras imagenes: 
for (i in imagenes) {
  tryCatch({
  
  # Leemos la imagen:
  img <- readJPEG(i)
  
  # Generamos un nuevo nombre para la imagen: 
  nuevo_nombre_imagen <- paste0("imagenes_bien/", i %>% str_remove(pattern = "imagenes|200 memes")) # Corre esto paso por paso para que veas que fue lo que se hizo aquí. 
  
  # Guardamos la nueva imagen en un formato cuadrado de 900x900: 
  jpeg(nuevo_nombre_imagen,
       width=900, height=900)
  
  # La ploteamos como raster (recordemos que las imagenes jpg son también matrices de puntos, como los rasters)
  plot(as.raster(img))
  dev.off()
  
  }, error = function(e){
    
    # En caso de que nos salga error: 
    str_c("error en ", imagenes_magical[i])
  })
  
}

Lo cual nos convertirá todas las imagenes de la carpeta previa en rectángulos de color blanco.

Ahora sí, con las imagenes correctas, armamos el gif. Si me siguen de antes, ya reconocerán este código para armar estas imagenes en movimiento:

# CREACIÓN DEL GIF.
# El gif se guardará en el directorio local: 
str_c("imagenes_bien/", list.files("imagenes_bien/")) %>%
  map(image_read) %>% # Lee rutas de los archivos.
  image_join() %>% # Junta imágenes
  image_animate(fps=5) %>% # Anima las imagenes, con 1 segundo entre imágenes.
  image_write("memes.gif") # Escribe el gif en el directorio.

Y ya. Eso es todo. Tenemos nuestro GIF de memes.

8. Conclusiones:

Si bien el descargar memes pudiera parecer algo trivial (y lo es), vale la pena pensar en las aplicaciones de esto para otro tipo de temas (como buscar imagenes de manifestaciones en tiempo real, de visualizaciones de datos, de mapas o de otros temas más serios). Igualmente, esto es otro ejemplo de las aplicaciones de los datos de Twitter para hacer más y más variadas visualizaciones, aparte de los análisis típicos como las redes y las nubes de palabras. Practica con otro tema, con otra fecha y cuéntame en los comentarios como te fue.

¡Saludos!

–Juvenal