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.

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

Dividir, Aplicar e Combinar (Split, Apply and Combine)


***

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.

As partes do livro não estão sendo publicadas na ordem – por exemplo, a seção abaixo não é uma explicação da família apply, mas sim da estratégia split-apply-combine e de algumas funções que podem ser utilizadas nesta estratégia (como o trio split, lapply, do.call,  a função tapply, ou a função aggregate).  A explicação sobre diferentes funções apply e derivados (applysapplymapplylapplyvapplyrapplyreplicate, rowSums, colSums, rowMeans, colMeans) é feita em seção anterior a essa seção (ainda não disponível no blog).

Volte para ver atualizações!

***

Um padrão recorrente

Nossa base de dados (clique aqui para download) contém preços tanto de aluguel quanto de venda de apartamentos. Suponha que queiramos tirar a médias dos preços. Não faz muito sentido tirar a média dos dois tipos (aluguel e venda) juntos, certo? Como poderíamos fazer isso então? Uma possível solução seria a seguinte:

Primeiramente, dividimos a base de dados, criando duas outras, uma para cada grupo: aluguel e venda.

# 1) separar a base em duas bases diferentes:
#    - aluguel; e,
#    - venda
aluguel <- dados[dados$tipo == "aluguel", ]
venda <- dados[dados$tipo == "venda", ]

Em seguida nós calculamos a média para cada uma das novas bases que criamos.

# 2) calcular a média para cada uma das bases
media_aluguel <- mean(aluguel$preco)
media_venda   <- mean(venda$preco)

Por fim, nós combinamos os resultados em um único vetor:

# 3) combinar os resultados em um único vetor
medias = c(aluguel = media_aluguel, venda = media_venda)
medias
##     aluguel       venda
##    4594.757 1232719.524

Pronto! Calculamos as duas médias que queríamos. Todavia, note que gastamos cerca de 5 linhas para chegar ao resultado – e que foram 5 linhas porque temos apenas 2 categorias (alguel e venda) neste exemplo. Imagine se tívessemos que analisar separadamente 200 ou 2000 categorias? Teríamos que criar uma base separada para cada uma delas?

Não necessariamente. Na verdade podemos fazer isso (ou coisas mais complexas) com apenas uma ou duas linhas. Para você saber aonde queremos chegar, seguem alguns exemplos mais sucintos que realizam o mesmo cálculo feito anteriormente:

# com tapply
tapply(X = dados$preco, INDEX = dados$tipo, FUN = mean)
##     aluguel       venda
##    4594.757 1232719.524
# com aggregate
aggregate(x = list(media = dados$preco), by = list(tipo = dados$tipo),
          FUN = mean)
##      tipo       media
## 1 aluguel    4594.757
## 2   venda 1232719.524
# com dplyr
library(dplyr)
dados %>% group_by(tipo) %>% summarise(media = mean(preco))
## Source: local data frame [2 x 2]
##
##      tipo       media
## 1 aluguel    4594.757
## 2   venda 1232719.524

Mas, antes de entrarmos nas formas mais concisas de escrever essas operações, vejamos, primeiramente, como fazê-las com algumas funções mais básicas do R, o que vai lhe permitir maior flexibilidade em alguns casos.

Dividir, Aplicar e Combinar (Split, Apply and Combine)

O padrão de análise descrito na seção anterior é bastante recorrente quando trabalhamos com dados. Dentro da comunidade do R, esse processo é conhecido como Dividir, Aplicar e Combinar (DAC) ou, em inglês, Split, Apply and Combine (SAC). Em nosso caso específico, nós pegamos um vetor (o vetor de preços), dividimos segundo algum critério (por tipo), aplicamos um função em cada um dos grupos separadamente (no nosso caso, a média) e depois combinamos os resultados novamente.

dac_1

Para quem conhece SQL, muitas dessas operações são similares ao group by, ou, para quem usa Excel, similar a uma tabela dinâmica – mas note que não são coisas exatamente equivalentes, pois o conceito aqui é mais flexível. Isso vai ficar mais claro na medida em que usarmos exemplos mais complicados.

