Tratamiento de textos. Primeras bases de datos

Cuadernos prácticos de Software II del Grado en Ciencia de Datos Aplicada (curso 2024-2025)

Author

Javier Álvarez Liébana

1 Profundizando en textos

Aunque no podamos hacer operaciones aritméticas con ellos si serán importante algunas operaciones que podamos realizar con las cadenas de texto. Para eso usaremos el paquete {stringr} (dentro del mismo «universo de paquetes» de {lubridate})

library(stringr)

Con dicho paquete vamos a trabajar particularmente con cuatro familias de funciones

  • Manipulación

  • Tratamiento de espacios

  • Búsqueda de patrones

1.1 Utilidades básicas

1.1.1 Longitud

La más obvia es una función que, dada una cadena de texto (un string) nos proporcione la longitud. Para ello podemos usar la función str_length()

str_length("abc")
[1] 3

Es importante advertir que cuenta tanto números como espacios, así como caracteres que no sean alfanuméricos.

str_length("abc 123 *")
[1] 9

Además si el texto es ausente devuelve ausente (recuerda: NA es ausente, "NA" es una cadena de texto más)

str_length(NA)
[1] NA

 

Las funciones del paquete están preparadas para ser vectorizadas lo que significa que si aplicamos una función a un vector de dos cadenas de texto la aplica a ambos de la misma forma.

str_length(c("abc", "defghi"))
[1] 3 6

1.1.2 Ordenar

Otra muy habitual es ordenar cadenas de texto (por orden alfabético). Para ello podemos usar la función str_order(), distinguiendo ..._sort() y ..._order() como con los números

x <- c("y", "i", "k")
str_order(x)
[1] 2 3 1
str_sort(x)
[1] "i" "k" "y"

1.2 Manipulación

1.2.1 Extraer subcadenas

  • Extraer subcadenas: dada una cadena de texto, str_sub(texto, star = ..., end = ...) nos extrae la subcadena desde la posición star hasta end (si es negativo empieza a contar por detrás).
str_sub("abcd efg", star = 4, end = 6)
[1] "d e"
str_sub("abcd efg", star = 5)
[1] " efg"
str_sub("abcd efg", star = 4, end = -2)
[1] "d ef"
  • Extraer subcadenas: la función str_sub() permite aplicarlo a de manera vectorial a múltiples cadenas de texto, e incluso usarla para asignar valores.
x <- c("abcdef", "ghifjk")
str_sub(x, star = 3, end = -2)
[1] "cde" "ifj"
str_sub(x, star = -1, end = -1)
[1] "f" "k"
# En ambas cadenas, sustituimos por * en la posición 2
str_sub(x, star = 2, end = 2) <- "*"

1.2.2 Duplicar cadenas

  • Duplicar cadenas: con str_dup(..., times = ...), dada una cadena de texto (o varias), podemos repetir una cadena times veces.
str_dup("abc", times = 3)
[1] "abcabcabc"
x <- c("abcdef", "ghifjk")
str_dup(x, times = c(2, 5))
[1] "abcdefabcdef"                   "ghifjkghifjkghifjkghifjkghifjk"

1.2.3 Concatenar cadenas

  • Concatenar cadenas: con str_c podemos concatenar distintas cadenas de texto (con sep = ... indicamos el caracter que hará de separador)
str_c("Buenos días", "Mi nombre es Javier")
[1] "Buenos díasMi nombre es Javier"
str_c("Buenos días", "Mi nombre es Javier", sep = ". ")
[1] "Buenos días. Mi nombre es Javier"

1.2.4 Mayúsculas y minúsculas

  • Mayúsculas/minúsculas: con str_to_...() podemos convertir textos a mayúsculas (..._upper), a minúsculas (..._lower) y a título (..._title, primera letra de cada palabra en mayúscula)
