Data Frames


***

Parte do livro Introdução à análise de dados com R.  Este trabalho está em andamento, o texto é bastante preliminar e sofrerá muitas alterações. 

Quer fazer sugestões? Deixe um comentário abaixo ou, se você sabe utilizar o github, acesse aqui.

Não copie ou reproduza este material sem autorização.

Volte para ver atualizações!

***

Data Frames: seu banco de dados no R

Por que um data.frame?

Até agora temos utilizado apenas dados de uma mesma classe, armazenados ou em um vetor ou em uma matriz. Mas uma base de dados, em geral, é feita de dados de diversas classes diferentes: no exemplo anterior, por exemplo, podemos querer ter uma coluna com os nomes dos funcionários, outra com o sexo dos funcionários, outra com valores… note que essas colunas são de classes diferentes, como textos e números. Como guardar essas informações?

A solução para isso é o data.frame. O data.frame é talvez o formato de dados mais importante do R. No data.frame cada coluna representa uma variável e cada linha uma observação. Essa é a estrutura ideal para quando você tem várias variáveis de classes diferentes em um banco de dados.

Criando um data.frame: data.frame() e as.data.frame()

É possível criar um data.frame diretamente com a função data.frame():

funcionarios <- data.frame(nome = c("João", "Maria", "José"),
                           sexo = c("M", "F", "M"),
                           salario = c(1000, 1200, 1300),
                           stringsAsFactors = FALSE)
funcionarios
##    nome sexo salario
## 1  João    M    1000
## 2 Maria    F    1200
## 3  José    M    1300

Também é coverter outros objetos em um data.frame com a função as.data.frame().

Discutiremos a opção stringsAsFactors = FALSE mais a frente.

Vejamos a estrutura do data.frame. Note que cada coluna tem sua própria classe.

str(funcionarios)
## 'data.frame':    3 obs. of  3 variables:
##  $ nome   : chr  "João" "Maria" "José"
##  $ sexo   : chr  "M" "F" "M"
##  $ salario: num  1000 1200 1300

Nomes de linhas e colunas

O data.frame sempre terá rownames e colnames.

rownames(funcionarios)
## [1] "1" "2" "3"

colnames(funcionarios)
## [1] "nome"    "sexo"    "salario"

Detalhe: a função names() no data.fram trata de suas colunas, pois os elementos fundamentais do data.frame são seus vetores coluna.

names(funcionarios)
## [1] "nome"    "sexo"    "salario"

Não parece tão diferente de uma matriz…

O que ocorreria com o data.frame funcionarios se o transformássemos em uma matriz? Vejamos:

as.matrix(funcionarios)
##      nome    sexo salario
## [1,] "João"  "M"  "1000"
## [2,] "Maria" "F"  "1200"
## [3,] "José"  "M"  "1300"

Perceba que todas as variáveis viraram character! Uma matriz aceita apenas elementos da mesma classe, e é exatamente por isso precisamos de um data.frame neste caso.

Manipulando data.frames como matrizes

Ok, temos mais um objeto do R, o data.frame … vou ter que reaprender tudo novamente? Não! Você pode manipular data.frames como se fossem matrizes!

Praticamente tudo o que vimos para selecionar e modificar elementos em matrizes funciona no data.frame. Podemos selecionar linhas e colunas do nosso data.frame como se fosse uma matriz:

## tudo menos linha 1
funcionarios[-1, ]
##    nome sexo salario
## 2 Maria    F    1200
## 3  José    M    1300

## seleciona primeira linha e primeira coluna (vetor)
funcionarios[1, 1]
## [1] "João"

## seleciona primeira linha e primeira coluna (data.frame)
funcionarios[1, 1, drop = FALSE]
##   nome
## 1 João

## seleciona linha 3, colunas "nome" e "salario"
funcionarios[3 , c("nome", "salario")]
##   nome salario
## 3 José    1300

E também alterar seus valores tal como uma matriz.

## aumento de salario para o João
funcionarios[1, "salario"] <- 1100

funcionarios
##    nome sexo salario
## 1  João    M    1100
## 2 Maria    F    1200
## 3  José    M    1300

Extra do data.frame: selecionando e modificando com $ e [[ ]]

Outras formas alternativas de selecionar colunas em um data.frame são o $ e o [[ ]]:

## Seleciona coluna nome
funcionarios$nome
## [1] "João"  "Maria" "José"

funcionarios[["nome"]]
## [1] "João"  "Maria" "José"

## Seleciona coluna salario
funcionarios$salario
## [1] 1100 1200 1300

funcionarios[["salario"]]
## [1] 1100 1200 1300

Tanto o $ quanto o [[ ]] sempre retornam um vetor como resultado.

Também é possível alterar a coluna combinando $ ou [[ ]] com <-:

## outro aumento para o João
funcionarios$salario[1] <- 1150

## equivalente
funcionarios[["salario"]][1] <- 1150
funcionarios
##    nome sexo salario
## 1  João    M    1150
## 2 Maria    F    1200
## 3  José    M    1300

Extra do data.frame: retornando sempre um data.frame com [ ]

Se você quiser garantir que o resultado da seleção será sempre um data.frame use drop = FALSE ou selecione sem a vírgula:

## Retorna data.frame
funcionarios[ ,"salario", drop = FALSE]
##   salario
## 1    1150
## 2    1200
## 3    1300

## Retorna data.frame
funcionarios["salario"]
##   salario
## 1    1150
## 2    1200
## 3    1300

Tabela resumo: selecionando uma coluna em um data.frame

Resumindo as formas de seleção de uma coluna de um data.frame.

screen-shot-2017-02-07-at-12-02-02-am

Criando colunas novas

Há diversas formas de criar uma coluna nova em um data.frame. O principal segredo é o seguinte: faça de conta que a coluna já exista, selecione ela com $, [,] ou [[]] e atribua o valor que deseja.

Para ilustrar, vamos adicionar ao nosso data.frame funcionarios mais três colunas.

Com $:

funcionarios$escolaridade <- c("Ensino Médio", "Graduação", "Mestrado")

Com [ , ]:

funcionarios[, "experiencia"] <- c(10, 12, 15)

Com [[ ]]:

funcionarios[["avaliacao_anual"]] <- c(7, 9, 10)

Uma última forma de adicionar coluna a um data.frame é, tal como uma matriz, utilizar a função cbind() (column bind).

funcionarios <- cbind(funcionarios,
                      prim_emprego = c("sim", "nao", "nao"),
                      stringsAsFactors = FALSE)

Vejamos como ficou nosso data.frame com as novas colunas:

funcionarios
##    nome sexo salario escolaridade experiencia avaliacao_anual prim_emprego
## 1  João    M    1150 Ensino Médio          10               7          sim
## 2 Maria    F    1200    Graduação          12               9          nao
## 3  José    M    1300     Mestrado          15              10          nao

E agora, temos colunas demais, como remover algumas delas?

Removendo colunas

A forma mais fácil de remover coluna de um data.fram é atribuir o valor NULL a ela:

## deleta coluna prim_emprego
funcionarios$prim_emprego <- NULL

Mas a forma mais segura e universal de remover qualquer elemento de um objeto do R é selecionar tudo exceto aquilo que você não deseja. Isto é, selecione todas colunas menos as que você não quer e atribua o resultado de volta ao seu data.frame:

## deleta colunas 4 e 6
funcionarios <- funcionarios[, c(-4, -6)]

Adicionando linhas

Uma forma simples de adicionar linhas é atribuir a nova linha com <-. Mas cuidado! O que irá acontecer com o data.frame com o código abaixo?

## CUIDADO!
funcionarios[4, ] <- c("Ana", "F", 2000,  15)

Note que nosso data.frame inteiro se transformou em texto! Você sabe explicar por que isso aconteceu? relembrar coerção

str(funcionarios)
## 'data.frame':    4 obs. of  4 variables:
##  $ nome       : chr  "João" "Maria" "José" "Ana"
##  $ sexo       : chr  "M" "F" "M" "F"
##  $ salario    : chr  "1150" "1200" "1300" "2000"
##  $ experiencia: chr  "10" "12" "15" "15"

Antes de prosseguir, transformemos as colunas salario e experiencia em números novamente:

funcionarios$salario <- as.numeric(funcionarios$salario) 

funcionarios$experiencia <- as.numeric(funcionarios$experiencia)

Se os elementos forem de classe diferente, use a função data.frame para evitar coerção:

funcionarios[4, ] <- data.frame(nome = "Ana", sexo = "F",
                                salario = 2000, experiencia = 15,
                                stringsAsFactors = FALSE)

Também é possível adicionar linhas com rbind():

rbind(funcionarios,
      data.frame(nome = "Ana", sexo = "F",
                 salario = 2000,  experiencia = 15,
                 stringsAsFactors = FALSE))

Atenção! Não fique aumentando um data.frame de tamanho adicionando linhas ou colunas. Sempre que possível pré-aloque espaço!

Removendo linhas

Para remover linhas, basta selecionar apenas aquelas linhas que você deseja manter:

## remove linha 4 do data.frame
funcionarios <- funcionarios[-4, ]
## remove linhas em que salario <= 1150
funcionarios <- funcionarios[funcionarios$salario > 1150, ]

Filtrando linhas com vetores logicos

Relembrando: se passarmos um vetor lógico na dimensão das linhas, selecionamos apenas aquelas que são TRUE. Assim, por exemplo, se quisermos selecionar aquelas linhas em que a coluna salario é maior do que um determinado valor, basta colocar esta condição como filtro das linhas:

## Apenas linhas com salario > 1000
funcionarios[funcionarios$salario > 1000, ]
##    nome sexo salario experiencia
## 2 Maria    F    1200          12
## 3  José    M    1300          15

## Apenas linhas com sexo == "F"
funcionarios[funcionarios$sexo == "F", ]
##    nome sexo salario experiencia
## 2 Maria    F    1200          12

Funções de conveniência: subset()

Uma função de conveniência para selecionar linhas e colunas de um data.frame é a função subset(), que tem a seguinte estrutura:

subset(nome_do_data_frame,
       subset = expressao_logica_para_filtrar_linhas,
       select = nomes_das_colunas,
       drop   = simplicar_para_vetor?)

Vejamos alguns exemplos:

## funcionarios[funcionarios$sexo == "F",]
subset(funcionarios, sexo == "F")
##    nome sexo salario experiencia
## 2 Maria    F    1200          12

## funcionarios[funcionarios$sexo == "M", c("nome", "salario")]
subset(funcionarios, sexo == "M", select = c("nome", "salario"))
##   nome salario
## 3 José    1300

Funções de conveniência: with

A função with() permite que façamos operações com as colunas do data.frame sem ter que ficar repetindo o nome do data.frame seguido de $ , [ , ] ou [[]] o tempo inteiro.

Para ilustrar:

## Com o with
with(funcionarios, (salario^3 - salario^2)/log(salario))
## [1] 2.4e+08 3.1e+08

## Sem o with
(funcionarios$salario^3 - funcionarios$salario^2)/log(funcionarios$salario)
## [1] 2.4e+08 3.1e+08

Quatro formas de fazer a mesma coisa (pense em outras formas possíveis):

subset(funcionarios, sexo == "M", select = "salario", drop = TRUE)
## [1] 1300

with(funcionarios, salario[sexo == "M"])
## [1] 1300

funcionarios$salario[funcionarios$sexo == "M"]
## [1] 1300

funcionarios[funcionarios$sexo == "M", "salario"]
## [1] 1300

Aplicando funções no data.frame: sapply e lapply, funções nas colunas (elementos)

Outras duas funções bastante utilizadas no R são as funções sapply() e lapply().

  • As funções sapply e lapply aplicam uma função em cada elemento de um objeto.
  • Como vimos, os elementos de um data.frame são suas colunas. Deste modo, as funções sapply e lapply aplicam uma função nas colunas de um data.frame.
  • A diferença entre uma e outra é que a primeira tenta simplificar o resultado enquanto que a segunda sempre retorna uma lista.

Testando no nosso data.frame:

sapply(funcionarios[3:4], mean)
##     salario experiencia
##        1250          14

lapply(funcionarios[3:4], mean)
## $salario
## [1] 1250
##
## $experiencia
## [1] 14

