Manipulação de texto com stringr - parte IV

Na quarta parte da série de posts sobre manipulação de texto com stringr veremos como usar funções um pouco mais técnicas. Também discutimos o caso da função case_when do dplyr que pode ser bastante útil ao se trabalhar com texto.

Miguel Cleaver
03-17-2020

Este é o quarto post da série de manipulação de texto com stringr. Já vimos as principais funções de manipulação de texto, então neste post aproveitaremos para abordar algumas funções do pacote um pouco mais técnicas como a função word e a função str_count, que associada a função boundary pode nos ajudar a contar o número de palavras em um texto. Ainda falaremos da função fixed e, por fim, falaremos da função case_when do dplyr que pode vir a ser bastante útil na tarefa de manipular texto.

Este post segue a mesma estrutura dos posts anteriores sobre manipulação de texto, isto é, está organizado na forma de perguntas e respostas.

pacotes que iremos utilizar


library(microbenchmark)
library(dplyr)
library(stringr)

carregamento de dados

Neste caso vamos trabalhar com o objeto sentences, disponível no pacote stringr. A partir do objeto sentences aproveitamos para criar uma tabela simples de duas colunas com a função tibble.


data(sentences)
df <- tibble(id = 1:length(sentences), sentences = sentences)
head(df)

# A tibble: 6 x 2
     id sentences                                  
  <int> <chr>                                      
1     1 The birch canoe slid on the smooth planks. 
2     2 Glue the sheet to the dark blue background.
3     3 It's easy to tell the depth of a well.     
4     4 These days a chicken leg is a rare dish.   
5     5 Rice is often served in round bowls.       
6     6 The juice of lemons makes fine punch.      

Já aprendemos como fazer que uma coluna tenha um tamanho fixo de caracteres (pode-se fazer isso com a função str_pad ou str_sub, por exemplo, a depender da situação), mas e se nós quiséssemos que uma coluna tivesse sempre as três primeiras palavras de determinado texto? O número de caracteres seria variável e essas funções que vimos não mais nos ajudariam.

Problema n. 1: como obter as primeiras três palavras de uma coluna de texto?

A solução é simples e podemos usar a função word do stringr:


df %>% 
  mutate(tres_palavras = word(sentences, 1 , 3))

# A tibble: 720 x 3
      id sentences                                   tres_palavras  
   <int> <chr>                                       <chr>          
 1     1 The birch canoe slid on the smooth planks.  The birch canoe
 2     2 Glue the sheet to the dark blue background. Glue the sheet 
 3     3 It's easy to tell the depth of a well.      It's easy to   
 4     4 These days a chicken leg is a rare dish.    These days a   
 5     5 Rice is often served in round bowls.        Rice is often  
 6     6 The juice of lemons makes fine punch.       The juice of   
 7     7 The box was thrown beside the parked truck. The box was    
 8     8 The hogs were fed chopped corn and garbage. The hogs were  
 9     9 Four hours of steady work faced us.         Four hours of  
10    10 Large size in stockings is hard to sell.    Large size in  
# ... with 710 more rows

Se quiséssemos as últimas 3 palavras poderíamos usar a função word da seguinte forma:


df %>% 
  mutate(ultimas_tres = word(sentences, -3, -1)) %>% 
  select(id, ultimas_tres)

# A tibble: 720 x 2
      id ultimas_tres         
   <int> <chr>                
 1     1 the smooth planks.   
 2     2 dark blue background.
 3     3 of a well.           
 4     4 a rare dish.         
 5     5 in round bowls.      
 6     6 makes fine punch.    
 7     7 the parked truck.    
 8     8 corn and garbage.    
 9     9 work faced us.       
10    10 hard to sell.        
# ... with 710 more rows

Problema n. 2: Como contar o número de palavras de um texto?

Para isso você vai precisar utilizar duas funções: str_count e boundary.


df %>% 
  mutate(n_palavras = str_count(sentences, boundary(type = "word")))

# A tibble: 720 x 3
      id sentences                                   n_palavras
   <int> <chr>                                            <int>
 1     1 The birch canoe slid on the smooth planks.           8
 2     2 Glue the sheet to the dark blue background.          8
 3     3 It's easy to tell the depth of a well.               9
 4     4 These days a chicken leg is a rare dish.             9
 5     5 Rice is often served in round bowls.                 7
 6     6 The juice of lemons makes fine punch.                7
 7     7 The box was thrown beside the parked truck.          8
 8     8 The hogs were fed chopped corn and garbage.          8
 9     9 Four hours of steady work faced us.                  7
