Manipulação de texto com stringr - parte III

Na terceira parte da série de posts sobre manipulação de texto com stringr veremos como usar funções que removem padrões encontrados no texto, que substituem padrões por outros e que eliminam espaços de formas diferentes. Também aprofundaremos com um pouco mais de regex.

Miguel Cleaver
11-06-2019

Na terceira parte sobre manipulação de texto com stringr, manteremos a lógica anterior dos posts da série, isto é, continuaremos a propor problemas e mostrar como os podemos resolver.

Antes de continuar será necessário “sujar” um pouco nossos dados. Para isso, carregamos os dados que já vinhamos usando anteriormente e inserimos aleatoriamente pontuações e espaços desnecessários na coluna “pais_port”. A ideia desse procedimento consiste em “sujar” os dados e simular uma base de dados bruta. A “sujeira” que fizemos é relativamente simples e, na vida real, você poderá encontrar bases de dados em estado muito pior, mas a ideia é que você consiga generalizar o conhecimento a partir dos exemplos mais didáticos que elaboramos.

Pacotes que iremos utilizar


library(dplyr)
library(stringr)
library(data.table)

Carregamento de dados

Abaixo carregamos os dados que iremos utilizar. Em seguida, “sujamos” a coluna "pais_port:


paises <- fread(
  "https://raw.githubusercontent.com/mgcleaver/correlacoes/master/paises_isoa.txt",
                encoding = "UTF-8"
)

# para reprodutibilidade o seu R precisa ser uma versão > 3.6.0.
set.seed(1)

indice1 <- sample(1:nrow(paises), floor(nrow(paises)/2)) %>% 
  sort()
indice2 <- setdiff(1:nrow(paises), indice1)

paises$pais_port[indice1] <- paste0(" ", paises$pais_port[indice1], ".")
paises$pais_port[indice2] <- paste0(paises$pais_port[indice2], "   ; ")

indice_espaco <- sample(1:nrow(paises), 10)
paises$pais_port[indice_espaco] <- paste0(" ", paises$pais_port[indice_espaco], " ")

indice_interrogacao <- sample(1:nrow(paises), 10)
paises$pais_port[indice_interrogacao] <- paste0(paises$pais_port[indice_espaco], "?")

head(paises)

   ISOA      pais_ing        pais_port
1:  ABW         Aruba           Aruba.
2:  AFG   Afghanistan Afeganistão   ; 
3:  AGO        Angola      Angola   ; 
4:  AIA      Anguilla     Anguila   ; 
5:  ALA Åland Islands Ilhas Aland   ; 
6:  ALB       Albania     Albânia   ; 

Problema n. 1: Como elimino os pontos que estão presentes na coluna pais_port?

Primeiramente, vejamos quais das nossas observações possuem ponto.

Como um primeiro exercício, tentemos identificar as 10 primeiras observações que possuem um ponto na coluna “pais_port”:


paises %>% 
  filter(str_detect(pais_port, ".")) %>% 
  head(10)

   ISOA             pais_ing                   pais_port
1   ABW                Aruba                      Aruba.
2   AFG          Afghanistan            Afeganistão   ; 
3   AGO               Angola                 Angola   ; 
4   AIA             Anguilla                Anguila   ; 
5   ALA        Åland Islands            Ilhas Aland   ; 
6   ALB              Albania                Albânia   ; 
7   AND              Andorra                    Andorra.
8   ARE United Arab Emirates Emirados Árabes Unidos   ; 
9   ARG            Argentina              Argentina   ; 
10  ARM              Armenia                Armênia   ; 

Podemos observar que nosso filtro não foi capaz de identificar quais observações tinham o ponto na coluna “pais_port”. O problema é que em regex o ponto é usado como uma espécie de coringa, pois pode igualar qualquer letra e número, além de outros símbolos de pontuação. Como as observações da coluna “pais_port” tem pelo menos um caractere, o filtro traz todas as observações, uma vez que todas elas atendem o requisito.

Para que identifiquemos quais observações possuem literalmente um ponto, devemos escapar a função regex atribuída ao ponto com duas contrabarras (\\), isto é, devemos inserir o seguinte padrão na função str_detect, no argumento correspondente: “\\.”. Vejamos:


pontos <- paises %>% 
  filter(str_detect(pais_port, "\\.")) %>% 
  pull(pais_port) %>% 
  .[1:10]

# ver as 10 primeiras observações da coluna pais_port com pontos
pontos

 [1] " Aruba."                                 
 [2] " Andorra."                               
 [3] " Terras Austrais e Antárticas Francesas."
 [4] " Antígua e Barbuda."                     
 [5] " Benin."                                 
 [6] " Países Baixos Caribenhos."              
 [7] " Burkina Faso."                          
 [8] " Bangladesh."                            
 [9] " Bulgária."                              