Filtrando variáveis antes de aplicar funções: filter()

Como data.frames podem ter variáveis de classe diferentes, muitas vezes é conveniente filtrar apenas aquelas colunas de determinada classe (ou que satisfaçam determinada condição). A função Filter() é uma maneira rápida de fazer isso:

# seleciona apenas colunas numéricas
Filter(is.numeric, funcionarios)
##   salario experiencia
## 2    1200          12
## 3    1300          15

# seleciona apenas colunas de texto
Filter(is.character, funcionarios)
##    nome sexo
## 2 Maria    F
## 3  José    M

Juntando filter() com sapply() você pode aplicar funções em apenas certas colunas, como por exemplo, calcular a média e máximo apenas nas colunas numéricas do nosso data.frame:

sapply(Filter(is.numeric, funcionarios), mean)
##     salario experiencia
##        1250          14

sapply(Filter(is.numeric, funcionarios), max)
##     salario experiencia
##        1300          15

Manipulando data.frames

Ainda temos muita coisa para falar de manipulação de data.framese isso merece um espaço especial. Veremos além de outras funções base do R alguns pacotes importantes como dplyr, reshape2 e tidyr em uma seção separada.

Computer age statistical inference e The undoing project


Segue minha sugestão de leitura para as férias de final de ano: um livro de estatística e outro de psicologia/economia comportamental.

Para falar a verdade, ainda não os li, mas já recomendo.

O primeiro livro é do Michael Lewis, sem dúvida um dos melhores cronistas da atualidade (entre outros ótimos livros: Flash Boys, Moneyball, The Big Short). Lewis conta a história da vida e amizade dos dois psicólogos israelenses que começaram a revolução da economia comportamental:Daniel Kahneman e Amos Tversky. Para quem ainda não conhece o trabalho da dupla, vale a pena recomendar de novo o já clássico Thiking, Fast and Slow.

O segundo livro é o mais novo lançamento dos estatísticos Bradley Efron e Trevor Hastie. Os dois fazem um tour histórico e técnico pela revolução computacional dos últimos 60 anos da estatística. Para quem está começando na área, Efron é mais conhecido por seu trabalho no bootstrap; Trevor (junto com Tibshirani), por seus trabalhos em GAMs e modelos esparsos entre outros. Trevor também é co-autor dos já famosos Elements of Statistical Learning e sua recente versão baby An introduction to Statistical Learning — ambos com versões gratuitas na internet (aqui e aqui).

O que tenho estudado — Causalidade


Para o blog não passar novembro em branco — o que tenho estudado e algumas referências.

Em causalidade acredito que essas sejam as referências básicas:

Morgan & Winship;
Imbens & Rubin;
Pearl, Glymour & Jewell;
Pearl.

Esses livros são muito mais do que suficiente para você começar no assunto. O mais amigável e completo para iniciantes é Morgan & Winship. Imbens & Rubin também é  bom, mas peca por ignorar DAGs. Se for para ler apenas um, escolha Morgan & Winship. Pearl, Glymour & Jewell é uma versão light e curta a nível de graduação —acabou de ser lançada.  Pearl é mais denso e se você nunca viu um DAG antes não comece por ele, comece por Pearl, Glymour & Jewell. Depois que tiver dominado DAGs, entretanto, não deixe de voltar para o livro seminal de Pearl.

Já tinha batido no livro do Wooldridge uma vez por não tratar mais claramente dos problemas de specification searches e multiple testing, que na prática é o que é feito no dia-a-dia do econometrista. Agora vale a pena bater de novo por conta da causalidade (mas é claro que esse problema não é só do Wooldridge, uso ele apenas como um exemplo em geral). Faz algum tempo que estou convencido de que é uma péssima prática ensinar estatística para cientistas sociais sem prover algum framework adequado para se falar de causalidade — e quando falo de framework adequado, não digo tricks para identificar efeitos causais como variáveis instrumentais ou regression discontinuity designs. Falo de um ferramental para ajudar o pesquisador a pensar rigorosamente e claramente sobre o assunto.

Depois vou tentar falar um pouco sobre o que tenho estudado em algoritmos, estatística computacional, pesquisa amostral e probabilidade nesse último trimestre.

Personalizando seu gráfico do ggplot2 – Exports and Imports, William Playfair


O ggplot2 é muito bom para explorar visualmente, de forma dinâmica, sua base de dados.  Mas às vezes queremos editar cada detalhe do gráfico para uma publicação, é possível fazer isso?

Como, por exemplo, reproduzir o famoso gráfico de exportações e importações do William Playfair?

Playfair-bivariate-area-chart

Hoje resolvi testar o quão difícil seria gerar uma imagem parecida e, brincando um pouco com os parâmetros, cheguei na figura abaixo. É um pouco trabalhoso – pois temos que colocar cada texto separadamente – mas não é difícil, nem tão demorado assim.

playfair

Se você tiver um pouco mais de paciência para ajustar detalhes talvez consiga tornar a reprodução ainda mais fiel. E, caso o faça, favor compartilhar o código com todos por aqui!

***

Segue abaixo o código para gerar o gráfico acima. Os dados bem como o próprio código também estão no github.

 

# load packages -----------------------------------------------------------
library(reshape2)
library(ggplot2)

# prepare data for ggplot2 ------------------------------------------------
## data extracted from https://plot.ly/~MattSundquist/2404/exports-and-imports-to-and-from-denmark-norway-from-1700-to-1780/#plot
playfair <- readRDS("william_playfair.rds")

## create min for geom_ribbon
playfair$min <- with(playfair, pmin(exp, imp))
year <- playfair$year

## melt data
molten_data <- melt(playfair, id.vars = c("year", "min"))

# ggplot2 -----------------------------------------------------------------
ggplot(molten_data, aes(x = year, y = value)) +
geom_line(aes(col = variable), size = 1) +
geom_ribbon(aes(ymin = min, ymax = value, fill = variable), alpha = 0.3) +
scale_color_manual(values = c("darkred", "gold3"), guide = F) +
scale_fill_manual(values = c("#90752d", "#BB5766"), guide = F) +
theme_bw() +
annotate("text", x = year[5], y = 100000, label = "Line", angle = 25, size = 3, family = "Garamond") +
annotate("text", x = year[6] - 100, y = 104000, label = "of", angle = 0, size = 3, family = "Garamond") +
annotate("text", x = year[7], y = 101000, label = "Imports", angle = 340, size = 3, family = "Garamond") +
annotate("text", x = year[5] + 400, y = 73000, label = "Line", angle = 345, size = 3, family = "Garamond") +
annotate("text", x = year[6], y = 70000, label = "of", angle = 330, size = 3, family = "Garamond") +
annotate("text", x = year[7] - 200, y = 64000, label = "Exports", angle = 335, size = 3, family = "Garamond") +
annotate("text", x = year[8], y = 83000, label = "italic('BALANCE AGAINST')", angle = 0, family = "Garamond", parse = TRUE) +
annotate("text", x = year[16] + 400, y = 110000, label = "italic('BALANCE in\nFAVOUR of\nENGLAND')", angle = 0, family = "Garamond", parse = TRUE) +
annotate("text", x = year[16], y = 82000, label = "Imports", angle = 30, size = 3, family = "Garamond") +
annotate("text", x = year[14] + 200, y = 131000, label = "Exports", angle = 65, size = 3, family = "Garamond") +
ggtitle("Exports and Imports to and from DENMARK & NORWAY from 1700 to 1780") +
scale_x_date(breaks = seq(year[1], year[18], by = "10 years"),
labels = format(seq(year[1], year[18], by = "10 years"), "%Y")) +
scale_y_continuous(breaks = seq(0, 190e3, by = 10e3),
labels = seq(0, 190, by = 10)) +
theme(title = element_text(size = 8, face = 'bold', family = "Garamond"),
axis.title = element_blank(),
axis.text = element_text(family = "Garamond"),
panel.grid.minor = element_blank())

Programação no R: if(), if() else e ifelse()


***

Parte do livro Introdução à análise de dados com R.  Este trabalho está em andamento, o texto é bastante preliminar e sofrerá muitas alterações. 

Quer fazer um curso presencialmente!? Estamos com turmas abertas em Brasília e São Paulo!

Quer fazer sugestões? Deixe um comentário abaixo ou, se você sabe utilizar o github, acesse aqui.

Não copie ou reproduza este material sem autorização.

Volte para ver atualizações!

***

Há ocasiões em queremos ou precisamos executar parte do código apenas se alguma condição for atendida. O R fornece três opções básicas para estruturar seu código dessa maneira: if(), if() else e ifelse(). Vejamos cada uma delas.

O if() sozinho

A estrutura básica do if() é a seguinte:

if (condicao) {

  # comandos que
  # serao rodados
  # caso condicao = TRUE

}
  • O início do código se dá com o comando if seguido de parênteses e chaves;
  • Dentro do parênteses temos uma condição lógica, que deverá ter como resultado ou TRUE ou FALSE;
  • Dentro das chaves temos o bloco de código que será executado se – e somente se – a condição do parênteses for TRUE.

Vejamos um exemplo muito simples. Temos dois blocos de código que criam as variáveis x e y, mas eles só serão executados se as variáveis cria_x e cria_y forem TRUE, respectivamente.

# vetores de condição lógica
cria_x <- TRUE
cria_y <- FALSE

# só executa se cria_x = TRUE
if (cria_x) {
  x <- 1
}

# só executa se cria_y = TRUE
if (cria_y) {
  y <- 1
}

# note que x foi criado
exists("x")
## [1] TRUE

# note que y não foi criado
exists("y")
## [1] FALSE

Note que somente a variável x foi criada. Vamos agora rodar o mesmo bloco mas com TRUE e FALSE diferentes.

# remove x que foi criado
rm(x)

# vetores de condição lógica
cria_x <- FALSE
cria_y <- TRUE

# só executa se cria_x = TRUE
if (cria_x) {
  x <- 1
}

# só executa se cria_y = TRUE
if (cria_y) {
  y <- 1
}

# note que x não foi criado
exists("x")
## [1] FALSE

# note que y foi criado
exists("y")
## [1] TRUE

Note que agora apenas o y foi criado.

O if() com o else

Outra forma de executar códigos de maneira condicional é acrescentar ao if() o opcional else.

A estrutura básica do if() else é a seguinte:

if (condicao) {

  # comandos que
  # serao rodados
  # caso condicao = TRUE

} else {

  # comandos que
  # serao rodados
  # caso condicao = FALSE

}
  • O início do código se dá com o comando if seguido de parênteses e chaves;
  • Dentro do parênteses temos uma condição lógica, que deverá ter como resultado ou TRUE ou FALSE;
  • Dentro das chaves do if() temos um bloco de código que será executado se – e somente se – a condição do parênteses for TRUE.
  • Logo em seguida temos o else seguido de chaves;
  • Dentro das chaves do else temos um bloco de código que será executado se – e somente se – a condição do parênteses for FALSE.

Como no caso anterior, vejamos primeiramente um exemplo bastante simples.

numero <- 1

if (numero == 1) {
  cat("o numero é igual a 1")
} else {
  cat("o numero não é igual a 1")
}
## o numero é igual a 1

É possível encadear diversos if() else em sequência:

numero <- 10

if (numero == 1) {
  cat("o numero é igual a 1")
} else if (numero == 2) {
  cat("o numero é igual a 2")
} else {
  cat("o numero não é igual nem a 1 nem a 2")
}
## o numero não é igual nem a 1 nem a 2

Para fins de ilustração, vamos criar uma função que nos diga se um número é par ou ímpar. Nela vamos utilizar tanto o if() sozinho quanto o if() else.

Vale relembrar que um número (inteiro) é par se for divisível por 2 e que podemos verificar isso se o resto da divisão (operador %% no R) deste número por 2 for igual a zero.

par_ou_impar <- function(x){

  # verifica se o número é um decimal comparando o tamanho da diferença de x e round(x)
  # se for decimal retorna NA (pois par e ímpar não fazem sentido para decimais)
  if (abs(x - round(x)) > 1e-7) {
    return(NA)
  }

  # se o número for divisível por 2 (resto da divisão zero) retorna "par"
  # caso contrário, retorna "ímpar"
  if (x %% 2 == 0) {
    return("par")
  } else {
    return("impar")
  }

}

