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()

5 pensamentos sobre “Dividir, Aplicar e Combinar (Split, Apply and Combine)

  1. Pingback: Introdução ao dplyr | Análise Real

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s