Limpieza y recodificación de datos de texto en R con {stringr}
8/6/2025
Índice
El paquete {stringr} facilita todo tipo de trabajo que implique texto en R.
En general, los datos que vienen como texto suelen necesitar una limpieza previa, y adicionalmente un procesamiento para poder aprovecharlos mejor.
Sigamos un ejemplo con una columna que viene con textos sobre compras en un servicio público:
library(dplyr)
library(stringr) # para trabajar con textos
datos <- tibble(
texto = c("Licitación pública N°3432",
"Trato directo #3341",
"Licitación privada 876",
"LICITACION PUBLICA N3430",
"Licitacion publica 3526 concluida",
"licitación pública 2986 ok",
"sin información")
)
| texto |
|---|
| Licitación pública N°3432 |
| Trato directo #3341 |
| Licitación privada 876 |
| LICITACION PUBLICA N3430 |
| Licitacion publica 3526 concluida |
| licitación pública 2986 ok |
| sin información |
Como es de esperar, el texto viene sucio: escrito de distintas maneras, con y sin tildes, con y sin mayúsculas, etc.
Crear variable a partir de la detección de texto
Una primera limpieza de los datos puede ser identificar si un texto específico está o no presente en la variable de texto sucio. Para ello podemos usar la función str_detect(), que retorna TRUE o FALSE si en el texto que se le entrega como primer argumento está presente el texto en su segundo argumento. Por ejemplo:
str_detect("un texto acá muy feo", "feo") # sí (TRUE)
[1] TRUE
str_detect("un texto acá muy bonito", "feo") #no (FALSE)
[1] FALSE
Aplicamos str_detect() dentro de un mutate() para crear una columna que indique la presencia de un texto a lo largo de la columna de texto:
datos |>
mutate(licitacion = str_detect(texto, "Licitación"))
# A tibble: 7 × 2
texto licitacion
<chr> <lgl>
1 Licitación pública N°3432 TRUE
2 Trato directo #3341 FALSE
3 Licitación privada 876 TRUE
4 LICITACION PUBLICA N3430 FALSE
5 Licitacion publica 3526 concluida FALSE
6 licitación pública 2986 ok FALSE
7 sin información FALSE
Vemos que sólo entrega dos TRUE, siendo que en las filas 4, 5 y 6 también debería encontrar coincidencias. Esto se debe a que existen diferencias en las letras minúsculas y mayúsculas.
Cambiar la capitalización de un texto
Un problema común con la limpieza de texto es encontrar diferencias en las mayúsculas de las palabras. {stringr} tiene varias funciones para ayudarnos a cambiar textos a mayúsculas, minúsculas, y más.
str_to_lower("HOLA, cómo estás?")
[1] "hola, cómo estás?"
str_to_upper("hola, cómo estás?!")
[1] "HOLA, CÓMO ESTÁS?!"
str_to_sentence("hola, cómo estás?")
[1] "Hola, cómo estás?"
Pero a veces necesitamos corregir la capitalización de textos que en su interior contienen palabras que empiezan con mayúsculas, o siglas/acrónimos, en cuyo caso convertir a minúsculas o a oración arruinarían la gramática correcta.
# texto que no empieza con mayúscula
texto <- "mi nombre es Cecilia y trabajo en la ONU."
# agregar mayúscula inicial, pero se pierden las demás mayúsculas
str_to_sentence(texto)
[1] "Mi nombre es cecilia y trabajo en la onu."
En estos casos podemos realizar un reemplazo de texto con str_replace() que solamente reemplace la primera letra del texto por su versión mayúscula, y deje las demás intactas:
# reemplazar sólo primera letra de la primera palabra
str_replace(texto, "^.", toupper)
[1] "Mi nombre es Cecilia y trabajo en la ONU."
Aquí usamos una expresión regular (^.) para indicar que queremos reemplazar solamente la primera letra del texto (el símbolo ^ indica el inicio del texto, y . indica cualquier símbolo).
Volviendo al ejemplo de los datos, para corregir la detección de datos con str_detect() cuando hay diferencias de mayúsculas, primero convertimos el texto a minúsculas con str_to_lower(), y buscamos el término en minúsculas:
datos |>
mutate(licitacion = str_detect(str_to_lower(texto), "licitación"))
# A tibble: 7 × 2
texto licitacion
<chr> <lgl>
1 Licitación pública N°3432 TRUE
2 Trato directo #3341 FALSE
3 Licitación privada 876 TRUE
4 LICITACION PUBLICA N3430 FALSE
5 Licitacion publica 3526 concluida FALSE
6 licitación pública 2986 ok TRUE
7 sin información FALSE
Obtenemos una coincidencia más! Pero siguen faltando dos casos (casos 4 y 5) que deberían retornar TRUE. En estos casos, el problema está con diferencias en los tildes de las palabras. Para solucionar esto, podemos hacer una búsqueda de texto usando regex.
Expresiones regulares
Las
expresiones regulares o regex son formas de escribir patrones de búsqueda, y son soportadas por todas las funciones de {stringr}. Uno de estos patrones es el operador o (|). El operador o puede usarse para encontrar coincidencias con varias palabras distintas separadas con |:
str_detect(c("hola", "holo", "holi"), "hola|holi")
[1] TRUE FALSE TRUE
En este ejemplo, se coincide con un TRUE tanto el texto hola como holi. Pero en este ejemplo ambas palabras son muy similares; 75% similares, para ser exactos 🤓☝🏼. Podemos poner entre paréntesis los caracteres específicos que varían, para que dentro de una misma palabra se acepten distintos caracteres:
str_detect(c("hola", "holo", "holi"), "hol(a|i)")
[1] TRUE FALSE TRUE
De este modo, se coincide con la palabra hol seguida tanto de a como de i; es decir, hola y holi.
Finalmente, también podemos pedirle que coincida una palabra que dentro de ella tenga cualquier caracter:
str_detect(c("hola", "holo", "holi"), "hol.")
[1] TRUE TRUE TRUE
Siguiendo estos ejemplos para volver a nuestros datos, podemos coincidir texto con y sin tildes al mismo tiempo si usamos regex para especificar que uno o varios caracteres pueden ser distintos; en este caso, la letra o con y sin tilde:
datos |>
mutate(licitacion = str_detect(str_to_lower(texto), "licitaci(ó|o)n"))
# A tibble: 7 × 2
texto licitacion
<chr> <lgl>
1 Licitación pública N°3432 TRUE
2 Trato directo #3341 FALSE
3 Licitación privada 876 TRUE
4 LICITACION PUBLICA N3430 TRUE
5 Licitacion publica 3526 concluida TRUE
6 licitación pública 2986 ok TRUE
7 sin información FALSE
Podemos usar el mismo código pero dentro de un ifelse() para que, en vez de TRUE y FALSE, retorne lo que queramos para las condicies verdadera y falsa:
datos |>
mutate(licitacion = ifelse(str_detect(str_to_lower(texto), "licitaci(ó|o)n"),
yes = "Licitación",
no = "Otros"))
# A tibble: 7 × 2
texto licitacion
<chr> <chr>
1 Licitación pública N°3432 Licitación
2 Trato directo #3341 Otros
3 Licitación privada 876 Licitación
4 LICITACION PUBLICA N3430 Licitación
5 Licitacion publica 3526 concluida Licitación
6 licitación pública 2986 ok Licitación
7 sin información Otros
También podemos usar el operador .* de regex para indicar cualquier cantidad de caracteres entre el texto antes y después del operador. Por ejemplo:
str_detect(c("hola", "hooooooola", "ho8787897la", "hola hola"), "ho.*la")
[1] TRUE TRUE TRUE TRUE
En el ejemplo, ho.*la significa coincidir con un texto que tenga ho, cualquier texto, y luego la; por lo tanto, coincide con hola, hooooooola, ho8787897la y cualquier otra variación.
Podemos usar esto para hacer coincidencias más flexibles:
datos |>
mutate(tipo = case_when(str_detect(str_to_lower(texto), "lic.*privada") ~ "Licitación privada",
str_detect(str_to_lower(texto), "lic.*p.blica") ~ "Licitación pública",
str_detect(str_to_lower(texto), "trato.*directo|contra.*direct") ~ "Trato directo")
)
# A tibble: 7 × 2
texto tipo
<chr> <chr>
1 Licitación pública N°3432 Licitación pública
2 Trato directo #3341 Trato directo
3 Licitación privada 876 Licitación privada
4 LICITACION PUBLICA N3430 Licitación pública
5 Licitacion publica 3526 concluida Licitación pública
6 licitación pública 2986 ok Licitación pública
7 sin información <NA>
Si combinamos los aprendizajes hasta el momento, podemos crear una columna nueva que entregue distintos valores dependiendo del texto detectado, gracias a case_when():
datos |>
# limpiar el texto de antemano
mutate(texto_2 = str_to_lower(texto)) |>
# detectar licitaciones
mutate(licitacion = str_detect(texto_2, "licitaci(ó|o)n")) |>
# detectar si son públicas, privadas, o de otro tipo
mutate(tipo = case_when(
# si son licitaciones, y si contiene "privada"
licitacion & str_detect(texto_2, "privad(o|a)") ~ "Licitación privada",
# si son licitaciones y si contiene "público"
licitacion & str_detect(texto_2, "p(ú|u)blic(a|o)") ~ "Licitación pública",
# otros valores
str_detect(texto_2, "trato directo") ~ "Trato directo",
# todos los demás que no coincidieron en las condiciones anteriores
.default = "Otros"))
# A tibble: 7 × 4
texto texto_2 licitacion tipo
<chr> <chr> <lgl> <chr>
1 Licitación pública N°3432 licitación pública n°3432 TRUE Lici…
2 Trato directo #3341 trato directo #3341 FALSE Trat…
3 Licitación privada 876 licitación privada 876 TRUE Lici…
4 LICITACION PUBLICA N3430 licitacion publica n3430 TRUE Lici…
5 Licitacion publica 3526 concluida licitacion publica 3526 co… TRUE Lici…
6 licitación pública 2986 ok licitación pública 2986 ok TRUE Lici…
7 sin información sin información FALSE Otros
Extraer caracteres desde un texto
Una última alternativa para limpiar estos datos sería extraer texto específico desde la variable de texto. Es decir, encontrar un tipo de texto dentro de otro texto, y solamente dejar ese texto extraído. Por ejemplo: entre un texto extenso, extraer solamente una palabra específica, si es que existe, o extraer solamente los números que esténdentro del texto.
Para esto podemos usar la función str_extract() combinada con un operador regex para extraer secuencias de números (\\d+):
datos |>
mutate(numero = str_extract(texto, "\\d+"))
# A tibble: 7 × 2
texto numero
<chr> <chr>
1 Licitación pública N°3432 3432
2 Trato directo #3341 3341
3 Licitación privada 876 876
4 LICITACION PUBLICA N3430 3430
5 Licitacion publica 3526 concluida 3526
6 licitación pública 2986 ok 2986
7 sin información <NA>
Para finalizar, unimos todas las técnicas que vimos en este ejemplo, para terminar con una tabla de datos mucho más útil que la que teníamos al inicio!
datos_limpios <- datos |>
# limpiar el texto de antemano
mutate(texto_2 = str_to_lower(texto)) |>
# detectar licitaciones
mutate(licitacion = ifelse(str_detect(str_to_lower(texto), "licitaci(ó|o)n"),
yes = "Licitación",
no = "Otros")) |>
# detectar si son públicas, privadas, o de otro tipo
mutate(tipo = case_when(
# si son licitaciones, y si contiene "privada"
licitacion == "Licitación" & str_detect(texto_2, "privad(o|a)") ~ "Licitación privada",
# si son licitaciones y si contiene "público"
licitacion == "Licitación" & str_detect(texto_2, "p(ú|u)blic(a|o)") ~ "Licitación pública",
# otros valores
licitacion == "Otros" & str_detect(texto_2, "trato directo") ~ "Trato directo",
# todos los demás que no coincidieron en las condiciones anteriores
.default = "Otros")) |>
# extraer números
mutate(numero = str_extract(texto, "\\d+"),
numero = as.numeric(numero)) |> # convertir números a numéricos
# eliminar columnas innecesarias
select(-contains("texto"))
| licitacion | tipo | numero |
|---|---|---|
| Licitación | Licitación pública | 3432 |
| Otros | Trato directo | 3341 |
| Licitación | Licitación privada | 876 |
| Licitación | Licitación pública | 3430 |
| Licitación | Licitación pública | 3526 |
| Licitación | Licitación pública | 2986 |
| Otros | Otros | NA |
- Fecha de publicación:
- June 8, 2025
- Extensión:
- 8 minute read, 1665 words
- Categorías:
- tutoriales
- Tags:
- limpieza de datos texto