Análise de Dados em R (FNDE) - Módulo 2
Allan Vieira
março de 2018
É a patriarca da família apply;
Basicamente, apply() opera em arrays. Para simplificar o entendimento, podemos dizer que é utilizada para matrizes (que são arrays de duas dimensões como vimos no módulo 1);
apply() vai aplicar alguma função às linhas ou às colunas de uma matriz;
Exemplo: calcular a média das linhas ou das colunas de uma matriz.
args(apply)
function (X, MARGIN, FUN, ...)
NULL
X é um array ou matriz (array de dimensão 2);
FUN é a função que você vai aplicar aos dados;
# Construindo uma matriz 5x6
X <- matrix(1:30, nrow=5, ncol=6)
X
[,1] [,2] [,3] [,4] [,5] [,6]
[1,] 1 6 11 16 21 26
[2,] 2 7 12 17 22 27
[3,] 3 8 13 18 23 28
[4,] 4 9 14 19 24 29
[5,] 5 10 15 20 25 30
# somar o TOTAL de cada coluna com apply()
apply(X, 2, sum)
[1] 15 40 65 90 115 140
Para entendermos o que está sendo feito no código ao lado, vejamos a figura:
Fonte: Datacamp
apply(X, 2, sum) siginifica: aplique a função sum sobre as colunas (ou ao longo da margem/dimensão 2) da matriz X.
Para somas e médias das dimensões de uma matriz, o R já possui alguns atalhos, os quais inclusive foram apresentados no módulo 1:
As funções atalhos são mais rápidas que suas respectivas versões em apply. No entanto, há alguns casos em que não temos funções atalhos e fatalmente teremos que usar apply();
Há casos em que temos que criar funções anônimas dentro da chamada de apply() para executar um cálculo ou operação personalizada (daqui 2 slides !!).
# Construindo uma matriz 5x6
X <- matrix(1:30, nrow=5, ncol=6)
# inserindo alguns NA's
X[1,2] <- NA
X[4,5] <- NA
X
[,1] [,2] [,3] [,4] [,5] [,6]
[1,] 1 NA 11 16 21 26
[2,] 2 7 12 17 22 27
[3,] 3 8 13 18 23 28
[4,] 4 9 14 19 NA 29
[5,] 5 10 15 20 25 30
# somar o valor de cada coluna com apply(), agora incluindo o parâmetro na.rm= TRUE
apply(X, 2, sum, na.rm = TRUE)
[1] 15 34 65 90 91 140
Note que o parâmetro na.rm, embora pertencente a função sum(), não é passado dentro desta, mas sim como um argumento de apply(). Será a função apply() que se encarregará de direcionar este argumento para a função sum();
Veja a diferença caso não tivessemos especificado na.rm = TRUE
apply(X, 2, sum)
[1] 15 NA 65 90 NA 140
Há duas opções:
Vejamos um exemplo …
# criando matriz
set.seed(1984) # semente para reproducibilidade dos valores
M <- matrix(sample(1:1000, 30), 5)
M
[,1] [,2] [,3] [,4] [,5] [,6]
[1,] 659 859 874 23 599 154
[2,] 437 33 12 295 4 826
[3,] 373 445 698 654 600 566
[4,] 330 824 711 906 839 493
[5,] 735 213 200 202 780 538
# 1º caso - utlizando função anônima
apply(M, 1, function(x) min(x)*1.025)
[1] 23.575 4.100 382.325 338.250 205.000
# 2º caso - criando uma função fora de apply()
myfun <- function(x) min(x)*1.025
apply(M, 1, myfun)
[1] 23.575 4.100 382.325 338.250 205.000
Os resultados são exatamente os mesmos!
Note que a criação da função anônima se dá da mesma forma em que criaríamos uma função normal, conforme visto no primeiro capítulo.
As únicas diferenças são: a função é criada dentro do argumento FUN da função da família apply e não atribuímos qualquer nome para a função que está sendo criada.
Veja a imagem exemplificando o funcionamento de lapply():
Fonte: Advanced R
args(lapply)
function (X, FUN, ...)
NULL
# criando 3 matrizes:
A <- matrix(1:9, 3)
B <- matrix(4:15, 4)
C <- matrix(rep(8:10, 2), 3)
# Criando lista de matrizes
MyList <- list(A,B,C)
# Extratraindo a primeira linha de todas as matrizes da lista `MyList`
lapply(MyList,"[", 1,)
[[1]]
[1] 1 4 7
[[2]]
[1] 4 8 12
[[3]]
[1] 8 8
# note como é passado o argumento da primira linha
A figura a seguir auxilia na compreensão do exemplo:
Fonte: Datacamp.
lapply(MyList,"[", 1,2)
[[1]]
[1] 4
[[2]]
[1] 8
[[3]]
[1] 8
lapply(MyList, function(x) mean(x[,1],na.rm=TRUE))
[[1]]
[1] 2
[[2]]
[1] 5.5
[[3]]
[1] 9
# OU
lapply(MyList, function(x, ...) mean(x[,1]), na.rm=TRUE)
[[1]]
[1] 2
[[2]]
[1] 5.5
[[3]]
[1] 9
# perceba o uso de ... e o posicionamento de na.rm nas duas formas equivalentes acima
# OU AINDA:
# de uma maneira menos eficiente (porque teríamos que criar um vetor, ocupando mais memória) e mais complicada porque teríamos que usar 2 vezes lapply()
out <- lapply(MyList, "[", ,1)
# esse só funciona com lapply. Com sapply não dá certo por causa da 3ª vírgula, me parece. Como sapply() é um wrapper para lapply, na hora de ela tentar mandar para lapply, ela entende a 3ª vírgula como se fosse um agumento e não um parâmetro.
lapply(out, mean)
[[1]]
[1] 2
[[2]]
[1] 5.5
[[3]]
[1] 9
# mean aplicado sobre uma lista não funciona
# ... então não adiantaria usar o código abaixo:
# mean(out, na.rm = TRUE)
Retomando aos looping patterns que apresentamos na seção 2.1.1, esses mesmos padrões de construção de loops podem ser utilizados com as funções da família apply.
Com lapply teríamos o seguinte:
Embora no caso dos loops havia a recomendação de que se opte pelo caso 2, na família apply as estruturas mais comuns serão tanto a 1 quanto a 2, dependendo do problema que se quer resolver.
lapply(seq_along(MyList), function(i){
MyList[[i]][1,2]
})
[[1]]
[1] 4
[[2]]
[1] 8
[[3]]
[1] 8
O fato de utilizarmos os índices para iteração, nos permite maior controle sobre o que deseamos fazer com os dados.
OBS: Note que em todos os casos que usamos lapply(), os objetos retornados eram listas. Se quiser confirmar repita os códigos anteriores salvando as saídas e depois aplicando str().
A função sapply() funciona como lapply() com a diferença de que ela tenta simplificar ( daí o s em sapply) o resultado para o objeto mais elementar quanto for possível - que é um vetor.
Em termos gerais, temos normalmente as seguintes situações:
sapply(MyList, function(x) mean(x[,1],na.rm=TRUE))
[1] 2.0 5.5 9.0
# OU
sapply(MyList, function(x, ...) mean(x[,1]), na.rm=TRUE)
[1] 2.0 5.5 9.0
# perceba o uso de ... e o posicionamento de na.rm nas duas formas equivalentes acima
sapply(seq_along(MyList), function(i){
MyList[[i]][1,2]
})
[1] 4 8 8
args(vapply)
function (X, FUN, FUN.VALUE, ..., USE.NAMES = TRUE)
NULL
Os diferenciais aqui são:
O principal argumento FUN.VALUE, que é o que nos permite restringir a saída de vapply() e identificar possíveis erros antes que aconteçam - digamos - em um ambiente de produção.
Comparando vapply() e sapply()…
# criando um data.frame
df <- data.frame(x = 1:10, y = letters[1:10])
# aplicando sapply
sapply(df, class)
x y
"integer" "factor"
# aplicando vapply
vapply(df, class, character(1))
x y
"integer" "factor"
# errando no tipo:
vapply(df, class, numeric(1))
# errando no tamanho:
vapply(df, class, character()) # R vai entender como tamanho zero.
# primeiro com sapply()
# criando um data.frame com uma coluna de datas
df2 <- data.frame(x = 1:10, y = Sys.time() + 1:10)
sapply(df2, class)
$x
[1] "integer"
$y
[1] "POSIXct" "POSIXt"
vapply(df2, class, character(1))
Error in vapply(df2, class, character(1)): values must be length 1,
but FUN(X[[2]]) result is length 2
Ao usar sapply(), esperávamos classes com um único nome (um único elemento por coluna);
Por outro lado, vapply() retorna um erro;
Observações:
sapply vs vapply:
Fonte: Advanced R.
mapply() é uma forma multivariada de lapply() que aplica a função desejada de forma paralela sobre um conjunto de argumentos;
Se você passar dois vetores como argumentos para uma função, na primeira rodada/ chamada, mapply() vai olhar para os primeiros elementos de ambos os vetores na primeira chamada, depois para os segundos, etc.
Com lapply() (e suas derivadas), somente um argumento da função pode variar. Os outros, obrigatoriamente, devem ser fixos. Para alguns casos, isso pode ser um problema.
Note que a função a ser aplicada agora será nossso primeiro argumento:
args(mapply)
function (FUN, ..., MoreArgs = NULL, SIMPLIFY = TRUE, USE.NAMES = TRUE)
NULL
# criando duas listas com sequências de vetores:
l1 <- list(a = c(1:5), b = c(6:10))
l2 <- list(c = c(11:20), d = c(21:30))
mapply(sum, l1$a, l1$b, l2$c, l2$d)
[1] 39 43 47 51 55 49 53 57 61 65
#Map(sum, l1$a, l1$b, l2$c, l2$d)
# Reduce('+', c(l1, l2))
O que está acontecendo acima é que estamos passando múltiplos argumentos a serem somados posição por posição pela função sum();
Se passarmos os vetores inteiros para a função sum() sem mapply() todos os elementos seriam somados de uma vez;
Ao usarmos mapply(), podemos passar vários vetores para função sum() de modo que ela some as resectivas posições de forma paralela: l1$a[1] + l1$b[1] + l2$c[1] + l2$d[1] = 39 e assim por diante.
mapply() nos permite, então, vetorizar argumentos para uma função que normalmente não funcionaria de forma vetorizada;
Com lapply() isso não seria possível, porque os argumentos adicionais são passados para todas as chamadas. Com mapply() é como se conseguíssemos segmentar conjuntos de argumentos para cada chamada.
out <- rep(1:10, 1:10)
out
[1] 1 2 2 3 3 3 4 4 4 4 5 5 5 5 5 6 6 6 6 6 6 7 7
[24] 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10
[47] 10 10 10 10 10 10 10 10 10
str(out)
int [1:55] 1 2 2 3 3 3 4 4 4 4 ...
rep() retornou um grande vetor, ao passo que precisávamos de elementos separados como objetos de uma lista;
Com mapply() isso ocorreria diretamente:
mapply(rep, 1:10, 1:10)
[[1]]
[1] 1
[[2]]
[1] 2 2
[[3]]
[1] 3 3 3
[[4]]
[1] 4 4 4 4
[[5]]
[1] 5 5 5 5 5
[[6]]
[1] 6 6 6 6 6 6
[[7]]
[1] 7 7 7 7 7 7 7
[[8]]
[1] 8 8 8 8 8 8 8 8
[[9]]
[1] 9 9 9 9 9 9 9 9 9
[[10]]
[1] 10 10 10 10 10 10 10 10 10 10
# Map(rep, 1:10, 1:10)
#Reduce(rep, c(1:10, 1:10))
Observações:
args(tapply)
function (X, INDEX, FUN = NULL, ..., default = NA, simplify = TRUE)
NULL
Um caso muito comum do uso de tapply() é quando queremos encontrar quantidades resumo de grupos dos nossos dados:
set.seed(123)
banco <- as.factor(sample( rep(c("BB", "CEF"), 100), 20) )
saldo <- sample(1:1000, 20)
df <- data.frame(saldo, banco)
# média por banco:
tapply(df$saldo, df$banco, mean)
BB CEF
565.2500 577.4167
# range (intervalo) por banco
tapply(df$saldo, df$banco, range)
$BB
[1] 228 705
$CEF
[1] 25 992
# criação de um novo vetor:
set.seed(123)
dias <- sample(1:360, 20)
df$dias <- dias
aggregate(df[,c("saldo", "dias")], by = df['banco'], mean)
banco saldo dias
1 BB 565.2500 245.5000
2 CEF 577.4167 164.5833
# como o data.frame eh em sua essência uma lista, seus elementos são as colunas. Então ao indexarmos smente pelas colunas, sem indicar as vírgulas, ele automaticamente traz um data.frame (ou seja, uma lista). Dessa forma, conseguimos manter os nomes das variáveis !!!
# str(df['banco'])
aggregate(df[,c("saldo", "dias")], by = df['banco'], range)
banco saldo.1 saldo.2 dias.1 dias.2
1 BB 228 705 17 357
2 CEF 25 992 15 356
Se não usarmos a forma natural de lista no argumento by, teremos que forçar o resultado como uma lista usando list();
Teremos o mesmo resultado, mas a diferença é que não teremos o nome da coluna de indexação trazido automaticamente como no exemplo acima:
aggregate(df[,c("saldo", "dias")], by = list(df$banco), mean)
Group.1 saldo dias
1 BB 565.2500 245.5000
2 CEF 577.4167 164.5833
aggregate(df[,c("saldo", "dias")], by = list(df$banco), range)
Group.1 saldo.1 saldo.2 dias.1 dias.2
1 BB 228 705 17 357
2 CEF 25 992 15 356
OBRIGADO!!