Esta es solo una pequeña introducción al trabajo con la tecnología de la información que dio origen a todo: la escritura. Siendo uno de los paquetes instalables más antiguos de la humanidad y considerando que el proceso de instalación demora unos cuantos años escolares se podrán imaginar que se trata de una librería bastante compleja. En este notebook vamos apenas a rascar la superficie de ese inmenso universo.
El texto escrito es una representación simbólica visual o táctil del lenguaje hablado. Esa representación nos permite almacenar sonidos y conceptos como una secuencia de caracteres discretos. A la vez, podemos decodificar esos símbolos y recuperar parcialmente la información de esos sonidos y conceptos. Exactamente lo que están haciendo ahora.
Asi como los números tienen sus formas de ser almacenados y manipulados por los distintos lenguajes de programación, los textos también las tienen. Ya estuvimos usando varias de ellas pero ahora las vamos a presentar más formalmente. En ‘R’ las variables que almacenan textos son de tipo character
pero también se les suele decir ‘strings’.
Para trabajar con strings vamos a usar algunas librerías especificas que nos van a simplificar la vida. Una de esas librerias es stringr
(como el personaje de The Wire - si no la vieron veanla). Es parte del ‘tidyverse’ asi que viene gratis al activarlo:
library(tidyverse)
## ── Attaching packages ─────────────────────────────────────── tidyverse 1.3.0 ──
## ✓ ggplot2 3.3.5 ✓ purrr 0.3.4
## ✓ tibble 3.1.4 ✓ dplyr 1.0.7
## ✓ tidyr 1.1.3 ✓ stringr 1.4.0
## ✓ readr 2.0.1 ✓ forcats 0.5.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## x dplyr::filter() masks stats::filter()
## x dplyr::lag() masks stats::lag()
Las funciones que nos interesan son las que empiezan con str_. Pueden consultar el cheatsheet de stringr con las distintas funciones y sus usos. Empecemos con un ejemplo, vamos a seguir sacandole el jugo al dataset de tragos. Traigamos al igual que en el notebook de grafos todas las bebidas.
require(jsonlite)
## Loading required package: jsonlite
##
## Attaching package: 'jsonlite'
## The following object is masked from 'package:purrr':
##
## flatten
bebidas = NULL
url_base = 'http://www.thecocktaildb.com/api/json/v1/1/search.php?f='
letra = 'a'
for(letra in letters){
print(paste('Letra:',letra))
dwld = fromJSON(paste(url_base,letra,sep=''))$drinks
bebidas = rbind(bebidas,dwld)
print(paste('Había',nrow(dwld),'bebidas'))
}
## [1] "Letra: a"
## [1] "Había 25 bebidas"
## [1] "Letra: b"
## [1] "Había 25 bebidas"
## [1] "Letra: c"
## [1] "Había 25 bebidas"
## [1] "Letra: d"
## [1] "Había 18 bebidas"
## [1] "Letra: e"
## [1] "Había 10 bebidas"
## [1] "Letra: f"
## [1] "Había 24 bebidas"
## [1] "Letra: g"
## [1] "Había 25 bebidas"
## [1] "Letra: h"
## [1] "Había 17 bebidas"
## [1] "Letra: i"
## [1] "Había 11 bebidas"
## [1] "Letra: j"
## [1] "Había 13 bebidas"
## [1] "Letra: k"
## [1] "Había 16 bebidas"
## [1] "Letra: l"
## [1] "Había 22 bebidas"
## [1] "Letra: m"
## [1] "Había 25 bebidas"
## [1] "Letra: n"
## [1] "Había 6 bebidas"
## [1] "Letra: o"
## [1] "Había 13 bebidas"
## [1] "Letra: p"
## [1] "Había 24 bebidas"
## [1] "Letra: q"
## [1] "Había 8 bebidas"
## [1] "Letra: r"
## [1] "Había 24 bebidas"
## [1] "Letra: s"
## [1] "Había 25 bebidas"
## [1] "Letra: t"
## [1] "Había 25 bebidas"
## [1] "Letra: u"
## [1] "Había bebidas"
## [1] "Letra: v"
## [1] "Había 13 bebidas"
## [1] "Letra: w"
## [1] "Había 11 bebidas"
## [1] "Letra: x"
## [1] "Había bebidas"
## [1] "Letra: y"
## [1] "Había 2 bebidas"
## [1] "Letra: z"
## [1] "Había 13 bebidas"
bebidas
nrow(bebidas)
## [1] 420
inst_IT = bebidas[,"strInstructionsIT"]
inst_DE =bebidas[,"strInstructionsDE"]
inst_EN = bebidas[,"strInstructions"]
Miremos uno de los textos en aleman:
inst_DE[1]
## [1] "Alle Zutaten in einen Cocktailshaker geben, mischen und über Eis in ein gekühltes Glas servieren."
Si queremos separar las palabras podemos usar la función str_split, que ya usamos en el TP de tragos. Queremos separar el string por cualquier caracter que NO sea una letra. Mirando el cheatsheet vemos que eso podemos hacerlo usando expresiones regulares. Una expresión regular es una manera de explicitar patrones de búsqueda dentro de secuencias. En este caso la expresión que nos va a servir es ‘[^[:alpha:]]’ es decir ‘[^]’ << cosas que no sean ‘[:alpha:]’ << caracteres alfabeticos. Probemos:
str_split(inst_DE, "[^[:alpha:]]")[1]
## [[1]]
## [1] "Alle" "Zutaten" "in" "einen"
## [5] "Cocktailshaker" "geben" "" "mischen"
## [9] "und" "über" "Eis" "in"
## [13] "ein" "gekühltes" "Glas" "servieren"
## [17] ""
Vemos que la expresión funciona bastante bien aunque extrae algunos strings vacíos que luego tendremos que eliminar.
El proceso de separar un texto en elementos constituyentes (usualmente palabras aunque no siempre) se llama con el anglicismo “tokenizar”. Este proceso es dependiente de cada idioma y suele ser un poco más complejo que aplicar una simple expresión regular; pero para nuestro pequeño experimento, este método nos va a alcanzar.
Si bien vamos a ver más adelante que hay funciones específicamente armadas para “tokenizar”, las expresiones regulares son una herramienta muy potente a la hora manipular strings y es recomendable aprender a usarlas. Ademas, suelen ser parte de las librerias básicas en la mayoría de los lenguajes. Dicho esto con un gran poder viene una gran responsabilidad como dijo superman. Y es muy fácil que las expreciones regulares se salgan de control (miren por ejemplo la regex para validar direcciones de mail).
Usando la variable stringr::words
que contiene algunas palabras comunes en ingles utilicen la función str_view
para mostrar como extraerían diferentes patrones. Pongan el parámetro de str_view en TRUE para que solo muestre las palabras en las que encuentran el patrón. Por ejemplo, para encontrar todas las palabras que terminan en ‘y’ usaríamos:
str_view(stringr::words, "y$", match = TRUE) # fijense que $ indica final de linea
Nótense que se resalta la porción del string que corresponde al patrón. Si queremos las que empiezan con ‘y’ usaríamos:
str_view(stringr::words, "^y", match = TRUE) # fijense que $ indica final de linea
Si queremos las que empiezan con ‘y’ solo de tres caracteres:
str_view(stringr::words, "^y..$", match = TRUE) # fijense que $ indica final de linea
Escriban una expresión regular para detectar:
Si ahora aplicamos la regex para separar palabras a todos los textos podemos contar cuantas veces aparece cada palabra y ordenarlas por frecuencia. Si lo graficamos en forma log-log tiene esta pinta:
words_DE = str_split(inst_DE, "[^[:alpha:]]") # aplicamos el split a todos los textos
words_DE = table(tolower(unlist(words_DE))) # llevamos todo a minusculas y contamos las palabras
words_DE = as.data.frame(words_DE) # convertimos en dataframe
colnames(words_DE)[1] = 'Word' # renombramos a la columna 1 como Word
words_DE = words_DE[words_DE$Word != '',] # Quitamos el string vacio
words_DE %>% arrange(by=desc(Freq)) %>% # Ordenamos en forma descendiente
mutate(rank=row_number()) %>% # Agregamos una columna con el orden
ggplot(aes(x=log(rank), y=log(Freq))) + # Graficamos
geom_point(shape=21, color='black', fill='white')
¿A qué les hace acordar?
Repitan lo anterior para el italiano y para el inglés. Pongan las curvas en un solo gráfico.
¿Qué les parece que está pasando? Pista: Ley de Zipf
Las nubes de palabras son una manera de visualizar grupos grandes de palabra y representar su importancia (usualmente por la frecuencia). Bien usadas son una manera de tener una vista a vuelo de pajaro que permita despertar y guiar nuevas preguntas. Sin embargo, como siempre, esas intuiciones tendrán que ser luego complementadas por métodos más cuantitativos. Hay un paquete de R especificamente diseñado para generar nubes de palabras que podemos usar: wordcloud Veamoslo con un ejemplo (recuerden instalarlo si no lo tienen).
require(wordcloud)
## Loading required package: wordcloud
## Loading required package: RColorBrewer
Podemos graficar las palabras anteriores segun su frecuencia:
wordcloud(words = words_DE$Word, freq = words_DE$Freq, min.freq = 1, max.words=200, random.order=FALSE, rot.per=0.35, colors=brewer.pal(8, "Dark2"))
Hagan las nubes de palabra según frecuencia para italiano y para inglés. ¿Qué notan sobre las palabras más frecuentes?
(Usen google translate para ver cuales son las palabras mas frecuentes.)
En general las palabras más frecuentes no suelen ser las más informativas. Son conectores, artículos, pronombres, etc. Si tuviésemos que representar lo mejor posible la receta de un trago en pocas palabras (resumir digamos) no elegiríamos las más frecuentes. ¿Cuáles elegirían?
¿Si quisiésemos buscar el trago más parecido a un Manhattan en base a su receta, como lo harían?
Hay una idea sencilla pero bastante potente para representar y operar sobre colecciones de textos que surge de esta observación de que la frecuencia relativa de las palabras nos habla de alguna manera de su ‘densidad de información’. Esa técnica se conoce como TF-IDF por sus siglas en inglés Term Frequency-Inverse Document Frequency. Aca hay un pequeño texto de divulgación sobre el tema que escribí hace un tiempo. Tómense 7 min para leerlo antes de seguir. Un poco autorreferencial autocitarme, lo se… pero la alternativa que era copiar y pegar me pareció peor.
Para esto vamos a usar un paquete ‘tidytext’ que nos va a simplificar la construcción de la tabla de palabras-documentos. Recuerden instalarla antes.
library(tidytext)
Vamos a volver a contar la frecuencia de las palabras pero guardando la identidad del documento del que vienen. Podemos aprovechar la función especifica de tidytext para esto unnest_tokens.
words_de <- tibble(text=inst_EN, doc=1:length(inst_EN)) %>%
unnest_tokens(word, text) %>%
count(doc, word, sort = TRUE)
Calculamos los pesos TFIDF con bind_tf_idf
words_tfidf = bind_tf_idf(words_de, word, doc, n)
Podemos ver cuanto se parecen los textos entre si simplemente calculando la ‘distancia’ de esos vectores que representan a cada texto. Para eso podemos usar la funcion pairwise_similarity de ‘widyr’ (recuerden instalarla si no la tienen)
library(widyr)
similarities = words_tfidf %>%
pairwise_similarity(doc, word, tf_idf, sort = TRUE)
En la tabla ‘similarities’ podemos buscar recetas que se parezcan. Por ejemplo, ¿Cuál es la receta más parecida a la del trago 80?
Receta del trago 80:
inst_EN[80]
## [1] "In a highball glass almost filled with ice cubes, combine the gin and ginger ale. Stir well. Garnish with the lime wedge."
similarities[similarities$item1==80,]
Vemos que la receta 11 es la que más se parece
inst_EN[11]
## [1] "Pour all of the ingredients into a highball glass almost filled with ice cubes. Stir well."
Usen los pesos de TFIDF para extraer los 4 términos más importantes de cada receta y compárenlos con los 4 términos más frecuentes de esa misma receta. Elijan recetas más o menos largas. ¿Qué ven? ¿Cuál les parece una mejor descripción resumida de la receta?