Vamos testar nossa função:

par_ou_impar(4)
## [1] "par"
par_ou_impar(5)
## [1] "impar"
par_ou_impar(2.1)
## [1] NA

Parece que está funcionando bem… só tem um pequeno problema. Se quisermos aplicar nossa função a um vetor de números, olhe o que ocorrerá:

x <- 1:5
par_ou_impar(x)
## Warning in if (abs(x - round(x)) > 1e-07) {: a condição tem comprimento > 1 e somente o primeiro
## elemento será usado
## Warning in if (x%%2 == 0) {: a condição tem comprimento > 1 e somente o primeiro elemento será usado
## [1] "impar"

Provavelmente não era isso o que esperávamos. O que está ocorrendo aqui?

A função ifelse()

Os comandos if() e if() else não são vetorizados. Uma alternativa para casos como esses é utilizar a função ifelse().

A função ifelse() tem a seguinte estrutura básica:

ifelse(vetor_de_condicoes, valor_se_TRUE, valor_se_FALSE)
  • o primeiro argumento é um vetor (ou uma expressão que retorna um vetor) com vários TRUE e FALSE;
  • o segundo argumento é o valor que será retornado quando o elemento do vetor_de_condicoes for TRUE;
  • o terceiro argumento é o valor que será retornado quando o elemento do vetor_de_condicoes for FALSE.

Primeiramente, vejamos um caso trivial, para entender melhor como funciona o ifelse():

ifelse(c(TRUE, FALSE, FALSE, TRUE), 1, -1)
## [1]  1 -1 -1  1

Note que passamos um vetor de condições com TRUE, FALSE, FALSE e TRUE. O valor para o caso TRUE é 1 e o valor para o caso FALSE é -1. Logo, o resultado é 1, -1, -1 e 1.

Façamos agora um exemplo um pouco mais elaborado. Vamos criar uma versão com ifelse da nossa função que nos diz se um número é par ou ímpar.

par_ou_impar_ifelse <- function(x){

  # se x for decimal, retorna NA, se não for, retorna ele mesmo (x)
  x <- ifelse(abs(x - round(x)) > 1e-7, NA, x)

  # se x for divisivel por 2, retorna 'par', se não for, retorna impar
  ifelse(x %% 2 == 0, "par", "impar")
}

Testemos a função com vetores. Perceba que agora funciona sem problemas!

par_ou_impar_ifelse(x)
## [1] "impar" "par"   "impar" "par"   "impar"
par_ou_impar_ifelse(c(x, 1.1))
## [1] "impar" "par"   "impar" "par"   "impar" NA

Vetorização e ifelse()

Um tema constante neste livro é fazer com que você comece a pensar em explorar a vetorização do R. Este caso não é diferente, note que poderíamos ter feito a função utilizando apenas comparações vetorizadas:

par_ou_impar_vec <- function(x){

  # transforma decimais em NA
  decimais <- abs(x - round(x)) > 1e-7
  x[decimais] <- NA

  # Cria vetor para aramazenar resultados
  res <- character(length(x))

  # verificar quem é divisível por dois
  ind <- (x %% 2) == 0

  # quem for é par
  res[ind] <- "par"

  # quem não for é ímpar
  res[!ind] <- "impar"

  # retorna resultado
  return(res)
}

Na prática, o que a função ifelse() faz é mais ou menos isso o que fizemos acima – comparações e substituições de forma vetorizada. Note que, neste caso, nossa implementação ficou inclusive um pouco mais rápida do que a solução anterior com ifelse():

library(microbenchmark)
microbenchmark(par_ou_impar_vec(1:1e3), par_ou_impar_ifelse(1:1e3))
## Unit: microseconds
##                         expr min  lq mean median  uq  max neval cld
##     par_ou_impar_vec(1:1000)  56  58   85     59  83 1428   100  a 
##  par_ou_impar_ifelse(1:1000) 322 324  411    326 414 2422   100   b

Loops no R: usando o for()


***

Parte do livro Introdução à análise de dados com R.  Este trabalho está em andamento, o texto é bastante preliminar e sofrerá muitas alterações. 

Quer fazer um curso presencialmente!? Estamos com turmas abertas em Brasília e São Paulo!

Quer fazer sugestões? Deixe um comentário abaixo ou, se você sabe utilizar o github, acesse aqui.

Não copie ou reproduza este material sem autorização.

Volte para ver atualizações!

***

Loops: for()

Um loop utilizando for() no R tem a seguinte estrutura básica:

for(i in conjunto_de_valores){
  # comandos que 
  # serão repetidos
}
  • O início do loop se dá com o comando for seguido de parênteses e chaves;
  • Dentro do parênteses temos um indicador que será usado durante o loop (no caso escolhemos o nome i) e um conjunto de valores que será iterado (conjunto_de_valores).
  • Dentro das chaves temos o bloco de código que será executado durante o loop.

Em outras palavras, no comando acima estamos dizendo que para cada elemento i contido no conjunto_de_valores iremos executar os comandos que estão dentro das chaves.

Para facilitar o entendimento, vejamos dois exemplos muito simples. Primeiro, vamos imprimir na tela os números de 1 a 5.

for(i in 1:5){
  print(i)
}
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5

Agora, vamos imprimir na tela as 5 primeiras letras do alfabeto (o R já vem com um vetor com as letras do alfabeto: letters).

for(i in 1:5){
  print(letters[i])
}
## [1] "a"
## [1] "b"
## [1] "c"
## [1] "d"
## [1] "e"

No mesmo exemplo, acima, ao invés correr o loop no índice de inteiros 1:5, vamos iterar diretamente sobre os primeiros 5 elementos do vetor letters:

for(letra in letters[1:5]){
  print(letra)
}
## [1] "a"
## [1] "b"
## [1] "c"
## [1] "d"
## [1] "e"

seq_along

Uma função bastante útil ao fazer loops é a função seg_along(). Ela cria um vetor de inteiros com índices para acompanhar o objeto.

# criando um vetor de exemplo
set.seed(119)
x <- rnorm(10)

# inteiros de 1 a 10
seq_along(x)
##  [1]  1  2  3  4  5  6  7  8  9 10

Também é possível criar um vetor de inteiros do tamanho do objeto fazendo uma sequência de 1 até length(x):

1:length(x)
##  [1]  1  2  3  4  5  6  7  8  9 10

Entretanto, a vantagem de seq_along() é que quando o vetor é vazio, ela retorna um vetor vazio e, deste modo, o loop não é executado (o que é o comportamento correto).

Já a sequência 1:length(x) retorna a sequência 1:0, isto é, uma sequência decrescente de 1 até 0, e loop é executado nestes valores.

Vejamos:

# cria vetor vazio
x <- numeric(0)

# 1:length(x)
# note que o loop é executado (o que é errado)
for(i in 1:length(x)) print(i)
## [1] 1
## [1] 0

# seq_along
# note que o loop não é executado (o que é correto)
for(i in seq_along(x)) print(i)

Vetorização, funções nativas e loops

Como vimos, o R é vetorizado. Muitas vezes, quando você pensar que precisa usar um loop, ao pensar melhor, descobrirá que não precisa. Em geral é possível resolver o problema de maneira vetorizada e usando funções nativas do R.

Para quem está aprendendo a programar diretamente com o R, isso é algo que virá naturalmente. Todavia, para quem já sabia programar em outras linguagens de programação – como C – pode ser difícil se acostumar a pensar desta maneira.

Vejamos um exemplo trivial. Suponha que você queira dividir os valores de um vetor x por 10. Se o R não fosse vetorizado, você teria que fazer algo como:

# criando vetor de exemplo
x <- 10:20

# divide cada elemento por 10
for(i in seq_along(x)) 
  x[i] <- x[i]/10

# resultado
x
##  [1] 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0

Mas o R é vetorizado e, portanto, este é o tipo de loop que não faz sentido na linguagem. É muito mais rápido e fácil de enteder escrever simplesmente x/10.

# recriando vetor de exemplo
x <- 10:20

# divide cada elemento por 10
x <- x/10
x
##  [1] 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0

Vejamos um caso um pouco mais complicado. Suponha que você queira, gerar um passeio aleatório com um algoritmo simples: a cada período você pode andar para frente (+1) ou para trás (-1) com probabilidades iguais.

set.seed(1)

# número de passos
n <- 1000

# vetor para armazenar o passeio aleatório
passeio <- numeric(n)

# primeiro passo
passeio[1] <- sample(c(-1, 1), 1)

# demais passos
for(i in 2:n){

  # passo i é o onte você estava (passeio[i-1]) 
  # mais o passo seguinte
  passeio[i] <- passeio[i - 1] + sample(c(-1, 1), 1)
}

É possível fazer tudo isso com apenas uma linha de maneira “vetorizada” e bem mais eficiente: crie todos os n passos de uma vez e faça a soma acumulada.

set.seed(1)
passeio2 <- cumsum(sample(c(-1, 1), n, TRUE))

# verifica se são iguais
all.equal(passeio, passeio2)
## [1] TRUE

Então, você deve estar se perguntando: “não é para usar loops nunca”?

Não é isso. Em algumas situações loops são inevitáveis e podem inclusive ser mais fáceis de ler e de entender. O ponto aqui é apenas lembrá-lo de explorar a vetorização do R.

Voltando ao nosso exemplo do passeio aleatório, você deve ter notado a linha passeio <- numeric(n) em que criamos um vetor numérico para ir armazenando os resultados das iterações. Discutamos um pouco mais esse ponto.

Pré-alocar espaço antes do loop

Um erro bastante comum de quem está começando a programar em R é “crescer” objetos durante o loop. Isto tem um impacto substancial na performance do seu programa! Sempre que possível, crie um objeto, antes de iniciar o loop, para armazenar os resultados de cada iteração.

Vejamos um exemplo um pouco mais elaborado: vamos calcular os n primeiros números da sequência de Fibonacci: F_1 = 0F_2 = 1, F_3 = 1, $latexF_4 = 2$, F_5 = 3, F_6 = 5, F_7 = 8, F_8 = 13, F_9 = 21, e assim por diante.

Note que a sequência de Fibonacci pode ser definida da seguinte forma, os primeiros dois números são 0 e 1, isto é, F_1 = 0, F_2 = 1. A partir daí, os números subsequentes são a soma dos dois números anteriores, isto é, F_i = F_{i-1} + F_{i-2} para todo i > 2.

Vejamos uma forma de implementar isto no R usando for() e criando um vetor para armazenar os resultados:

n <- 9

# crie um vetor de tamanho n 
# para armazenar os n resultados
fib <- numeric(n)

# comece definindo as condições iniciais
# F1 = 0 e F2 = 1
fib[1] <- 0
fib[2] <- 1

# Agora para todo i > 2 
# calculamos Fi = F(i-1) + F(i - 2)
for(i in 3:n){
  fib[i] <- fib[i - 1] + fib[i - 2]
}

# conferindo resultado
fib
## [1]  0  1  1  2  3  5  8 13 21

Vamos comparar a performance deste código com outro sem pré-alocar um vetor de resultados. Primeiro, transformemos nosso loop em uma função:

fib <- function(n){
  # vetor para armazenar resultados
  fib <- numeric(n)

  # condições iniciais
  fib[1] <- 0
  fib[2] <- 1

  # calculandos o números de 3 a n
  for(i in 3:n){
    fib[i] <- fib[i - 1] + fib[i - 2]
  }

  return(fib)
}

Agora, criemos outra função em que o vetor fib cresce a cada iteração:

fib_sem_pre_alocar <- function(n){

  # condições iniciais
  fib    <- 0
  fib    <- c(fib, 1)

  # calculandos o números de 3 a n
  for(i in 3:n){
    fib <- c(fib, fib[i - 1] + fib[i - 2])
  }

  return(fib)
}

Comparando as duas implementações:

library(microbenchmark)
set.seed(5)
microbenchmark(fib(5000), fib_sem_pre_alocar(5000))
## Unit: milliseconds
##                      expr  min lq mean median   uq max neval cld
##                 fib(5000)  5.8  6  6.5    6.4  6.9  10   100  a 
##  fib_sem_pre_alocar(5000) 39.0 53 60.5   56.4 58.9 195   100   b