Apesar de só estarmos introduzindo o conceito de DAC agora, nós, na verdade, já tínhamos visto este padrão diversas vezes quando estudamos as funções do tipo apply. Por exemplo, ao aplicar uma função por linhas, você está dividindo a matriz por uma das dimensões (a dimensão das linhas), aplicando funções a cada uma das partes (a cada uma das linhas) e combinando os resultados em um único vetor:

dac_2

Vejamos algumas peças para construir essa estratégia de análise de dados usando as funções base do R.

Dividir: a função split

A primeira função que você deve conehcer é a função split() (dividir, em inglês) que, literalmente, divide um objeto segundo um conjunto de características. A função tem a seguinte estrutura:

str(split)
## function (x, f, drop = FALSE, ...)

\pause

Em que os principais parâmetros são:

  • x: vetor ou data.frame que será divido;
  • f: fatores que irão definir os grupos de divisão.

O resultado da função é uma lista para cada fator, contendo os vetores ou data.frames do respectivo grupo.

Voltando ao nosso exemplo, vamos dividir nosso data.frame segundo a lista de fatores tipo (aluguel ou venda). Note que o resultado é uma lista contendo dois vetores: (i) um para aluguel; (ii) e outro para venda.

alug_venda <- split(dados$preco, dados$tipo)
str(alug_venda, max.level = 1)
## List of 2
##  $ aluguel: num [1:76085] 650 750 800 800 800 820 850 850 850 860 ...
##  $ venda  : num [1:216517] 159000 170000 175000 180000 180000 180000 185000 185000 190000 195000 ...

Aplicar e combinar – voltando à família apply

Como visto, o resultado do split é uma lista para cada categoria. Queremos aplicar uma função a cada um dos elementos dessa lista. Ora, já vimos uma função que faz isso: o lapply(). Dessa forma, com o comando lapply(alug_venda, mean) podemos calcular a média de cada um dos elementos da lista, separadamente:

medias <- lapply(alug_venda, mean)
medias
## $aluguel
## [1] 4594.757
##
## $venda
## [1] 1232720

Combinando os resultados com unlist()

Ok, estamos quase lá, já temos o resultado final, mas ele está no formato de uma lista, que não é um dos formatos mais convenientes. Geralmente, em casos como esse, queremos um vetor. Vamos, assim, tentar simplificar este objeto. Uma das formas de fazer isso seria utilizar uma função que nós tambem já vimos quando estudamos os objetos básicos do R: a função unlist()

unlist(medias)
##     aluguel       venda
##    4594.757 1232719.524

Combinando com do.call()

Existe outra função de conveniência que, em conjunto com rbind() e cbind() pode ser bastante útil para simplificar nossos resultados: a função do.call().

Como ela funciona?

A função do.call() tem a seguinte sintaxe: na primeira posição passamos, em formato texto, uma função que queremos utilizar (como cbind()); já na segunda posição passamos a lista de argumentos que que serão utilizadas pela função que está na primeira posição.

Em outras palavras – e mais diretamente – o comando:

do.call("alguma_funcao", lista_de_argumentos)

É equivalente a:

alguma_funcao(lista_de_argumentos[1],  lista_de_argumentos[2], ..., lista_de_argumentos[n])

No nosso caso, temos apenas duas médias, então não seria complicado elencar um a um os elementos no rbind() ou no cbind(). Todavia, imagine um caso em que temos centenas de médias. Nesta situação, a função do.call() é bastante conveniente.

do.call("rbind", medias)
##                [,1]
## aluguel    4594.757
## venda   1232719.524
do.call("cbind", medias)
##       aluguel   venda
## [1,] 4594.757 1232720

Aplicando e simplificando ao mesmo tempo: sapply()

Agora podemos encaixar conceitualmente outra função que já tínhamos aprendido, o sapply(). Essa função tenta fazer os dois passos do DAC ao mesmo tempo: Aplicar e Combinar (que neste caso é sinônimo de simplificar):

sapply(alug_venda, mean)
##     aluguel       venda
##    4594.757 1232719.524

Aplicando e simplificando ao mesmo tempo: vapply()

Existe, ainda, uma versão mais restrita do sapply(): o vapply(). A principal diferença entre eles é que o vapply() exige que você especifique o formato do resultado esperado da operação.