str_to_upper("me llamo Javi")
[1] "ME LLAMO JAVI"
str_to_lower("me llamo Javi")
[1] "me llamo javi"
str_to_title("me llamo Javi")
[1] "Me Llamo Javi"

1.2.5 Reemplazar

  • Reemplazar: str_replace() busca un patrón dado en una cadena de texto y, si la encuentra, la sustituye pro otra de reemplazo
str_replace(c("javi", "sandra", "carlos"), pattern = "i", replacement = "*")
[1] "jav*"   "sandra" "carlos"

. . .

Con str_replace_all() reemplazamos todas las coincidencias (por defecto sino solo se reemplaza la primera)

str_replace(c("javi", "sandra", "carlos"), pattern = "a", replacement = "*")
[1] "j*vi"   "s*ndra" "c*rlos"
str_replace_all(c("javi", "sandra", "carlos"), pattern = "a", replacement = "*")
[1] "j*vi"   "s*ndr*" "c*rlos"

1.3 Espacios en blanco

1.3.1 Rellenar espacios

  • Rellenar: la función str_pad() rellena una cadena con espacios (al inicio por defecto) para que tenga anchura indicada. Con side = "both" como argumento extra nos añade en ambos lados. Con side = "right" los añade al final. Con pad = ... podemos decidir si queremos rellenar con otro tipo de caracter (espacio por defecto).
str_pad("abc", width = 6)
[1] "   abc"
str_pad("abc", 12, side = "both")
[1] "    abc     "
str_pad("abc", 6, side = "right", pad = "*")
[1] "abc***"

. . .

Si width es menor que la longitud de la cadena, no hace nada.

str_pad("abc",  width = 2)
[1] "abc"

1.3.2 Eliminar espacios

  • Eliminar espacios: con str_trim() podemos eliminar espacios en blanco al inicio y al final de la cadena. Si añadimos side = ... podemos cambiar si queremos que solo los elimine al final o al inicio (por defecto, en ambos). Con str_squish() cambiamos cualquier sucesión de espacios en blanco en medio del texto por uno solo (y elimina al inicio y final)
str_trim(" abcde   fghi ")
[1] "abcde   fghi"
str_trim(" abcde   ")
[1] "abcde"
str_trim(" abcde   ", side = "left")
[1] "abcde   "
str_squish(" abcde   fghi ")
[1] "abcde fghi"

1.4 Patrones

1.4.1 Detectar. Expresiones regulares.

  • Detectar: con str_detect() podemos detectar si una cadena de texto contiene o no una secuencia de caracteres
str_detect(c("javi álvarez", "javi reyes", "sandra reyes"), pattern = "javi")
[1]  TRUE  TRUE FALSE
str_detect(c("javi álvarez", "javi reyes", "sandra reyes"), pattern = "reyes")
[1] FALSE  TRUE  TRUE
str_detect(c("javi álvarez", "javi reyes", "sandra reyes"), pattern = "carlos")
[1] FALSE FALSE FALSE
  • Expresiones regulares: no solo vamos a poder detectar patrones simples sino que podemos hacer uso de las conocidas como expresiones regulares, indicándole por ejemplo que queremos localizar todo patrón que sea, al menos una letra
str_detect(c("a", "ab", "abc", "abcd"), pattern = "[a-z]")
[1] TRUE TRUE TRUE TRUE

. . .

Si tras los corchetes indicamos {n} podemos detectar aquellas cadenas con n letras consecutivas

str_detect(c("a", "ab", "abc", "abcd"), pattern = "[a-z]{3}")
[1] FALSE FALSE  TRUE  TRUE
  • Expresiones regulares: un buen manejo de estas expresiones puede sernos muy útil para, por ejemplo, detectar formatos correctos en DNI o números de teléfono (de Madrid, por ejemplo).

Vamos a considerar que un formato correcto de DNI es aquel seguido por 8 números ([0-9]{8}) seguido directamente de una letra mayúscula ([A-Z]).