Note que quanto maior o número de simulações, maior a queda na performance: com n = 5000 a função fib_sem_pre_alocar() chega a ser mais de 10 vezes mais lenta do que a função fib().

Exemplo: entendendo a família apply

Vamos calcular a média de cada uma das colunas do data.frame mtcars usando loops.

Para isso precisamos: (i) saber quantas colunas existem no data.frame; (ii) criar um vetor para armazenar os resultados; (iii) nomear o vetor de resultados com os nomes das colunas; e (iv) fazer um loop para cada coluna.

# (i) quantas colunas no data.frame
n <- ncol(mtcars)

# (ii) vetor para armazenar resultados
medias <- numeric(n)

# (iii) nomeando vetor com nomes das colunas
names(medias) <- colnames(mtcars)

# (iv) loop para cada coluna
for(i in seq_along(mtcars)){
  medias[i] <- mean(mtcars[,i])
}

# resultado final
medias
##    mpg    cyl   disp     hp   drat     wt   qsec     vs     am   gear   carb    mpg    cyl   disp 
##  20.09   6.19 230.72 146.69   3.60   3.22  17.85   0.44   0.41   3.69   2.81  20.09   6.19 230.72 
##     hp   drat     wt   qsec     vs     am   gear   carb 
## 146.69   3.60   3.22  17.85   0.44   0.41   3.69   2.81

Gastamos várias linhas para fazer essa simples operação. Como já vimos, é bastante fácil fazer isso no R com apenas uma linha:

sapply(mtcars, mean)
##    mpg    cyl   disp     hp   drat     wt   qsec     vs     am   gear   carb    mpg    cyl   disp 
##  20.09   6.19 230.72 146.69   3.60   3.22  17.85   0.44   0.41   3.69   2.81  20.09   6.19 230.72 
##     hp   drat     wt   qsec     vs     am   gear   carb 
## 146.69   3.60   3.22  17.85   0.44   0.41   3.69   2.81

Imagine que não existisse a função sapply() no R. Se quiséssemos aplicar outra função para cada coluna, teríamos que copiar e colar todo o código novamente, certo?

Sim, você poderia fazer isso, mas não seria uma boa prática. Neste caso, como já vimos, o ideal seria criar uma função.

Façamos, portanto, uma função que nos permita aplicar uma fução arbitrária nas colunas de um data.frame.

meu_sapply <- function(x, funcao){

  n <- length(x)

  resultado <- numeric(n)

  names(resultado) <- names(x)

  for(i in seq_along(x)){
    resultado[i] <- funcao(x[[i]])
  }

  return(resultado)
}

Perceba que ficou bastante simples percorrer todas as colunas de um data.frame para aplicar a função que você quiser:

meu_sapply(mtcars, mean)
##    mpg    cyl   disp     hp   drat     wt   qsec     vs     am   gear   carb    mpg    cyl   disp 
##  20.09   6.19 230.72 146.69   3.60   3.22  17.85   0.44   0.41   3.69   2.81  20.09   6.19 230.72 
##     hp   drat     wt   qsec     vs     am   gear   carb 
## 146.69   3.60   3.22  17.85   0.44   0.41   3.69   2.81

meu_sapply(mtcars, sd)
##    mpg    cyl   disp     hp   drat     wt   qsec     vs     am   gear   carb    mpg    cyl   disp 
##   6.03   1.79 123.94  68.56   0.53   0.98   1.79   0.50   0.50   0.74   1.62   6.03   1.79 123.94 
##     hp   drat     wt   qsec     vs     am   gear   carb 
##  68.56   0.53   0.98   1.79   0.50   0.50   0.74   1.62

meu_sapply(mtcars, max)
##   mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb   mpg   cyl  disp    hp  drat 
##  33.9   8.0 472.0 335.0   4.9   5.4  22.9   1.0   1.0   5.0   8.0  33.9   8.0 472.0 335.0   4.9 
##    wt  qsec    vs    am  gear  carb 
##   5.4  22.9   1.0   1.0   5.0   8.0

meu_sapply(mtcars, min)
##  mpg  cyl disp   hp drat   wt qsec   vs   am gear carb  mpg  cyl disp   hp drat   wt qsec   vs   am 
## 10.4  4.0 71.1 52.0  2.8  1.5 14.5  0.0  0.0  3.0  1.0 10.4  4.0 71.1 52.0  2.8  1.5 14.5  0.0  0.0 
## gear carb 
##  3.0  1.0

É isso o que as funções da família apply são: são funções que fazem loops para você. Elas automaticamente cuidam de toda a parte chata do loop como, por exemplo, criar um objeto de tamanho correto para pré-alocar os resultados. Além disso, em grande parte das vezes essas funções serão mais eficientes do que se você mesmo fizer a implementação.

Por curiosidade, vamos comparar a eficiência do sapply() do R com meu_sapply()

microbenchmark(sapply(mtcars, mean), meu_sapply(mtcars, mean))
## Unit: microseconds
##                      expr min  lq mean median  uq max neval cld
##      sapply(mtcars, mean)  98 104  113    113 121 180   100  a 
##  meu_sapply(mtcars, mean) 239 275  286    285 294 360   100   b

Exercícios

As funções que você irá implementar aqui, usando for(), serão até mais de 100 vezes mais lentas do que as funções nativas do R. Estes exercícios são para você treinar a construção de loops, um pouco de lógica de programação, e entender o que as funções do R estão fazendo – de maneira geral – por debaixo dos panos.

1) Crie uma função que encontre o máximo de um vetor (use for() na sua função). Compare os resultados e a performance de sua implementação com a função max() do R. Sua função é quantas vezes mais lenta?

2) Crie uma função que calcule o fatorial de n (use for() na sua função). Compare os resultados e a performance de sua implementação com a função factorial() do R. Sua função é quantas vezes mais lenta?

3) Crie uma função que calcule a soma de um vetor (use for() na sua função). Compare os resultados e a performance de sua implementação com a função sum() do R. Sua função é quantas vezes mais lenta?

4) Crie uma função que calcule a soma acumulada de um vetor (use for() na sua função). Compare os resultados e a performance de sua implementação com a função cumsum() do R. Sua função é quantas vezes mais lenta?

Respostas

Criando vetor aleatório para comparar as funções:

# cria vetor para comparar resultados
set.seed(123)
x <- rnorm(100)

# Pacote para comparar resultados 
library(microbenchmark)

Resposta sugerida ex-1:

# 1) loop para encontrar máximo

max_loop <- function(x){
  max <- x[1]

  for(i in 2:length(x)){
    if(x[i] > max){
      max <- x[i]
    }

  }
  return(max)
}

all.equal(max(x), max_loop(x))
## [1] TRUE

microbenchmark(max(x), max_loop(x))
## Unit: nanoseconds
##         expr   min    lq  mean median    uq    max neval cld
##       max(x)   378   544   701    594   672   8366   100  a 
##  max_loop(x) 83031 85970 91228  88492 94796 156594   100   b

Resposta sugerida ex-2:

# 2) loop para fatorial

fatorial <- function(n){
  fat <- 1
  for(i in 1:n){
    fat <- fat*i
  }
  return(fat)
}

all.equal(factorial(10), fatorial(10))
## [1] TRUE

microbenchmark(factorial(10), fatorial(10))
## Unit: nanoseconds
##           expr  min   lq mean median   uq   max neval cld
##  factorial(10)  323  381  514    464  552  4755   100  a 
##   fatorial(10) 3207 3612 4820   3738 3955 29361   100   b

Resposta sugerida ex-3:

# 3) loop para soma 

soma <- function(x){

  n <- length(x)

  soma <- numeric(n)

  soma <- x[1]

  for(i in 2:n){
    soma <- x[i] + soma
  }

  return(soma)
}

all.equal(soma(x), sum(x))
## [1] TRUE

microbenchmark(sum(x), soma(x))
## Unit: nanoseconds
##     expr   min    lq  mean median    uq   max neval cld
##   sum(x)   351   474   662    567   642  8938   100  a 
##  soma(x) 42717 46671 50345  51072 52788 85080   100   b

Resposta sugerida ex-4:

# 4) loop para soma acumulada

soma_acumulada <- function(x){

  n <- length(x)

  soma <- numeric(n)

  soma[1] <- x[1]

  for(i in 2:n){
    soma[i] <- x[i] + soma[i-1]
  }

  return(soma)
}

all.equal(soma_acumulada(x), cumsum(x))
## [1] TRUE

microbenchmark(cumsum(x), soma_acumulada(x))
## Unit: nanoseconds
##               expr    min     lq   mean median     uq    max neval cld
##          cumsum(x)    543    616    852    875    942   4625   100  a 
##  soma_acumulada(x) 128217 139040 145288 143372 149446 184268   100   b

Manipulação de Textos – Parte 1


***

Parte do livro Introdução à análise de dados com R.  Este trabalho está em andamento, o texto é bastante preliminar e sofrerá muitas alterações. 

Quer fazer sugestões? Deixe um comentário abaixo ou, se você sabe utilizar o github, acesse aqui.

Não copie ou reproduza este material sem autorização.

Volte para ver atualizações!

***

Criando textos

No R, textos são representados por vetores do tipo character. Você pode criar manualmente um elemento do tipo character colocando o texto entre aspas, podendo ser tanto aspas simples (‘texto’) quanto aspas duplas (“texto”).

# criando um vetor de textos
# aspas simples
x1 <- 'texto 1'

# aspas duplas
x2 <- "texto 2"

Como já vimos, para saber se um objeto é do tipo texto você pode utilizar a função is.character() e também é possível converter objetos de outros tipos para textos utilizando a função as.character().

# criando um vetor de inteiros
x3 <- 1:10

# É texto? Não.
is.character(x3)
## [1] FALSE
# Convertendo para texto
x3 <- as.character(x3)

# Agora é texto? Sim.
is.character(x3)
## [1] TRUE

Operações com textos

Operações como ordenação e comparações são definidas para textos. A ordenação de um texto é feita de maneira lexicográfica, tal como em um dicionário.

# ordenação de letras
sort(c("c", "d", "a", "f"))
## [1] "a" "c" "d" "f"
# ordenação de palavras
# tal como um dicionário
sort(c("cor", "casa", "árvore", "zebra", "branco", "banco"))
## [1] "árvore" "banco"  "branco" "casa"   "cor"    "zebra"

Como a comparação é lexicográfica, é preciso tomar alguns cuidados. Por exemplo, o texto “2” é maior do que o texto “100”. Se por acaso seus números forem transformados em texto, você não vai receber uma mensagem de erro na comparação "2" > "100" mas sim um resultado errado: TRUE.

# CUIDADO!
2   >  100
## [1] FALSE
"2" > "100"
## [1] TRUE
# b > a
"b" > "a"
## [1] TRUE
# A > a
"A" > "a"
## [1] TRUE
# casa > banana
"casa" > "banana"
## [1] TRUE

Imprimindo textos

Se você estiver usando o R de modo interativo, chamar o objeto fará com que ele seja exibido na tela usando print().

# Imprime texto na tela
print(x1)
## [1] "texto 1"
# Quando em modo interativo
# Equivalente a print(x1)
x1
## [1] "texto 1"

Se você não estiver usando o R de modo interativo — como ao dar source() em um script ou dentro de um loop — é preciso chamar explicitamente uma função que exiba o texto na tela.

# sem print não acontece nada
for(i in 1:3) i

# com print o valor de i é exibido
for(i in 1:3) print(i)
## [1] 1
## [1] 2
## [1] 3

Existem outras opções para “imprimir” e formatar textos além do print(). Uma função bastante utilizada para exibir textos na tela é a função cat() (concatenate and print).

cat(x1)
## texto 1
cat("A função cat exibe o texto sem aspas:", x1)
## A função cat exibe o texto sem aspas: texto 1

Por padrão, cat() separa os textos com um espaço em branco, mas é possível alterar este comportamento com o argumento sep.

cat(x1, x2)
## texto 1 texto 2
cat(x1, x2, sep = " - ")
## texto 1 - texto 2

