2023/01/31 (updated: 2023-12-09)

Análise exploratória de dados

Análise exploratória de dados

  • Nas última décadas houve um grande crescimento na variedade e no volume de dados analisados por economistas aplicados.
  • Um tipo de análise cada vez mais comum é a análise exploratória de dados que nos permite aprender sobre os padrões e estruturas dos dados.
  • Nesta unidade vamos explorar três tipos de dados que ainda não discutimos no curso: dados de texto, dados de redes e dados espaciais.

Dados de texto

  • O crescimento da internet levou a uma gigantesca quantidade de textos espalhada em e-mail, sites de internet, redes sociais, etc. Análise de artigos de jornais podem ajudar a entender assuntos importantes relativo ao comportamento e opiniões de uma sociedade, o mesmo vale para textos em blogs ou redes sociais.
  • A digitalização de livros, artigos e documentos oficiais também cria inúmeras oportunidades para cientistas sociais aplicados desenvolverem novas pesquisas.

Dados de texto

  • O exemplo para essa seção trata de uma questão antiga que remonta a um dos primeiros casos de análise de texto na literatura de estatística.
  • Vamos analisar o texto em The Federalist, conhecido como The Federalist papers, que consiste de 85 ensaios escritos por Alexander Hamilton, John Jay e James Madison entre 1787 e 1788 para encorajar o povo de Nova Iorque a ratificar a recém escrita Constituição do EUA.
  • Como Hamilton e Madison ajudaram a escrever a constituição, estudiosos consideram que The Federalist papers reflete as intenções dos autores da Constituição americana.

Dados de texto

  • Os ensaios do The Federalist papers foram originalmente publicados anonimamente em vários jornais de Nova Iorque com o pseudônimo de Publius. Por conta disso, a autoria de cada ensaio é objeto de pesquisa.
  • De acordo com a Biblioteca do Congresso Americano especialistas acreditam que Hamilton escreveu 51 ensaios, Madison escreveu 15, Jay Jhon escreveu 5 e três foram escritos em co-autoria por Hamilton e Madison. Os outros 11 ensaios foram escritos por Madison ou Hamilton.

Dados de texto

  • Nosso exemplo consiste em analisar os textos dos ensaios para tentar determinar a autoria.
  • O exemplo segue: Frederick Mosteller e David Wallace. Inference in an authorship problem; Journal of the American Statstitical Association, v.58, n.302, 1963.
  • Os textos de cada um dos ensaios foram obtidos na Biblioteca do Congresso dos EUA e foram salvos em arquivos .txt, cada arquivo tem o nome fpXX.txt onde XX é o número do ensaio.

Dados de texto

  • Antes de analisar, é preciso processar os dados, existem vários pacotes do R para realizar essa tarefa.
  • Para desenvolver o exemplo vamos usar os pacotes: tm, de text mining, SnowballC, stringr e tidytext. Esse último segue os princípios de tidy data do tidyverse.

Dados de texto

library(tm)
library(SnowballC)
library(stringr)
library(tidytext)
library(tidyverse)

Dados de texto

  • O primeiro passo é carregar os textos. Para isso será usada a função DirSource() para especificar o diretorio e o padrão dos nomes dos arquivos que serão usados na função VCorpus() que cria a coleção de textos, ou corpus de texto.
  • As funçoes VCorpus() e DirSource() são do pacote tm.

Dados de texto

DIR_SOURCE <- "D:/Roberto/Cursos/Analise de Dados 2023/Curso 2024/Unidade 5/federalist"

corpus_raw <- VCorpus(DirSource(directory = DIR_SOURCE, pattern = "fp"))

corpus_raw
## <<VCorpus>>
## Metadata:  corpus specific: 0, document level (indexed): 0
## Content:  documents: 85

Dados de texto

  • Agora podemos começar o pré-processamento do corpus. Pré-processamento envolve uma série de passos, segue uma lista com alguns desses passos:
    • Tokenize: partir o texto em tokens menores, geralmente palavras.
    • Make lower: converter todo o texto para letras minúsculas
    • Strip white spaces: remover espaços, quebras de linhas, etc
    • Strip punctuations: remover a pontuação

