Análise de Dados em R (FNDE) - Módulo 2
Allan Vieira
março de 2018
No Módulo 1 vimos…
Tudo estará na apostila que foi feita totalmente em R!
Este módulo é uma continuidade do anterior;
Divide -se em 2 partes:
Primeiro faremos uma continuação “linear” do que aprendemos no Módulo1;
Depois veremos um outro paradigma de programação em R:
Uma breve noção sobre o Tidyverse…
# vetores
(v1 <- 1:3)
[1] 1 2 3
(v2 <- c("R", "Fnde", "Allan"))
[1] "R" "Fnde" "Allan"
(v3 <- c(TRUE, FALSE, T))
[1] TRUE FALSE TRUE
# matriz
(m <- matrix(1:9, nrow=3, byrow=TRUE))
[,1] [,2] [,3]
[1,] 1 2 3
[2,] 4 5 6
[3,] 7 8 9
# data.frame
(df <- data.frame("col1" = v1, "col2" = v2, "col3" = v3))
col1 col2 col3
1 1 R TRUE
2 2 Fnde FALSE
3 3 Allan TRUE
# ou
# df <- data.frame(v1, v2, v3)
# colnames(df) <- paste("col", 1:3, sep = "")
# lista
(minha_lista <- list(v1, v2, v3, m, df))
[[1]]
[1] 1 2 3
[[2]]
[1] "R" "Fnde" "Allan"
[[3]]
[1] TRUE FALSE TRUE
[[4]]
[,1] [,2] [,3]
[1,] 1 2 3
[2,] 4 5 6
[3,] 7 8 9
[[5]]
col1 col2 col3
1 1 R TRUE
2 2 Fnde FALSE
3 3 Allan TRUE
No módulo 1, vimos que R é uma linguagem essencialmente funcional (Hadley Wickham).
Então, está faltando um tipo de objeto muito importante que também vimos no Módulo 1…
FUNÇÕES
Vimos que, grosso modo, as funções tem a seguinte estrutura:
função(argumentos)
lembre-se que, para criar os vetores e demais objetos anteriormente, você utilizou as funções: c(), data.frame(), matrix(), list();
Fonte: Advanced R
Assim como objetos, funções também podem receber um nome ao serem criadas.
Veja o exemplo abaixo:
myfun <- function(argumentos){
# executar algum comando
# (...)
return(objeto_a_retornar)
}
Ao atribuirmos nomes e salvarmos nossa função como um objeto, podemos:
Veremos alguns exemplos disso mais a frente - vocês já até fizeram algo parecido no Módulo 1.
myfun <- function(argumentos){
# executar algum comando
# (...)
return(objeto_a_retornar)
}
Observação Importante:
Quando criamos uma função, automaticamente estamos definindo um environment separado.
Fonte: Advanced R
Quando temos funções aninhadas, uma forma de “carregar” informação do environment de uma função para um environment “superior” é justamente pelo return().
OBSERVAÇÃO:
Exemplo - criação de função e retorno de múltiplos objetos:
myfun2 <- function(n_rep){
df1 <- data.frame(col1 = rep("R", n_rep), col2 = rep(2018, n_rep))
df2 <- data.frame(col1 = rep("R", n_rep), col2 = rep("FNDE", n_rep))
return(list(df1,df2))
}
myfun2(n_rep=3)
[[1]]
col1 col2
1 R 2018
2 R 2018
3 R 2018
[[2]]
col1 col2
1 R FNDE
2 R FNDE
3 R FNDE
myfun2 <- function(n_rep){
df1 <- data.frame(col1 = rep("R", n_rep), col2 = rep(2018, n_rep))
df2 <- data.frame(col1 = rep("R", n_rep), col2 = rep("FNDE", n_rep))
}
res <- myfun2(n_rep=3) # atribuindo a saída da função a um objeto
# sem return(), ela não imprime no console
res
col1 col2
1 R FNDE
2 R FNDE
3 R FNDE
O fato de o R ser de código aberto traz uma grande vantagem: as coisas não são uma caixa preta!
Você consegue investigar (e também alterar!) o funcionamento de tudo (todas as funções/ programas);
Para ver o código de uma função já existente no R, você precisa somente digitar o nome dela no console;
Vamos ver como funciona a função scan(), por exemplo:
scan
function (file = "", what = double(), nmax = -1L, n = -1L, sep = "",
quote = if (identical(sep, "\n")) "" else "'\"", dec = ".",
skip = 0L, nlines = 0L, na.strings = "NA", flush = FALSE,
fill = FALSE, strip.white = FALSE, quiet = FALSE, blank.lines.skip = TRUE,
multi.line = TRUE, comment.char = "", allowEscapes = FALSE,
fileEncoding = "", encoding = "unknown", text, skipNul = FALSE)
{
na.strings <- as.character(na.strings)
if (!missing(n)) {
if (missing(nmax))
nmax <- n/pmax(length(what), 1L)
else stop("either specify 'nmax' or 'n', but not both.")
}
if (missing(file) && !missing(text)) {
file <- textConnection(text, encoding = "UTF-8")
encoding <- "UTF-8"
on.exit(close(file))
}
if (is.character(file))
if (file == "")
file <- stdin()
else {
file <- if (nzchar(fileEncoding))
file(file, "r", encoding = fileEncoding)
else file(file, "r")
on.exit(close(file))
}
if (!inherits(file, "connection"))
stop("'file' must be a character string or connection")
.Internal(scan(file, what, nmax, sep, dec, quote, skip, nlines,
na.strings, flush, fill, strip.white, quiet, blank.lines.skip,
multi.line, comment.char, allowEscapes, encoding, skipNul))
}
<bytecode: 0x55ae1ee534a0>
<environment: namespace:base>
Vamos resolver agora um problema de ordem prática através da criação de uma nova função e modificação de uma já existente.
Lembra-se do parâmetro stringsAsFactors da função read.table?
a recomendação era que sempre fosse “setado” como FALSE;
Para não termos que ficar digitando stringsAsFactors=FALSE toda vez que formos ler um arquivo .txt, .csv, etc, vamos criar uma função nossa que já traz automaticamente este parâmetro setado como false:
my_read.fun <- function(file, stringsAsFactors=FALSE, ...){
read.table(file, stringsAsFactors = stringsAsFactors, ...)
}
my_read.fun <- function(file, stringsAsFactors=FALSE, ...){
read.table(file, stringsAsFactors = stringsAsFactors, ...)
}
OBSERVAÇÕES:
não utilizamos return(), então o úlitmo objeto avaliado será retornado - que é nosso arquivo de dados lido;
esse arquivo é lido pela read.table chamada de dentro da nossa my_read.fun
nossa nova função recebe 3 parâmetros, que por consequência serão passados para read.table de dentro;
file indica a localização do nosso arquivo, igual em read.table;
stringsAsFactors já vai de “fábrica” setado como FALSE - que é nosso objetivo;
os ...
indicam que o usuário poderá, caso deseje, ainda passar outros parâmetros (que serão aproveitados em read.table)
Vamos olhar agora para os argumentos dentro da read.table:
my_read.fun <- function(file, stringsAsFactors=FALSE, ...){
read.table(file, stringsAsFactors = stringsAsFactors, ...)
}
a indicação stringsAsFactors=stringsAsFactors serve para deixar ao usuário a opção de mudar o padrão do parâmetro stringsAsFactors para TRUE ao utilizar my_read.fun
caso isso não seja feito, automaticamente será buscado o default de sringsAsFactors em read.table, que é TRUE e sua função será inócua;
o melhor é sempre deixarmos nossas funções mais flexíveis quanto seja possível para o usuário, mesmo quando somente nós iremos utilizá-las.
#df.loc <- file.choose()
df.loc <- "/home/allan/Documents/teaching/FNDE/cursoR_FNDE_mod1/sessao_06/REPASSE122014.csv"
df <- my_read.fun(df.loc, sep=";", header=TRUE)
Error in file(file, "rt"): cannot open the connection
Também teste removendo stringsAsFactors=stringsAsFactors de dentro de read.table:
## sem stringsAsFactors=stringsAsFactors
# 1)
my_read.fun <- function(file, stringsAsFactors=FALSE, ...){
read.table(file, ...)
} # não obedece e pega o default de read.table() que eh TRUE
df <- my_read.fun(df.loc, sep=";", header=TRUE,)
Error in file(file, "rt"): cannot open the connection
str(df)
'data.frame': 3 obs. of 3 variables:
$ col1: int 1 2 3
$ col2: Factor w/ 3 levels "Allan","Fnde",..: 3 2 1
$ col3: logi TRUE FALSE TRUE
#2)
my_read.fun <- function(file, ...){
read.table(file, stringsAsFactors=FALSE,...)
} # INFLEXÍVEL - não há opção de mudar para TRUE!
df <- my_read.fun(df.loc, sep=";", header=TRUE, stringsAsFactors = TRUE)
Error in read.table(file, stringsAsFactors = FALSE, ...): formal argument "stringsAsFactors" matched by multiple actual arguments
# Erro!
# por isso o correto eh a forma com stringsasFactors=stringsAsFactors
Teste agora sem …
# 1)
# sem ... na my_read.fun
my_read.fun <- function(file, stringsAsFactors=FALSE){
read.table(file, stringsAsFactors=stringsAsFactors, ...)
} # reclama de parâmetros inexistentes
df <- my_read.fun(df.loc, sep=";", header=TRUE)
Error in my_read.fun(df.loc, sep = ";", header = TRUE): unused arguments (sep = ";", header = TRUE)
# 2)
# sem ... na read.table
my_read.fun <- function(file, stringsAsFactors=FALSE, ...){
read.table(file, stringsAsFactors=stringsAsFactors)
} # erro de leitura
df <- my_read.fun(df.loc, sep=";", header=TRUE)
Error in file(file, "rt"): cannot open the connection
# 3)
# sem ... nas duas
my_read.fun <- function(file, stringsAsFactors=FALSE){
read.table(file, stringsAsFactors=stringsAsFactors)
}
#df <- my_read.fun(df.loc) # erro de leitura
df <- my_read.fun(df.loc, sep=";", header=TRUE) # erro nos parâmetros
Error in my_read.fun(df.loc, sep = ";", header = TRUE): unused arguments (sep = ";", header = TRUE)
Se você quiser editar/ criar uma função fora do seu script para melhor organização do código, utilize a função edit(). Será aberto um editor fora do arquivo de script.
newfun <- edit(function(){})
my_read.fun <- edit(my_read.fun)
Se quiser identificar quais são os argumentos de uma função, para que não tenha que olhar todo o help, você pode utilizar:
args(my_read.fun)
function (file, stringsAsFactors = FALSE)
NULL
ou
formals(my_read.fun)
$file
$stringsAsFactors
[1] FALSE
Obs:
Para identificar o códio do “corpo” da função use:
body(my_read.fun)
{
read.table(file, stringsAsFactors = stringsAsFactors)
}
Para identificar o environment de onde a função é chamada:
environment(my_read.fun)
<environment: R_GlobalEnv>
O R tem um processo inteligente de argument macthing - por isso, muitas vezes não precisamos especificar o nome do argumento que estamos passando em uma função!
O R tenta “casar” os argumentos passados utilizando a seguinte sequência:
# criando um vetor:
df <- data.frame(col1 = c(1,2,3,NA,5), col2 = c(7,8,9,10,NA))
# verificando os argumentos da função colSums
colSums
function (x, na.rm = FALSE, dims = 1L)
{
if (is.data.frame(x))
x <- as.matrix(x)
if (!is.array(x) || length(dn <- dim(x)) < 2L)
stop("'x' must be an array of at least two dimensions")
if (dims < 1L || dims > length(dn) - 1L)
stop("invalid 'dims'")
n <- prod(dn[id <- seq_len(dims)])
dn <- dn[-id]
z <- if (is.complex(x))
.Internal(colSums(Re(x), n, prod(dn), na.rm)) + (0+1i) *
.Internal(colSums(Im(x), n, prod(dn), na.rm))
else .Internal(colSums(x, n, prod(dn), na.rm))
if (length(dn) > 1L) {
dim(z) <- dn
dimnames(z) <- dimnames(x)[-id]
}
else names(z) <- dimnames(x)[[dims + 1L]]
z
}
<bytecode: 0x55ae1f19af28>
<environment: namespace:base>
O método de arguments matching do R faz com que todos os códigos a seguir sejam equivalentes:
colSums(df) #positional
col1 col2
NA NA
colSums(x=df) #exact
col1 col2
NA NA
colSums(x=df, na.rm=FALSE) #exact
col1 col2
NA NA
colSums(na.rm=FALSE, x=df) #exact
col1 col2
NA NA
colSums(na=FALSE, df) #partial & positional (descobriu um que não era o primeiro, o próximo é lido como x=)
col1 col2
NA NA
colSums(na.rm=FALSE, df) #exact & positional
col1 col2
NA NA
# criando um vetor:
vetor1 <- c(1:20)
O método de arguments matching do R faz com que todos os códigos a seguir sejam equivalentes:
sum(vetor1) #positional
[1] 210
sum(x=vetor1) #exact
[1] 210
sum(x=vetor1, na.rm=FALSE) #exact
[1] 210
sum(na.rm=FALSE, x=vetor1) #exact
[1] 210
sum(na=FALSE, vetor1) #partial & positional (descobriu um que não era o primeiro, o próximo é lido como x=)
[1] 210
sum(na.rm=FALSE, vetor1) #exact & positional
[1] 210
paste
function (..., sep = " ", collapse = NULL)
.Internal(paste(list(...), sep, collapse))
<bytecode: 0x55ae1ef8def8>
<environment: namespace:base>
sum
function (..., na.rm = FALSE) .Primitive("sum")
por definição, o R só utiliza os argumentos que necessariamente forem chamados/ utilizados no código;
isso se chama lazy evaluation;
Faça o seguinte teste:
f <- function(a, b){
a^2
}
f(2)
[1] 4
Note que, embora houvesse um parâmetro “b”, a função não retornou um erro ao passarmos somente o parâmetro “a”;
Isso se deve ao processo de lazy evaluation: a função nunca usará o parâmetro (não está no seu corpo), então tanto faz recebê-lo ou não;
O número 2 passado foi atribuído ao parâmetro “a” via positional matching.
Vamos construir uma função agora que obrigatoriamente utilizará o parâmetro “b”:
Faça o seguinte teste:
f <- function(a, b){
print(a)
print(b)
}
f("FNDE")
[1] "FNDE"
Error in print(b): argument "b" is missing, with no default
Neste caso há um erro: o R reclama de não termos passado nenhum valor para o parâmetro “b”
Dessa vez, o parâmetro “b” é obrigatoriaente utilizado no corpo da função - linha print(b).
closures são funções que retornam outras funções;
o nome closure vem do fato de as funções da parte interna da estrutura anexarem/incluirem (enclose) o environment da função parent (pai) de modo que este possa ser acessado pela função de dentro.
A utilidade disso está no fato de possibilitar dois níveis de parâmetros: um nível parent que controla a operação e um child que faz o trabalho.
power <- function(exponent) {
function(x) {
x ^ exponent
}
}
Note que a função do nível interno não recebe qualquer nome;
O que a função power retorna?
Se atribuirmos power() com um parâmetro especificado a um nome (criando um novo objeto), o que será esse novo objeto?
square <- power(2)
square(2)
[1] 4
cube <- power(3)
cube(2)
[1] 8
power <- function(exponent) {
function(x) {
x ^ exponent
}
}
square <- power(2)
square(2)
Fonte: Advanced R
Resumo da ideia:
Mas qual seria um caso prático em que as closures seriam uteis?
add_2 <- function(x) {
x + 2
}
Mas e se você descobrisse que para alguns casos deve somar 10, em outros 5 e em outros 7, além do 2?
add <- function(x){
function(a){
x + a
}
}
add_2 <- add(2)
add_10 <- add(10)
add_5 <- add(5)
add_7 <- add(7)
Vamos testar as closures que criamos em um vetor de 1's:
# criando vetor de 1's
teste <- rep(1, 5)
add_2(teste)
[1] 3 3 3 3 3
add_10(teste)
[1] 11 11 11 11 11
add_5(teste)
[1] 6 6 6 6 6
add_7(teste)
[1] 8 8 8 8 8
Altere o parâmetro na.rm da função colSums() para que seu default passe a ser TRUE. Utilize algum data.frame do módulo 1 que possuía colunas numéricas com missing values e teste sua nova função.
OBRIGADO!!