str_detect(c("5055A198-W", "50508040W", "5050505W", "50508040-W"),
           pattern = "[0-9]{8}[A-Z]")
[1] FALSE  TRUE FALSE FALSE

. . .

Podemos buscar distintos patrones a la vez concatenándolos con una |

str_detect(c("5055A198-W", "50508040W", "5050505W", "50508040-W"),
           pattern = "[0-9]{8}[A-Z]|[0-9]{8}[-][A-Z]")
[1] FALSE  TRUE FALSE  TRUE

1.4.2 Contar

  • Contar patrones: con str_count() podemos contar cuantas veces aparece un mismo patrón
str_count(c("abcd defg", "ab defg", "ab cd"), pattern = "[a-z]{4}")
[1] 2 1 0

1.4.3 Localizar posiciones

  • Localizar posiciones: str_locate() nos permite localizar la primera posición en la que se produce un patrón. Con str_locate_all() obtenemos todos
str_locate(c("abcde abcd", "cba", "*a*"), pattern = "a")
     start end
[1,]     1   1
[2,]     3   3
[3,]     2   2
str_locate_all(c("abcde abcd", "cba", "*a*"), pattern = "a")
[[1]]
     start end
[1,]     1   1
[2,]     7   7

[[2]]
     start end
[1,]     3   3

[[3]]
     start end
[1,]     2   2

1.4.4 Extraer patrones

  • Extraer patrones: con str_extract() podemos extraer patrones (con str_extract_all() todos ellos) de una cadena de texto.
str_extract(c("DNI: 5050W", "DNI: 50558040W, DNI: 50558080-W", "DNI: 50558080-W"),
            pattern = "[0-9]{8}[A-Z]|[0-9]{8}[-][A-Z]")
[1] NA           "50558040W"  "50558080-W"
str_extract_all(c("DNI: 5050W", "DNI: 50558040W, DNI: 50558080-W", "DNI: 50558080-W"),
            pattern = "[0-9]{8}[A-Z]|[0-9]{8}[-][A-Z]")
[[1]]
character(0)

[[2]]
[1] "50558040W"  "50558080-W"

[[3]]
[1] "50558080-W"

1.4.5 Dividir cadenas

  • Dividir: con str_split() podemos localizar un patrón y dividir la cadena de texto siempre que aparezca (con str_split_fixed() podemos dividir en un número concreto de trozos)
str_split(c("a-b-c", "ab-c-d-e"), pattern = "-")
[[1]]
[1] "a" "b" "c"

[[2]]
[1] "ab" "c"  "d"  "e" 
str_split_fixed(c("a-b-c", "ab-c-d-e"), pattern = "-", n = 2)
     [,1] [,2]   
[1,] "a"  "b-c"  
[2,] "ab" "c-d-e"

. . .

Si usamos boundary() como patrón podemos dividir en base a caracteres, frases, palabras, etc.

x <- "Esto es una frase. Y esto otra."
str_split(x, boundary("word"))
[[1]]
[1] "Esto"  "es"    "una"   "frase" "Y"     "esto"  "otra" 
str_split(x, boundary("sentence"))
[[1]]
[1] "Esto es una frase. " "Y esto otra."       

1.5 💻 Tu turno (textos)

Intenta realizar los siguientes ejercicios sin mirar las soluciones

