Limpieza y recodificación de datos de texto en R con {stringr}

8/6/2025

limpieza de datos texto

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
Ver también:
Extracción de datos estructurados desde texto usando IA
Renombrar archivos desde R
Redactar textos basados en datos usando IA desde R