10    10 Large size in stockings is hard to sell.             8
# ... with 710 more rows

A função str_count detecta o padrão inserido na função e conta quantas vezes esse padrão aparece em um segmento de texto. Por exemplo, str_count("o teste consiste em um teste de manhã e um teste de tarde", "teste") buscaria o padrão “teste” e contaria quantas vezes esse padrão aparece. Nesse caso o resultado seria 3.

Já a função boundary(type = "word") imputa um padrão de fronteira para palavras, o que permite que a função str_count conte as palavras em um segmento de texto.

Existe alguma forma de acelerar operações baseadas de funções do stringr?

Sim, a função fixed aumenta a velocidade do processamento de funções do stringr. Ao comparar os bytes de um texto, é possível encontrar correspondências de forma veloz, mas isso somente pode ser usado para um conjunto literal de caracteres ASCII. Em outras palavras, nós não poderíamos fazer uma busca regex do tipo “[0-9]{1,2}” para localizar o padrão “90” ou “22”. Ao utilizar a função fixed com o input “[0-9]{1,2}” só haveria correspondência se esse texto aparecesse exatamente dessa forma (por exemplo, para o trecho “o padrão pode ser [0-9]{1,2}” haveria correspondência).

Para mostrar que a função fixed acelera funções do stringr, iremos fazer uma operação de filtro simples para obter frases com o padrão “the” e usaremos o pacote microbenchmark para medir a velocidade com e sem a função fixed (a unidade de tempo abaixo está em microssegundos).


microbenchmark(sem_fixed = df %>% 
                 filter(str_detect(sentences, "the")),
               com_fixed = df %>% 
                 filter(str_detect(sentences, fixed("the"))))

Unit: microseconds
      expr   min     lq     mean median      uq    max neval
 sem_fixed 690.4 823.00 1092.103 995.20 1185.85 3714.9   100
 com_fixed 478.0 545.85  777.342 679.05  768.60 4101.0   100

Com efeito, podemos observar que a utilização da função fixed torna o filtro simples mais rápido. Para o exemplo trabalhado, não há diferença prática, pois há poucas observações e a diferença de tempo em microssegundos é imperceptível para nós, mas à medida que o tamanho de observações cresce, a função pode ter grande utilidade ao aumentar a velocidade da localização de correspondências.

Como substituir nomes de observações por outros nomes?

A função case_when do dplyr permite que você edite o texto das observações de uma coluna sempre que uma condição for atendida. Para exemplificar o uso da função case_when vamos trabalhar com os dados do objeto starwars, disponíveis no pacote do dplyr.


data(starwars)

sw <- starwars %>% 
  select(name, eye_color)

sw

# A tibble: 87 x 2
   name               eye_color
   <chr>              <chr>    
 1 Luke Skywalker     blue     
 2 C-3PO              yellow   
 3 R2-D2              red      
 4 Darth Vader        yellow   
 5 Leia Organa        brown    
 6 Owen Lars          blue     
 7 Beru Whitesun lars blue     
 8 R5-D4              red      
 9 Biggs Darklighter  brown    
10 Obi-Wan Kenobi     blue-gray
# ... with 77 more rows

Vamos supor que nós desejamos traduzir as observações da coluna eye_color para o português da seguinte forma: “blue” para “azul”, “yellow” para “amarelo”, “brown” para “marrom” e as demais cores que aparecem em inglês para um grupo geral chamado “outras cores”. Com algumas linhas de código é possível alterar o nome da coluna eye_color de forma imediata:


sw %>% 
  mutate(eye_traducao = case_when(
    eye_color == "blue" ~ "azul",
    eye_color == "yellow" ~ "amarelo",
    eye_color == "brown" ~ "marrom",
    TRUE ~ "outras cores"
  ))

# A tibble: 87 x 3
   name               eye_color eye_traducao
   <chr>              <chr>     <chr>       
 1 Luke Skywalker     blue      azul        
 2 C-3PO              yellow    amarelo     
 3 R2-D2              red       outras cores
 4 Darth Vader        yellow    amarelo     
 5 Leia Organa        brown     marrom      
 6 Owen Lars          blue      azul        
 7 Beru Whitesun lars blue      azul        
 8 R5-D4              red       outras cores
 9 Biggs Darklighter  brown     marrom      