[10] " Barém."                                 

Agora que conseguimos ver quais observações tem o ponto, como eliminamos o ponto das observações? Devemos utilizar a função str_remove. No primeiro argumento dessa função inserimos o vetor que desejamos modificar. No segundo argumento inserimos o padrão que desejamos remover. Vejamos, então, como aplicar a função:


str_remove(pontos, "\\.")

 [1] " Aruba"                                 
 [2] " Andorra"                               
 [3] " Terras Austrais e Antárticas Francesas"
 [4] " Antígua e Barbuda"                     
 [5] " Benin"                                 
 [6] " Países Baixos Caribenhos"              
 [7] " Burkina Faso"                          
 [8] " Bangladesh"                            
 [9] " Bulgária"                              
[10] " Barém"                                 

Comparando o resultado acima com o anterior, verificamos que não há mais pontos em nossos dados. Alteremos o objeto paises para incorporar a modificação proposta.


paises <- paises %>% 
  mutate(pais_port = str_remove(pais_port, "\\."))

Agora quando buscamos pelo ponto na coluna “pais_port” não mais o encontramos em nossas observações:


paises %>% 
  filter(str_detect(pais_port, "\\."))

[1] ISOA      pais_ing  pais_port
<0 rows> (or 0-length row.names)

Problema resolvido. Mas, note o seguinte: a função str_remove apenas elimina a primeira ocorrência do padrão encontrado. Se houvesse dois pontos em uma observação, a função str_remove apenas removeria o ponto que encontrasse primeiro, deixando ainda um ponto remanescente no meio do texto. Se precisássemos remover todos os pontos, utilizaríamos a função str_remove_all. Em essência ela faz a mesma coisa que a função str_remove, mas em vez de remover apenas a primeira ocorrência, ela remove todas.

Problema n. 2: Como eliminamos os espaços anteriores e posteriores ao texto?

Primeiramente, verifiquemos se existem observações com espaços antes do início do texto na coluna “pais_port”. Se você não entende porque o padrão “^” detecta observações que se iniciam com espaço, veja a a parte II desse post.


espaco1 <- paises %>% 
  filter(str_detect(pais_port, "^ ")) %>% 
  pull(pais_port) %>% 
  .[1:10]

espaco1

 [1] " Aruba"                                 
 [2] " Andorra"                               
 [3] " Terras Austrais e Antárticas Francesas"
 [4] " Antígua e Barbuda"                     
 [5] " Benin"                                 
 [6] " Países Baixos Caribenhos"              
 [7] " Burkina Faso"                          
 [8] " Bangladesh"                            
 [9] " Bulgária"                              
[10] " Barém"                                 

Ok. Agora verifiquemos se também existem espaços após o fim do texto na coluna “pais_port”:


espaco2 <- paises %>% 
  filter(str_detect(pais_port, " $")) %>% 
  pull(pais_port) %>% 
  .[1:10]

espaco2

 [1] "Afeganistão   ; "            "Angola   ; "                
 [3] "Anguila   ; "                "Ilhas Aland   ; "           
 [5] "Albânia   ; "                "Emirados Árabes Unidos   ; "
 [7] "Argentina   ; "              "Armênia   ; "               
 [9] "Samoa Americana   ; "        "Antártica   ; "             

Após investigar os dados encontramos que há espaços antes do texto começar bem como depois do texto terminar. Para resolver nosso problema devemos utilizar a função str_trim. Essa função elimina espaços que ficam do lado esquerdo ou do lado direito do dado textual. No primeiro argumento inserimos o vetor que queremos modificar. No segundo argumento escolhemos o lado dos espaços que queremos eliminar. Para eliminar espaços que ficam apenas do lado esquerdo do texto devemos utilizar o argumento “left”. Para eliminar espaços que ficam apenas do lado direito devemos utilizar o argumento “right”. Se quisermos eliminar espaços tanto do lado esquerdo como do lado direito devemos utilizar o argumento “both” (é justamente isso que queremos neste problema). Vejamos como ficam os objetos espaco1 e espaco2:


str_trim(espaco1, "both")

 [1] "Aruba"                                 
 [2] "Andorra"                               
 [3] "Terras Austrais e Antárticas Francesas"
 [4] "Antígua e Barbuda"                     
 [5] "Benin"                                 
 [6] "Países Baixos Caribenhos"              
 [7] "Burkina Faso"                          
 [8] "Bangladesh"                            
 [9] "Bulgária"                              
[10] "Barém"                                 