Outra funções úteis são sprintf() e format(), principalmente para formatar e exibir números. Para mais detalhes sobre as funções, olhar a ajuda ?sprintf e ?format.

# %.2f (float, 2 casas decimais)
sprintf("R$ %.2f", 312.12312312)
## [1] "R$ 312.12"
# duas casas decimais, separador de milhar e decimal
format(10500.5102, nsmall=2, big.mark=".", decimal.mark=",")
## [1] "10.500,51"

Caracteres especiais

Como fazemos para gerar um texto com separação entre linhas no R? Criemos a separação de linhas manualmente para ver o que acontece:

texto_nova_linha <- "texto
com nova linha"

texto_nova_linha
## [1] "texto\ncom nova linha"

Note que aparece um \n no meio do texto. Isso é um caractere especial: \n simboliza justamente uma nova linha. Quando você exibe um texto na tela com print(), caracteres especiais não são processados e aparecem de maneira literal. Já se você exibir o texto na tela usando cat(), os caracteres especiais serão processados. No nosso exemplo, o \n será exibido como uma nova linha.

# print: \n aparece literalmente
print(texto_nova_linha)
## [1] "texto\ncom nova linha"
# cat: \n aparece como nova linha
cat(texto_nova_linha)
## texto
## com nova linha

Caracteres especiais são sempre “escapados” com a barra invertida \ . Além da nova linha (\n), outros caracteres especiais recorrentes são o tab (\t) e a própria barra invertida, que precisa ela mesma ser escapada (\\). Vejamos alguns exemplos:

cat("colocando uma \nnova linha")
## colocando uma 
## nova linha
cat("colocando um \ttab")
## colocando um     tab
cat("colocando uma \\ barra")
## colocando uma \ barra
cat("texto com novas linhas e\numa barra no final\n\\")
## texto com novas linhas e
## uma barra no final
## \

Para colocar aspas simples ou duplas dentro do texto há duas opções. A primeira é alternar entre as aspas simples e duplas, uma para definir o objeto do tipo character e a outra servido literalmente como aspas.

# Aspas simples dentro do texto
aspas1 <- "Texto com 'aspas' simples dentro"
aspas1 
## [1] "Texto com 'aspas' simples dentro"
# Aspas duplas dentro do texto
aspas2  <- 'Texto com "aspas" duplas dentro'
cat(aspas2)
## Texto com "aspas" duplas dentro

Outra opção é colocar as aspas como caracter expecial. Neste caso, não é preciso alternar entre aspas simples e duplas.

aspas3 <- "Texto com \"aspas\" duplas"
cat(aspas3)
## Texto com "aspas" duplas
aspas4 <- 'Texto com \'aspas\' simples'
cat(aspas4)
## Texto com 'aspas' simples

Utilidade das funções de exibição

Qual a utilidade de funções que exibam coisas na tela?

Um caso bastante comum é exibir mensagens durante a execução de alguma rotina ou função. Por exemplo, você pode exibir o percentual de conclusão de um loop a cada 25 rodadas:

for(i in 1:100){ 

  # imprime quando o resto da divisão
  # de i por 25 é igual a 0
  if(i %% 25 == 0){
    cat("Executando: ", i, "%\n", sep = "")
  }

  # alguma rotina
  Sys.sleep(0.01)
}
## Executando: 25%
## Executando: 50%
## Executando: 75%
## Executando: 100%

Outro uso frequente é criar métodos de exibição para suas próprias classes. Vejamos um exemplo simples de uma fução base do R, a função rle(), que computa tamanhos de sequências repetidas de valores em um vetor. O resultado da função é uma lista, mas ao exibirmos o objeto na tela, o print não é igual ao de uma lista comum:

x <- rle(c(1,1,1,0))

# resultado é uma lista
str(x)
## List of 2
##  $ lengths: int [1:2] 3 1
##  $ values : num [1:2] 1 0
##  - attr(*, "class")= chr "rle"
# print do objeto na tela 
# não é como uma lista comum
x
## Run Length Encoding
##   lengths: int [1:2] 3 1
##   values : num [1:2] 1 0
# tirando a classe do objeto
# veja que o print agora é como uma lista comum
unclass(x)
## $lengths
## [1] 3 1
## 
## $values
## [1] 1 0

Isso ocorre porque a classe rle tem um método de print próprio, print.rle():

print.rle <- function (x, digits = getOption("digits"), prefix = "", ...) 
{
    if (is.null(digits)) 
        digits <- getOption("digits")
    cat("", "Run Length Encoding\n", "  lengths:", sep = prefix)
    utils::str(x$lengths)
    cat("", "  values :", sep = prefix)
    utils::str(x$values, digits.d = digits)
    invisible(x)
}

Tamanho do texto

A função nchar() retorna o número de caracteres de um elemento do tipo texto. Note que isso é diferente da função length() que retorna o tamanho do vetor.

# O vetor x1 tem apenas um elemento
length(x1)
## [1] 1
# O elemento do vetor x1 tem 7 caracteres
# note que espaços em brancos contam
nchar(x1) 
## [1] 7

A função nchar() é vetorizada.

# vetor do tipo character
y <- c("texto 1", "texto 11")

# vetor tem dois elementos
length(y)
## [1] 2
# O primeiro elemento tem 7 caracteres
# O segundo 8.
nchar(y) # vetorizada
## [1] 7 8

Manipulando textos

Manipulação de textos é uma atividade bastante comum na análise de dados. O R possui uma série de funções para isso e suporta o uso de expressões regulares. Nesta seção veremos exemplos simples das principais funções de manipulação de textos. Na próxima seção abordaremos um pouco de expressões regulares.

Colando (ou concatenando) textos

A função paste() é uma das funções mais úteis para manipulação de textos. Como o próprio nome diz, ela serve para “colar” textos.

# Colando textos
tipo <- "Apartamento"
bairro <- "Asa Sul"
texto <- paste(tipo,"na", bairro )
texto
## [1] "Apartamento na Asa Sul"

Por default, paste() separa os textos com um espaço em branco. Você pode alterar isso modificando o argumento sep. Caso não queira nenhum espaço entre as strings, basta definir sep = "" ou utilizar a função paste0(). Como usual, todas essas funções são vetorizadas.

# separação padrão
paste("x", 1:5)
## [1] "x 1" "x 2" "x 3" "x 4" "x 5"
# separando por ponto
paste("x", 1:5, sep=".")
## [1] "x.1" "x.2" "x.3" "x.4" "x.5"
# sem separação
paste("x", 1:5, sep ="")
## [1] "x1" "x2" "x3" "x4" "x5"
# sem separação, usando paste0.
paste0("x", 1:5)
## [1] "x1" "x2" "x3" "x4" "x5"

Note que foram gerados 5 elementos diferentes nos exemplos acima. É possível “colar” todos os elementos em um único texto com a opção collapse().

paste("x", 1:5, sep="", collapse=" ")
## [1] "x1 x2 x3 x4 x5"

Separando textos

Outra atividade frequente em análise de dados é separar um texto em elementos diferentes. Por exemplo, suponha que você tenha que trabalhar com um conjunto de números, mas que eles estejam em um formato de texto separados por ponto e vírgula:

dados <- "1;2;3;4;5;6;7;8;9;10"
dados
## [1] "1;2;3;4;5;6;7;8;9;10"

Com a função strsplit() é fácil realizar essa tarefa:

dados_separados <- strsplit(dados, split=";")
dados_separados
## [[1]]
##  [1] "1"  "2"  "3"  "4"  "5"  "6"  "7"  "8"  "9"  "10"

Note que o resultado da função é uma lista. Agora é possível converter os dados em números e trabalhar normalmente.

# convertendo o resultado em número
dados_separados <- as.numeric(dados_separados[[1]])

# agora é possível trabalhar com os números
# média
mean(dados_separados)
## [1] 5.5
# soma
sum(dados_separados)
## [1] 55

Encontrando partes de um texto

Quando você estiver trabalhando com suas bases de dados, muitas vezes será preciso encontrar certas palavras ou padrões dentro do texto. Por exemplo, imagine que você tenha uma base de dados de aluguéis de apartamentos e você gostaria de encontrar imóveis em um certo endereço. Vejamos este exemplo com dados online de aluguel em Brasília.

# Carrega arquivo de anúncios de aluguel (2014)
arquivo <- url("https://dl.dropboxusercontent.com/u/44201187/aluguel.rds")
con <- gzcon(arquivo)
aluguel <- readRDS(con)
close(con)

Vejamos a estrutura da nossa base de dados:

str(aluguel)
## 'data.frame':    2612 obs. of  5 variables:
##  $ bairro  : chr  "Asa Norte" "Asa Norte" "Sudoeste" "Asa Norte" ...
##  $ endereco: chr  "CLN 310 BLOCO A " "SCRN 716 BLOCO G ENT. 26 3º ANDAR" "QMSW 06 ED.STUDIO IN" "CLN 406 BLOCO D - ED. POP CENTER (APARTAMENTO)" ...
##  $ quartos : num  1 1 1 1 1 1 1 1 1 1 ...
##  $ m2      : num  22.9 26 30 30 30 30 30 30 28 30 ...
##  $ preco   : num  650 750 800 800 800 820 850 850 850 860 ...
##  - attr(*, "na.action")=Class 'omit'  Named int [1:120] 15943 16001 17264 17323 18600 18659 19935 19996 21278 22617 ...
##   .. ..- attr(*, "names")= chr [1:120] "15943" "16001" "17264" "17323" ...

Temos mais de 2 mil anúnciso, como encontrar aqueles apartamentos que queremos, como, por exemplos, os que contenham “CLN 310” no endereço? Neste caso você pode utilizar a função grep() para encontrar padrões dentro do texto. A função retornará o índice das observações que contém o texto:

busca_indice <- grep(pattern = "CLN 310", aluguel$endereco)
busca_indice
## [1]    1 1812
aluguel[busca_indice, ]
##         bairro                            endereco quartos   m2 preco
## 1    Asa Norte                    CLN 310 BLOCO A        1 22.9   650
## 1812 Asa Norte CLN 310 BLOCO E ENTRADA 52 SALA 216       0 30.0   900

Uma variante da função grep() é a função grepl(), que realiza a mesma coisa, mas ao invés de retornar um índice numérico, retorna um vetor lógico:

busca_logico <- grepl(pattern = "CLN 310", aluguel$endereco)
str(busca_logico)
##  logi [1:2612] TRUE FALSE FALSE FALSE FALSE FALSE ...
aluguel[busca_indice, ]
##         bairro                            endereco quartos   m2 preco
## 1    Asa Norte                    CLN 310 BLOCO A        1 22.9   650
## 1812 Asa Norte CLN 310 BLOCO E ENTRADA 52 SALA 216       0 30.0   900

Nossa busca é útil, mas ainda é simples. Quando aprendermos expressões regulares, essas buscas ficarão bem mais poderosas. Lá também aprenderemos outras funções como regexpr(), gregexpr(), regexec() e regmatches().

Substituindo partes de um texto

A função sub() substitui o primeiro padrão (pattern) que encontra:

texto2 <- paste(texto, ", Apartamento na Asa Norte")
texto2
## [1] "Apartamento na Asa Sul , Apartamento na Asa Norte"
# Vamos substituir "apartamento" por "Casa"
# Mas apenas o primeiro caso
sub(pattern = "Apartamento",
    replacement = "Casa",
    texto2) 
## [1] "Casa na Asa Sul , Apartamento na Asa Norte"

Já a função gsub() substitui todos os padrões que encontra:

# Vamos substituir "apartamento" por "Casa"
# Agora em todos os casos
gsub(pattern="Apartamento",
    replacement="Casa",
    texto2)
## [1] "Casa na Asa Sul , Casa na Asa Norte"

Você pode usar as funções sub() e gsub() para “deletar” partes indesejadas do texto, basta colocar como replacement um caractere vazio "". Um exemplo bem corriqueiro, quando se trabalha com com nomes de arquivos, é a remoção das extensões:

# nomes dos arquivos
arquivos <- c("simulacao_1.csv","simulacao_2.csv")

# queremos eliminar a extensão .csv
# note que o ponto precisa ser escapado
nomes_sem_csv <- gsub("\\.csv", "", arquivos)
nomes_sem_csv
## [1] "simulacao_1" "simulacao_2"