Enquanto o o primeiro é mais prático para uso interativo, o segundo é mais seguro para programar suas próprias funções, pois, se o resultado não vier conforme esperado, ele irá acusar o erro.

Por exemplo, no comando abaixo estamos dizendo explicitamente ao R que queremos um resultado do tipo numérico. Assim, se por acaso vier algo diferente, o R acusará um erro.

vapply(alug_venda, mean, numeric(1))
##     aluguel       venda
##    4594.757 1232719.524

Para ilustrar essa situação, suponha que esperássemos que o resultado do nosso cálculo fosse um vetor do tipo character com um elemento. Note agora R fornece uma mensagem de erro bastante explicativa.

vapply(alug_venda, mean, character(1))
## Error in vapply(alug_venda, mean, character(1)): valores devem ser do tipo 'character',
##  mas o resultado de FUN(X[[1]]) é de tipo 'double'

Dividir, Aplicar e Combinar: fazendo tudo ao mesmo tempo

Conhecer as funções mais fundamentais da estrutura Split, Apply, Combine é importante para você ter a flexibilidade de fazer análises mais personalizadas quando precisar. O exemplo anterior é bastante simples para podermos nos concentrar nos conceitos, entretanto, não se engane: combinando apenas as funções do tipo apply() com as funções split() e do.call() é possível fazer diversas operações relativamente complexas com poucas linhas (prepare-se para os exercícios!).

Contudo, existem funções que realizam grande parte do processo de manipulação de dados de uma forma mais simples e compacta (e muitas vezes mais intuitiva, como o dplyr!) e essas funções serão suficientes para a maior parte do seu trabalho!

Nessa seção veremos duas funções base do R: tapply() e aggregate().

DAC com tapply

A função tapply() tem a seguinte estrutura:

str(tapply)
## function (X, INDEX, FUN = NULL, ..., simplify = TRUE)
  • X: o objeto que será agregado. Ex: preços;
  • INDEX: uma lista de vetores que servirão de índice para agregar o objeto. Ex: bairros;
  • FUN: a função que será aplicada a X para cada INDEX. Ex: mediana.
  • simplify: tentará simplificar o resultado para uma estrutura mais simples?

Voltemos, assim, ao nosso exemplo inicial: calcular a mediana do metro quadrado para aluguel e para venda. Como ficaria com o tapply? Como tínhamos visto, basta uma única linha:

tapply(dados$pm2, dados$tipo, median)
##    aluguel      venda
##   35.71429 9000.00000

Podemos passar mais de um fator para a função? Sim! Vamos, por exemplo, calcular a mediana dos preços separadas por aluguel, venda e bairro. Note que o resultado, agora, não é um vetor, mas uma matriz:

tapply(dados$pm2, list(dados$bairro, dados$tipo), median)
##             aluguel    venda
## Asa Norte  35.55556 9000.000
## Asa Sul    38.68472 9210.526
## Lago Norte 34.61538 6753.247
## Lago Sul   32.83582 5516.129
## Noroeste   37.83784 9666.561
## Sudoeste   33.84307 9473.684

É possível colocar mais fatores ainda? Claro! Que tal a mediana por aluguel e venda, separada por tipo de imóvel e por bairro? Ao invés de uma matriz, como passamos três fatores, teremos como resultado um array com três dimensões:

tabelas <- tapply(dados$pm2, list(dados$imovel, dados$tipo, dados$bairro), median)
str(tabelas)
##  num [1:5, 1:2, 1:6] 31.4 29.2 32.7 48 45.7 ...
##  - attr(*, "dimnames")=List of 3
##   ..$ : chr [1:5] "apartamento" "casa" "kitinete" "loja" ...
##   ..$ : chr [1:2] "aluguel" "venda"
##   ..$ : chr [1:6] "Asa Norte" "Asa Sul" "Lago Norte" "Lago Sul" ...

Note que a primeira dimensão separa os resultados por tipo do imóvel; a segunda dimensão, por aluguel e venda; e, por fim, a terceira dimensão separa os resultados por bairros. Deste modo você pode filtrar o array em qualquer uma das três dimensões para selecionar um subconjunto dos valores. Por exemplo, ao selecionar apenas Asa Norte, obtemos uma matriz com as medianas de aluguel e venda, separadas por tipo de imóvel, mas somente deste bairro:

tabelas[,,"Asa Norte"]
##                 aluguel     venda
## apartamento    31.42857  9000.000
## casa           29.16667  5823.529
## kitinete       32.66667  8035.714
## loja           48.00000  5868.545
## sala-comercial 45.71429 10733.453

Já se selecionarmos vendas, obtemos uma matriz com as medianas de venda por tipo de imóvel, separadas por bairro. E assim por diante.

tabelas[,"venda",] # venda por tipo de imóvel e bairro
##                Asa Norte   Asa Sul Lago Norte Lago Sul  Noroeste Sudoeste
## apartamento     9000.000  9066.667   8329.250 5199.120  9654.000 9689.922
## casa            5823.529  5611.728   4375.000 5510.204        NA       NA
## kitinete        8035.714 12142.857   7621.467 8648.649  9641.136 8166.667
## loja            5868.545 11666.667   7818.182 8648.649 18729.515 8800.603
## sala-comercial 10733.453 10346.487  10600.000 6111.111 17872.222 8931.034

DAC com aggregate

O aggregate() é similar ao tapply() mas, ao invés de retornar um array, retorna um data.frame com dados empilhados (veremos mais detalhes sobre dados empilhados ao final deste capítulo) colocando uma coluna diferente para cada índice e apenas uma coluna com os valores.

A função aggregate() tem duas sintaxes principais.

A primeira, similar ao tapply() é:

aggregate(dados$valor, by=list(dados$indice1,
                               dados$indice2), funcao)

Já a segunda sintaxe utiliza a formula interface do R e é do tipo:

aggregate(valor ~ indice1 + indice2, dados, funcao)

Trataremos mais fundo como funcionam fórmulas no R em outro capítulo. Por equanto você pode ler a formula valor~indice1+indice2 da seguinte forma: queremos a variável valor separada (~) pelo indice1 e (+) pelo indice2.

Vejamos um exemplo do aggregate(). Vamos calcular a mediana do preço por metro quadrado, separada por bairro, venda ou aluguel, e tipo de imóvel. Note a diferença do formato deste resultado para o formato do tapply. Ao invés de termos três dimensões, temos um data.frame com uma coluna para cada fator (bairro, tipo e imovel) e apenas uma coluna de valores (pm2):

pm2_bairro_tipo_imovel <- aggregate(pm2 ~ bairro + tipo + imovel, data=dados, median)
str(pm2_bairro_tipo_imovel)
## 'data.frame':    55 obs. of  4 variables:
##  $ bairro: chr  "Asa Norte" "Asa Sul" "Lago Norte" "Lago Sul" ...
##  $ tipo  : chr  "aluguel" "aluguel" "aluguel" "aluguel" ...
##  $ imovel: chr  "apartamento" "apartamento" "apartamento" "apartamento" ...
##  $ pm2   : num  31.4 30.8 34 29.3 37.5 ...
head(pm2_bairro_tipo_imovel)
##       bairro    tipo      imovel      pm2
## 1  Asa Norte aluguel apartamento 31.42857
## 2    Asa Sul aluguel apartamento 30.83333
## 3 Lago Norte aluguel apartamento 34.04255
## 4   Lago Sul aluguel apartamento 29.28870
## 5   Noroeste aluguel apartamento 37.50000
## 6   Sudoeste aluguel apartamento 32.60870

O aggregate() também é mais flexível que o tapply() em outros aspectos. É possível passar mais de uma variável a ser agregada utilizando cbind() e, além disso, passar argumentos para fazer a análise de apenas um subconjunto (subset) dos dados.

Vejamos outro exemplo: vamos calcular a mediana do preço, do metro quadrado e do preço por metro quadrado dos valores de aluguel de apartamento separados por bairro.

mediana_aluguel <- aggregate(cbind(preco, m2, pm2) ~ bairro,
                             data = dados,
                             subset = (dados$tipo=="aluguel" &
                                       dados$imovel=="apartamento"),
                             FUN = median)