El dataset será discursos (extraído de https://github.com/lirondos/discursos-de-navidad) donde están guardados los discursos navidad de los jefes de Estado de España desde 1946 hasta 2021 (en dictadura y en democracia).

load(file = "./datos/discursos.RData")

📝 Convierte todos los discurso a minúscula.

Code
# Convertimos a minúscula
discursos$texto <- str_to_lower(discursos$texto)

📝 Elimina signos de puntuación tales como “:”, “,”, “.”, “;”, “¡”, “!”, “¿” y “?”. Tras ello elimina espacios adelante, atrás y en medio si existen solo deja uno de ellos.

Code
# Eliminamos los signos de puntuación
discursos$texto <-
  str_remove_all(discursos$texto, pattern = "\\:|\\,|\\.|\\;|\\¡|\\!|\\¿|\\?")

# Tras ello eliminamos espacios adelante, atrás y en medio solo dejamos uno
discursos$texto <- str_squish(discursos$texto)

📝 Crea una nueva variable long con la longitud de cada discurso

Code
# nueva variable
discursos$long <- str_length(discursos$texto)

📝 Añade una nueva variable n_words con el nº palabras de cada discurso. Pista: si tras dividir cada discurso en palabras usas length() te devolverá seguro 76 ya que lo ha guardado en un tipo de dato llamado lista. Para calcular la longitud de cada uno de los 76 elementos de la lista usaremos lengths()

Code
lista <- list("a" = 1:2, "b" = 1:3, "c" = 1:4) # Explicación length
length(lista)
lengths(lista)
# Dividimos y aplicamos lengths
discursos$n_words <- lengths(str_split(discursos$texto, boundary("word")))

📝 Determina los 5 años con mayor longitud, y los 5 años con menor número de palabras

Code
# 5 años con mayor longitud (usamos order para obtener índices)
discursos$year[order(discursos$long, decreasing = TRUE)[1:5]]

# 5 años con menor cantidad de palabras
discursos$year[order(discursos$n_words)[1:5]]

📝 Incorpora una nueva variable llamada spain que calcule el número de veces que se dice “españoles”, “españolas” o “españa” en el discurso. Determina los 5 años dónde menos se menten dichas palabras

Code
# Contamos
discursos$spain <- str_count(discursos$texto, pattern = "españoles|españolas|españa")

# Años con más menciones
discursos$year[order(discursos$spain, decreasing = TRUE)[1:5]]

📝 De los 76 años calcula el número de discursos en los que las palabras “mujer” o “mujeres” se nombren más que las palabras “hombre” u “hombres”

Code
sum(str_count(discursos$texto, pattern = "mujer|mujeres") >
      str_count(discursos$texto, pattern = "hombre|hombres"))

📝 Detecta los discursos donde aparece “cataluña”, “catalanes”, “catalán” o “catalanas” y quédate de la base de datos solo con aquellos que lo cumpla

Code
discursos[str_detect(discursos$texto, pattern = "cataluña|catalanes|catalán|catalanas"), ]

2 Primera base de datos

Cuando analizamos datos solemos tener varias variables de cada individuo: necesitamos una «tabla» que las recopile.

2.1 Primer intento: matrices

La opción más inmediata son las matrices: concatenación de variables del mismo tipo e igual longitud.

Imagina que tenemos estaturas y pesos de 4 personas. ¿Cómo crear un dataset con las dos variables?

 

La opción más habitual es usando cbind(): concatenamos (bind) vectores en forma de columnas (c)

estaturas <- c(150, 160, 170, 180)
pesos <- c(63, 70, 85, 95)
datos_matriz <- cbind(estaturas, pesos)
datos_matriz
     estaturas pesos
[1,]       150    63
[2,]       160    70
[3,]       170    85
[4,]       180    95

También podemos construir la matriz por filas con la función rbind() (concatenar - bind - por filas - rows), aunque lo recomendable es tener cada variable en columna e individuo en fila como luego veremos.

rbind(estaturas, pesos) # Construimos la matriz por filas
          [,1] [,2] [,3] [,4]
estaturas  150  160  170  180
pesos       63   70   85   95
  • Podemos «visualizar» la matriz con View(matriz).

  • Podemos comprobar las dimensiones con dim(), nrow() y ncol(): las matrices son un tipo de datos tabulados (organizados en filas y columnas)

dim(datos_matriz)
[1] 4 2
nrow(datos_matriz)
[1] 4
ncol(datos_matriz)
[1] 2

También podemos «darle vuelta» (matriz transpuesta) con t().

t(datos_matriz)
          [,1] [,2] [,3] [,4]
estaturas  150  160  170  180
pesos       63   70   85   95

Dado que ahora tenemos dos dimensiones en nuestros datos, para acceder a elementos con [] deberemos proporcionar dos índices separados por comas: índice de la fila y de la columna

datos_matriz[2, 1] # segunda fila, primera columna
estaturas 
      160 
datos_matriz[1, 2] # primera fila, segunda columna 
pesos 
   63 

En algunas casos querremos obtener los datos totales de un individuo (una fila concreta pero todas las columnas) o los valores de toda una variable para todos los individuos (una columna concreta pero todas las filas). Para ello dejaremos sin rellenar uno de los índices

datos_matriz[2, ] # segundo individuo
estaturas     pesos 
      160        70 
datos_matriz[, 1] # primera variable
[1] 150 160 170 180

Mucho de lo aprendido con vectores podemos hacerlo con matrices, así podemos por ejemplo acceder a varias filas y/o columnas haciendo uso de las secuencias de enteros 1:n

datos_matriz[c(1, 3), 1] # primera variable para el primer y tercer individuo
[1] 150 170

También podemos definir una matriz a partir de un vector numérico, reorganizando los valores en forma de matriz (sabiendo que los elementos se van colocando por columnas).

z <- matrix(1:9, ncol = 3) 
z
     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    2    5    8
[3,]    3    6    9

Incluso podemos definir una matriz de valores constantes, por ejemplo de ceros (para luego rellenar)

matrix(0, nrow = 2, ncol = 3)
     [,1] [,2] [,3]
[1,]    0    0    0
[2,]    0    0    0

2.1.1 Operaciones con matrices

Con las matrices sucede como con los vectores: cuando aplicamos una operación aritmética lo hacemos elemento a elemento

z/5
     [,1] [,2] [,3]
[1,]  0.2  0.8  1.4
[2,]  0.4  1.0  1.6
[3,]  0.6  1.2  1.8

Para realizar operaciones en un sentido matricial deberemos añadir %%, por ejemplo, para multiplicar matrices será %*%

z * t(z)
     [,1] [,2] [,3]
[1,]    1    8   21
[2,]    8   25   48
[3,]   21   48   81
z %*% t(z)
     [,1] [,2] [,3]
[1,]   66   78   90
[2,]   78   93  108
[3,]   90  108  126

También podemos realizar operaciones por columnas/filas sin recurrir a bucles con la función apply(), y le indicaremos como argumentos

  • la matriz
  • el sentido de la operación (MARGIN = 1 por filas, MARGIN = 2 por columnas)
  • la función a aplicar
  • argumentos extra que necesite la función

Por ejemplo, para aplicar una media a cada variable, será mean aplicada con MARGIN = 2 (misma función para cada columna)

# Media (mean) por columnas (MARGIN = 2)
apply(datos_matriz, MARGIN = 2, FUN = "mean")
estaturas     pesos 
   165.00     78.25 

2.2 💻 Tu turno (matrices)

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Modifica el código inferior para definir una matriz x de unos, de 3 filas y 7 columnas.

x <- matrix(0, nrow = 2, ncol = 3)
x
Code
x <- matrix(1, nrow = 3, ncol = 7)
x

📝 A la matriz anterior, suma un 1 a cada número de la matriz y divide el resultado entre 5. Tras ello calcula su transpuesta

Code
new_matrix <- (x + 1)/5
t(new_matrix)

📝 ¿Por qué el código inferior nos devuelve dicho mensaje de aviso?

matrix(1:15, nrow = 4)
Warning in matrix(1:15, nrow = 4): data length [15] is not a sub-multiple or
multiple of the number of rows [4]
     [,1] [,2] [,3] [,4]
[1,]    1    5    9   13
[2,]    2    6   10   14
[3,]    3    7   11   15
[4,]    4    8   12    1

📝 Define la matriz x <- matrix(1:12, nrow = 4). Tras ello obtén los datos del primer individuo, los datos de la tercera variable, y el elemento (4, 1).

Code
x <- matrix(1:12, nrow = 4)
x[1, ] # primera fila
x[, 3] # tercera columna
x[4, 1] # elemento (4, 1)

📝 Define una matriz de 2 variables y 3 individuos tal que cada variable capture la estatura y la edad 3 personas, de manera que la edad de la segunda persona sea desconocida (ausente). Tras ello calcula la media de cada variable (¡nos debe de volver un número!)

Code
datos <- cbind("edad" = c(20, NA, 25), "estatura" = c(160, 165, 170))
apply(datos, MARGIN = 2, FUN = "mean", na.rm = TRUE) # media por columnas

📝 ¿Por qué devuelve error el código inferior? ¿Qué está mal?

matriz <- cbind("edad" = c(15, 20, 25), "nombres" = c("javi", "sandra", "carlos"))
matriz
     edad nombres 
[1,] "15" "javi"  
[2,] "20" "sandra"
[3,] "25" "carlos"
matriz + 1
Error in matriz + 1: non-numeric argument to binary operator

2.3 Segundo intento: data.frame

Las matrices tienen el mismo problema que los vectores: si juntamos datos de distinto tipo, se perturba la integridad del dato ya que los convierte (fíjate en el código inferior: las edades y los TRUE/FALSE los ha convertido a texto)

edades <- c(14, 24, NA)
soltero <- c(TRUE, NA, FALSE)
nombres <- c("javi", "laura", "lucía")
matriz <- cbind(edades, soltero, nombres)
matriz
     edades soltero nombres
[1,] "14"   "TRUE"  "javi" 
[2,] "24"   NA      "laura"
[3,] NA     "FALSE" "lucía"

De hecho al no ser números ya no podemos realizar operaciones aritméticas

matriz + 1
Error in matriz + 1: non-numeric argument to binary operator

Para poder trabajar con variables de distinto tipo tenemos en R lo que se conoce como data.frame: concatenación de variables de igual longitud pero que pueden ser de tipo distinto.

tabla <- data.frame(edades, soltero, nombres)
class(tabla)
[1] "data.frame"
tabla
  edades soltero nombres
1     14    TRUE    javi
2     24      NA   laura
3     NA   FALSE   lucía

Dado que un data.frame es ya un intento de «base de datos» las variables no son meros vectores matemáticos: tienen un significado y podemos (debemos) ponerles nombres que describan su significado

library(lubridate)

Attaching package: 'lubridate'
The following objects are masked from 'package:base':

    date, intersect, setdiff, union
tabla <-
  data.frame("edad" = edades, "estado" = soltero, "nombre" = nombres,
             "f_nacimiento" = as_date(c("1989-09-10", "1992-04-01", "1980-11-27")))
tabla
  edad estado nombre f_nacimiento
1   14   TRUE   javi   1989-09-10
2   24     NA  laura   1992-04-01
3   NA  FALSE  lucía   1980-11-27

¡TENEMOS NUESTRO PRIMER CONJUNTO DE DATOS! (estrictamente no podemos hablar de base de datos pero de momento como lo si fuesen). Puedes visualizarlo escribiendo su nombre en consola o con View(tabla)

2.3.1 Acceso a variables

Si queremos acceder a sus elementos, al ser de nuevo datos tabulados, podemos acceder como en las matrices (no recomendable): de nuevo tenemos dos índices (filas y columnas, dejando libre la que no usemos)

tabla[2, ]  # segunda fila (todas sus variables)
  edad estado nombre f_nacimiento
2   24     NA  laura   1992-04-01
tabla[, 3]  # tercera columna (de todos los individuos)
[1] "javi"  "laura" "lucía"
tabla[2, 1]  # primera característica de la segunda persona
[1] 24