Extraindo partes específicas de um texto

Às vezes você precisa extrair apenas algumas partes específicas de um texto, em determinadas posições. Para isso você pode usar as funções substr() e substring().

Para essas funções, você basicamente passa as posições dos caracteres inicial e final que deseja extrair.

# extraindo caracteres da posição 4 à posição 8
x <- "Um texto de exemplo"
substr(x, start = 4, stop = 8)
## [1] "texto"

É possível utilizar essas funções para alterar partes específicas do texto.

# substituindo caracteres da posição 4 à posição 8
substr(x, start = 4, stop = 8) <- "TEXTO"
x
## [1] "Um TEXTO de exemplo"

A principal diferença entre substr() e substring() é que a segunda permite você passar vários valores iniciais e finais:

# pega caracteres de (4 a 8) e de (10 a 11)
substring(x, first = c(4, 10), last = c(8, 11))
## [1] "TEXTO" "de"
# pega caracteres de (1 ao último), (2 ao último) ...
substring("abcdef", first = 1:6)
## [1] "abcdef" "bcdef"  "cdef"   "def"    "ef"     "f"

**** A SEGUIR ****

  • expressões regulares
    • regmatches, regexpr, gregexpr, regexc
  • fuzzy matching
  • stringr

Simulações – Parte 1


***

Parte do livro Introdução à análise de dados com R.  Este trabalho está em andamento, o texto é bastante preliminar e sofrerá muitas alterações. 

Quer fazer sugestões? Deixe um comentário abaixo ou, se você sabe utilizar o github, acesse aqui.

Não copie ou reproduza este material sem autorização.

Volte para ver atualizações!

***

Distribuições de probabilidade

O R vem com diversas funções para simular distribuições estatísticas. Em geral essas funções têm o seguinte formato: rnomedadistribuicao, dnomedadistribuicao, pnomedadistribuicao ou qnomedadistribuicao. Mais detalhadamente, a primeira letra da função, que pode ser r, d, p ou q, indica, respectivamente, se a função é: (i) geradora de variáveis aleatórias; (ii) de densidade; (iii) de distribuição acumulada; ou, (iv) de quantil. E, logo em seguida, temos um nome abreviado da distribuição de probabilidade.

Captura de Tela 2016-01-17 às 13.58.01

Dessa forma, por exemplo, se você quiser gerar dados aleatórios de uma distribuição normal a função para tanto é rnorm (r pois trata-se de um gerador de números aleatórios e norm pois trata-se da distribuição normal).

Na tabela abaixo temos várias das distribuições presentes de forma nativa no R:

Captura de Tela 2016-01-17 às 13.58.19

Sementes para as simulações

Durante todo o livro nós utilizamos o comando set.seed quando fizemos simulações. Isso garante que os resultados obtidos possam ser reproduzidos em qualquer computador.

Veja que, se rodarmos o comando rnorm sem definir o estado do gerador de números aleatórios com set.seed, você não conseguirá obter os mesmos valores em seu computador:

rnorm(1)
## [1] 0.5748481

rnorm(1)
## [1] 0.4052027

Contudo, uma vez definida a semente, obteremos sempre o mesmo valor:

set.seed(1)
rnorm(1)
## [1] -0.6264538

set.seed(1)
rnorm(1)
## [1] -0.6264538

O básico de r,d p,q com a distribuição normal

Para começar a entender o que cada função do R faz, trabalhemos cada uma delas usando a distribuição normal. A função densidade da distribuição normal-padrão (uma normal com média zero e desvio-padrão igual a um) tem a seguinte forma:

plot of chunk grafico densidade

A primeira dúvida que alguém pode ter é: como extrair valores aleatoriamente desta distribuição? Vejamos:

# semente para reproducibilidade
set.seed(2)

# gerando 5 variáveis aleatórias da distribuição Normal(0,1)
x1 <- rnorm(5)
x1
## [1] -0.89691455  0.18484918  1.58784533 -1.13037567 -0.08025176

Com o comando acima geramos 5 valores da normal-padrão.

Mas e se quisermos valores de uma normal com média e desvio-padrão diferentes? Para isso, basta mudarmos os parâmetros mean sd (standard deviation) da função rnorm:

# semente para reproducibilidade
set.seed(2)

# gerando 6 variáveis aleatórias da distribuição Normal(10,2)
x2 <- rnorm(5, mean = 10, sd = 2)
x2
## [1]  8.206171 10.369698 13.175691  7.739249  9.839496

Com o código acima, geramos 5 valores de uma distribuição normal com média 10 e desvio-padrão 2. Entretanto, você também poderia ter gerado os mesmos valores a partir da normal-padrão: x2 nada mais é do que x1*2 + 10:

all.equal(x1*2 + 10, x2)
## [1] TRUE

Saber como tranformar uma distribuição em outra é algo bastante útil e pode poupar bastante tempo na hora de fazer simulações. Veremos exemplos práticos disso nos exercícios.

Às vezes, ao invés de gerar números aleatórios, nós temos valores que, presume-se, foram gerados por uma distribuição normal, e queremos saber a densidade ou a probabilidade associada àquele valor.

Por exemplo, supondo uma distribuição normal-padrão, qual a probabilidade de x ser menor do que 1.65? Isto é, queremos saber o valor da área hachurada da curva de densidade:

plot of chunk grafico area densidade
Para responder essa pergunta, você vai usar a função pnorm:

# probabilidade de X < 1.65
pnorm(1.65)
## [1] 0.9505285

Note que aproximadamente 95% dos valores da normal-padrão estão abaixo de 1.65. E se quisermos fazer a pergunta contrária: qual o valor de x tal que 95% dos valores da curva estejam abaixo deste valor? Para isso usamos a função qnorm:

qnorm(0.95)
## [1] 1.644854

Para calcularmos os valores da função densidade utilizamos a função dnorm. Vejamos como fazer isso reproduzindo os gráficos da função densidade exibidos anteriormente:

# Sequencia de -3 a 3 igualmente espaçada e
# com valores redondos
x <- pretty(c(-3, 3), 1000)

# Função densidade de -3 a 3
y <- dnorm(x)

# Gráfico Função Densidade
plot.new()
plot.window(xlim=range(x), ylim=range(y))
axis(1); axis(2)
polygon(x, y, col = "lightblue")
title(main = "Distribuição Normal \nFunção Densidade")

# Gráfico Função Densidade Hachurado
plot.new()
plot.window(xlim=range(x), ylim=range(y))
axis(1);axis(2)
polygon(x, y, col = "lightblue")
title(main = "Distribuição Normal \nFunção Densidade")
z <- 1.65
lines(c(z, z), y = c(dnorm(-3), dnorm(z)))
polygon(c(x[x<=z], z), c(y[x<=z], min(y)), density = 10, angle = 45)
text(x = z + 0.3, y = dnorm(z) + 0.01,"1.65")

Exemplo 1: Teorema Central do Limite

O teorema cental do limite nos diz que, sob certas condições de regularidade (como variância finita), quanto mais observações tivermos, a distribuição amostral da média de uma variável aleatória será aproximadamente normal, independentemente do formato original da distribuição.

Vejamos um exemplo com a distribuição exponencial. A função densidade da exponencial pode ser escrita como f(x) = \lambda e^{-\lambda x}, com média E(X) = \frac{1}{\lambda} e desvio padrão DP(X) = \frac{1}{\lambda}.

Para nosso exemplo, tomaremos \lambda = 1. Assim, temos que E(X) = DP(X) = 1 e, segundo o teorema central do limite, a variável (\bar{x}-1)\sqrt{n} tende a uma normal-padrão (\bar{x} é a média amostral de x)

Note que o formato do histograma da exponencial (\lambda = 1) não se parece com o formato de sino da distribuição normal, que vimos na seção anterior:

# semente para reproducibilidade
set.seed(10)

# geramos 1000 variávels aleatórias de uma distribuição exponencial
x <- rexp(n = 1000, rate = 1)

# histograma
hist(x, col = "lightblue", main = "Distribuição Exponencial", freq = F)

plot of chunk hist exp

Entretanto, o que ocorre com a distribuição de (\bar{x}-1)\sqrt{n}  quando aumentamos o valor de n? Façamos uma simulação para seis valores de tamanho amostral diferentes: 1, 5, 10, 100, 500 e 1000.

# Simulacõees TCL - exponencial

# semente para reproducibilidade
set.seed(100)

# diferentes tamanhos amostrais que iremos simular
n <- c(1, 5, 10, 100, 500, 1000)

# número de replicações para cada n
n.rep <- 1000

## simulações
sims <- lapply(n, function(n) replicate(n.rep, (mean(rexp(n)) - 1)*sqrt(n)))

Na prática, a simulação toda foi feita com apenas uma linha, combinando o lapply com replicate. Explicando melhor o código acima, com o comando lapply(n, ...) estamos dizendo para o R que iremos aplicar uma função para cada valor de n. Mas que função estamos aplicando? Neste caso, a função anômima function(n) replicate(n.rep, (mean(rexp(n)) - 1)*sqrt(n)). Mais detalhadamente, com o comando replicate(n.rep, (mean(rexp(n)) - 1)*sqrt(n))) repetimos n.rep vezes a expressão (mean(rexp(n)) - 1)*sqrt(n)), que nada mais é do que a média padronizada de uma exponencial (\lambda = 1) de tamanho amostral n multiplicada por \sqrt{n}

O resultado de nossas simulações está na lista sims que tem a seguinte estrutura:

## nomes para as listas
names(sims) <- as.character(n)

## estrutura do resultado
str(sims)
## List of 6
##  $ 1   : num [1:1000] -0.0758 -0.2762 -0.8954 2.0974 -0.3752 ...
##  $ 5   : num [1:1000] -0.25122 0.00986 -1.19437 -1.29553 1.14441 ...
##  $ 10  : num [1:1000] -0.237 1.042 0.523 1.228 0.929 ...
##  $ 100 : num [1:1000] -0.303 0.43 -0.25 -0.339 -0.659 ...
##  $ 500 : num [1:1000] 2.072 0.239 -0.91 0.421 -0.353 ...
##  $ 1000: num [1:1000] -1.0039 0.0282 -0.0259 -0.0925 -0.9421 ...

Perceba que temos uma lista com 6 elementos, um para cada n diferente. Você pode acessar os resultados da lista ou pelo índice ou pelo nome do elemento:

# pega os resultados de n = 1000
sims[[6]]
sims[["1000"]]

Vejamos todos os resultados da simulação ao mesmo tempo em um gráfico. O histograma dos valores simulados estão em azul claro e a função densidade da normal-padrão em vermelho.

plot of chunk tcl ggplot2

Quando n = 1, a distribuição segue o mesmo formato da exponencial. Todavia, note que a convergência para a distribuição normal ocorre bem rapidamente neste exemplo. Com n = 100 as diferenças entre a normal e os dados simulados já se tornam bastante pequenas.

Fizemos o gráfico acima com o ggplot2:

library(ggplot2)
library(reshape2)

# Prepara base de dados para gráfico
## Transforma em data.frame
sims.df <- as.data.frame(do.call("cbind", sims))

## Empilha para o ggplot2
sims.df <- melt(sims.df,
                variable.name = "n",
                value.name = "Valor")
sims.df$n <- paste("n =", sims.df$n)
sims.df$n <- factor(sims.df$n, levels = unique(sims.df$n))

# Histogramas vs Densidade Normal (ggplot2)
ggplot(sims.df, aes(x = Valor)) +
  # Histograma
  geom_histogram(aes(y = ..density..),
                 fill = "lightblue",
                 col = "black",
                 binwidth = 0.5) + xlim(c(-6, 6)) +
  # Uma faceta para cada n
  facet_wrap(~n) +
  ## Densidade da normal(0,1) para comparação
  stat_function(fun = dnorm,
                col = "red", size = 0.8) +
  # Titulo principal e do eixo Y
  ggtitle("Teorema Central do Limite\nDistribuição Expoencial") +
  ylab("Densidade") +
  ## Tema em preto e branco
  theme_bw()

Sua vez!