mediana_aluguel <- mediana_aluguel[order(mediana_aluguel$pm2, decreasing=TRUE), ]
mediana_aluguel
##       bairro preco   m2      pm2
## 5   Noroeste  2600 74.6 37.50000
## 3 Lago Norte  1800 55.0 34.04255
## 6   Sudoeste  2700 82.0 32.60870
## 1  Asa Norte  2300 70.0 31.42857
## 2    Asa Sul  2800 80.0 30.83333
## 4   Lago Sul  1500 51.0 29.28870

Note que fizemos várias coisas – filtramos o data.frame para selecionar apenas os dados de aluguel de apartamento, separamos por bairro, e calculamos a mediana para três variáveis – diretamente com o aggregate()

Funções: Definição, argumentos e operadores binários


***

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. As partes do livro não estão sendo publicadas na ordem. Volte para ver atualizaçõ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.

***

Por que funções?

Uma das grandes vantagens de usar uma linguagem de programação é automatizar o seu trabalho ou análise. Você será capaz de realizar grande parte do trabalho utilizando as funções internas do R ou de pacotes de terceiros em um script. Entretanto, você ganha ainda mais flexibilidade e agilidade criando suas próprias funções.

Vejamos um exemplo de script:

# vetor de precos em formato de texto ao invés de numérico
# e com registros errados (0.1 e 10000000)
# queremos converter para numérico, retirar os dados discrepantes
# dividir por 1000 e arredondar o resultado
precos <- c("0.1", "1250.55", "2346.87", "3467.40", "10000000")
precos <- as.numeric(precos)
precos <- precos[!(precos < 1 | precos > 10000)]
precos <- precos/1000
precos <- round(precos)
precos
## [1] 1 2 3

Nosso script faz o trabalho corretamente. Mas imagine que você queira realizar o mesmo procedimento com um vetor de preços diferente, digamos, precos2. Da forma como o seu código foi feito, você terá que copiar e colar os comandos e substituir os nomes.

# novo vetor de precos
precos2 <- c("0.0074", "5547.85", "2669.98", "8789.45", "150000000")
precos2 <- as.numeric(precos2)
precos2 <- precos2[!(precos2 < 1 | precos2 > 10000)]
precos2 <- precos2/1000
precos2 <- round(precos2)
precos2
## [1] 6 3 9

Note como isto é ineficiente. Além de ter que repetir todo o seu código para cada análise diferente que você desejar fazer, você ainda estará sujeito a diversos erros operacionais, como esquecer de trocar um dos nomes ao copiar e colar.

O ideal, aqui, é criar uma função que realize este trabalho.

Definindo funções

Uma função, no R, é definida da seguinte forma:

nomeDaFuncao <- function(arg1, arg2, arg3 = default3,  ...){
  # corpo da função: uma série de comados válidos.
  return(resultado) # opcional
}
  • o comando function() diz para o R que você está definindo uma função.
  • os valores dentro dos parênteses de function() são os argumentos (ou parâmetros) da função. Argumentos podem ter valores default (padrão), que são definidos com o sinal de igualdade (no caso arg3 tem como default o valor default3). Existe um parâmetro “coringa” muito útil, o ..., que permite passsar argumentos para outras funções. Veremos mais sobre o ... em seguida.
  • dentro das chaves encontra-se o “corpo” da função, isto é, uma série de comandos válidos que serão realizados.
  • o comando return() encerra a função e retorna seu argumento. Como veremos, o return() é opcional. Caso omitido, a função retorna o último objeto calculado.

Criemos nossas primeiras funções:

# retorna o quadrado de um número
quadrado <- function(x){
  x^2
}
quadrado(3)
## [1] 9
## forma mais sucinta
## se o corpo da função for na mesma linha não é necessário colocar chaves
quadrado <- function(x) x^2
quadrado(3)
## [1] 9
## função mais geral, elevar um número x à potência n
elevado_n <- function(x,n) x^n
elevado_n(3, 3)
## [1] 27

Funções criam um ambiente local e, em geral, não alteram o objeto ao qual são aplicadas. Isto é, se você passa um valor x para uma função que eleva x ao quadrado, o valor original de x não muda. Funções tomam objetos como argumentos e criam outro objeto, modificado, como resultado. Na maior parte dos casos, a idéia é que uma função no R não tenha efeitos colaterais, isto é, que ela não modifique objetos fora de seu ambiente.

