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