Nós simulamos o teorema central do limite usando funções da família apply: lapply e replicate. Isso permite nos expressarmos de maneira bastante concisa, em apenas uma linha.

Como você faria a mesma simulação usando loops? Compare os resultados e veja se eles são idênticos.

# Resposta sugerida

# Com FOR
## para reproducibilidade
set.seed(100)
## tamanho amostral
n <- c(1, 5, 10, 100, 500, 1000)
## numero de replicacoes
n.rep <- 1000

# lista para armazenar os resultados para cada n
sims.for <- vector("list", length(n))

## começo do for
## faremos n.rep replicacoes para cada n
## para cada i de n
for (i in seq_along(n)) {

  # crie um vetor temporario para realizar n.rep repetições
  temp <- numeric(n.rep)

  # realiza n.rep repetições de (mean(rexp(n[i])) - 1)*sqrt(n[i])
  for (j in 1:n.rep) {
    temp[j] <- (mean(rexp(n[i])) - 1)*sqrt(n[i])
  }

  # guarda resultado na lista
  sims.for[[i]] <- temp
}

# nomes para os resultados da lista
names(sims.for) <- n

# compara com simulação anterior
all.equal(sims, sims.for)
## [1] TRUE

Introdução ao ggplot2


***

Parte do livro Introdução à análise de dados com R.  Este trabalho está em andamento, o texto é bastante preliminar e sofrerá muitas alterações. 

Quer fazer sugestões? Deixe um comentário abaixo ou, se você sabe utilizar o github, acesse aqui.

Não copie ou reproduza este material sem autorização.

Volte para ver atualizações!

***

Utilizando gráficos para explorar sua base de dados

Os gráficos base do R são bastante poderosos e com eles é possível fazer muita coisa. Entretanto, eles podem ser um pouco demorados para explorar dinamicamente sua base de dados. O pacote ggplot2 é uma alternativa atraente para resolver este problema. O ggplot2 é um pouco diferente de outros pacotes gráficos pois não segue a lógica de desenhar elementos na tela; ao invés disso, a sintaxe do ggplot2 segue uma “gramática de gráficos estatísticos” baseada no Grammar of Graphics de Wilkinson (2005).

No começo, pode parecer um pouco diferente essa forma de construir gráficos. Todavia, uma aprendidos os conceitos básicos da gramática, você vai pensar em gráficos da mesma forma que pensa numa análise de dados, construindo seu gráfico iterativamente, com visualizações que ajudem a revelar padrões e informações interessantes gastando poucas linhas de código. É um investimento que vale a pena.

Nesta seção, faremos uma breve introdução ao pacote ggplot2, destacando seus principais elementos. Para um tratamento mais aprofundado, recomenda-se o livro do Hadley Wickham.

Antes de continuar, você precisa instalar e carregar os pacotes que vamos utilizar nesta seção. Além do próprio ggplot2, vamos utilizar também os pacotes ggthemes e gridExtra.

# Instalando os pacotes (caso não os tenha instalados)
install.packages(c("ggplot2","ggthemes", "gridExtra"))

# Carregando os pacotes
library(ggplot2)
library(ggthemes)
library(gridExtra)

Também vamos utilizar uma base de dados de anúncio de imóveis de Brasília que você pode baixar aqui ou carregar com o comando abaixo. Vamos utilizar apenas os dados de venda.

# Carrega arquivo
arquivo <- url("https://dl.dropboxusercontent.com/u/44201187/imoveis.rds")
con <- gzcon(arquivo)
dados <- readRDS(con)

#  Filtra apenas para venda
venda <- dados[dados$tipo == "venda", ]

A “gramática dos gráficos”

Mas o que seria essa gramática de gráficos estatísticos? Podemos dizer que um gráfico estatístico é um mapeamento dos dados para propriedades estéticas (cor, forma, tamanho) e geométricas (pontos, linhas, barras) da tela. O gráfico também pode conter transformações estatísticas e múltiplas facetas para diferentes subconjuntos dos dados. É a combinação de todas essas camadas que forma seu gráfico estatístico.

Deste modo, os gráficos no ggplot2 são construídos por meio da adição de camadas. Cada camada, grosso modo, é composta de:

  • Uma base de dados (um data.frame, preferencialmente no formato long);
  • Atributos estéticos (aesthetics);
  • Objetos geométricos;
  • Transformações estatísticas;
  • Facetas; e,
  • Demais ajustes.

Vejamos um exemplo simples de scatter plot com os dados de preço e metro quadrado dos imóveis da nossa base de dados.

ggplot(data=venda, aes(x=m2, y=preco)) + geom_point()

Traduzindo o comando acima do gpplot2, nós começamos chamando a função ggplot() que inicializa o gráfico com os seguintes parâmetros:

  • data: aqui indicamos que estamos usando a base de dados venda;
  • aes: aqui indicamos as estéticas que estamos mapeando. Mais especificamente, estamos dizendo que vamos mapear o eixo x na variável m2 e o eixo y na variável preco.

Em seguida, adicionamos um objeto geométrico:

  • geom_point(): estamos falando ao ggplot que queremos adicionar o ponto como objeto geométrico.

Com relação às transformações estatísticas, neste caso não estamos realizando nenhuma. Isto é, estamos plotando os dado sem quaisquer modificações. Em termos esquemáticos, nós estamos fazendo o seguinte mapeamento:

O que resulta no seguinte gráfico:

plot of chunk unnamed-chunk-5

aes: mapeando cor, tamanho, forma etc

Um gráfico no plano tem apenas duas coordenadas, x e y, mas nossa base de dados tem, em geral, vários colunas… como podemos representá-las? Uma forma de fazer isso é mapear variáveis em outras propriedades estéticas do gráfico, tais como cor, tamanho e forma. Isto é, vamos expandir as variáveis que estamos meapeando nos aesthetics.

Para exemplificar, vamos mapear cada bairro em uma cor diferente e o número de quartos no tamanho dos pontos.

ggplot(data=venda, aes(x = m2, y = preco, color = bairro, size = quartos)) + 
  geom_point()

Nosso esquema anterior ficaria da seguinte forma.

E o gráfico resultante:

plot of chunk unnamed-chunk-7

Note que este gráfico revela aspectos diferentes da base de dados, como alguns registros possivelmente errados (imóvel com 30 quartos) e concentração de imóveis grandes em determinados bairros.

Mapear é diferente de determinar

Uma dúvida bastante comum quando as pessoas começam a aprender o ggplot2 é a diferença entre mapear variáveis em certo atributo estético e determinar certo atributo estético.

Quando estamos mapeando variáveis, fazemos isso dentro do comando aes(). Quando estamos apenas mudando a estética do gráfico, sem vincular isso a alguma variávei, fazemos isso fora do comando aes().

Por exemplo, no comando abaixo mudamos a cor, o tamanho e a forma dos pontos do scatter plot. Entretanto, essas mudanças foram apenas cosméticas e não representam informações de variáveis da base de dados e, portanto, não possuem legenda.

# muda o tamanho, a cor e a forma dos pontos
# note que não há legenda, pois não estamos 
# mapeando os dados a atributos estéticos
ggplot(data=venda, aes(x=m2, y=preco)) + 
  geom_point(color="darkblue", shape=21, size = 5)

plot of chunk unnamed-chunk-8

geoms: pontos, retas, boxplots, regressões

Até agora vimos apenas o geom_poin(), mas o ggplot2 vem com vários geoms diferentes e abaixo listamos os mais utilizados:

Tipo de Gráfico geom
scatterplot (gráfico de dispersão) geom_point()
barchart (gráfico de barras) geom_bar()
boxplot geom_boxplot()
line chart (gráfico de linhas) geom_line()
histogram (histograma) geom_histogram()
density (densidade) geom_density()
smooth (aplica modelo estatístico) geom_smooth()

Aqui, em virtude do espaço, mostraremos apenas um exemplo de gráfico de densidade e boxplot. Experimente em seu computador diferentes geoms na base de dados de imóveis.

# Density
ggplot(data=venda, aes(x=preco)) + geom_density(fill = "darkred")

# Boxplot
ggplot(data=venda, aes(x=bairro, y=preco)) + geom_boxplot(aes(fill = bairro))

Combinando aes e geom

Os gráficos do ggplot2 são construídos em etapas e podemos combinar uma série de camadas compostas de aes e geoms diferentes, adicionando informações ao gráfico iterativamente.

Toda informação que você passa dentro do comando inicial ggplot() é repassada para os geoms() seguintes. Assim, as estéticas que você mapeia dentro do comando ggplot() valem para todas as comadas subsequentes; por outro lado, as estéticas que você mapeia dentro dos geoms valem apenas para aquele geom especificamente. Vejamos um exemplo.

O comando abaixo mapeia o bairro como cor dentro do comando ggplot(). Dessa forma, tanto nos pontos geom_point(), quanto nas regressões geom_smooth() temos cores mapeando bairros, resultando em várias regressões diferentes.

# aes(color) compartilhado
ggplot(venda, aes(m2, preco, color=bairro)) + geom_point() + 
  geom_smooth(method="lm") 

plot of chunk unnamed-chunk-11

Mas e se você quisesse manter os pontos com cores diferentes com apenas uma regressão para todas observações? Neste caso, temos que mapear os bairros nas cores apenas para os pontos. Note que no comando a seguir passamos a estética color = bairro apenas para geom_poin().

# aes(color) apenas nos pontos
ggplot(venda, aes(m2, preco)) + geom_point(aes(color=bairro)) + 
  geom_smooth(method="lm") 

plot of chunk unnamed-chunk-13

Revelando padrões

A combinação simples de estéticas e formas geométricas pode ser bastante poderosa para revelar padrões interessantes nas bases de dados. Vejamos um caso ilustrativo.

Cilindradas, cilindros e Milhas por Galão

A base de dados mpg contém informações sobre eficiência no uso de combustível para diversos modelos de carro de 1999 a 2008. Vejamos um scatter plot relacionando cilindradas e consumo medido por milhas por galão:

ggplot(mpg, aes(displ, hwy)) + geom_point()

plot of chunk unnamed-chunk-14

A imagem parece revelar uma relação não linear entre cilindradas e milhas por galão. Vejamos, todavia, o mesmo gráfico mapeando o número de cilindros nas cores:

ggplot(mpg, aes(displ, hwy, col=factor(cyl))) + geom_point() + 
  geom_smooth(method = "lm")

plot of chunk unnamed-chunk-15

Note que o gráfico parece revelar que, uma vez condicionada ao número de cilindros, a relação entre cilindradas e milhas por galão é razoavelmente linear!

Adicionando facetas

No ggplot2(), você pode dividir o gráfico em diversos subgráficos utilizando variáveis categóricas. Vejamos um exemplo utilizando facet_wrap().

ggplot(venda, aes(m2, preco)) + 
  geom_point(aes(col=factor(quartos))) + 
  geom_smooth(method="lm") + 
  facet_wrap(~bairro) 

plot of chunk unnamed-chunk-16

Personalizando seu o gráfico

Depois de chegar em um gráfico interessante, você provavelmente vai querer personalizar detalhes estéticos deste gráfico para apresentá-lo ao público. No ggplot2 é possível fazer o ajuste fino de diversos elementos do seu gráfico e o detalhamento disso fugiria ao escopo deste livro.

Entretanto, vejamos um exemplo de histograma com a personalização de alguns elementos, adicionando labels, títulos, e mudando o fundo para branco:

media <- mean(log(venda$preco))
dp <- sd(log(venda$preco))
ggplot(data=venda, aes(x=log(preco))) + 
  geom_histogram(aes(y = ..density..), binwidth=0.3, fill="lightblue", col="black") +
  stat_function(fun=dnorm, args=list(mean=media, sd=dp), color="red") +
  geom_rug() + # adiciona rug no eixo x
  xlab("Log do Preço") + # adiciona descrição do eixo x
  ylab("Densidade") + # adiciona descrição do eixo y
  ggtitle("Histograma Preços de Imóveis") + # adiciona título
  theme_bw() # adciona tema "Black and White"

plot of chunk unnamed-chunk-17

Ficou bonito, não?

Temas pré prontos – ggthemes