x <- 10
elevado_n(x, 2) # isso alterou o valor de x?
## [1] 100
# note que não
x
## [1] 10
# se você quer salvar o resultado
# tem que atribuí-lo a outro objeto
y <- elevado_n(x, 2)
y
## [1] 100

Voltando ao exemplo

Montemos uma função que realiza o tratamento dos dados visto anteriormente:

limparDados <- function(dados){
  dados <- as.numeric(dados)
  dados <- dados[!(dados < 1 | dados > 10000)]
  dados <- dados/1000
  dados <- round(dados)
  return(dados)
}
ls() # note que a função foi criada
## [1] "elevado_n"   "limparDados" "precos"      "precos2"     "quadrado"
## [6] "x"           "y"

Vejamos em detalhes:

  • o comando function() diz para o R que você está definindo uma função.
  • os valores dentro dos parênteses de function() são os argumentos da função. No nosso caso definimos um único argumento chamado dados.
  • dentro das chaves encontra-se o “corpo” da função, isto é, as operações que serão realizadas. Neste caso, transformamos dados em numeric, retiramos aqueles valores menores do que 1 e maiores do que 10000, dividimos por 1000 e, finalmente, arredondamos.
  • a função return() encerra a função e retorna o vetor dados modificado.

Pronta a função, sempre que você quiser realizar essas operações em um vetor diferente, basta utilizar limparDados(). Testemos com novos vetores:

precos3 <-  c("0.02", "4560.45", "1234.32", "7894.41", "12000000")
precos4 <- c("0.001", "1500000", "1200.9", "2000.2", "4520.5")
precos5 <- c("0.05", "1500000", "1000000", "7123.4", "9871.5")

# limpando os dados
limparDados(precos3)
## [1] 5 1 8
limparDados(precos4)
## [1] 1 2 5
limparDados(precos5)
## [1]  7 10

Ficou bem melhor do que o script. Note que tínhamos 3 vetores diferentes e bastou chamar a função três vezes, ao invés de ter que copiar e colar três vezes o código. Note, também, que se houver algum erro, temos que corrigir apenas a definicão da função e não três partes distintas do código.

Mais argumentos

Podemos refinar mais a função limparDados(). Por exemplo, perceba que não função ainda não é geral o suficiente. Da forma como está escrita, os valores de corte de mínimo e de máximo serão sempre 1 e 10000; além disso, os resultados sempre serão dividos por 1000. E se quisermos modificar esses valores?

Para isso é necessário definir mais argumentos:

limparDados <- function(dados, min, max, div){
  dados <- as.numeric(dados)
  dados <- dados[!(dados < min | dados > max)]
  dados <- dados/div
  dados <- round(dados)
  return(dados)
}

Agora você pode alterar os valores de min, max e div ao aplicar a função.

precos3 <-  c("0.02", "4560", "1234", "7894", "12000000")

limparDados(precos3, min=0, max=5000, div=2)
## [1]    0 2280  617
limparDados(precos3, min=0, max=4000, div=4)
## [1]   0 308
limparDados(precos3, min=-Inf, max=Inf, div=1)
## [1]        0     4560     1234     7894 12000000

Veja que os argumentos são nomeados. Dessa forma, se você colocar os argumentos com seus respectivos nomes, a ordem dos argumentos não importa. Você também pode omitir os nomes dos argumentos, desde que os coloque na ordem correta.

#  argumentos em ordem diferente
limparDados(max=5000, div=2, min=0, dados=precos3)
## [1]    0 2280  617
# argumentos sem nomes (na ordem correta)
limparDados(precos3, 0, 4000, 4)
## [1]   0 308

Argumentos Default

Colocar mais argumentos em uma função, contudo, causa um certo incômodo. Você passa a ter que especificar todos estes argumentos cada vez que for chamar a função. Ao se esquecer de especificar algum, ocorrerá um erro:

limparDados(precos3, max=5000, div=1)
## Error in limparDados(precos3, max = 5000, div = 1): argumento "min" ausente, sem padrão

Existe uma forma simples de solucionar isto: basta definir valores padrão (default):