str_trim(espaco2, "both")

 [1] "Afeganistão   ;"            "Angola   ;"                
 [3] "Anguila   ;"                "Ilhas Aland   ;"           
 [5] "Albânia   ;"                "Emirados Árabes Unidos   ;"
 [7] "Argentina   ;"              "Armênia   ;"               
 [9] "Samoa Americana   ;"        "Antártica   ;"             

Agora vemos que não há mais espaços do lado esquerdo nem do lado direito do texto. Contudo, após aplicar a função no objeto espaco2, observamos que existem espaços excessivos entre a última letra do nome do país e o ponto e vírgula.

Será que também conseguimos transformar mais de um espaço para um único espaço? Já já veremos como resolver isso, mas antes modifiquemos o objeto paises com intuito de modificá-lo conforme a proposta deste problema:


paises <- paises %>% 
  mutate(pais_port = str_trim(pais_port, "both"))

Problema n. 3: Como transformar mais de um espaço entre palavras ou sinais de pontuação em um único espaço?

A função que poderá nos ajudar com essa tarefa se chama str_squish. Note que essa função, além de eliminar espaços duplos, também cumpre a função de eliminar espaços à direita e espaços à esquerda do texto. Diferentemente, da função str_trim, a função str_squish não nos permite escolher um lado específico para eliminar espaços. Trata-se de uma função extremamente útil para padronizar dados textuais. No caso da coluna “pais_port”, nós já eliminamos espaços à direita e espaços à esquerda. Portanto a função somente eliminará os espaços duplos contidos na coluna. A função também elimina quebras de linha e outros tipos de espaço. Vejamos primeiro como estão os dez primeiros elementos com mais de um espaço:


mais_espacos <- paises %>% 
  filter(str_detect(pais_port, " {2,}")) %>% 
  pull(pais_port) %>% 
  .[1:10]

mais_espacos

 [1] "Afeganistão   ;"            "Angola   ;"                
 [3] "Anguila   ;"                "Ilhas Aland   ;"           
 [5] "Albânia   ;"                "Emirados Árabes Unidos   ;"
 [7] "Argentina   ;"              "Armênia   ;"               
 [9] "Samoa Americana   ;"        "Antártica   ;"             