O pacote ggthemes já vem com vários temas pré-programados, replicando formatações de sites como The Economist, The Wall Street Journal, FiveThirtyEight, ou de outros aplicativos como o Stata, Excel entre outros. Esta é uma forma rápida e fácil de adicionar um estilo diferente ao seu gráfico.

Experimente com os temas abaixo (gráficos omitidos aqui):

grafico <- ggplot(mpg, aes(displ, hwy, col=factor(cyl))) + geom_point() + 
  geom_smooth(method = "lm", se = F) + ggtitle("Cilindradas, cilindros e Milhas por Galão") + 
  ylab("Milhas por galão") + xlab("Cilindradas")

# Gráfico original
grafico 
# Tema "The Economist" com respectiva escala de cores
grafico + theme_economist() + scale_color_economist()
# Tema "The Wall Street Journal" com respectiva escala de cores
grafico + theme_wsj() + scale_color_wsj()
# Tema "Excel" com respectiva escala de cores
grafico + theme_excel() + scale_color_excel()
# Tema "fivethirtyeight"
grafico + theme_fivethirtyeight() 
# Tema "highcharts" com respectiva escala de cores
grafico + theme_hc()  + scale_color_hc()
# Tema "Tufte" 
grafico + theme_tufte() 
# Tema "Stata" com respectiva escala de cores
grafico + theme_stata() + scale_color_stata()

Vários gráficos juntos

Por fim, uma última dica e como colocar vários gráficos juntos com a função grid.arrange().

g1 <- grafico + theme_fivethirtyeight() 
g2 <- grafico + theme_hc() + scale_color_hc()
g3 <- grafico + theme_tufte() 
g4 <- grafico + theme_stata() + scale_color_stata()
grid.arrange(g1, g2, g3, g4)

plot of chunk unnamed-chunk-19

Introdução ao dplyr


***

Parte do livro Introdução à análise de dados com R.  Este trabalho está em andamento, o texto é bastante preliminar e sofrerá muitas alterações. 

Quer fazer sugestões? Deixe um comentário abaixo ou, se você sabe utilizar o github, acesse aqui.

Não copie ou reproduza este material sem autorização.

Volte para ver atualizações!

***

Eficiente e intuitivo

Com as funções da família apply e similares, você consegue fazer praticamente tudo o que você precisa para explorar os dados e deixá-los no(s) formato(s) necessário(s) para análise. E é importante você ser exposto a essas funções para se familiarizar com o ambiente base do R.

Entretanto, muitas vezes essas funções podem ser pouco intuitivas para o iniciante e, além disso, deixar a desejar em performance. Como alternativa, existe um pacote bastante rápido para manipulação de data.frames e com sintaxe muito intuitiva chamado dplyr. É provável que para o grosso de suas necessidades o dplyr seja a solução mais rápida e mais eficiente.

Se você ainda não tem o dplyr instalado na sua máquina, rode o seguinte comando.

install.packages("dplyr", repos = "http://cran.r-project.org")

Depois carregue o pacote:

library(dplyr)

Nesta seção continuaremos utilizando a base de dados de web scraping de imóveis utilizada na seção anterior (você pode baixar aqui).

Funções principais

As funções do dplyr são todas em formas de verbo e, se você sabe um pouco de inglês, são razoavelmente auto-explicativas. Vamos ver aqui as seis principais funções que, em conjunto, já te permitem fazer bastante coisa. Todas as funções possuem a mesma estrutura: o primeiro argumento é o data.frame que você quer manipular e os argumentos subsequentes dizem o que fazer com os dados. Além disso, as funções do dplyr sempre retornam um data.frame, isto é, ela não vai tentar simplificar o resultado para um vetor ou outro tipo de objeto.

  • filter: filtra um data.frame com vetores lógicos. Em outras palavras, deixa apenas as linhas que satisfazem a certo critério. Por exemplo, nos nossos dados de imóveis, poderíamos filtrar somente aqueles anúncios de aluguel com valores de preço por metro quadrado menores ou maiores do que determinado nível.
# com dplyr
filter(dados, pm2 > 96, tipo == "aluguel") 

# equivalente com R base
dados[dados$pm2 > 96 & dados$tipo == "aluguel", ]
  • select: seleciona uma ou mais colunas de um data.frame. Por exemplo, poderíamos selecionar a coluna de preços e quartos do anúncio.
# com dplyr
select(dados, preco, quartos)

# equivalente com R base
dados[, c("preco", "quartos")]
  • mutate: cria uma nova coluna. Por exemplo, vamos criar a coluna pm2 como preco/m2.
# com dplyr
dados <- mutate(dados, pm2 = preco/m2)

# equivalente com R base
dados$pm2 <- dados$preco/dados$m2
  • arrange: orderna o data.frame com base em uma coluna. Por exemplo, ordernar observações dos apartamentos segundo os preços por metro quadrado.
# com dplyr
arrange(dados, pm2)

# equivalente com R base
dados[order(dados$pm2), ]

Os próximos dois verbos são group_by e summarise que, em geral, são utilizados em conjunto com outros verbos. Deixaremos, assim, para ver exemplos com essas estruturas logo a seguir, quando conectarmos todos os verbos do dplyr com o operador %>%.

  • group_by: agrupa um data.frame segundo um vetor de categorias. “Agrupar” aqui quer dizer que todas as operações subsequentes serão feitas separadas por grupos. É equivalente ao split, que vimos antes.
  • summarise: o summarise transforma um vetor com vários números (por exemplo, um vetor de preços) em um único número de acordo com uma função (por exemplo, preço médio ou preço mediano).

Conectando tudo com %>%

Até agora talvez você não tenha visto muita vantagem em utilizar o dplyr. Escrever filter(dados, pm2 > 96, tipo == "aluguel") pode ser mais intuitivo do que dados[dados$pm2 > 96 & dados$tipo == "aluguel", ] mas talvez não tanto assim. A grande vantagem do pacote e das funções em formas de verbo aparece quando concatenamos várias operações em sequência com o auxílio do operador %>%.

O dplyr foi desenhado para trabalhar com o pipe operator %>% do pacote magritrr. Basicamente, o operador faz com que você possa escrever x %>% f() ao invés de f(x). Pode não parecer muito, mas na prática esse pequeno detalhe tem uma grande utilidade: você vai poder escrever o código de manipulação dos dados da mesma forma que você pensa nas atividades.

Pense numa receita que tenha as seguintes instruções: junte os igredientes, misture e leve ao forno. Na forma usual do R, tais instruções provavelmente teriam a seguinte forma:

forno(misture(junte(ingredientes)))

Note que temos que pensar “de dentro para fora”. O primeiro comando que lemos é forno que, na verdade, é a última operação que será realizada! Com o pipe operator, por outro lado, você escreveria algo mais ou menos assim:

ingredientes %>% junte %>% misture %>% forno

É quase igual a ler instruções verbais da receita. Parece mais intuitivo, não?

Vejamos exemplos de manipulações de dados com o dplyr usando nossa base de dados de imóveis.

Filtrando, selecionando e criando colunas

Instrução: Filtre apenas os dados coletados de apartamento, selecione as colunas bairro e preco, crie uma coluna pm2 = preco/m2, ordene os dados de forma decrescente em pm2 e mostre apenas as 6 primeiras linhas (head).

Código:

dados %>% filter(imovel=="apartamento") %>%  
  select(bairro, preco, m2) %>% mutate(pm2 = preco/m2) %>% 
  arrange(desc(pm2)) %>% head
## Source: local data frame [6 x 4]
## 
##    bairro   preco     m2      pm2
##     (chr)   (dbl)  (dbl)    (dbl)
## 1 Asa Sul 4259579 215.72 19745.87
## 2 Asa Sul 4259579 215.72 19745.87
## 3 Asa Sul 4259579 215.72 19745.87
## 4 Asa Sul 4259579 215.72 19745.87
## 5 Asa Sul 4259579 215.72 19745.87
## 6 Asa Sul 4259579 215.72 19745.87

Agrupando e sumarizando

Instrução: Filtre apenas os dados de venda de apartamento. Agrupe os dados por bairro. Calcule as medianas do preco, m2 e pm2, bem como o número de observações. Filtre apenas os grupos com mais de 30 observações. Ordene de forma decrescente com base na mediana de pm2.

Código:

dados %>% 
  filter(imovel=="apartamento", tipo=="venda") %>% 
  group_by(bairro) %>% 
  summarise(Mediana_Preco = median(preco),
            Mediana_M2 = median(m2),
            Mediana_pm2 = median(pm2),
            Obs = length(pm2)) %>% 
  filter(Obs > 30) %>%
  arrange(desc(Mediana_pm2))
## Source: local data frame [6 x 5]
## 
##       bairro Mediana_Preco Mediana_M2 Mediana_pm2   Obs
##        (chr)         (dbl)      (dbl)       (dbl) (int)
## 1   Sudoeste        850000      86.00    9689.922 20356
## 2   Noroeste        950000     100.90    9654.000 36610
## 3    Asa Sul        950000     107.00    9066.667 35241
## 4  Asa Norte        780000      94.00    9000.000 40023
## 5 Lago Norte        470000      57.55    8329.250  5938
## 6   Lago Sul        488307      88.55    5199.120   477

Exercícios de fixação

Considerando a base de dados, responda:

  • Qual o bairro com o maior preço mediano de venda?
  • Qual o bairro com o maior preço por m2 de venda?
  • Qual o bairro com o maior preço mediano de venda para apartamentos?
  • Qual o bairro com o maior preço mediano de venda para lojas?

Soluções:

# Qual o bairro com o maior preço mediano de venda?
dados %>% 
  filter(tipo == "venda") %>% # Filtra tipo "venda"
  group_by(bairro) %>%  # agrupa por bairro
  summarise(mediana = median(preco)) %>% # calcula mediana do preco
  arrange(desc(mediana)) # ordena de forma decrescente
## Source: local data frame [6 x 2]
## 
##       bairro   mediana
##        (chr)     (dbl)
## 1   Lago Sul 2800000.0
## 2   Noroeste  950000.0
## 3    Asa Sul  834572.1
## 4 Lago Norte  679000.0
## 5  Asa Norte  600000.0
## 6   Sudoeste  520000.0
# Qual o bairro com o maior preço por m2 de venda?
dados %>% 
  filter(tipo == "venda") %>% # Filtra tipo "venda"
  group_by(bairro) %>%  # agrupa por bairro
  summarise(mediana = median(pm2)) %>% # calcula mediana do pm2
  arrange(desc(mediana)) # ordena de forma decrescente
## Source: local data frame [6 x 2]
## 
##       bairro  mediana
##        (chr)    (dbl)
## 1   Noroeste 9666.561
## 2   Sudoeste 9473.684
## 3    Asa Sul 9210.526
## 4  Asa Norte 9000.000
## 5 Lago Norte 6753.247
## 6   Lago Sul 5516.129
# Qual o bairro com o maior preço mediano de venda para apartamentos?
dados %>% 
  filter(tipo == "venda", imovel == "apartamento") %>% # Filtra tipo "venda" e imovel "apartamento"
  group_by(bairro) %>%  # agrupa por bairro
  summarise(mediana = median(preco)) %>% # calcula mediana do preco
  arrange(desc(mediana)) # ordena de forma decrescente
## Source: local data frame [6 x 2]
## 
##       bairro mediana
##        (chr)   (dbl)
## 1    Asa Sul  950000
## 2   Noroeste  950000
## 3   Sudoeste  850000
## 4  Asa Norte  780000
## 5   Lago Sul  488307
## 6 Lago Norte  470000
# Qual o bairro com o maior preço mediano de venda para lojas?
dados %>% 
  filter(tipo == "venda", imovel == "loja") %>% # Filtra tipo "venda" e imovel "loja"
  group_by(bairro) %>%  # agrupa por bairro
  summarise(mediana = median(preco)) %>% # calcula mediana do preco
  arrange(desc(mediana)) # ordena de forma decrescente
## Source: local data frame [6 x 2]
## 
##       bairro mediana
##        (chr)   (dbl)
## 1   Noroeste 1107735
## 2    Asa Sul 1050000
## 3  Asa Norte  550000
## 4 Lago Norte  430000
## 5   Sudoeste  330000
## 6   Lago Sul  320000