8  Expresiones Regulares en R

En este capítulo aprenderás sobre expresiones regulares, un lenguaje conciso y poderoso para describir patrones dentro de cadenas de texto. Estas son abreviadas comúnmente como regex. Las expresiones regulares te permiten detectar, contar, extraer y modificar patrones en texto, lo cual es invaluable para tareas de análisis y limpieza de datos.

Para esta clase utilizaremos las funciones de expresiones regulares del paquete stringr de la colección tidyverse, así como datos del paquete babynames.

8.1 Conceptos básicos de patrones

El patrón más simple consiste en letras y números que coinciden exactamente con esos caracteres:

str_view(fruit, "berry")
 [6] │ bil<berry>
 [7] │ black<berry>
[10] │ blue<berry>
[11] │ boysen<berry>
[19] │ cloud<berry>
[21] │ cran<berry>
[29] │ elder<berry>
[32] │ goji <berry>
[33] │ goose<berry>
[38] │ huckle<berry>
[50] │ mul<berry>
[70] │ rasp<berry>
[73] │ salal <berry>
[76] │ straw<berry>

La mayoría de los caracteres de puntuación tienen significados especiales en regex y se llaman metacaracteres. Por ejemplo, el punto (.) coincide con cualquier carácter:

str_view(c("a", "ab", "ac", "bd"), "a.")
[2] │ <ab>
[3] │ <ac>
str_view(fruit, "a...e")
 [1] │ <apple>
 [7] │ bl<ackbe>rry
[48] │ mand<arine>
[51] │ nect<arine>
[62] │ pine<apple>
[64] │ pomegr<anate>
[70] │ r<aspbe>rry
[73] │ sal<al be>rry

Los cuantificadores permiten controlar cuántas veces puede coincidir un patrón:

  • ? coincide 0 o 1 veces.
  • + coincide al menos 1 vez.
  • * coincide cualquier cantidad de veces, incluso 0.
str_view(c("a", "ab", "abb"), "ab?")
[1] │ <a>
[2] │ <ab>
[3] │ <ab>b
str_view(c("a", "ab", "abb"), "ab+")
[2] │ <ab>
[3] │ <abb>
str_view(c("a", "ab", "abb"), "ab*")
[1] │ <a>
[2] │ <ab>
[3] │ <abb>

Las clases de caracteres te permiten definir un conjunto de caracteres, por ejemplo, [aeiou] coincide con cualquier vocal. También puedes invertir el patrón usando ^.

str_view(words, "[aeiou]x[aeiou]")
[284] │ <exa>ct
[285] │ <exa>mple
[288] │ <exe>rcise
[289] │ <exi>st
str_view(words, "[^aeiou]y[^aeiou]")
[836] │ <sys>tem
[901] │ <typ>e

Se puede usar el operador | para combinar patrones, por ejemplo, a|e coincide con “a” o “e”.

str_view(fruit, "apple|melon|nut")
 [1] │ <apple>
[13] │ canary <melon>
[20] │ coco<nut>
[52] │ <nut>
[62] │ pine<apple>
[72] │ rock <melon>
[80] │ water<melon>
str_view(fruit, "aa|ee|ii|oo|uu")
 [9] │ bl<oo>d orange
[33] │ g<oo>seberry
[47] │ lych<ee>
[66] │ purple mangost<ee>n

8.2 Funciones clave para trabajar con expresiones regulares

8.2.1 Detectar coincidencias

La función str_detect() devuelve un vector lógico que indica si el patrón coincide en cada elemento.

str_detect(c("a", "b", "c"), "[aeiou]")
[1]  TRUE FALSE FALSE

Se puede usar con filter() para seleccionar filas que coincidan con un patrón.

babynames |>
  filter(str_detect(name, "x")) |>
  count(name, wt = n, sort = TRUE)
# A tibble: 974 × 2
   name            n
   <chr>       <int>
 1 Alexander  665492
 2 Alexis     399551
 3 Alex       278705
 4 Alexandra  232223
 5 Max        148787
 6 Alexa      123032
 7 Maxine     112261
 8 Alexandria  97679
 9 Maxwell     90486
10 Jaxon       71234
# ℹ 964 more rows

Se puede encontrar la proporción de nombres que comienzan con “X”, “Y” o “Z” a lo largo del tiempo.