10 Obi-Wan Kenobi     blue-gray outras cores
# ... with 77 more rows

Trata-se de uma forma elegante de editar ou modificar colunas de texto. Note que a função case_when acima testa diversas condições e, quando verdadeiras, a função altera o texto para o termo após o til “~” no código acima.

No caso das observações cujas condições não resultem no resultado lógico TRUE (verdadeiro), é preciso adicionar uma linha adicional com resultado TRUE e, após o til “~”, selecionar o termo de substituição padrão. Se isso não for feito, as observações que não resultam em condições verdadeiras serão transformadas em NA.

No nosso caso, indicamos que as cores em inglês que não sejam nem “blue”, nem “yellow” e nem “brown” deverão ser chamadas de “outras cores”. Isso se dá com a introdução do termo TRUE ~ "outras cores". Se quiséssemos manter os nomes originais com exceção de “blue”, “yellow” e “brown”, poderíamos usar TRUE ~ eye_color:


sw %>% 
  mutate(eye_traducao_parcial = case_when(
    eye_color == "blue" ~ "azul",
    eye_color == "yellow" ~ "amarelo",
    eye_color == "brown" ~ "marrom",
    TRUE ~ eye_color
  ))

# A tibble: 87 x 3
   name               eye_color eye_traducao_parcial
   <chr>              <chr>     <chr>               
 1 Luke Skywalker     blue      azul                
 2 C-3PO              yellow    amarelo             
 3 R2-D2              red       red                 
 4 Darth Vader        yellow    amarelo             
 5 Leia Organa        brown     marrom              
 6 Owen Lars          blue      azul                
 7 Beru Whitesun lars blue      azul                
 8 R5-D4              red       red                 
 9 Biggs Darklighter  brown     marrom              
10 Obi-Wan Kenobi     blue-gray blue-gray           
# ... with 77 more rows

Palavras finais sobre manipulação de texto

Nestas quatro partes sobre stringr tentamos apresentar as principais funções do pacote. Ainda há diversas outras funções de manipulação de texto (ver pacote stringi) e algumas outras que não comentamos do pacote stringr. Apesar disso, as funções que abordamos resolvem grande parte dos problemas de organização e limpeza textual. Além dessas funções, também é importante dominar o uso de expressões regulares (regex), pois grande parte do que é possível fazer com o stringr reside na captura e manipulação de padrões textuais.

Uma dica valiosa para quem está começando a trabalhar com texto é a seguinte: antes de iniciar um trabalho extenso de manipulação de texto, planeje a forma mais adequada de fazer um tratamento inicial nos seus dados textuais. O tratamento ao qual nos referimos visa simplificar os padrões de regex que você terá que escrever e aumentar o controle sobre a previsibilidade dos diversos padrões que possam surgir.

Em regra, recomendaríamos passar o texto inteiro para caixa baixa ou caixa alta, assim como eliminar espaços desnecessários. Além disso, em alguns casos, também recomendaríamos eliminar os diversos acentos da língua portuguesa (ou de outras línguas, se for o caso), passando por exemplo, o “ã” para “a” e o “ó” para “o”. Ao trabalhar com dados textuais, erros de acentuação são comuns (em especial em campos de texto livre) e podem dificultar sua vida na extração de padrões que você procura.

Todavia, não existe um conjunto de regras que funcione como uma panaceia. Há casos, por exemplo, em que os diferentes espaçamentos contidos no texto irão funcionar como pontos de demarcação para extrair padrões. O mesmo pode ocorrer com a caixa original das letras. É preciso conhecer seu texto, investigar e fazer experimentos antes de implementar a abordagem de limpeza inicial.

Citation

For attribution, please cite this work as

Cleaver (2020, March 17). Fulljoin: Manipulação de texto com stringr - parte IV. Retrieved from https://www.fulljoin.com.br/posts/2020-03-02-manipulao-de-texto-com-stringr-parte-iv/

BibTeX citation

@misc{cleaver2020manipulação,
  author = {Cleaver, Miguel},
  title = {Fulljoin: Manipulação de texto com stringr - parte IV},
  url = {https://www.fulljoin.com.br/posts/2020-03-02-manipulao-de-texto-com-stringr-parte-iv/},
  year = {2020}
}