Dados de texto

  • Continuação:
    • Strip numbers: remover os números
    • Strip stop words: remover palavras comuns (the, an, to, etc, em português seria a, o, de, etc)
    • Stemming: reduzir as palavras removendo prefixos ou sufixos para linguagens específicas (por exemplo, studies para studi)
    • Lemmatization: Reduz palavras para raiz tomando por base regras liguísticas de uma língua específica (por exemplo, studies p

Dados de texto

  • Como costuma ser o caso, existem várias maneiras de pré-processar um texto no R. Vamos começar convertendo o corpus em um data.frame, especificamente em um tibble usando a função tidy().
  • A função tidy() quando aplicada a um corpus adiciona metadados, por exemplo, o autor do documento. Esses metadados estão em branco no nosso exemplo, por isso vamos ficar apenas com as colunas id e text.
  • A primeira guarda o nome do arquivo do texto e a segunda o conteúdo do texto.

Dados de texto

corpus_tidy <- tidy(corpus_raw) %>%
  select(id, text)

Dados de texto

## # A tibble: 6 × 2
##   id       text                                                                 
##   <chr>    <chr>                                                                
## 1 fp01.txt "AFTER an unequivocal experience of the inefficiency of the subsisti…
## 2 fp02.txt "WHEN the people of America reflect that they are now called upon to…
## 3 fp03.txt "IT IS not a new observation that the people of any country (if, lik…
## 4 fp04.txt "MY LAST paper assigned several reasons why the safety of the people…
## 5 fp05.txt "QUEEN ANNE, in her letter of the 1st July, 1706, to the Scotch Parl…
## 6 fp06.txt "THE three last numbers of this paper have been dedicated to an enum…

Dados de texto

  • Será útil ter o número do ensaio como um inteiro. Para fazer isso vamo susar a função str_sub() do pacote stringr. Essa função extrai ou substitui elementos em uma determinada posição em uma string.
  • Como o nome dos arquivos são todos do tipo fpXX.tx vamos ficar apenas com o terceiro e quarto caractere de cada string.
corpus_tidy <- corpus_tidy %>%
  mutate(new_id = as.integer(str_sub(id, start=3, end=4)))

glimpse(corpus_tidy)
## Rows: 85
## Columns: 3
## $ id     <chr> "fp01.txt", "fp02.txt", "fp03.txt", "fp04.txt", "fp05.txt", "fp…
## $ text   <chr> "AFTER an unequivocal experience of the inefficiency of the sub…
## $ new_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, …

Dados de texto

glimpse(corpus_tidy)
## Rows: 85
## Columns: 3
## $ id     <chr> "fp01.txt", "fp02.txt", "fp03.txt", "fp04.txt", "fp05.txt", "fp…
## $ text   <chr> "AFTER an unequivocal experience of the inefficiency of the sub…
## $ new_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, …

Dados de texto

  • Com os textos em um tibble podemos separar o texto em tokens (pequenas pedaços do texto). No exemplo vamos separar por palavras, mas podia ser por frases, parágrafos, etc. Para isso vamos usar a função unnest_tokens() do pacote tidytext.
  • Depois vamos usar a função wordStem() do pacote SnowballC para retirar prefixos e sufixos de cada palavra.

Dados de texto

  • Na sequênica retiramos os números usando a função str_replace_all(), do strngr com uma expressão regular, regex.
  • Uma expressão regular especifica um padrão de texto, no exemplo usaremos \d+ que diz ao R para retirar todos os números e substituir por uma string vazia. Por fim retiramos todos as strings vazias.

Dados de texto

tokens_raw <- corpus_tidy %>%
  unnest_tokens(word, text, to_lower = TRUE) %>%
  mutate(stem = wordStem(word)) %>%
  mutate(word = str_replace_all(word, "\\d+", "")) %>%
  filter(word != "")

Dados de texto

glimpse(tokens_raw)
## Rows: 187,412
## Columns: 4
## $ id     <chr> "fp01.txt", "fp01.txt", "fp01.txt", "fp01.txt", "fp01.txt", "fp…
## $ new_id <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ word   <chr> "after", "an", "unequivocal", "experience", "of", "the", "ineff…
## $ stem   <chr> "after", "an", "unequivoc", "experi", "of", "the", "ineffici", …

Dados de texto

  • O último passo é remover as stop words, para isso vamos carregar a lista de stop words do tidytext e usar a função anti_join() do tidyverse.
  • A função anti_join() retira as observações comuns aos dois data.frames.

Dados de texto

data("stop_words", package = "tidytext")

glimpse(stop_words)
## Rows: 1,149
## Columns: 2
## $ word    <chr> "a", "a's", "able", "about", "above", "according", "accordingl…
## $ lexicon <chr> "SMART", "SMART", "SMART", "SMART", "SMART", "SMART", "SMART",…

Dados de texto

tokens <- tokens_raw %>%
  anti_join(stop_words, by="word")

glimpse(tokens)
## Rows: 64,106
## Columns: 4
## $ id     <chr> "fp01.txt", "fp01.txt", "fp01.txt", "fp01.txt", "fp01.txt", "fp…
## $ new_id <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ word   <chr> "unequivocal", "experience", "inefficiency", "subsisting", "fed…
## $ stem   <chr> "unequivoc", "experi", "ineffici", "subsist", "feder", "govern"…

Dados de texto

  • Caso seja de interesse recuperar um ensaio do corpus original podemos usar a função content() com o nome do corpus e um número inteiro cercado de [[]] para indicar o número do ensaio.

Dados de texto

head(content(corpus_raw[[10]]), n=10)
##  [1] "AMONG the numerous advantages promised by a well-constructed Union, none "          
##  [2] "        deserves to be more accurately developed than its tendency to break and "   
##  [3] "        control the violence of faction. The friend of popular governments never "  
##  [4] "        finds himself so much alarmed for their character and fate, as when he "    
##  [5] "        contemplates their propensity to this dangerous vice. He will not fail, "   
##  [6] "        therefore, to set a due value on any plan which, without violating the "    
##  [7] "        principles to which he is attached, provides a proper cure for it. The "    
##  [8] "        instability, injustice, and confusion introduced into the public councils, "
##  [9] "        have, in truth, been the mortal diseases under which popular governments "  
## [10] "        have everywhere perished; as they continue to be the favorite and fruitful "

Dados de texto

  • Uma maneira rápida de analisar um texto é contar o número de vezes que cada palavra foi utilizada, o número de vezes que uma determinada palavra aparece em um documento é chamado de frequência do termo.
  • Podemos calcular a frequência de cada termo em tokens a função count()

Dados de texto

tokens_counts <- count(tokens, new_id, stem)
head(tokens_counts)
## # A tibble: 6 × 3
##   new_id stem           n
##    <int> <chr>      <int>
## 1      1 absurd         1
## 2      1 accid          1
## 3      1 acknowledg     1
## 4      1 act            1
## 5      1 actuat         1
## 6      1 add            1

Dados de texto

  • A frequênica dos termos pode ser resumida em uma matriz de documentos e termos, document-term matrix, que é uma estrutura retangular onde as linhas representam os documentos e as colunas representam os termos únicos. O elemento \((i,j)\) detsa matriz informa quantas vezes o j-ésimo termo apareceu no i-ésimo documento.
  • Podemos transpor a matriz que se tornará uma matriz de termos e documentos, term-document matrix, onde as linhas representam os termos e as colunas representam os documentos.
  • A função cast_dtm() cria uma matriz de documentos e termos, a função cast_tdm() cria uma matriz de termo-documento, ambas são do pacote tidytext.

Dados de texto

dtm <- cast_dtm(tokens_counts, document = new_id, term = stem, value = n)

dtm
## <<DocumentTermMatrix (documents: 85, terms: 4674)>>
## Non-/sparse entries: 37214/360076
## Sparsity           : 91%
## Maximal term length: 17
## Weighting          : term frequency (tf)

Dados de texto

  • Como o objeto dtm é de uma classe especial, no lugar de mostrar o contéudo do objeto o R mostra um sumário da matriz de documento e termos.
  • No sumário aparecem os números de documentos e de termos, o número de elementos diferentes de zero (não esparsos) e o número de elementos iguais a zero (esparsos). O termo esparsidade se refere a proproção de elementos iguais a zero na matriz. Matrizes de termos e documentos tipicamente são esparsas.
  • Por fim, aparecem o termo de maior tamanho e quantidade pela qual os elementos da matriz são ponderados. No exemplo cada entrada representa a frequência do termo.
  • A função inspect() do pacote tm permite olhar a matriz de documentos e termos.

Dados de texto

inspect(dtm[1:5, 1:8])
## <<DocumentTermMatrix (documents: 5, terms: 8)>>
## Non-/sparse entries: 16/24
## Sparsity           : 60%
## Maximal term length: 10
## Weighting          : term frequency (tf)
## Sample             :
##     Terms
## Docs absurd accid acknowledg act actuat add addit address
##    1      1     1          1   1      1   1     1       1
##    2      0     0          0   0      0   0     0       0
##    3      0     0          2   1      1   1     1       0
##    4      0     0          0   1      0   0     0       1
##    5      0     0          0   1      0   0     0       0

Dados de texto

  • Outro método é usar as.matrix() para forçar o objeto em uma matriz padrão do R
dtm.mat <- as.matrix(dtm)
dtm.mat[1:5, 1:8]
##     Terms
## Docs absurd accid acknowledg act actuat add addit address
##    1      1     1          1   1      1   1     1       1
##    2      0     0          0   0      0   0     0       0
##    3      0     0          2   1      1   1     1       0
##    4      0     0          0   1      0   0     0       1
##    5      0     0          0   1      0   0     0       0

Dados de texto

  • Para analisar e visualizar a matriz de documentos e termos vamos começar olhando para o conjunto de palavras usadas, bag-of-words. Esse tipo de análise tem foco na frequência das palavras e ignora questões gramaticais e a ordem das palavras.
  • Desta forma, análises bag-of-words não detectam significados sutis das palavras em um texto, na verdade nem mesmo diferenças não tão sutis. Em uma análise desse tipo frases com aumentar juros e reduzir juros dão a mesma frequência à palavra juros.
  • Por outro lado, analisar a frequência com que termos aparecem é útil para identificar os tópicos discutidos em um documento.

Dados de texto

  • Um método popular para visualizar a frequência das palavras em um documento é conhecido como nuvem de palavras, word cloud. Nela palavras usadas com mais frequência aparecem em fontes maiores.
  • A função wordcloud() do pacote wordcloud cria uma nuvem de palavras. Vamos usar essa função para fazer a nuvem de palavras dos ensaios 12 e 24. Comecemos carregando o wordcloud.

Dados de texto

library(wordcloud)
## Carregando pacotes exigidos: RColorBrewer

Dados de texto

doc_12 <- filter(tokens_counts, new_id == 12)
wordcloud(words = doc_12$stem, freq = doc_12$n, max.words = 15)

doc_24 <- filter(tokens_counts, new_id == 24)
wordcloud(words = doc_24$stem, freq = doc_24$n, max.words = 15)

Dados de texto

  • Ensaio 12

Dados de texto

  • Ensaio 24

Dados de texto

  • Uma comparação das duas nuvens mostra que o ensaio 12 contém palavras ligadas a economia, por exemplo: revenu (raiz de revenue), commerc (commerce), trade, tax e land.
  • Já no ensaio 24 aparecem palavras ligadas a segurança, por exemplo: power, peac (peace), garrison e armi (army).

Dados de texto

  • A função stemCompletion() do pacote tm recupera a versão completa da raiz da palavra.
stemCompletion(c("revenu", "commerc", "peac", "armi"), corpus_raw)
##     revenu    commerc       peac       armi 
##  "revenue" "commerce"    "peace"   "armies"

Dados de texto

  • Ensaio 12 com palavras completas

Dados de texto

  • A análise por tópicos parece consistente com os títulos dos ensaios.
  • O título do ensaio 12 é: The Utility of the Union in Respect to Revenue
  • O título do ensaio 24 é: The Powers Necessary to the Common Defense Further Considered

Dados de texto

  • A análise acima considerou a frequência dos termos em cada documento. Porém, um certo termo aparecer muito em um documento pode signficar pouco se o termo aparece com frequência em todos os documentos do corpus.
  • Para resolver esse problema podemos dar peso menor a termos que aparecem com mais frequência em todos os documentos.

Dados de texto

  • Isso é feito com a estatística frequência do termo–inverso da frequência nos documentos, chamada de tf-idf (do inglês term frequency–inverse document frequency).
  • Para um documento d e um termo w, a tf-idf(w,d) é definida por: \[\mbox{tf-idf}(w,d)=\mbox{tf}(w,d)\times\mbox{idf}(w)\]
  • A frequência do termo, \(\mbox{tf}(w,d)\), representa o número de vezes (ou frequência) que o termo \(w\) aparece no documento \(d\), em alguns casos valores positivos de \(\mbox{tf}(w,d)\) são convertidos para logaritmo.

Dados de texto

  • O inverso da frequência nos documentos, \(\mbox{idf}(w)\), é dado por: \[\mbox{idf}(w)=\ln \frac{N}{df(w)}\]
  • Onde \(N\) é o número de documentos e \(\mbox{df}(w)\) é o número de documentos que contém o termo \(w\).

Dados de texto

  • A função bind_tf_idf() do pacote tidytext calcula e adicona o tf-idf a um data.frame.
tokens_counts <- bind_tf_idf(tokens_counts,
                             term = stem,
                             document = new_id,
                             n = n)

Dados de texto

head(tokens_counts)
## # A tibble: 6 × 6
##   new_id stem           n      tf   idf   tf_idf
##    <int> <chr>      <int>   <dbl> <dbl>    <dbl>
## 1      1 absurd         1 0.00186 1.73  0.00323 
## 2      1 accid          1 0.00186 3.75  0.00698 
## 3      1 acknowledg     1 0.00186 1.55  0.00289 
## 4      1 act            1 0.00186 0.400 0.000744
## 5      1 actuat         1 0.00186 2.14  0.00399 
## 6      1 add            1 0.00186 1.35  0.00252

Dados de texto

  • Outra maneira de calcular o tf-idf é usando a função weightTfIdf() do pacote tm. Essa função toma como input uma matriz de documento e termo e tem um argumento chamado normalize cujo o default é TRUE, se for definido como FALSE então a frequência do termo, \(\mbox{tf}(w,d)\), não será dividida pelo número de termos no documento \(d\). Neste caso, não ponderamos pelo tamanho do documento.

Dados de texto

dtm_tf_idf <- weightTfIdf(dtm)

dtm_tf_idf
## <<DocumentTermMatrix (documents: 85, terms: 4674)>>
## Non-/sparse entries: 37044/360246
## Sparsity           : 91%
## Maximal term length: 17
## Weighting          : term frequency - inverse document frequency (normalized) (tf-idf)

Dados de texto

  • Calculados os tf-idf de cada termo podemos checar quais os termos mais importantes em cada ensaio.
  • Usaremos a função slice_max() do tidyverse para extrair os dez maiores it-idf dos ensaios 12 e 24. Os dados estão no data.frame tokens_counts.

Dados de texto

tokens_counts %>%
  filter(new_id == 12) %>%
  slice_max(tf_idf, n=10)
## # A tibble: 10 × 6
##    new_id stem           n      tf   idf tf_idf
##     <int> <chr>      <int>   <dbl> <dbl>  <dbl>
##  1     12 revenu        11 0.0138   1.22 0.0169
##  2     12 contraband     3 0.00376  4.44 0.0167
##  3     12 patrol         3 0.00376  4.44 0.0167
##  4     12 excis          5 0.00627  2.65 0.0166
##  5     12 coast          3 0.00376  3.75 0.0141
##  6     12 tax            8 0.0100   1.31 0.0131
##  7     12 trade          6 0.00752  1.73 0.0130
##  8     12 cent           2 0.00251  4.44 0.0111
##  9     12 gallon         2 0.00251  4.44 0.0111
## 10     12 commerc        8 0.0100   1.11 0.0111

Dados de texto

tokens_counts %>%
  filter(new_id == 24) %>%
  slice_max(tf_idf, n=10)
## # A tibble: 10 × 6
##    new_id stem           n      tf   idf tf_idf
##     <int> <chr>      <int>   <dbl> <dbl>  <dbl>
##  1     24 garrison       6 0.00926  2.83 0.0262
##  2     24 dock           3 0.00463  4.44 0.0206
##  3     24 yard           3 0.00463  4.44 0.0206
##  4     24 settlement     3 0.00463  3.75 0.0174
##  5     24 spain          4 0.00617  2.36 0.0146
##  6     24 armi           7 0.0108   1.26 0.0137
##  7     24 frontier       3 0.00463  2.83 0.0131
##  8     24 arsen          2 0.00309  3.75 0.0116
##  9     24 western        3 0.00463  2.50 0.0116
## 10     24 nearer         2 0.00309  3.34 0.0103

Dados de texto

  • Os resultados reforçam que o ensaio 12 trata de economia e o 24 de segurança.

Dados de texto

  • A análise de documentos com base na frequência dos termos tem fundamento na hipótese de bag-of-words que ignora a ordem das palavras. Para medir a importância relativa de cada termo em um documento é calculada a frequência do termo–inverso da frequência nos documentos, tf-idf, que representa a frequência relativa do termo ponderada pela inversa do número de documentos que o termo aparece.

Dados de texto

  • Agora podemos agrupar os textos escritos por Hamilton com base na tf-idf. Para isso usaremos o algoritmo k-means que vimos na Unidade 3. Como o ponto de partida desse algoritmo é aleatório os resultados podem mudar a cada vez que usamos o algoritmo. Para evitar esse problema usaremos a função set.seed().
  • O número de clusters será 5, é um número arbitrário que foi obtido com experimentação.
  • Inicialmente serão selecionados os ensaios escritos por Hamilton, depois usaremos a função cast_dtm() para criar a matriz de termos e documentos e finalmente usaremos a função kmeans().

Dados de texto

hamilton <- c(1, 6:9, 11:13, 15:17, 21:36, 59:61, 65:85)
hamilton_docs <- tokens_counts %>%
  filter(new_id %in% hamilton)

hamilton_dtm <- cast_dtm(hamilton_docs,
                         document = new_id, term = stem,
                         value = n) %>%
  weightTfIdf()

Dados de texto

hamilton_dtm
## <<DocumentTermMatrix (documents: 51, terms: 3918)>>
## Non-/sparse entries: 22478/177340
## Sparsity           : 89%
## Maximal term length: 17
## Weighting          : term frequency - inverse document frequency (normalized) (tf-idf)

Dados de texto

inspect(hamilton_dtm[1:5, 1:4])
## <<DocumentTermMatrix (documents: 5, terms: 4)>>
## Non-/sparse entries: 6/14
## Sparsity           : 70%
## Maximal term length: 10
## Weighting          : term frequency - inverse document frequency (normalized) (tf-idf)
## Sample             :
##     Terms
## Docs      absurd       accid acknowledg          act
##    1 0.003672226 0.008700978 0.00437709 0.0012521887
##    6 0.000000000 0.000000000 0.00000000 0.0000000000
##    7 0.000000000 0.000000000 0.00000000 0.0008533317
##    8 0.000000000 0.000000000 0.00000000 0.0009001678
##    9 0.000000000 0.000000000 0.00000000 0.0000000000

Dados de texto

set.seed(1234)
km.out <- kmeans(hamilton_dtm, centers = 5)
km.out$iter
## [1] 3

Dados de texto

  • Agora vamos avaliar os resultados. Primeiro usaremos a função table() para saber quantos documentos ficaram em cada cluster.
table(km.out$cluster)
## 
##  1  2  3  4  5 
##  8  6  1  1 35

Dados de texto

  • Na sequência queremos saber os ensaios e os dez termos mais importantes de cada cluster.
  • Como vamos executar 5 vezes o mesmo código, uma para cada cluster, é válido usar um loop do tipo for.
  • Usaremos a função colnames() para dar a cada centro de cluster o termo correspondente e a função enframe() para tranformar o vetor com nomes em um tibble de modo a usar as facilidades do tidyverse.

Dados de texto

colnames(km.out$centers) <- colnames(hamilton_dtm)

for (i in 1:5) {
  print(str_c("CLUSTER ", i))
  print("Top 10 words: ")
  cluster_centers <- enframe(km.out$centers[i, ]) %>%
    slice_max(value, n=10)
  print(cluster_centers)
  print("Federalist papers classified:")
  cluster_docs <- enframe(km.out$cluster, "document", "cluster") %>%
    filter(cluster == i)
  print(as.vector(cluster_docs$document))
  cat("\n")
}

Dados de texto

## [1] "CLUSTER 1"
## [1] "Top 10 words: "
## # A tibble: 10 × 2
##    name       value
##    <chr>      <dbl>
##  1 senat    0.0222 
##  2 presid   0.0194 
##  3 governor 0.0118 
##  4 pardon   0.0114 
##  5 treati   0.0113 
##  6 offic    0.0104 
##  7 appoint  0.0103 
##  8 impeach  0.00974
##  9 nomin    0.00950
## 10 vote     0.00727
## [1] "Federalist papers classified:"
## [1] "66" "68" "69" "74" "75" "76" "77" "79"

Dados de texto

## [1] "CLUSTER 2"
## [1] "Top 10 words: "
## # A tibble: 10 × 2
##    name        value
##    <chr>       <dbl>
##  1 armi      0.0229 
##  2 militia   0.0225 
##  3 militari  0.0141 
##  4 disciplin 0.00963
##  5 garrison  0.00805
##  6 peac      0.00793
##  7 troop     0.00758
##  8 liberti   0.00619
##  9 corp      0.00566
## 10 neighbor  0.00553
## [1] "Federalist papers classified:"
## [1] "8"  "24" "25" "26" "28" "29"

Dados de texto

## [1] "CLUSTER 3"
## [1] "Top 10 words: "
## # A tibble: 10 × 2
##    name          value
##    <chr>         <dbl>
##  1 northern     0.0607
##  2 southern     0.0455
##  3 confederaci  0.0448
##  4 list         0.0326
##  5 frontier     0.0265
##  6 comprehens   0.0238
##  7 civil        0.0219
##  8 jersei       0.0218
##  9 pennsylvania 0.0216
## 10 navig        0.0200
## [1] "Federalist papers classified:"
## [1] "13"

Dados de texto

## [1] "CLUSTER 4"
## [1] "Top 10 words: "
## # A tibble: 10 × 2
##    name     value
##    <chr>    <dbl>
##  1 vacanc  0.0947
##  2 recess  0.0552
##  3 session 0.0483
##  4 senat   0.0482
##  5 claus   0.0465
##  6 fill    0.0453
##  7 appoint 0.0312
##  8 expir   0.0237
##  9 presid  0.0216
## 10 unfound 0.0192
## [1] "Federalist papers classified:"
## [1] "67"

Dados de texto

## [1] "CLUSTER 5"
## [1] "Top 10 words: "
## # A tibble: 10 × 2
##    name        value
##    <chr>       <dbl>
##  1 court     0.00783
##  2 juri      0.00490
##  3 tax       0.00457
##  4 jurisdict 0.00395
##  5 taxat     0.00373
##  6 elect     0.00338
##  7 trial     0.00335
##  8 land      0.00334
##  9 revenu    0.00327
## 10 claus     0.00316
## [1] "Federalist papers classified:"
##  [1] "1"  "6"  "7"  "9"  "11" "12" "15" "16" "17" "21" "22" "23" "27" "30" "31"
## [16] "32" "33" "34" "35" "36" "59" "60" "61" "65" "70" "71" "72" "73" "78" "80"
## [31] "81" "82" "83" "84" "85"

Dados de texto

  • A análise das dez palavras mais importantes de cada cluster sugere que o primeiro, com 8 ensaios, trata da nomeação e remoção no legislativo e no executivo; o segundo, com 6 ensaios, trata de segurança e questões militares; o terceito, com 1 ensaio, trata de geografia e fronteiras; o quarto, com 1 ensaio, trata de recesso legislativo; e o quinto, com 35 ensaios, trata de justiça e tributação.
  • Uma comparação entre esses tópicos e o conteúdo do The Federalist Papers é encontrado um razoável grau de validação para os resultados obtidos com o algoritmo k-means usando cinco agrupamentos.

Dados de texto

  • O uso do The Federalist Papers como exemplo de como análise de textos pode revelar tópicos é apenas didático. Por ser um texto muito conhecido e relativamente curto, é fácil ler todos os ensaios e tirar conclusões sobre do que trata cada um deles.
  • A técnica, porém, pode ser usada em outras ciscunstâncias onde a leitura do corpus pode não ser tão simples.

Dados de texto

  • Como foi dito na introdução, existem dúvidas sobre a autoria de 11 dos 85 ensaios. Usaremos os 66 ensaios atribuidos a Hamilton ou Madison para tentar descobrir a autoria desses 11 ensaios.
  • Como cada ensaio trata de tópicos diferentes o foco será no uso de adjetivos, advérbios, preposições e conjunções. Especificamente, serão analisadas as frequências de 10 palavras: although, always, commonly, consequently, considerable, enough, there, upon, while e whislt.

Dados de texto

  • O primeiro passo é calcular a frequência de cada termo, por 1.000 palavras, nos textos de cada autor. Já criamos o objeto hamilton com os ensaios do Hamilton. Vamos criar um para cada autor. Depois criamos um objeto com as palavras que desejamos analisar e uma variável identificando o autor de cada ensaio no data.frame tokens_raw.

Dados de texto

madison <- c(10, 14, 37:48, 58)
jay <- c(2:5, 64)
joint <- c(18:20)

STYLE_WORDS <- c("although", "always", "commonly", "consequently", 
                 "considerable", "enough",
                 "there", "upon", "while", "whilst")

tokens_raw <- tokens_raw %>%
  mutate(author = case_when(new_id %in% hamilton ~ "Hamilton",
                            new_id %in% madison ~ "Madison",
                            new_id %in% jay ~ "Jay",
                            new_id %in% joint ~ "Joint",
                            TRUE ~ "Disputed"))

Dados de texto

head(tokens_raw)
## # A tibble: 6 × 5
##   id       new_id word        stem      author  
##   <chr>     <int> <chr>       <chr>     <chr>   
## 1 fp01.txt      1 after       after     Hamilton
## 2 fp01.txt      1 an          an        Hamilton
## 3 fp01.txt      1 unequivocal unequivoc Hamilton
## 4 fp01.txt      1 experience  experi    Hamilton
## 5 fp01.txt      1 of          of        Hamilton
## 6 fp01.txt      1 the         the       Hamilton

Dados de texto

  • Agora é calcular quantas vezes cada autor usa cada uma das palavras escolhidas a cada mil palavras.

Dados de texto

tfm <- tokens_raw %>%
  group_by(author, word) %>%
  summarize(n = n()) %>%
  ungroup() %>%
  group_by(author) %>%
  mutate(tf_thou = n/sum(n) * 1000) %>%
  filter(word %in% STYLE_WORDS) %>%
  select(-n) %>%
  pivot_wider(names_from = word, values_from = tf_thou) %>%
  mutate_at(vars(always:consequently), replace_na, 0)
## `summarise()` has grouped output by 'author'. You can override using the
## `.groups` argument.

Dados de texto

head(tfm)
## # A tibble: 5 × 11
## # Groups:   author [5]
##   author although always commonly consequently considerable there  upon   whilst
##   <chr>     <dbl>  <dbl>    <dbl>        <dbl>        <dbl> <dbl> <dbl>    <dbl>
## 1 Dispu…  0.137    0.228   0.0456       0.365         0.228 1.32  0.137  0.411  
## 2 Hamil…  0.00904  0.551   0.190        0.0361        0.416 3.41  3.34   0.00904
## 3 Jay     0.597    0.955   0.119        0.477         0.119 1.19  0.119 NA      
## 4 Joint  NA        0.355   0.178        0             0.355 0.711 0.355  0.355  
## 5 Madis…  0.196    0.171   0            0.318         0.122 0.857 0.171  0.294  
## # ℹ 2 more variables: enough <dbl>, `while` <dbl>

Dados de texto

  • A análise sugere que Hamilton usa com frequência termos como there e upon que raramente são usados por Madison. Por outro lado, consequently e whistl são usados frequentemente por Madison e raramente por Hamilton.
  • Vamos usar a frequência desses quatro termos como preditores de um modelo linear usado para identificar os autores dos textos com autoria desconhecida.

Dados de texto

  • Para fazer a regressão será preciso arrumar os dados e criar uma variável de resultado que indique o autor do texto.
  • A variável será criada de forma que seja 1 para Hamilton e -1 para Madison, se não for nenhum dos dois será NA_real_.

Dados de texto

reg_data <- tokens_raw %>%
  group_by(author, new_id, word) %>%
  summarize(n = n()) %>%
  mutate(tf_thou = n/sum(n) * 1000) %>%
  filter(word %in% STYLE_WORDS) %>%
  mutate(author_outcome = case_when(author == "Hamilton" ~ 1,
                                    author == "Madison" ~ -1,
                                    TRUE ~ NA_real_)) %>%
  select(-n) %>%
  pivot_wider(names_from = word, values_from = tf_thou) %>%
  mutate_at(vars(always:`while`), replace_na, 0) %>%
  ungroup()
## `summarise()` has grouped output by 'author', 'new_id'. You can override using
## the `.groups` argument.

Dados de texto

head(reg_data)
## # A tibble: 6 × 13
##   author   new_id author_outcome always consequently there whilst although  upon
##   <chr>     <int>          <dbl>  <dbl>        <dbl> <dbl>  <dbl>    <dbl> <dbl>
## 1 Disputed     49             NA  0.608        0.608 1.22   0.608    0     0    
## 2 Disputed     50             NA  0            0     0      0        0.908 0.908
## 3 Disputed     51             NA  0            1.04  2.09   1.04     0     0    
## 4 Disputed     52             NA  0.542        0     0      0        0     0    
## 5 Disputed     53             NA  0            0     0.924  0.462    0.462 0    
## 6 Disputed     54             NA  0            1.00  0.501  0        0.501 1.00 
## # ℹ 4 more variables: considerable <dbl>, commonly <dbl>, enough <dbl>,
## #   `while` <dbl>

Dados de texto

Dados de texto

  • Com os dados prontos podemos fazer a regressão da variável de resultado contra as quatro palavras que destacamos: upon, there, consequently e whilst

Dados de texto

hm.fit <- lm(author_outcome ~ upon + there + consequently + whilst, data = reg_data)
hm.fit
## 
## Call:
## lm(formula = author_outcome ~ upon + there + consequently + whilst, 
##     data = reg_data)
## 
## Coefficients:
##  (Intercept)          upon         there  consequently        whilst  
##      -0.1955        0.2128        0.1180       -0.5964       -0.9090

Dados de texto

  • Os resultados são consistentes com nossa análise preliminar, os coeficientes estimados para upon e there foram positivos e os coeficientes estimados para consequently e whilst foram negativos. Isso implica que os primeiros termos estão mais relacionados a Hamilton, que tem valor 1 na variável de resultado, e os segundos a Madison, que tem valor -1 na variável de resultado.

Dados de texto

  • Quão bem o modelo consegue prever o autor de um ensaio? Para responder essa pergunta vamos considerar o valor estimado pelo modelo, se for positivo daremos a autoria a Hamilton e se for negativo a Madison.
  • Depois calculamos a proporção de prediçoes corretas.

Dados de texto

broom::augment(hm.fit)
## # A tibble: 65 × 12
##    .rownames author_outcome  upon there consequently whilst .fitted   .resid
##    <chr>              <dbl> <dbl> <dbl>        <dbl>  <dbl>   <dbl>    <dbl>
##  1 12                     1  3.78  1.26            0      0   0.757  0.243  
##  2 13                     1  2.06  4.12            0      0   0.729  0.271  
##  3 14                     1  4.88  3.99            0      0   1.31  -0.314  
##  4 15                     1  1.51  1.01            0      0   0.244  0.756  
##  5 16                     1  2.02  1.52            0      0   0.415  0.585  
##  6 17                     1  2.40  3.21            0      0   0.695  0.305  
##  7 18                     1  3.26  4.19            0      0   0.993  0.00738
##  8 19                     1  2.08  9.38            0      0   1.35  -0.354  
##  9 20                     1  3.25  5.85            0      0   1.19  -0.186  
## 10 21                     1  2.95  1.96            0      0   0.663  0.337  
## # ℹ 55 more rows
## # ℹ 4 more variables: .hat <dbl>, .sigma <dbl>, .cooksd <dbl>, .std.resid <dbl>

Dados de texto

library(modelr)
author_data <- reg_data %>%
  add_predictions(hm.fit) %>%
  mutate(pred_author = if_else(pred >= 0, "Hamilton", "Madison"))

Dados de texto

Dados de texto

author_data %>%
  filter(!is.na(author_outcome)) %>%
  group_by(author) %>%
summarize(`Proportion Correct` = mean(author == pred_author))
## # A tibble: 2 × 2
##   author   `Proportion Correct`
##   <chr>                   <dbl>
## 1 Hamilton                    1
## 2 Madison                     1

Dados de texto

  • O modelo acertou 100% dos autores! Porém…
  • Nossa medida de acerto do modelo tem por base predições dentro da amostra, ou seja, os mesmos dados que usamos para estimar o modelo foram usados para testar os acertos das previsões.
  • Essa pode não ser uma boa estratégia por conta do overfit, que ocorre quando o modelo captura aspectos muito específicos da amostra que está sendo usada e perde padrões que ocorrem em diferentes amostras.

Dados de texto

  • Vamos agora considerar previsões fora da amostra. Especificamente vamos usar uma técnica chamada validação cruzada, cross-validation, deixando um de fora.
  • Essa técnica consiste em retirar uma observação da amostra e prever a variável de resultado dessa observação no modelo estimado sem essa observação. Vamos fazer isso para cada observação da amostra.

Dados de texto

  • Validação cruzada, cross-validation, é uma metodologia para medir o grau de acerto de previsões de modelos sem usar previsões dentro da amostra, que pode levar a overfit. Suponha que temos uma amostra com n observações. O procedimento de validação cruzada deixando um de fora repete os passos abaixo para cada observação \(i=1, \dots, n\):
    • Retire a i-ésima observação da amostra;
    • Estime o modelo com as n-1 observações restantes;
    • Use o modelo estimado para prever a variável de resultado da i-ésima observação. Calcule o erro de predição. Por fim, calcule a média dos erros e previsão entre as n observações.

Dados de texto

  • Faremos esse procedimento usando um loop do tipo for. Existem vários pacotes específicos para cross-validation com formas mais eficientes de fazer o trabalho.
  • Comecemos criando um data.frame apenas com os ensaios escritos Hamilton ou Madison:
ham_mad <- reg_data %>% filter(!is.na(author_outcome))

Dados de texto

n <- nrow(ham_mad)

hm.classify <- as.vector(rep(NA, n), mode = "list")

for (i in 1:n){
  sub.fit <- lm(author_outcome ~ upon + there + consequently + whilst, 
                data = ham_mad[-i, ])
  hm.classify[[i]] <- slice(ham_mad, i) %>% add_predictions(sub.fit)
}

Dados de texto

bind_rows(hm.classify) %>%
  mutate(pred_author = if_else(pred >= 0, "Hamilton", "Madison")) %>%
  group_by(author) %>%
  summarize(`Proportion Correct` = mean(author == pred_author))
## # A tibble: 2 × 2
##   author   `Proportion Correct`
##   <chr>                   <dbl>
## 1 Hamilton                1    
## 2 Madison                 0.786

Dados de texto

  • Com validação cruzada o modelo “acertou” todas as autorias de Hamilton e 78,6% das autorias de Madison.
  • Segue o jogo!

Dados de texto

  • Para terminar vamos usar um gráfico mostrando nossas previsões para os ensaios de autoria desconhecida.
  • Os ensaios de Hamilton serão marcados com quadrados vermelhos, os de Madison com círculos azuis e os desconhecidos com triangulos pretos.
  • Quando o valor previsto estiver acima de zero (triangulo acima da linha pontilhada em zero) a autoria é estimada sendo do Hamilton, quando estiver abaixo da linha o modelo estima que o autor é o Madison.

Dados de texto

plot_data <- author_data %>%
  filter(!author %in% c("Jay", "Joint"))

plot_data %>%
  ggplot(aes(new_id, pred, color = author, shape=author)) +
  geom_hline(yintercept = 0, linetype = "dashed") +
  geom_point(size=3) +
  scale_y_continuous(breaks = seq(10,80, by=10), minor_breaks = seq(5,80,by=5)) +
  scale_color_manual(values = c("Madison" = "blue",
                                "Hamilton" = "red",
                                "Disputed" = "black")) +
  scale_shape_manual(values = c("Madison" = 16, "Hamilton" = 15, "Disputed" = 17)) +
  labs(color = "Author", shape = "Author", x="Federalist Papers", y="Predicted values") +
  theme_classic()

Dados de texto

Dados de texto

  • De acordo com o modelo, 10 dos 11 ensaios com autoria desconhecida foram escritos por Madison. Qual foi escrito por Hamilton?
plot_data %>%
  filter(author == "Disputed", pred > 0) %>%
  select(author, new_id, pred, pred_author)
## # A tibble: 1 × 4
##   author   new_id   pred pred_author
##   <chr>     <int>  <dbl> <chr>      
## 1 Disputed     55 0.0932 Hamilton

Dados de rede

  • Dados de rede descrevem relações entre unidades em vez de unidades. Exemplos são redes de amigos (quem é amigo de quem), redes de citações de artigos científicos (quem cita quem) e redes de comércio entre países (quem faz comércio com quem).
  • Análise de dados de rede difere das análises que fizemos até agora porque a unidade de análise é a relação.

Dados de rede

  • Como exemplo para começarmos a discutir dados de rede vamos usar uma base de dados com redes de casamento em Florença na época do Renascimento (entre os séculos XIV e XVI).
  • A referência para este exemplo está em: John Padget e Chrstopher Ansell. Robust action and the rise of the Medici, 1400 - 1434. American Journal of Sociology, v.98, n.6, 1993.

Dados de rede

  • Os dados estão florentine na forma de matriz de adjacência, uma matriz onde cada elemento representa a existência de relação entre duas unidades (uma na linha e outra na coluna).
  • Especificamente, os dados são de 16 famílias da elite florentina, logo a matriz de adjacência tem 16 linhas e 16 colunas. Se o elemnto (i,j) for 1, as famílias na linha i e coluna j tem relação de casamento, se for 0 as famílias não tem relação de casamento.

Dados de rede

florentine <- read.csv("florentine.csv")
florentine[1:6, 1:6]
##      FAMILY ACCIAIUOL ALBIZZI BARBADORI BISCHERI CASTELLAN
## 1 ACCIAIUOL         0       0         0        0         0
## 2   ALBIZZI         0       0         0        0         0
## 3 BARBADORI         0       0         0        0         1
## 4  BISCHERI         0       0         0        0         0
## 5 CASTELLAN         0       0         1        0         0
## 6    GINORI         0       1         0        0         0

Dados de rede

  • Esta matriz de adjacência representa uma rede não direcionada, poderíamos colocar direção incorporando qual família propôs o casamento, mas não temos essa informação.
  • Mais na frente vamos analisar dados do Twitter, os dados serão de uma rede direcionada pois as relações entre um par de unidades possuem alguém que enviou e alguém que recebeu.
  • A matriz de adjacência de uma rede não direcionada é simétrica, o elemento (i,j) é igual ao elemento (j,i).

Dados de rede

  • Podemos calcular qual família tem o maior número de relações de casamento somando os elementos das linhas da matriz.
  • Primeiro usaremos a função rowwise() do pacote tidyverse, que diz ao R para somar por linhas e não por colunas como é o padrão. Depois, para indicar quais variáveis serão somadas, usaremos a função c_across().

Dados de rede

florentine %>%
  group_by(FAMILY) %>%
  rowwise() %>%
  summarize(connections = sum(c_across(ACCIAIUOL:TORNABUON))) %>%
  arrange(desc(connections))

Dados de rede

Dados de rede

  • A família Medici é a quem tem mais relações de casamento.
  • Por meio dessa rede de casamentos a família Medici se tornou a mais poderosa de Florença e chegou a tomar o poder da cidade.

Dados de rede

  • Uma rede traz informações sobre a relação entre unidades. Uma rede direcionada considera a direção da relação entre quem envia, senders, e quem recebe, receivers, enquanto uma rede não direcionada não tem informações sobre a direção. A Matriz de adjacência, na qual os elementos indicam a ausência ou existência de relações entre duas unidades é uma maneira de representar dados de rede.

Dados de rede

  • A maneira mais comum de visualizar dados de rede é por meio de um grafo., que também é um objeto matemático.
  • Um grafo, \(\mathcal{G}\), consiste em um conjunto de nódulos (ou vértices) \(V\) e um conjunto de arestas (ou geodésicas), edges, \(E\), ou seja, \(\mathcal{G}=(V,E)\).
  • Um nódulo representa uma unidade, no exemplo uma família, e tipicamente é representado por um círculo sólido. Uma aresta representa uma relação entre qualquer par de nódulos, no exemplo um casamento entre as famílias, por meio de uma linha conectando os círculos.

Dados de rede

  • O pacote igraph facilita a visualização de dados em rede na forma de grafo.
  • Para começar vamos transformar florentine de tibble para uma matriz. Para isso vamos usar a função column_to_rownames() para transformar o tibble em um data.frame com os nomes das linhas e colunas dados pelos nomes das família.
  • Depois aplicamos as.matrix() no data.frame.

Dados de rede

  • O grafo será feito com a função graph.adjacency() do pacote igraph. O argumento mode é definido como undirected e diag como FALSE para indicar a hipótese que não há casamento na mesma família.
  • Finalmente usaremos a função plot() para fazer o gráfico.

Dados de rede

library(igraph)

florence <- florentine %>%
  column_to_rownames(var = "FAMILY") %>%
  as.matrix()

florence <- graph.adjacency(florence, mode="undirected", diag = FALSE)

plot(florence)

Dados de rede

Dados de rede

  • Repare o papel central da família Medici com seis arestas ligando a outras famílias.
  • Mas o que é central? Vamos apresentar alguma medidas com base no grafo que permitem quantificar centralidade, no sentido de quanto nódulos estão ligados a outros nódulos.

Dados de rede

  • A medida de centralidade mais “bruta” é o número de arestas de cada unidade, essa medida é chamada de grau.
...

Dados de rede

  • A função degree() retorna o grau de cada nódulo.
degree(florence)
## ACCIAIUOL   ALBIZZI BARBADORI  BISCHERI CASTELLAN    GINORI  GUADAGNI LAMBERTES 
##         1         3         2         3         3         1         4         1 
##    MEDICI     PAZZI   PERUZZI     PUCCI   RIDOLFI  SALVIATI   STROZZI TORNABUON 
##         6         1         3         0         3         2         4         3

Dados de rede

  • O problema do grau é que contar o número de arestas saindo de um determinado nódulo diz pouco sobre a estrutura do grafo para além dos vizinhos imediatos.
  • Uma alternativa é considerar a soma das arestas que saem de um nódulo para todos os outros nódulos no grafo, mesmo aqueles que não são diretamente conectados. Essa medida, chamada de distância (farness), descreve o quão longe um vértice está de todos os outros vértices do gráfico.

Dados de rede

  • A proximidade (closeness) é claculada como o inverso da distância: \[\mbox{closeness}(v)=\frac{1}{\mbox{farness}(v)}=\frac{1}{\sum_{u\in V, u \neq v}\mbox{distância entre u e v}}\]
  • A distância entre dois vértices é o número de arestas do caminho mais curto.

Dados de rede

  • A figura abaixo ilustra a medida de proximidade:
...

Dados de rede

  • A função closeness() do pacote igraph calcula a proximidade de cada vértice.
closeness(florence)
##  ACCIAIUOL    ALBIZZI  BARBADORI   BISCHERI  CASTELLAN     GINORI   GUADAGNI 
## 0.02631579 0.03448276 0.03125000 0.02857143 0.02777778 0.02380952 0.03333333 
##  LAMBERTES     MEDICI      PAZZI    PERUZZI      PUCCI    RIDOLFI   SALVIATI 
## 0.02325581 0.04000000 0.02040816 0.02631579        NaN 0.03571429 0.02777778 
##    STROZZI  TORNABUON 
## 0.03125000 0.03448276

Dados de rede

  • Para facilitar a interpretação dessa medida, podemos calcular o número médio de arestas saindo de um dado vértice para os outros.
  • Para fazer isso basta dividir a distância pelo número de outros nódulos no grafo, no exemplo, que tem 16 nódulos, dividimos por 15 (16-1).

Dados de rede

1/(closeness(florence) * 15)
## ACCIAIUOL   ALBIZZI BARBADORI  BISCHERI CASTELLAN    GINORI  GUADAGNI LAMBERTES 
##  2.533333  1.933333  2.133333  2.333333  2.400000  2.800000  2.000000  2.866667 
##    MEDICI     PAZZI   PERUZZI     PUCCI   RIDOLFI  SALVIATI   STROZZI TORNABUON 
##  1.666667  3.266667  2.533333       NaN  1.866667  2.400000  2.133333  1.933333

Dados de rede

  • O resultado mostra que, em média, existem 1,7 arestas entre a família Medici e as outras familias. Esse valor é o menor entre todas as famílias da amostra.

Dados de rede

  • Uma terceira medida de centralidade é a de intermediação, betweenness. De acordo com essa medida um vértice é considerado central se é responsável por conectar outros vértices.
  • Se considerarmos que a comunicação entre um par de vértices ocorre pelo caminho mais curto entre os dois, então um vértice que esteja em vários caminhos mais curtos pode estar em um aposição de destaque na rede. \[\mbox{betweeness}(v)=\\\sum_{(t,u), t \neq v, u \neq v} \frac{\mbox{número de caminhos mais curtos contendo o vértice v}}{\mbox{número de caminhos mais curtos entre t e u}}\]

Dados de rede

  • A figura baixo ilustra o conceito de intermediação.
...

Dados de rede

  • A função betweenness() calcula a medida de intermediação.
betweenness(florence)
## ACCIAIUOL   ALBIZZI BARBADORI  BISCHERI CASTELLAN    GINORI  GUADAGNI LAMBERTES 
##  0.000000 19.333333  8.500000  9.500000  5.000000  0.000000 23.166667  0.000000 
##    MEDICI     PAZZI   PERUZZI     PUCCI   RIDOLFI  SALVIATI   STROZZI TORNABUON 
## 47.500000  0.000000  2.000000  0.000000 10.333333 13.000000  9.333333  8.333333

Dados de rede

  • A família Medice está em 47,5% do caminhos mais curtos existentes entre as outras famílias.
  • Repare que em todas as medidas de centralidade que usamos a família Medice tem uma posição de destaque nas redes de casamento da Florença antiga.

Dados de rede

  • Podemos usar o tamanho dos vértices para mostrar a medida de centralidade em um gráfico. Para isto usamos o argumento vertex.size da função plot() aplicada em um objeto do tipo igraph.

Dados de rede

  • Será preciso retirar a família Pucci, porque a medida de proximidade dela deu NaN (a família Pucci não tem conexões com as outras famílias).
florence2 <- delete.vertices(florence, "PUCCI")
plot(florence2, 
     vertex.size = closeness(florence2)*1000, main = "Closeness")

Dados de rede

Dados de rede

plot(florence, 
     vertex.size = betweenness(florence), main = "Betweenness")

Dados de rede

Dados de rede

  • A rede de casamentos em Florença foi um exemplo de rede não direcionada, como exemplo de rede direcionada, ou orientada, vamos analisar dados de senadores americanos do Twitter.
  • Usaremos dois data.frames, o primeiro com informações sobre quem segue quem no Twitter e o segundo com dados dos senadores americanos.

Dados de rede

  • Variáveis (Twiter)
    • following: Nome no Twitter do senador que está seguindo
    • followed: Nome no Twitter do senador que é seguido

Dados de rede

  • Variáveis(Senadores)
    • screen_name: nome no Twitter
    • name: Nome do senador
    • party: Partido do senador (D=Democrata, R=Republicano, I=Independente)
    • state: Estado do senador (sigla)

Dados de rede

twitter.following <- read.csv("twitter-following.csv")
twitter.senator <- read.csv("twitter-senator.csv")

follow <- twitter.following
senator <- twitter.senator

Dados de rede

head(follow)
##      following        followed
## 1 SenAlexander        RoyBlunt
## 2 SenAlexander     SenatorBurr
## 3 SenAlexander     JohnBoozman
## 4 SenAlexander SenJohnBarrasso
## 5 SenAlexander     SenBennetCO
## 6 SenAlexander     SenDanCoats

Dados de rede

head(senator)
##       screen_name            name party state
## 1    SenAlexander Lamar Alexander     R    TN
## 2        RoyBlunt       Roy Blunt     R    MO
## 3    SenatorBoxer   Barbara Boxer     D    CA
## 4 SenSherrodBrown   Sherrod Brown     D    OH
## 5     SenatorBurr    Richard Burr     R    NC
## 6  SenatorBaldwin   Tammy Baldwin     D    WI

Dados de rede

  • Os dados em follow são uma lista de arestas (edge-list), cada linha representa uma aresta entre dois vértices.
  • Para transformar a lista de arestas em um objeto de classe igraph vamos primeiro converter a lista em matriz, usando a função as.matrix(), e depois aplicar a função graph_from_edgelist() com argumento directed com TRUE.

Dados de rede

twitter_adj <- graph_from_edgelist(as.matrix(follow), 
                                   directed = TRUE)

Dados de rede

  • As medidas de centralidade que definimos anteriormente devem ser adaptadas para uma rede direcionada.
  • O grau de recepção, indegree, corresponde ao número de arestas que chegam no vértice, no exemplo o número de senadores que seguem o senador do vértice.
  • O grau de emissão, outdegree, corresponde ao número de arestas que saem o vértice, no exemplo o número se senadores que o senador do vértice segue.

Dados de rede

  • Medidas de grau em redes orientadas
...

Dados de rede

  • A função degree() pode ser usada em redes orientadas, para isso devemos definir o argumento mode como in, para grau de recepção, out para grau de emissão ou total para a soma dos graus de emissão e recepção.
  • Vamos adicionar ao data.frame senator os graus de emissão e recepção de cada senador. Como na matriz twitter_adj os senadores estão na mesma ordem que no data.frame senator podemos apenas criar as variáveis usando mutate().

Dados de rede

senator <- senator %>%
  mutate(indegree = degree(twitter_adj, mode = "in"),
         outdegree = degree(twitter_adj, mode = "out"))

Dados de rede

  • Agora podemos usar a função slice_max() para descobrir os senadores mais seguidos por outros senadores.
senator %>% slice_max(order_by = indegree, n=3) %>%
  arrange(desc(indegree)) %>%
  select(name, party, state, indegree, outdegree)
##                name party state indegree outdegree
## 1        Tom Cotton     R    AR       64        15
## 2 Richard J. Durbin     D    IL       60        87
## 3     John Barrasso     R    WY       58        79
## 4      Joe Donnelly     D    IN       58         9
## 5    Orrin G. Hatch     R    UT       58        50

Dados de rede

  • As outras duas medidas, proximidade e intermediação, também precisam ser adpatadas para redes orientadas.
  • Para isso considere que existem três maneiras de definir um caminho de vértice para outro. Podemos ignorar a direção, como nas redes não direcionadas, ou incorporar direção em uma das duas maneiras:
    • Viajar ao longo de uma caminho se saída seguindo a direção do caminho.
    • Viajar ao longo de um caminho de saída contra direção (andando pela contra-mão).

Dados de rede

  • A medida de proximidade para caminhos de saída correponde ao grau de emissão, outdegree, a medida de proximidade para caminhos de entrada corresponde ao grau de recepção, indegree.
...

Dados de rede

  • A função closeness() calcula a medida de proximidade, o argumento mode deve ser definido como in, out ou total, no último caso o cálculo ignora a orientação.

Dados de rede

  • Vamos comparar as duas medidas de proximidade orientadas por meio de um gráfico.
  • Inicialmente vamos criar as escalas de forma que possamos usar novamente em outros gráficos.

Dados de rede

scale_color_parties <- scale_color_manual("Party",
                                          values = c(R = "red",
                                                     D = "blue",
                                                     I = "green"),
                                          labels = c(R = "Republican",
                                                     D = "Democrat",
                                                     I = "Independent"))

scale_shape_parties <- scale_shape_manual("Party",
                                          values = c(R = 16,
                                                     D = 17,
                                                     I = 4),
                                          labels = c(R = "Republican",
                                                     D = "Democrat",
                                                     I = "Independent"))

Dados de rede

  • Agora façamos o gráfico com as duas medidas de proximidade
senator %>%
  mutate(closeness_in = closeness(twitter_adj, mode = "in"),
         closeness_out = closeness(twitter_adj, mode = "out")) %>%
  ggplot(aes(closeness_in, closeness_out, color = party, shape = party)) +
  geom_point(size=3) +
  scale_color_parties +
  scale_shape_parties +
  labs(title = "Closeness", x = "Incoming path", y = "Outgoing path") +
  theme_classic(base_size = 22) +
  theme(legend.position = "none")

Dados de rede

Dados de rede

  • Repare que as duas medidas estão pouco relacioanadas, aparentemente na rede de senadores americanos no Twitter não há relação entre quem segue muitos senadores e quem é seguido por muitos senadores.
data.frame(c_in = closeness(twitter_adj, mode = "in"), 
           c_out = closeness(twitter_adj, mode = "out")) %>%
  na.exclude() %>%
  cor()
##             c_in      c_out
## c_in   1.0000000 -0.1371397
## c_out -0.1371397  1.0000000

Dados de rede

  • Para medida de intermediação não existem apenas duas opções pois, para essa medida, não faz sentido diferenciar se o caminho está saindo ou chegando. Logo só importa se a rede é orientada ou não.
...

Dados de rede

  • Desta forma, a função betweenness() tem o argumento directed que deve ser definido como verdadeiro ou falso.
  • Usaremos essa função para fazer um gráfico comparando a medida de intermediação considerando e sem considerar a orientação da rede.

Dados de rede

senator %>%
  mutate(betweenness_dir = betweenness(twitter_adj, directed = TRUE),
         betweenness_undir = betweenness(twitter_adj, directed = FALSE)) %>%
  ggplot(aes(x=betweenness_dir, y=betweenness_undir, color=party, shape=party)) +
  geom_point(size=3) +
  scale_color_parties +
  scale_shape_parties +
  labs(title = "Betweenness", x="Directed", y="Undirecetd") +
  theme_classic(base_size=22) +
  theme(legend.position = "none")

Dados de rede

Dados de rede

  • As duas medidas de intermediação parecem estar relacionadas.
cor(betweenness(twitter_adj, directed = TRUE), 
    betweenness(twitter_adj, directed = FALSE))
## [1] 0.8203257

Dados de rede

  • Uma última medida de centralidade que abordaremos é conhecida como PageRank e foi desenvolvida por Sergey Grin e Larry Page, co-fundadores do Google. A medida tem como função otimizar o ranking de páginas que são exibidas em uma busca no Google. O valor dessa medida é calculado por um algoritmo interativo.
  • A lógica da PageRank é que vértices com um maior número de arestas chegando são mais importantes, no exemplo do Twitter quem tem mais seguidores seria mais importante.

Dados de rede

  • Outra característica do algoritmo é que um vértice que tem uma aresta chegando de outro vértice com muitas arestas chegando ganha importância (pense em arestas chegando como votos para o vértice), em termos de Twitter isso signfica que ser seguido por quem tem muitos seguidores é motivo para ganhar importância.
  • Por fim, a soma do valor da PageRank de todos os vértices de uma rede é um.

Dados de rede

  • O algoritmo começa dando um valor inicial de PageRank para todos os vértices. A cada interação, o valor do PageRank do vértice j é atualizado usando a equação: \[\mbox{PageRank}=\frac{1-d}{n} + d \times \sum_{i=1}^n \underbrace{\frac{A_{ij}\times \mbox{PageRank}_i}{\mbox{grau de emissão}_i}}_{\mbox{'voto' do vértice i para o vértice j}}\]
  • \(A_{ij}\) é o (i,j)-ésimo elemento da matrz de adjacência indicando se existe (ou não) uma aresta ligando o vértice \(i\) ao \(j\); \(d\) é uma constante que precisa ser especificada (normalmente é 0,85) e \(n\) é o número de vértices.

Dados de rede

  • A interpretação é que o PageRank para um dado vértice é igual a soma dos “votos” de outros vértices que possuem uma aresta chegando no vértice \(j\).
  • Se não existe aresta entre os vértices \(i\) e \(j\), então \(A_{ij}=0\) e não há “voto” de \(i\) em \(j\).
  • Se \(A_{ij}=1\) então o “voto” de \(i\) em \(j\) é igual ao valor do PageRank do voto \(i\) dividido pelo grau de emissão de \(i\). Desta forma, cada vértice divide seu PageRank com todos os vértices com quem tem aresta de emissão. Por exemplo, se um vértice tem PageRank de 0,1 e tem grau de emissão igual a dois, então cada vértice ligado por emissão recebe 0,05 “votos”.
  • O algoritmo para quando o PageRank não se altera em nenhum vértice.

Dados de rede

  • Existem várias medidas de centralidade para redes orientadas, entre elas graus de emissão e de recepção, proximidade com base em arestas cehgando, saindo ou amabas e intermediação com ou sem orientação. PageRank é um algoritmo interativo que produz uma medida de centralidade onde cada vértice distribui seus votos igualmente para outros vértices.

Dados de rede

  • A função page_rank() do pacote igraph calcula o PageRank. A função pode ser aplicada em redes não orientadas, para isso defina o argumento directed como falso.
  • A função page_rank() retorna um objeto do tipo lista, o valor do PageRank está em um elemento da lista chamdo vector.

Dados de rede

  • Para criar um gráfico ilustrando o PageRank vamos criar um novo objeto do tipo igraph chamado net. Da vez passada criamos o objeto do tipo igrpah a partir de uma lista de arestas, desta vez vamos usar a função graph_from_data_frame() para criar direto do data.frame. Para isso vamos ter de especificar a lista de arestas, d, e os vértices, vertice.
  • Antes, porém, vamos adicionar o PageRank ao data.frame senator

Dados de rede

senator <- senator %>%
  mutate(page_rank = page_rank(twitter_adj)[["vector"]])

net <- graph_from_data_frame(d = follow,
                             vertices = senator, directed = TRUE)

Dados de rede

  • Vamos daruma olhada no objeto que criamos.
net
## IGRAPH 0e49632 DN-- 91 3859 -- 
## + attr: name (v/c), party (v/c), state (v/c), indegree (v/n), outdegree
## | (v/n), page_rank (v/n)
## + edges from 0e49632 (vertex names):
##  [1] Lamar Alexander->Roy Blunt         Lamar Alexander->Richard Burr     
##  [3] Lamar Alexander->John Boozman      Lamar Alexander->John Barrasso    
##  [5] Lamar Alexander->Michael F. Bennet Lamar Alexander->Daniel Coats     
##  [7] Lamar Alexander->Susan M. Collins  Lamar Alexander->John Cornyn      
##  [9] Lamar Alexander->Bob Corker        Lamar Alexander->Michael B. Enzi  
## [11] Lamar Alexander->Joni Ernst        Lamar Alexander->Chuck Grassley   
## [13] Lamar Alexander->Cory Gardner      Lamar Alexander->Orrin G. Hatch   
## + ... omitted several edges

Dados de rede

  • A função E() retorna as arestas, edges
head(E(net))
## + 6/3859 edges from 0e49632 (vertex names):
## [1] Lamar Alexander->Roy Blunt         Lamar Alexander->Richard Burr     
## [3] Lamar Alexander->John Boozman      Lamar Alexander->John Barrasso    
## [5] Lamar Alexander->Michael F. Bennet Lamar Alexander->Daniel Coats

Dados de rede

  • A função V() retorna os vértices
head(V(net))
## + 6/91 vertices, named, from 0e49632:
## [1] Lamar Alexander Roy Blunt       Barbara Boxer   Sherrod Brown  
## [5] Richard Burr    Tammy Baldwin
head(V(net)$party)
## [1] "R" "R" "D" "D" "R" "D"

Dados de rede

  • Em uma análise mais refinada é possível dar pesos as arestas, em vez de existir ou não (zero/um), podemos dar um peso as arestas. Relações mais intensas possuem um peso maior do que relação menos intensas.
  • Para isso o valor dos pesos deve ser atribuido ao elemento weight das arestas.
E(net)$weight <- vetor com os pesos

Dados de rede

  • Assim como em outros exemplos de redes, o gráfico será feito com a função plot() do R básico. Essa função também permite definir atributos como, por exemplo, cores.
col <- senator %>%
  mutate(col = case_when(party == "R" ~ "red",
                         party == "D" ~ "blue",
                         TRUE ~ "black")) %>%
  select(col) %>% pull()

plot(net, vertex.size = V(net)$page_rank * 1000,
     vertex.label = NA, vertex.color = col,
     edge.arrow.size = 0.1, edge.width = 0.5)

Dados de rede

Dados de rede

  • O gráfico mostra que as redes de Twitter dos senadores americanos é densa, mas também é polarizada. A separação entre Republicanos (vermelho) e Democratas (azul) é bem clara.
  • O valor do PageRank dos Republicanos parece um pouco maior do que o dos democratas, mas muito pouco para caracterizar um padrão relevante.

Dados de rede

senator %>%
  group_by(party) %>%
  summarize(PageRank = mean(page_rank))
## # A tibble: 3 × 2
##   party PageRank
##   <chr>    <dbl>
## 1 D       0.0109
## 2 I       0.0119
## 3 R       0.0110

Dados de rede

  • Para entender melhor o algoritmo e apresentar o loop do tipo while vamos criar uma função para calcular o PageRank e implementar um algoritmo de cálculo do PageRank.
  • Os argumentos da função são: o número de vértices (n), matriz \(n \times n\) de adjacência (A), uma constante d e o vetor com os PageRanks iniciais, pr.

Dados de rede

PageRank <- function(n,A,d,pr) {
  deg <- degree(A, mode = "out")
  for (j in 1:n) {
    pr[j] <- (1-d)/n + d * sum(A[,j]*pr/deg)
  }
  return(pr)
}

Dados de rede

  • Em um loop do tipo while os comandos são executados até que a condição especificada seja atendida, esse loop tem a forma:
while(condição) {
  comandos
}

Dados de rede

  • No nosso algoritmo, a cada interação vamos calcular o PageRank de cada vértice, depois calculamos a diferença entre o Pagerank atual e o da interação anterior. A condição de parada é que a maior diferença em valor absoluto seja menor do que um valor arbitrário que será estabelecido.
  • Nos próximos slides vamos criar a rede e aplicar o algoritmo.

Dados de rede

nodes <- 4

adj <- matrix(c(0,1,0,1,1,0,1,0,0,1,0,0,0,1,0,0), ncol=nodes, nrow=nodes, byrow = TRUE)
adj
##      [,1] [,2] [,3] [,4]
## [1,]    0    1    0    1
## [2,]    1    0    1    0
## [3,]    0    1    0    0
## [4,]    0    1    0    0
adj <- graph.adjacency(adj)

Dados de rede

d <- 0.85
pr <- rep(1/nodes, nodes)
diff <- 100 #valor bem grande para iniciar

while (diff > 0.001) {
  pr.pre <- pr
  pr <- PageRank(n=nodes, A=adj, d=d, pr=pr)
  diff <- max(abs(pr - pr.pre))
}

pr
## [1] 0.2213090 0.4316623 0.2209565 0.1315563

Dados de rede

  • Os resultados mostram que a segunda observação tem o maior valor de PageRank.
  • Faz sentido, pois a segundo observação tem o maior número de arestas chegando (representadas nas colunas)

Dados espaciais

  • Um último tipo de dado que será discutido nesta Unidade são dados relacionados a alguma dimensão espacial.
  • Dados espaciais são melhores visualizados em mapas. Trabalharemos com dois tipos de dados espaciais: dados espaciais de pontos, que podem ser colocados como pontos em um mapa; e dados espaciais em polígonos, que representam uma sequência de pontos conectados em um mapa correspondendo a áreas como condados, distritos, ou províncias.
  • Também falaremos de dados espaciais-temporais, que são dados espaciais, de pontos ou de polígonos, indexados no tempo.

Dados espaciais

  • O pacote maps do R fornece várias ferramentas para trabalhar com mapas e também bases dados espaciais para várias cidades do mundo.
  • Por exemplo, o pacote tem um data.frame com as cidades americanas chamado us.cities. Vamos dar uma olhada.

Dados espaciais

  • Variáveis
    • name: nome da cidade
    • country.etc: estado
    • pop: população
    • lat: latitude
    • long: longitude
    • capital: 1 se capital do país, 2 se capital do estado, 0 se não for capital

Dados espaciais

data("us.cities", package = "maps")
head(us.cities)
##         name country.etc    pop   lat    long capital
## 1 Abilene TX          TX 113888 32.45  -99.74       0
## 2   Akron OH          OH 206634 41.08  -81.52       0
## 3 Alameda CA          CA  70069 37.77 -122.26       0
## 4  Albany GA          GA  75510 31.58  -84.18       0
## 5  Albany NY          NY  93576 42.67  -73.80       2
## 6  Albany OR          OR  45535 44.62 -123.09       0

Dados espaciais

  • Nosso primeiro exercício será colocar as capitais dos estados americanos no mapa dos EUA.
  • Começamos filtrando as capitais e excluindo Alasca e Havaí. Depois usaremos a função map_data() do pacote ggplot2 para transformar os dados do pacote maps em uma estrutura adequada para o ggplo2. A função geom_map() faz o mapa com os dados gerados pela função map_data() e a função borders() coloca as fronteiras.
  • A função coord_quickmap() faz a projeção do mapa.

Dados espaciais

capitals <- filter(us.cities, capital == 2, !country.etc %in% c("HI", "AK"))

usa_map <- map_data("usa")

ggplot() +
  geom_map(map = usa_map) +
  borders(database = "usa") +
  geom_point(aes(x=long, y=lat, size=pop), data = capitals) +
  scale_size_area(guide = "none") +
  coord_quickmap() +
  theme_void(base_size = 12) +
  labs(x=NULL, y=NULL)

Dados espaciais

Dados espaciais

  • Como segundo exemplo vamos fazer o mapa da Califórnia destacando as sete cidades mais populosas.
cal_cities <- filter(us.cities, country.etc == "CA") %>% slice_max(pop, n=7)

ggplot() +
  borders(database = "state", regions = "California") +
  geom_point(aes(long, lat), data = cal_cities, size=3) +
  geom_text(aes(long,lat, label = name), data=cal_cities, hjust=-0.05) +
  coord_quickmap() +
  theme_void() +
  labs(x=NULL, y=NULL)

Dados espaciais

Dados espaciais

  • É útil entender um pouco melhor como funcionam os dados polígonais no R. Para isso vamos dar uma olhada no data.frame usa_map que criamos para o primeiro exemplo.

Dados espaciais

head(usa_map)
##        long      lat group order region subregion
## 1 -101.4078 29.74224     1     1   main      <NA>
## 2 -101.3906 29.74224     1     2   main      <NA>
## 3 -101.3620 29.65056     1     3   main      <NA>
## 4 -101.3505 29.63911     1     4   main      <NA>
## 5 -101.3219 29.63338     1     5   main      <NA>
## 6 -101.3047 29.64484     1     6   main      <NA>
dim(usa_map)
## [1] 7243    6

Dados espaciais

  • O mapa dos EUA é formado por 7.243 pares de coordenadas (latitude e longitude).
  • A função borders() conecta esses pontos para cosntruir o mapa.

Dados espaciais

  • Dados espaciais contém informações sobre padrões no espaço e podem ser visualizados por meio de mapas. Os dados espaciais em pontos representam a localização do evento como ponto em um mapa, dados espaciais em polígonos representam áreas geográficas conectando pontos em um mapa.

Dados espaciais

  • Cores são importantes para mostrar dados em geral e mapas não são exceções. Até agora usamos cores por nomes, como red ou blue. O Rconhece o nome de 657 cores.
  • A função colors() mostra os nomes dessas cores.

Dados espaciais

allcolors <- colors()
head(allcolors)
## [1] "white"         "aliceblue"     "antiquewhite"  "antiquewhite1"
## [5] "antiquewhite2" "antiquewhite3"
length(allcolors)
## [1] 657

Dados espaciais

  • Para além dessas cores que o R reconhece pelo nome, existem (muitas) outras cores que podem ser usadas no R.
  • Para usar todas as cores possíveis podemos usar o código hexadecimal da cor. Os números hexadecmiais tem base 16 com os inteiros de 0 a 9 e as letras de A a F representando valores de 0 a 15. O código hexadecimal de uma cor é uma sequência de 6 caracteres começando com uma hashtag, (#).

Dados espaciais

  • Cada conjunto de dois caracteres representa a intensidade das três cores primárias (vermelho, verde e azul, ou RGB de red, green e blue) tomando valores de 0 a 255 (\(2^8\) níveis).
  • Por exemplo, intensidade média de vermelho e azul juntas são roxo. No sistema RGB roxo é representado como (127,0,127), vermelho e azul com meia intensidade e verde com zero. No sistema hexadecimal, 127 é escrito como 7F, logo, o código hexadecimal para o roxo é #7F007F.

Dados espaciais

  • A função rgb() ajuda a criar os códigos hexadecimais para as cores a partir de valores numéricos. A função tem três argumentos, red, green e blue que podem ter valores de zero a um.
  • Os valores entre zero e um são transformados em inteiros entre 0 e 255 que são transformados em hexadecimais. Exemplos vão ajudar a entender como usar a função rgb().

Dados espaciais

  • Código hexadecimal para vermelho, verde e azul:
red <- rgb(red=1, green=0, blue=0)
green <- rgb(red=0, green=1, blue=0)
blue <- rgb(red=0, green=0, blue=1)

c(red, green, blue)
## [1] "#FF0000" "#00FF00" "#0000FF"

Dados espaciais

  • Código hexadecimal para preto e branco:
black <- rgb(red=0, green=0, blue=0)
white <- rgb(red=1, green=1, blue=1)

c(black, white)
## [1] "#000000" "#FFFFFF"

Dados espaciais

  • É possível passar um vetor como argumento para criar códigos para mais de uma cor. No exemplo vamos criar roxo, 50% vermelho e 50% azul, e amarelo, 100% vermelho e 100% green.
rgb(red = c(0.5,1), green=c(0,1), blue=c(0.5,0))
## [1] "#800080" "#FFFF00"

Dados espaciais

  • O sistema hexadecimal para representar cores permite colocar graus de transparência adicionando mais dois dígitos ao código.
  • Na função rgb() isso é feito definindo valores entre zero e um para o argumento alpha.

Dados espaciais

  • Código hexadecimal para azul e preto semi-transparentes:
blue.trans <- rgb(red=0, green=0, blue=1, alpha=0.5)
black.trans <- rgb(red=0, green=0, blue=0, alpha=0.5)

c(blue.trans, black.trans)
## [1] "#0000FF80" "#00000080"

Dados espaciais

  • Uma vez conhecido o código hexadecimal da cor, podemos usar esse código para usar a cor em um gráfico.
  • Façamos um gráfico com vários graus de transparência para o preto e o azul. Primeiro vamos construir um data.frame com as cores (azul e preto) e uma escala para alpha, então vamos usar as funções scale_color_identity() e scale_alpha_identity() para criar as escalar de cor e transparência.

Dados espaciais

sample_data <- tibble(x=rep(1:4, each=2), y=x+rep(c(0,0.2), times=2),
                      color = rep(c("#000000", "#0000FF"), each=4),
                      alpha = c(1,1,0.5,0.5,1,1,0.5,0.5))

sample_data
## # A tibble: 8 × 4
##       x     y color   alpha
##   <int> <dbl> <chr>   <dbl>
## 1     1   1   #000000   1  
## 2     1   1.2 #000000   1  
## 3     2   2   #000000   0.5
## 4     2   2.2 #000000   0.5
## 5     3   3   #0000FF   1  
## 6     3   3.2 #0000FF   1  
## 7     4   4   #0000FF   0.5
## 8     4   4.2 #0000FF   0.5

Dados espaciais

sample_data %>%
  ggplot(aes(x,y,color=color, alpha=alpha)) +
  geom_point(size=15) +
  scale_color_identity() +
  scale_alpha_identity() +
  labs(x=NULL, y=NULL) +
  theme_classic()

Dados espaciais

Dados espaciais

  • Existem vários aspectos que devem ser considerados quando combinamos cores. Algumas combinações não permitem que pessoas com algum grau de daltonismo identifiquem as cores, outras cores ficam idênticas quando o documento é impresso em preto e branco, por fim, algumas combinações de cores ficam muito feias.
  • Desta forma, se você não sabe o que está fazendo ao criar padrões de cores o melhor a fazer é usar paletas pré-definidas. O pacote RColorBrewer contém várias paletas de cores que podem ser usadas em figuras no R.

Dados espaciais

  • Conhecendo os fundamentos de como funcionam as cores no R podemos colorir nossos mapas. Como exemplo, vamos colocar cores no mapa dos EUA para representar os resultados das eleições de 2008, os dados estão no arquivo pres08 que usamos na Unidade 4.
  • O mapa será colorido de duas formas. Na primeira os estados onde Obama ganhou serão pintados de azul e os estados onde McCain ganhou serão pintados de vermelho. Na segunda vamos misturar azul com vermelho de acordo com a prorporção de votos dos partidos em cada estado.
  • Para começar, vamos fazer o mapa da California com a cor do vitorioso (azul) e com a mistura de cores.

Dados espaciais

pres08 <- read.csv("pres08.csv")

pres08 <- pres08 %>%
  mutate(Dem = Obama/(Obama+McCain),
         Rep = McCain/(Obama+McCain))

cal_color <- pres08 %>%
  filter(state == "CA") %>%
  mutate(purple_shade = rgb(red=Rep, green=0, blue=Dem)) %>%
  select(purple_shade) %>% pull()

Dados espaciais

ggplot() +
  borders(database="state", regions = "California", fill="blue") +
  coord_quickmap() +
  theme_void()

Dados espaciais

Dados espaciais

ggplot() +
  borders(database="state", regions = "California", fill=cal_color) +
  coord_quickmap() +
  theme_void()

Dados espaciais

Dados espaciais

  • Agora vamos para o mapa com todos os estados (menos Havaí, Alasca e D.C.). Primeiro preparamos os dados:
pres08 <- pres08 %>% mutate(state = str_to_lower(state.name)) %>%
  filter(!state %in% c("hawaii", "d.c.", "alaska"))

states <- map_data("state") %>%
  filter(!region %in% c("hawaii", "district of columbia", "alaska")) %>%
  full_join(pres08, by = c("region" = "state")) %>%
  mutate(party = if_else(Dem > Rep, "Dem", "Rep"),
         purple_shade = rgb(red=Rep, green=0, blue=Dem))

Dados espaciais

glimpse(states)
## Rows: 15,527
## Columns: 14
## $ long         <dbl> -87.46201, -87.48493, -87.52503, -87.53076, -87.57087, -8…
## $ lat          <dbl> 30.38968, 30.37249, 30.37249, 30.33239, 30.32665, 30.3266…
## $ group        <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ order        <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17…
## $ region       <chr> "alabama", "alabama", "alabama", "alabama", "alabama", "a…
## $ subregion    <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ state.name   <chr> "Alabama", "Alabama", "Alabama", "Alabama", "Alabama", "A…
## $ Obama        <int> 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 3…
## $ McCain       <int> 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 6…
## $ EV           <int> 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, …
## $ Dem          <dbl> 0.3939394, 0.3939394, 0.3939394, 0.3939394, 0.3939394, 0.…
## $ Rep          <dbl> 0.6060606, 0.6060606, 0.6060606, 0.6060606, 0.6060606, 0.…
## $ party        <chr> "Rep", "Rep", "Rep", "Rep", "Rep", "Rep", "Rep", "Rep", "…
## $ purple_shade <chr> "#9B0064", "#9B0064", "#9B0064", "#9B0064", "#9B0064", "#…

Dados espaciais

  • Gráfico com vermelho ou azul.
states %>%
  ggplot() +
  geom_polygon(aes(group=group, x=long, y=lat, fill=party)) +
  borders("state") +
  coord_quickmap() +
  scale_fill_manual(values = c("Rep" = "red", Dem="blue"), guide="none") +
  theme_void() +
  labs(x=NULL, y=NULL)

Dados espaciais

Dados espaciais

  • Gráfico com tonalidades entre vermelho e azul.
states %>%
  ggplot() +
  geom_polygon(aes(group=group, x=long, y=lat, fill=purple_shade)) +
  borders("state") +
  coord_quickmap() +
  scale_fill_identity() +
  theme_void() +
  labs(x=NULL, y=NULL)

Dados espaciais

Dados espaciais

  • O último tópico desta unidade consiste em analisar dados no espaço e no tempo. A expansão do Walmart será o exemplo que vai guiar a apresentação desse tópico.
  • O exemplo segue o artigo: Thomas Holmes. The difusion of Wal-Mart and economics of density. Econometrica, v.79, n.1, 2011,
  • Os dados estão no data.frame walmart.

Dados espaciais

  • Variáveis:
    • opendate: data em que a loja foi inaugurada
    • st.address: endereço da loja
    • city: cidade da loja
    • state: estado da loja
    • type: tipo de loja (Wal-Mart Store, SuperCenter, #DistributionCenter)
    • long: longitude da loja
    • lat: latitude da loja

Dados espaciais

walmart <- read.csv("walmart.csv")
str(walmart)
## 'data.frame':    3251 obs. of  7 variables:
##  $ opendate  : chr  "1962-03-01" "1962-07-01" "1964-08-01" "1965-08-01" ...
##  $ st.address: chr  "5801 SW Regional Airport Blvd" "2110 WEST WALNUT" "1417 HWY 62/65 N" "2901 HWY 412 EAST" ...
##  $ city      : chr  "Bentonville" "Rogers" "Harrison" "Siloam Springs" ...
##  $ state     : chr  "AR" "AR" "AR" "AR" ...
##  $ long      : num  -94.2 -94.1 -93.1 -94.5 -92.3 ...
##  $ lat       : num  36.4 36.3 36.2 36.2 34.8 ...
##  $ type      : chr  "DistributionCenter" "SuperCenter" "SuperCenter" "SuperCenter" ...

Dados espaciais

walmart <- walmart %>%
  mutate(opendate = as.Date(opendate))

str(walmart)
## 'data.frame':    3251 obs. of  7 variables:
##  $ opendate  : Date, format: "1962-03-01" "1962-07-01" ...
##  $ st.address: chr  "5801 SW Regional Airport Blvd" "2110 WEST WALNUT" "1417 HWY 62/65 N" "2901 HWY 412 EAST" ...
##  $ city      : chr  "Bentonville" "Rogers" "Harrison" "Siloam Springs" ...
##  $ state     : chr  "AR" "AR" "AR" "AR" ...
##  $ long      : num  -94.2 -94.1 -93.1 -94.5 -92.3 ...
##  $ lat       : num  36.4 36.3 36.2 36.2 34.8 ...
##  $ type      : chr  "DistributionCenter" "SuperCenter" "SuperCenter" "SuperCenter" ...

Dados espaciais

head(walmart)
##     opendate                    st.address              city state      long
## 1 1962-03-01 5801 SW Regional Airport Blvd       Bentonville    AR -94.23982
## 2 1962-07-01              2110 WEST WALNUT            Rogers    AR -94.07141
## 3 1964-08-01              1417 HWY 62/65 N          Harrison    AR -93.09345
## 4 1965-08-01             2901 HWY 412 EAST    Siloam Springs    AR -94.50208
## 5 1967-10-01        3801 CAMP ROBINSON RD. North Little Rock    AR -92.30229
## 6 1967-10-01         1621 NORTH BUSINESS 9         Morrilton    AR -92.75858
##        lat               type
## 1 36.35088 DistributionCenter
## 2 36.34224        SuperCenter
## 3 36.23698        SuperCenter
## 4 36.17990        SuperCenter
## 5 34.81327      Wal-MartStore
## 6 35.15649        SuperCenter

Dados espaciais

  • Vamos começar criando um mapa com a localização de todas as lojas. Cada tipo de loja será associada a uma cor.
  • Tipos de loja:
    • Wal-MartStore: loja padrão da Walmart.
    • SuperCenter: loja padrão com supermercado completo e outros tipos de lojas (farmácias, serviços para carros, etc).
    • DistributionCenter: centros de distribuição de comida e outros bens para os outros tipos de loja.
  • Os centros de distribuição serão marcados com pontos maiores que os outros tipos de loja.
  • Usaremos a função recode() do pacote tidyverse para ‘arrumar’ os nomes dos tipo de loja.

Dados espaciais

walmart <- walmart %>%
  mutate(size = if_else(type == "DistributionCenter", 2, 1),
         type = recode(type,
                       "DistributionCenter" = "Distribution \ncenter",
                       "SuperCenter" = "Supercenter",
                       "Wal-MartStore" = "Walmart"))

Dados espaciais

ggplot() +
  borders(database = "state") +
            geom_point(aes(long,lat, color=type, size=size),
                       data = walmart, alpha = 1/3) +
  coord_quickmap() +
  scale_size_identity() +
  theme_void(base_size=12) +
  labs(color="Type")

Dados espaciais

Dados espaciais

  • Note que no Nordeste e na Costa Oeste prevalecem as lojas do tipo padrão, enquanto no Sul e no Meio Oeste aparecem mais supercentros.

Dados espaciais

  • Para estudar a expansão da Walmart no tempo devemos fazer um gráfico como o anterior para diversos períodos.
  • No R é comum usar funções para realizar tarefas repetidas. Com isso em mente vamos criar uma função para fazer o mapa das lojas da Walmart. A função terá dois argumentos: o primeiro um data.frame que deve ter uma coluna chamada opendate com as datas da abertura das lojas, os dados dessa coluna devem ser da classe Date; o segundo também será um objeto de classe Date contendo o data a ser usada para fazer o gráfico.

Dados espaciais

walmart.map <- function(data, date){
  temp <- filter(data, opendate <= date) %>%
    mutate(size = if_else(type == "DistributionCenter", 2, 1))
  ggplot() +
    borders(database = "state") +
    geom_point(aes(long,lat, color=type, size=size),
                       data = temp, alpha = 1/3) +
    coord_quickmap() +
    scale_size_identity() +
    theme_void(base_size=12) +
    labs(color="Type") +
    ggtitle(date)
}

Dados espaciais

  • Agora podemos criar gráficos para diferentes períodos apenas usando a função walmart.map().
library(gridExtra)

grid.arrange(
walmart.map(walmart, as.Date("1974-12-31")),
walmart.map(walmart, as.Date("1984-12-31")),
walmart.map(walmart, as.Date("1994-12-31")),
walmart.map(walmart, as.Date("2004-12-31")),
ncol = 2, nrow=2)

Dados espaciais

Dados espaciais

  • Um método para ver melhor o padrão de crescimento da Walmart nos EUA é fazer uma animação. O pacote gganimate permite criar animações usando a estrutura do ggplot2.
  • Uma animação é essencialmente uma série de imagens estáticas mostradas em sequência. Usaremos a função transition_states(). A ideia é criar um gráfico para cada ano e mostrar todos os gráficos em sequência.

Dados espaciais

  • Para criar uma variável com os anos usaremos a função floor_date() do pacote lubridade. Essa função arredonda uma data para a unidade mais próxima, no caso ano.
  • Na função transition_states() o argumento state será definido como o ano que criamos com a função floor_date().

Dados espaciais

  • O argumento state_length diz por quanto tempo será exibida cada imagem e o argumento transition_length define o tempo da transição. Finalmente, a função shadow_mark() deixa visível os pontos dos anos anteriores.
  • A função anim_save() salva a animação como GIF (ou outros formatos).

Dados espaciais

library(gganimate)
library(lubridate)

walmart <- walmart %>%
  mutate(year = floor_date(opendate, unit = "year"))

Dados espaciais

walmart_animated <- ggplot() +
  borders(database = "state") +
  geom_point(aes(long,lat, color=type),
                       data = walmart) +
  coord_quickmap() +
  theme_void() +
  transition_states(states = year,
                    transition_length = 0,
                    state_length = 1) +
  shadow_mark()

anim_save("walmart.gif")

walmart_animated

Dados espaciais