babynames |>
  group_by(year) |>
  summarize(prop_x = mean(str_detect(name, "^[XYZ]"))) |>
  ggplot(aes(x = year, y = prop_x)) +
  geom_line()

8.2.2 Contar coincidencias

La función str_count() cuenta cuántas veces aparece un patrón en cada cadena.

str_count(c("manzana", "banana", "pera"), "a")
[1] 3 3 1

En este ejemplo, contamos cuántas veces aparece cada vocal y consonante en los nombres:

babynames |>
  count(name) |>
  mutate(
    vowels = str_count(name, "[aeiou]"),
    consonants = str_count(name, "[^aeiou]")
  )
# A tibble: 97,310 × 4
   name          n vowels consonants
   <chr>     <int>  <int>      <int>
 1 Aaban        10      2          3
 2 Aabha         5      2          3
 3 Aabid         2      2          3
 4 Aabir         1      2          3
 5 Aabriella     5      4          5
 6 Aada          1      2          2
 7 Aadam        26      2          3
 8 Aadan        11      2          3
 9 Aadarsh      17      2          5
10 Aaden        18      2          3
# ℹ 97,300 more rows

Las expresiones regulares son sensibles a las mayúsculas por lo que el primer caso Aaban solo cuenta dos vocales. Podemos resolver esto de varias formas

babynames |>
  count(name) |>
  mutate(
    vowels = str_count(name, "[aeiouAEIOU]"),
    consonants = str_count(name, "[^aeiouAEIOU]")
  )
# A tibble: 97,310 × 4
   name          n vowels consonants
   <chr>     <int>  <int>      <int>
 1 Aaban        10      3          2
 2 Aabha         5      3          2
 3 Aabid         2      3          2
 4 Aabir         1      3          2
 5 Aabriella     5      5          4
 6 Aada          1      3          1
 7 Aadam        26      3          2
 8 Aadan        11      3          2
 9 Aadarsh      17      3          4
10 Aaden        18      3          2
# ℹ 97,300 more rows
babynames |>
  count(name) |>
  mutate(
    name = str_to_lower(name),
    vowels = str_count(name, "[aeiou]"),
    consonants = str_count(name, "[^aeiou]")
  )
# A tibble: 97,310 × 4
   name          n vowels consonants
   <chr>     <int>  <int>      <int>
 1 aaban        10      3          2
 2 aabha         5      3          2
 3 aabid         2      3          2
 4 aabir         1      3          2
 5 aabriella     5      5          4
 6 aada          1      3          1
 7 aadam        26      3          2
 8 aadan        11      3          2
 9 aadarsh      17      3          4
10 aaden        18      3          2
# ℹ 97,300 more rows

8.2.3 Reemplazar coincidencias

Para reemplazar coincidencias, usamos str_replace() o str_replace_all().

x <- c("manzana", "pera", "banana")
str_replace_all(x, "[aeiou]", "-")
[1] "m-nz-n-" "p-r-"    "b-n-n-" 

Esta función es útil para eliminar caracteres que cumplen con un patrón.

x <- c("apple", "pear", "banana")
str_remove_all(x, "[aeiou]")
[1] "ppl" "pr"  "bnn"

8.2.4 Extraer variables

La función separate_wider_regex() permite extraer datos a partir de un patrón en una columna.

df <- tribble(
  ~str,
  "<Sheryl>-F_34",
  "<Kisha>-F_45",
  "<Brandon>-N_33",
  "<Sharon>-F_38",
  "<Penny>-F_58",
  "<Justin>-M_41",
  "<Patricia>-F_84",
)
df |>
  separate_wider_regex(str,
    patterns = c("<", nombre = "[A-Za-z]+", ">-", genero = ".", "_", edad = "[0-9]+")
  )
# A tibble: 7 × 3
  nombre   genero edad 
  <chr>    <chr>  <chr>
1 Sheryl   F      34   
2 Kisha    F      45   
3 Brandon  N      33   
4 Sharon   F      38   
5 Penny    F      58   
6 Justin   M      41   
7 Patricia F      84   

8.3 Detalles de patrones

8.3.1 Escapando caracteres especiales

Para hacer coincidir un carácter especial, como un punto, necesitamos “escaparlo” usando una doble barra invertida (\\). Por ejemplo:

str_view(c("abc", "a.c", "bef"), "a\\.c")
[2] │ <a.c>

