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 ***

3 pensamentos sobre “Funções: Definição, argumentos e operadores binários

  1. Pingback: Outra Dica R do Dia – Análise Real e seu livro… | De Gustibus Non Est Disputandum

  2. Ia comentar algo que está no final como observação:
    *** falar sobre escopo e orientação a objetos ***

    Ao falar sobre criação de funções acho interessante focar sobre esse lance de escopo (que me parece ser a diferença entre o R e o S+, assim como a principal ideia das funções anônimas) e os closures (que você falou discretamente ao criar a função elevado_n).

    Sobre a utilização do argumento ‘…’, pode-se pegar os valores que são passados pelo ‘…’ transformando o argumento em uma lista.

    Nos posts você poderia falar um pouco da sua experiência com cada fragmento desses que é apresentado – como por exemplo as funções do seu pacote do Benford Analysis e/ou alguma experiência que possa ser compartilhada com a utilização do R aí no BACEN, sei lá – o que acha?

    No aguardo da próxima parte. 🙂

    Curtir

    • Obrigado pelos comentários, Evandro! Tentarei atender todos os pontos. Estou tentando primeiro colocar o “esqueleto” do livro no ar, e depois ir acertando partes e recheando o conteúdo.

      Só um detalhe, na verdade a função evelado_n teria que ser da seguinte forma para gerarmos closures:
      elevado_n <- function(n) function(x) x^n

      Eu não estava pensando em falar sobre closures, mas acho que pode ser uma boa idéia mesmo, nem que seja colocando como uma seção de tópicos extras.
      Abs

      Curtir

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 )

Foto do Facebook

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

Conectando a %s