Vamos primeiro interpretar o padrão que inserimos no segundo argumento na função str_detect. O primeiro termo é um espaço (" “). O termo que segue o espaço é o”{2,}“. Em regex, este último termo significa que procuramos padrões textuais que tenham 2 elementos ou mais iguais ao caractere imediatamente anterior. Por exemplo, se tivéssemos inserido o termo” {2,4}", a nossa busca encontraria todas as observações com dois, três ou quatro espaços.

Observemos nossa função str_squish em ação:


str_squish(mais_espacos)

 [1] "Afeganistão ;"            "Angola ;"                
 [3] "Anguila ;"                "Ilhas Aland ;"           
 [5] "Albânia ;"                "Emirados Árabes Unidos ;"
 [7] "Argentina ;"              "Armênia ;"               
 [9] "Samoa Americana ;"        "Antártica ;"             

Compare o resultado acima com o resultado imediatamente anterior.

Vejamos um outro exemplo só para fixar o entendimento:


teste <- c(" isso     é    um \nteste para   ver como \ta função str_squish   funciona   ")
teste

[1] " isso     é    um \nteste para   ver como \ta função str_squish   funciona   "

Para que você veja o que os \n e \t significam na prática utilizemos a função cat. Vejamos:


cat(teste)

 isso     é    um 
teste para   ver como   a função str_squish   funciona   

Como podemos ver, o primeiro termo (\n) representa uma quebra de linha; o segundo (\t), representa um espaço do tipo “tab”.

De novo, vejamos mais uma vez str_squish em ação:


str_squish(teste)

[1] "isso é um teste para ver como a função str_squish funciona"

Agora alteremos o objeto paises com intuito de eliminar os espaços excedentes:


paises <- paises %>% 
  mutate(pais_port = str_squish(pais_port))

Problema n. 4: Como substituir padrões encontrados no texto por outros padrões?

Para este exercício suponhamos que queremos substituir a pontuação ponto e vírgula e ponto de interrogação (?), respectivamente, pelos termos “- observação1” e “- observação2”. Trata-se de um exercício apenas para fins didáticos.

Abaixo identificamos as observações com os padrões “;” e “?” na coluna “pais_port”:


padrao1 <- paises %>%
  filter(str_detect(pais_port, ";")) %>%
  pull(pais_port) %>% 
  .[1:10]

padrao1

 [1] "Afeganistão ;"            "Angola ;"                
 [3] "Anguila ;"                "Ilhas Aland ;"           
 [5] "Albânia ;"                "Emirados Árabes Unidos ;"
 [7] "Argentina ;"              "Armênia ;"               
 [9] "Samoa Americana ;"        "Antártica ;"             

Devemos escapar a interrogação com duas contrabarras, uma vez que esse sinal de pontuação também é, como o ponto, uma expressão do regex.


padrao2 <- paises %>%
  filter(str_detect(pais_port, "\\?")) %>%
  pull(pais_port) %>% 
  .[1:10]

padrao2

 [1] "Panamá ; ?"    "Islândia ?"    "Cingapura ; ?" "Jamaica ; ?"  
 [5] "China ?"       "Turquia ?"     "Comores ?"     "Honduras ; ?" 
 [9] "Guadalupe ; ?" "Estônia ; ?"  

Agora vejamos a função que nos poderá ajudar com este exercício. Ela se chama str_replace. No primeiro argumento da função inserimos o vetor que desejamos modificar. No segundo argumento inserimos o padrão que queremos localizar e substituir. Por fim, no terceiro argumento inserimos o padrão de substituição.


str_replace(padrao1, ";", "- observação1")

 [1] "Afeganistão - observação1"           
 [2] "Angola - observação1"                
 [3] "Anguila - observação1"               
 [4] "Ilhas Aland - observação1"           
 [5] "Albânia - observação1"               
 [6] "Emirados Árabes Unidos - observação1"
 [7] "Argentina - observação1"             
 [8] "Armênia - observação1"               
 [9] "Samoa Americana - observação1"       
[10] "Antártica - observação1"             

Como no objeto padrao2 existem dois termos, devemos fazer duas substituições por meio da função str_replace.


str_replace(padrao2, "\\?", "- observação2") %>% 
  str_replace(";", "- observacao1")

 [1] "Panamá - observacao1 - observação2"   
 [2] "Islândia - observação2"               
 [3] "Cingapura - observacao1 - observação2"
 [4] "Jamaica - observacao1 - observação2"  
 [5] "China - observação2"                  
 [6] "Turquia - observação2"                
 [7] "Comores - observação2"                
 [8] "Honduras - observacao1 - observação2" 
 [9] "Guadalupe - observacao1 - observação2"
[10] "Estônia - observacao1 - observação2"  

A função str_replace apenas substitui o primeiro padrão encontrado. Se houvesse mais padrões do mesmo tipo, a função não faria a substituição dos termos excedentes. Para substituir todos os padrões que tivessem a especificação inserida seria necessário utilizar a função str_replace_all (de forma semelhante à função str_remove_all)

Alteremos o objeto paises para incorporar as mudanças propostas neste exercício e vejamos como ficaram os dados da coluna pais_port.


paises <- paises %>% 
  mutate(pais_port = str_replace(pais_port, ";", "- observação1") %>% 
           str_replace("\\?", "- observação2"))

paises %>% 
  select(ISOA, pais_port) %>% 
  rmarkdown::paged_table()

Para encerrar

Utilizemos um pouco mais de regex para deixar apenas os nomes dos países na coluna “pais_port”. Como podemos fazer isso? Uma solução, dentre várias, é fazer o seguinte:


paises %>% 
  mutate(pais_port = str_remove(pais_port, " -.+")) %>% 
  select(ISOA, pais_port) %>% 
  rmarkdown::paged_table()

Explicando o padrão regex acima: o termo " -" localiza um espaço e um hífen. Por construção, sabemos que esses termos separam o fim do nome do país dos termos “observação1” e “observação2”. Como visto anteriormente o “.” pode representar letras e espaços, além de outros caracteres, e o “+” significa que queremos que o termo imediatamente anterior esteja presente uma ou mais vezes. Assim, " -.+" captura toda a parte do texto que queremos eliminar.

Dica:

Uma função útil para ver o que uma construção regex captura a partir de um padrão inserido é a função str_view. Para usar essa função você precisa instalar o pacote htmlwidgets.

Vejamos:


paises %>% 
  pull(pais_port) %>% 
  .[1:10] %>% 
  str_view(" -.")

Como podemos observar, o padrão acima apenas captura " - ". E o padrão abaixo captura tudo após o fim do nome do país como já havíamos explicado.


paises %>% 
  pull(pais_port) %>% 
  .[1:10] %>% 
  str_view(" -.+")

Citation

For attribution, please cite this work as

Cleaver (2019, Nov. 6). Fulljoin: Manipulação de texto com stringr - parte III. Retrieved from https://www.fulljoin.com.br/posts/2019-11-06-manipulao-de-texto-com-stringr-parte-iii/

BibTeX citation

@misc{cleaver2019manipulação,
  author = {Cleaver, Miguel},
  title = {Fulljoin: Manipulação de texto com stringr - parte III},
  url = {https://www.fulljoin.com.br/posts/2019-11-06-manipulao-de-texto-com-stringr-parte-iii/},
  year = {2019}
}