Si se necesita encontrar un carácter como ., *, +, ?, ^, $, |, (, ), [, {, }, \, se puede escapar con \\ o se usar como una clase [.], [*], [+], etc.

str_view(c("abc", "a.c", "a*c", "a c"), "a[.]c")
[2] │ <a.c>
str_view(c("abc", "a.c", "a*c", "a c"), ".[*]c")
[3] │ <a*c>

8.3.2 Anclas

Las anclas permiten que el patrón coincida solo al principio (^) o al final ($) de una cadena.

str_view(fruit, "^a")
[1] │ <a>pple
[2] │ <a>pricot
[3] │ <a>vocado
str_view(fruit, "a$")
 [4] │ banan<a>
[15] │ cherimoy<a>
[30] │ feijo<a>
[36] │ guav<a>
[56] │ papay<a>
[74] │ satsum<a>
str_view(fruit, "apple")
 [1] │ <apple>
[62] │ pine<apple>
str_view(fruit, "^apple$")
[1] │ <apple>

El operador \b coincide con el límite de una palabra, es decir, el espacio entre un carácter de palabra y un carácter que no es de palabra. Esto permite coincidir con palabras completas.

x <- c("summary(x)", "summarize(df)", "rowsum(x)", "sum(x)")
str_view(x, "sum")
[1] │ <sum>mary(x)
[2] │ <sum>marize(df)
[3] │ row<sum>(x)
[4] │ <sum>(x)
str_view(x, "\\bsum\\b")
[4] │ <sum>(x)
str_view("abc", c("$", "^", "\\b"))
[1] │ abc<>
[2] │ <>abc
[3] │ <>abc<>
str_replace_all("abc", c("$", "^", "\\b"), "--")
[1] "abc--"   "--abc"   "--abc--"

8.3.3 Clases de caracteres

Las clases de caracteres permiten coincidir cualquier carácter de un conjunto. Por ejemplo, [abc] coincide con “a”, “b” o “c”.

x <- "abcd ABCD 12345 -!@#%."
str_view(x, "[abc]+")
[1] │ <abc>d ABCD 12345 -!@#%.
str_view(x, "[a-z]+")
[1] │ <abcd> ABCD 12345 -!@#%.
str_view(x, "[^a-z0-9]+")
[1] │ abcd< ABCD >12345< -!@#%.>
str_view("a-b-c", "[a-c]")
[1] │ <a>-<b>-<c>
str_view("a-b-c", "[a\\-c]")
[1] │ <a><->b<-><c>

Existe uan serie de clases predefinidas que se pueden usar en lugar de definir manualmente las clases de caracteres:

  • \d coincide con cualquier dígito.
  • \D coincide con cualquier carácter que no sea un dígito.
  • \w coincide con cualquier carácter de palabra (letras, dígitos o guiones bajos).
  • \W coincide con cualquier carácter que no sea de palabra.
  • \s coincide con cualquier espacio en blanco.
  • \S coincide con cualquier carácter que no sea un espacio en blanco.
x <- "abcd ABCD 12345 -!@#%."
str_view(x, "\\d+")
[1] │ abcd ABCD <12345> -!@#%.
str_view(x, "\\D+")
[1] │ <abcd ABCD >12345< -!@#%.>
str_view(x, "\\s+")
[1] │ abcd< >ABCD< >12345< >-!@#%.
str_view(x, "\\S+")
[1] │ <abcd> <ABCD> <12345> <-!@#%.>
str_view(x, "\\w+")
[1] │ <abcd> <ABCD> <12345> -!@#%.
str_view(x, "\\W+")
[1] │ abcd< >ABCD< >12345< -!@#%.>

8.3.4 Cuantificadores

Los cuantificadores permiten especificar el número de coincidencias exactas:

  • {n} coincide exactamente n veces.
  • {n,} coincide al menos n veces.
  • {n,m} coincide entre n y m veces.
x <- c("2023-07-11", "2023-08-01", "2023-09-15", "2023-10-01")

str_view(x, "\\d{4}")
[1] │ <2023>-07-11
[2] │ <2023>-08-01
[3] │ <2023>-09-15
[4] │ <2023>-10-01

8.3.5 Precedencia y paréntesis

El uso de paréntesis no solo define la precedencia, sino que también crea grupos de captura.

Este patrón coincide dos letras seguidas de la mismas dos letras:

str_view(fruit, "(..)\1")

Por otro lado, este patrón coincide las palabras que comienzan y terminan con la mismas dos letras:

str_view(words, "^(..).*\\1$")
[152] │ <church>
[217] │ <decide>
[617] │ <photograph>
[699] │ <require>
[739] │ <sense>

8.4 Practica con Wordle

Vamos a jugar con https://wordly.org/ para practicar con expresiones regulares.

Tenemos esta base de datos de las soluciones de wordle: Soluciones Wordle.

wordle <- read_csv("data/wordle_solutions.csv")
Rows: 2315 Columns: 1
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (1): word

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
dim(wordle)
[1] 2315    1
head(wordle)
# A tibble: 6 × 1
  word 
  <chr>
1 aback
2 abase
3 abate
4 abbey
5 abbot
6 abhor

Ahora hagamos un pequeño análisis de los datos. Primero revisemos cuales letras son las más comunes en las soluciones.

(df_words <- wordle |>
  separate_longer_position(word, width = 1))
# A tibble: 11,575 × 1
   word 
   <chr>
 1 a    
 2 b    
 3 a    
 4 c    
 5 k    
 6 a    
 7 b    
 8 a    
 9 s    
10 e    
# ℹ 11,565 more rows
df_words <- df_words |>
  group_by(word) |>
  summarize(n = n(), .groups = "drop")
ggplot(df_words, aes(x = reorder(word, n), y = n)) +
  geom_col() +
  coord_flip()

Busquemos cuál sería la letra más común en la primera posición de las palabras.

df_primera_letra <- wordle |>
  mutate(first_letter = str_extract(word, "^.")) |>
  count(first_letter, sort = TRUE)

ggplot(df_primera_letra, aes(x = reorder(first_letter, n), y = n)) +
  geom_col() +
  coord_flip()

df_segunda_letra <- wordle |>
  mutate(second_letter = str_extract(word, ".{2}")) |>
  mutate(second_letter = str_extract(second_letter, ".$")) |>
  count(second_letter, sort = TRUE)

ggplot(df_segunda_letra, aes(x = reorder(second_letter, n), y = n)) +
  geom_col() +
  coord_flip()

Ahora busquemos palabras que contengan comiencen con “s” y contenga las letras “earotl”:

wordle |>
  filter(
    str_detect(word, "^s"),
    str_detect(word, "[earotl]{4}")
  )
# A tibble: 14 × 1
   word 
   <chr>
 1 slate
 2 sleet
 3 solar
 4 stale
 5 stall
 6 stare
 7 start
 8 state
 9 steal
10 steel
11 steer
12 stole
13 stool
14 store

Comencemos con la palabra “stole” ya que es la tiene todas sus letras únicas.

wordle |>
  filter(
    str_detect(word, "^c"),
    str_detect(word, "[arlisn]{4}")
  )
# A tibble: 4 × 1
  word 
  <chr>
1 cairn
2 canal
3 class
4 crass

Un buen segundo intento es “cairn” ya que es la única palabra que tiene todas sus letras únicas.

Primero intento:

X - - - X
S T O L E

Segundo intento

X - X X X
C A I R N
wordle |>
  filter(str_detect(
    word,
    "[^secirn][^secirnta][^secirnto][^secirnl][^secirn]"
  ))
# A tibble: 78 × 1
   word 
   <chr>
 1 abbot
 2 adapt
 3 aglow
 4 album
 5 allay
 6 allot
 7 allow
 8 alloy
 9 alpha
10 awful
# ℹ 68 more rows

El problema acá es la posición de las letras “t”, “o”, “l” y “a” también importan. Entonces para esto usaremos la expresión regular (?=) que significa “lookahead”.

wordle |>
  filter(str_detect(
    word,
    "(?=[^secirn][^secirnta][^secirnto][^secirnl][^secirn])(?=.*t.*)(?=.*o.*)(?=.*l.*)(?=.*a.*)"
  ))
# A tibble: 2 × 1
  word 
  <chr>
1 allot
2 loath

8.4.1 Su turno

Hagan equipos de 2 y jueguen con las palabras de la base de datos de Wordle. El equipo que logre adivinar más palabras gana.

8.5 Regexp 101

Esta página es una excelente herramienta para practicar con expresiones regulares: https://regex101.com/.