# colocando valores padrnao para min, max e div
limparDados <- function(dados, min=1, max=10000, div=1000){
  dados <- as.numeric(dados)
  dados <- dados[!(dados < min | dados > max)]
  dados <- dados/div
  dados <- round(dados)
  return(dados)
}

Agora podemos usar a função omitindo os argumentos que possuem default.

# usa o default para min
limparDados(precos3, max=5000, div=1)
## [1] 4560 1234
# usa o default para min e div
limparDados(precos3, max=Inf)
## [1]     5     1     8 12000
# usa o default para tudo
limparDados(precos3)
## [1] 5 1 8

Funções também podem ser argumentos

Funções também podem ser passadas como argumentos de funções. Por exemplo, suponha que você não queira sempre usar o round() para arredondamento. Você pode colocar a função que é aplicada a dados como um dos argumentos de limparDados().

limparDados <- function(dados, min=1,
                        max=10000, div=1000, funcao=round){
  dados <- as.numeric(dados)
  dados <- dados[!(dados < min | dados > max)]
  dados <- dados/div
  dados <- funcao(dados) # função que será aplicada agora é um argumento
  return(dados)
}

Se quisermos usar a função floor() ao invés de round(), basta trocar o argumento funcao.

# usou os defaults
limparDados(precos3)
## [1] 5 1 8
# usa floor ao invés de round
limparDados(precos3, funcao=floor)
## [1] 4 1 7
# funcao anonima que pega x e retorna x (não faz nada com x)
limparDados(precos3, funcao = function(x) x)
## [1] 4.560 1.234 7.894

Funções anônimas

Como vimos no final do exemplo anterior, você pode definir uma nova função no próprio argumento de limparDados(). Estas funções são chamadas de anônimas.

limparDados(precos3, funcao = function(x) x) # não faz nada com os dados ao final
## [1] 4.560 1.234 7.894
limparDados(precos3, funcao = function(x) x^2) # eleva dados ao quadrado
## [1] 20.793600  1.522756 62.315236
limparDados(precos3, funcao = function(x) log(x+1)) # tira o log de (dados +1 )
## [1] 1.7155981 0.8037937 2.1853769
limparDados(precos3, funcao = function(x) { # reliaza diversas operações
  x <- round(x)
  x <- as.complex(x)
  x <- (-x)^(x/10)
} )
## [1]  0.0000000-2.236068i  0.9510565-0.309017i -4.2700173-3.102349i

O …

O R tem ainda um argumento coringa os “três pontos” ....

O ... permite repassar argumentos para outras funções dentro da sua função, sem necessariamente ter que elencar todas as possibilidades de antemão. Não ficou muito claro? Vejamos um exemplo.

A função round() tem o argumento digits. Ou a nossa função elevado_n() tem o argumento n. Muitas vezes vamos querer mudar esses arguementos, mas como incluir todos em nossa função? Não temos como prever exatamente que funções ou que argumentos vamos precisar. Mas não precisamos fazer isso, podemos repassar argumentos arbitrários para essas funções por meio do ....

limparDados <- function(dados, min=1,
                        max=10000, div=1000, funcao=round, ...){
  dados <- as.numeric(dados)
  dados <- dados[!(dados < min | dados > max)]
  dados <- dados/div
  dados <- funcao(dados, ...) # note os três pontos
  return(dados)
}

Agora, tudo que for colocado em ... é repassado para funcao(dados, ...).

limparDados(precos3)
## [1] 5 1 8
limparDados(precos3, digits=1) # passamos o argumento digits = 1 para a função round (default)
## [1] 4.6 1.2 7.9
limparDados(precos3, funcao=elevado_n, n=2) # passamos o argumento n = 2 para a função elevado_n
## [1] 20.793600  1.522756 62.315236

Operadores binários

Lembra que, nos primeiros capítulos, mostramos que o operador +, no R, na verdade é uma função e pode ser chamado com a seguinte sintaxe: '+'(x,y)? Funções deste tipo são chamadas de operadores binários. No R, você também pode definir seus próprios operadores binários com o auxílio do caracter especial %.

Para ilustrar, façamos um operador binário que cole textos:

`%+%` <- function(x, y) paste(x, y)

Agora podemos colar textos usando %+%:

"colando" %+% "textos" %+% "com nosso" %+% "operador"
## [1] "colando textos com nosso operador"

Vejamos outro exemplo:

`%depois%` <- function(x, fun) fun(x)

Olhe que interessante:

set.seed(10)
x <- rnorm(100)
sqrt(exp(mean(x)))
## [1] 0.9340041
x %depois% mean %depois% exp %depois% sqrt
## [1] 0.9340041

Ou seja, com esse simples operador, você mudou a sintaxe das operações. Ao invés de escrever sqrt(exp(mean(x))), que te força a pensar “de fora para dentro” (tirar a raiz quadrada do exponencial da média de x) você pode escrever na “ordem natural” das operações x %depois% mean %depois% exp %depois% sqrt (pegue x, tire a média, exponencie e tire a raiz quadrada). Na verdade, este caso é uma versão super simplificada da lógica do operador %>% do pacote magrittr, muito usado no dplyr, que veremos na seção de manipulação de data.frames.

A imaginação é o limite.

Exercícios

Sua vez.

  • Defina uma função que retorne o mínimo, a mediana e o máximo de um vetor. Faça com que a função lide com NA’s e que isso seja um argumento com default;
  • Defina uma versão “operador binário” da função rep. Faça com que tenha seguinte sintaxe: x %rep% n retorna o objeto x repetido n vezes.
  • Defina uma função que normalize/padronize um vetor (isto é, subtraia a média e divida pelo desvio-padrão). Faça com que a função tenha a opção de ignorar NA’s. Permita ao usuário escolher outros parâmetros para a média (Dica: use …);
  • Dados um vetor y e uma matriz de variáveis explicativas X, defina uma função que retorne os parâmetros de uma regressão linear de x contra y, juntamente com os dados originais usados na regressão. (Dicas: use álgebra matricial. Use uma lista para retornar o resultado)

Soluções sugeridas (você pode fazer melhor!)

mmm <- function(x, na.rm=TRUE){

  # calcula min, median, e max, guarda em resultado
  resultado <- c(min(x, na.rm = na.rm),
                 median(x, na.rm = na.rm),
                 max(x, na.rm = na.rm))

  # nomeia o vetor para facilitar consulta
  names(resultado) <- c("min", "mediana", "max")

  #retorna vetor
  return(resultado)
}

mmm(c(1,2,3, NA))
##     min mediana     max
##       1       2       3
`%rep%` <- function(x, n) rep(x, n)

7 %rep% 5
## [1] 7 7 7 7 7
padronize <- function(x, na.rm=TRUE,...){

  m <- mean(x,na.rm=na.rm, ...) # calcule a média
  dp <- sd(x, na.rm=na.rm)      # calcule o dp
  pad <- (x-m)/dp               # padronize os dados

  attr(pad, "media") <- m       # guarda a média original p/ consulta
  attr(pad, "dp") <- dp         # guarda o dp original p/ consulta

  return(pad)                   # retorna o vetor pad já com atributos
}

padronize(1:5)
## [1] -1.2649111 -0.6324555  0.0000000  0.6324555  1.2649111
## attr(,"media")
## [1] 3
## attr(,"dp")
## [1] 1.581139
ols <- function(X, y){

  b <- solve(t(X)%*%X)%*%t(X)%*%y  # ols ((X'X)^-1)X'Y

  # guarda resultados em lista nomeada
  resultado <- list(coef = b, X = X, y = y)

  # retorna resultado
  return(resultado)
}

# cria dados simulados
set.seed(10)                       # para reproducibilidade
X <- matrix(rnorm(300), ncol=3)    # vetor X
y <- X%*%c(3,6,9) + rnorm(100)     # y = Xb + e, b=c(3, 6, 9), e~N(0,1)

# vamos testar a formula
resultado <- ols(X, y)
str(resultado)
## List of 3
##  $ coef: num [1:3, 1] 3.01 6.01 9
##  $ X   : num [1:100, 1:3] 0.0187 -0.1843 -1.3713 -0.5992 0.2945 ...
##  $ y   : num [1:100, 1] 7.93 5.53 1.53 11.11 -12.52 ...
resultado$coef
##          [,1]
## [1,] 3.012419
## [2,] 6.011090
## [3,] 8.998595

*** falar sobre escopo e orientação a objetos ***