class: center, middle, inverse, title-slide # Técnicas de programação ### Fernando Mayer ### 2019-08-29 .footnotesize[(última atualização 2022-02-09)] --- # Introdução <br> - "O R não é uma linguagem rápida". Não é por acaso: - Foi desenvolvido para análise de dados interativa - Não foi pensado em ser o mais eficiente possível - Para a maioria das tarefas, é rápido o bastante - Por isso, é importante: - Identificar os "gargalos" no código, que podem ser otimizados - Identificar expressões que causam erros, e como lidar com estes erros - Técnicas gerais de programação: - Identificar e arrumar problemas de performance - Identificar e arrumar erros (*bugs*) --- # Introdução As técnicas que precisamos conhecer são: <br> .pull-left[ ## Performance de código - **Benchmarking** é o processo de avaliar a performance de operações específicas repetidamente. - **Profiling** é o processo de fazer o benchmark para cada instrução de uma função/rotina. ] .pull-right[ ## Tratamento de erros - **Debugging** é o processo de buscar e resolver erros. Técnicas de debugging permitem avaliar a função passo a passo quando chamada. - **Error handling** (*tratamento de erro*) é o processo de "prever" possíveis erros e fazer alguma ação quando ocorrerem. - Também conhecido por *exception handling* (tratamento de excessão) ] --- class: center, middle, inverse # Performance de código --- # Benchmarking Funções úteis: - `base::system.time()`: apenas uma execução - `microbenchmark::microbenchmark()`: avalia a expressão várias vezes Usando `system.time()` .pull-left[ ```r (df <- data.frame(v = 1:4, name = letters[1:4])) ``` ``` # v name # 1 1 a # 2 2 b # 3 3 c # 4 4 d ``` - Cada expressão precisa ser executada em uma linha - Cada expressão é executada uma única vez ] .pull-right[ ```r system.time(df[3, 2]) ``` ``` # user system elapsed # 0 0 0 ``` ```r system.time(df[3, "name"]) ``` ``` # user system elapsed # 0 0 0 ``` ```r system.time(df$name[3]) ``` ``` # user system elapsed # 0 0 0 ``` ] --- # Benchmarking Usando `microbenchmark()` ```r library(microbenchmark) (mb1 <- microbenchmark(df[3, 2], df[3, "name"], df$name[3])) ``` ``` # Unit: nanoseconds # expr min lq mean median uq max neval cld # df[3, 2] 8462 8862.5 9488.08 9054 9294.5 43542 100 b # df[3, "name"] 8107 8881.5 9227.14 9136 9501.5 13684 100 b # df$name[3] 695 894.0 1073.21 1024 1206.0 4103 100 a ``` Outro exemplo: ```r x <- runif(1e6) (mb2 <- microbenchmark(sqrt(x), x^0.5)) ``` ``` # Unit: milliseconds # expr min lq mean median uq max neval cld # sqrt(x) 3.654129 5.405842 5.685478 5.497392 5.603549 15.16638 100 a # x^0.5 20.983463 22.947615 23.222339 23.081480 23.239946 31.87988 100 b ``` <br> - Várias expressões podem ser passadas na mesma chamada da função - Cada expressão é avaliada 100 vezes (por padrão), obtendo assim um sumário dos tempos de execução (veja o argumento `times`) --- # Benchmarking .pull-left[ ```r boxplot(mb1) ``` <img src="data:image/png;base64,#figures/01_tecnicas/unnamed-chunk-6-1.png" width="90%" style="display: block; margin: auto;" /> ] .pull-right[ ```r boxplot(mb2) ``` <img src="data:image/png;base64,#figures/01_tecnicas/unnamed-chunk-7-1.png" width="90%" style="display: block; margin: auto;" /> ] --- # Benchmarking Voltando ao exemplo da aula anterior: ```r ## Vetor com uma sequência de 1 a 1.000.000 x <- 1:1e7 ## Cria um objeto de armazenamento com o mesmo tamanho do resultado st1 <- system.time({ out1 <- numeric(length(x)) for(i in 1:length(x)){ out1[i] <- x[i]^2 } }) st1 ``` ``` # user system elapsed # 0.603 0.020 0.623 ``` ```r ## Cria um objeto de tamanho "zero" e vai "crescendo" esse vetor st2 <- system.time({ out2 <- numeric(0) for(i in 1:length(x)){ out2[i] <- x[i]^2 } }) st2 ``` ``` # user system elapsed # 2.552 0.133 2.690 ``` --- # Benchmarking Voltando ao exemplo da aula anterior (com `microbenchmark()`): .pull-left[ ```r ## Vetor com uma sequência de 1 a 1.000.000 x <- 1:1e7 ## Cria uma função para o primeiro caso st1 <- function(x) { out1 <- numeric(length(x)) for(i in 1:length(x)){ out1[i] <- x[i]^2 } return(x) } ## Cria uma função para o segundo caso st2 <- function(x) { out2 <- numeric(0) for(i in 1:length(x)){ out2[i] <- x[i]^2 } return(x) } (mb <- microbenchmark(st1, st2, times = 1000)) ``` ``` # Unit: nanoseconds # expr min lq mean median uq max neval cld # st1 18 25 39.826 25 26 13841 1000 a # st2 17 22 23.090 23 23 350 1000 a ``` ] .pull-right[ ```r boxplot(mb) ``` <img src="data:image/png;base64,#figures/01_tecnicas/unnamed-chunk-10-1.png" width="90%" style="display: block; margin: auto;" /> ] - A diferença é que aqui fica mais fácil passarmos uma função, ao invés das expressões --- # Profiling Ferramentas: - `base::Rprof()`: resultados básicos - A função `Rprof()` cria um arquivo chamado `Rprof.out`, que armazena cada passo interno da execução das expressões - Um resumo pode ser visto com a função `summaryRprof()` - `profvis::profvis()`: mais detalhes em uma interface Exemplo com `Rprof()`: .pull-left[ ```r ## Caso 1 Rprof() ## Abre a conexão out1 <- numeric(length(x)) for(i in 1:length(x)){ out1[i] <- x[i]^2 } Rprof(NULL) ## Fecha a conexão summaryRprof() ## Resumo do profiling ``` ] .pull-right[ ```r ## Caso 2 Rprof() out2 <- numeric(0) for(i in 1:length(x)){ out2[i] <- x[i]^2 } Rprof(NULL) summaryRprof() ``` ] --- # Profiling Exemplo com `profvis()` - Após e execução da expressão, uma interface gráfica é aberta com informações de tempo de execução e uso de memória *para cada linha* .pull-left[ ```r library(profvis) ## Caso 1 profvis({ out1 <- numeric(length(x)) for(i in 1:length(x)){ out1[i] <- x[i]^2 } }) ``` ] .pull-right[ ```r ## Caso 2 profvis({ out2 <- numeric(0) for(i in 1:length(x)){ out2[i] <- x[i]^2 } }) ``` ] --- # Profiling ## Exemplo prático .pull-left-40[ Simula de um modelo linear: $$ Y_i = \beta_0 + \beta_1 X_i + \epsilon_i, \quad i = 1, \ldots, n $$ Onde: $$ `\begin{aligned} X_i &\sim N(150, 15^2) \\ \epsilon_i &\sim N(0, \sigma^2) \\ \beta_0 &= 10 \\ \beta_1 &= 0.5 \\ \sigma^2 &= 20 \\ n &= 1000 \end{aligned}` $$ ] .pull-right-60[ Simulação dos dados: ```r set.seed(123) n <- 1000; b0 <- 10; b1 <- 0.5 x <- rnorm(n, mean = 150, sd = 15) sigma2 <- 20 y <- b0 + b1*x + rnorm(n, mean = 0, sd = sqrt(sigma2)) plot(x, y) ``` <img src="data:image/png;base64,#figures/01_tecnicas/unnamed-chunk-15-1.png" width="65%" style="display: block; margin: auto;" /> ] --- # Profiling .pull-left[ Um modelo linear ajustado aos dados resulta em: .code80[ ```r ## Modelo m <- lm(y ~ x) ## b0 e b1 coef(m) ``` ``` # (Intercept) x # 6.2459652 0.5262506 ``` ```r ## sigma^2 summary(m)$sigma^2 ``` ``` # [1] 20.25663 ``` ```r plot(x, y); abline(m, col = 2) ``` <img src="data:image/png;base64,#figures/01_tecnicas/unnamed-chunk-16-1.png" width="55%" style="display: block; margin: auto;" /> ] ] .pull-right[ Uma forma alternativa de se obter estimativas pontuais e variâncias de parâmetros é através da técnica de **bootstrap**: 1. A partir do conjunto de dados coletados, gere uma amostra aleatória (com reposição) de tamanho `\(m < n\)`. 2. Ajuste o modelo para esse subconjunto dos dados e obtenha as estimativas pontuais dos parâmetros. 3. Repita esse procedimento `\(r\)` vezes (onde `\(r\)` deve ser algo entre 1000 e 100000, por exemplo). 4. Verifique a distribuição das `\(r\)` estimativas: a média será a estimativa pontual, e a variância será uma estimativa da variância. ] --- # Profiling Implementando o bootstrap e já aplicando a função `profvis()`: ```r ## Número de amostras r <- 1e4 ## Número de elementos em cada amostra m <- 100 *profvis({ ## Vetores para armazenar os resultados b0.boot <- numeric(r) b1.boot <- numeric(r) s2.boot <- numeric(r) set.seed(123) for(i in 1:r){ select <- sample(1:length(y), size = m, replace = TRUE) x.boot <- x[select] y.boot <- y[select] mm <- lm(y.boot ~ x.boot) b0.boot[i] <- coef(mm)[1] b1.boot[i] <- coef(mm)[2] s2.boot[i] <- summary(mm)$sigma^2 } *}) ```
--- # Profiling Resultados do bootstrap .pull-left-60[ ```r par(mfrow = c(1, 3)) hist(b0.boot); abline(v = b0, col = 2, lwd = 2) hist(b1.boot); abline(v = b1, col = 2, lwd = 2) hist(s2.boot); abline(v = sigma2, col = 2, lwd = 2) par(mfrow = c(1, 1)) ``` <img src="data:image/png;base64,#figures/01_tecnicas/unnamed-chunk-18-1.png" width="80%" style="display: block; margin: auto;" /> ] .pull-right-40.code80[ ```r lapply(list(b0.boot, b1.boot, s2.boot), summary) ``` ``` # [[1]] # Min. 1st Qu. Median Mean 3rd Qu. Max. # -10.651 3.306 6.271 6.310 9.357 24.228 # # [[2]] # Min. 1st Qu. Median Mean 3rd Qu. Max. # 0.4125 0.5057 0.5261 0.5258 0.5458 0.6353 # # [[3]] # Min. 1st Qu. Median Mean 3rd Qu. Max. # 11.71 18.24 20.10 20.23 22.07 33.17 ``` ] --- class: center, middle, inverse # Tratamento de erros --- # Tipos de erros - **Error**: único capaz de parar e execução da função, e não cria objetos - **Warning**: avisa que algo pode estar errado, mas executa a função e retorna valores - **Message**: apenas alguma mensagem que possa ser útil, mas não interfere nos resultados ```r stop("Isso é um erro") ``` ``` # Error in eval(expr, envir, enclos): Isso é um erro ``` ```r warning("Isso é um warning") ``` ``` # Warning: Isso é um warning ``` ```r message("Isso é uma mensagem") ``` ``` # Isso é uma mensagem ``` **Obs.:** evite usar `cat()` e `print()` para informar erros ou mensagens em funções. --- # Encontrando erros Considere a fórmula de Baskara para encontrar as raízes de uma equação de segundo grau: $$ x = \frac{-\text{b} \pm \sqrt{\text{b}^2 - 4\text{a}\text{c}}}{2\text{a}} $$ Uma implementação *naive* ("ingênua") no R seria: ```r baskara <- function(a, b, c) { denom <- 2 * a delta <- b^2 - 4 * a * c sqrt_delta <- sqrt(delta) x1 <- (-b - sqrt_delta)/denom x2 <- (-b + sqrt_delta)/denom return(c(x1, x2)) } ``` - Por quê "ingênua"? 1. É possível fazer tudo isso em uma linha 2. Não existem mecanismos para lidar com possíveis erros --- # Encontrando erros Alguns resultados: ```r baskara(-3, 2, 1) ``` ``` # [1] 1.0000000 -0.3333333 ``` ```r baskara(0, 2, 1) # 2 * a = 0 ``` ``` # [1] -Inf NaN ``` ```r baskara(3, 2, 1) # 2^2 - 4 * 3 * 1 = -8 ``` ``` # Warning in sqrt(delta): NaNs produced ``` ``` # [1] NaN NaN ``` Usando `traceback()` para tentar identificar onde está o erro: ```r traceback() # No traceback available ``` Não é informativo, pois não existe um erro definido formalmente na função. --- # Encontrando erros Quais os possíveis problemas (numéricos)? 1. Quando `\(a = 0\)` 2. Quando `\(\sqrt{\Delta} < 0\)` -- Resolvendo o primeiro problema: ```r baskara <- function(a, b, c) { * if(a == 0) stop("Argumento `a` não pode ser zero.") denom <- 2 * a delta <- b^2 - 4 * a * c sqrt_delta <- sqrt(delta) x1 <- (-b - sqrt_delta)/denom x2 <- (-b + sqrt_delta)/denom return(c(x1, x2)) } ``` ```r baskara(0, 2, 1) ``` ``` # Error in baskara(0, 2, 1): Argumento `a` não pode ser zero. ``` ```r traceback() # 2: stop("Argumento `a` não pode ser zero.") at #2 # 1: baskara(0, 2, 1) ``` --- # Encontrando erros Resolvendo o segundo problema: ```r baskara <- function(a, b, c) { if(a == 0) stop("Argumento `a` não pode ser zero.") denom <- 2 * a delta <- b^2 - 4 * a * c * if(delta < 0) stop("Delta é negativo.") sqrt_delta <- sqrt(delta) x1 <- (-b - sqrt_delta)/denom x2 <- (-b + sqrt_delta)/denom return(c(x1, x2)) } ``` ```r baskara(3, 2, 1) ``` ``` # Error in baskara(3, 2, 1): Delta é negativo. ``` ```r traceback() # 2: stop("Delta é negativo.") at #5 # 1: baskara(3, 2, 1) ``` - O resultado é lido de baixo para cima. - O `traceback()` para na função que causou o erro (nesse caso óbvio), e mostra qual a linha correspondente dentro da função. --- # Encontrando erros Usando `browser()` - Permite "entrar" no ambiente (temporário) da função em um ponto específico, depois de chamar a função. ```r baskara <- function(a, b, c) { denom <- 2 * a delta <- b^2 - 4 * a * c * browser() sqrt_delta <- sqrt(delta) x1 <- (-b - sqrt_delta)/denom x2 <- (-b + sqrt_delta)/denom return(c(x1, x2)) } ``` ```r baskara(3, 2, 1) # Called from: baskara(3, 2, 1) # Browse[1]> debug at #5: sqrt_delta <- sqrt(delta) # Browse[2]> ``` --- # Encontrando erros Note que: - O prompt do console muda de `> ` para `Browse[2]> ` - Você está agora dentro do ambiente da função. Isso significa que: - Os objetos que você tem acesso são os argumentos e outras expressões executadas até esse ponto na função. Veja `ls()`. - Os objetos criados no seu ambiente global não ficam visíveis. - Você pode criar e/ou alterar objetos da função. - Cada <kbd>Enter</kbd> executa uma nova linha, até a última. - Você pode alterar os argumentos para ver o resultado. - Para sair digite `Q`. (Rode o script para ver como funciona). --- # Encontrando erros Usando `debug()`: - Uma forma mais geral para entrar no modelo `browser` da função. - Não especifica nada de especial dentro da função. - A função entra em modo `browser()` desde a primeira linha. Voltando com a função `baskara()` *naive*: ```r baskara <- function(a, b, c) { denom <- 2 * a delta <- b^2 - 4 * a * c sqrt_delta <- sqrt(delta) x1 <- (-b - sqrt_delta)/denom x2 <- (-b + sqrt_delta)/denom return(c(x1, x2)) } ``` ```r ## Para iniciar o debug, use: debug(baskara) ## Para entrar em modo de debug, chame a função com argumentos: baskara(3, 2, 1) ## Para sair do modo debug da função, faça: undebug(baskara) ## Ou, para entrar em modo de debug apenas uma vez e sair, use: debugonce(baskara) ``` --- # Ignorando erros Existem duas funções para ignorar erros: - `try()`: ignora o erro, mas cria um objeto com a classe `try-error` - `tryCatch()`: define alguma ação quando ocorrer um erro Voltando ao exemplo da função `baskara()` com as funções `stop()`: ```r baskara <- function(a, b, c) { * if(a == 0) stop("Argumento `a` não pode ser zero.") denom <- 2 * a delta <- b^2 - 4 * a * c * if(delta < 0) stop("Delta é negativo.") sqrt_delta <- sqrt(delta) x1 <- (-b - sqrt_delta)/denom x2 <- (-b + sqrt_delta)/denom return(c(x1, x2)) } ``` --- # Ignorando erros **Usando `try()`:** .pull-left[ Sem o `try()`, o objeto não é criado: .code80[ ```r baskara(0, 2, 1) ``` ``` # Error in baskara(0, 2, 1): Argumento `a` não pode ser zero. ``` ```r er1 <- baskara(0, 2, 1) ``` ``` # Error in baskara(0, 2, 1): Argumento `a` não pode ser zero. ``` ```r er1 ``` ``` # Error in eval(expr, envir, enclos): object 'er1' not found ``` ] ] .pull-right[ Com o `try()`, o objeto é criado e possui a classe `try-error`: .code80[ ```r try(baskara(0, 2, 1)) ``` ``` # Error in baskara(0, 2, 1) : Argumento `a` não pode ser zero. ``` ```r er2 <- try(baskara(0, 2, 1)) ``` ``` # Error in baskara(0, 2, 1) : Argumento `a` não pode ser zero. ``` ```r er2 ``` ``` # [1] "Error in baskara(0, 2, 1) : Argumento `a` não pode ser zero.\n" # attr(,"class") # [1] "try-error" # attr(,"condition") # <simpleError in baskara(0, 2, 1): Argumento `a` não pode ser zero.> ``` ```r class(er2) ``` ``` # [1] "try-error" ``` ] ] --- # Ignorando erros **Usando `try()` e `tryCatch()`:** .pull-left-60[ Não existe uma forma pronta de lidar com a classe `try-error`, mas você pode definir o que deve acontecer: ```r fn <- function(...) { res <- try(baskara(...)) ## if(class(res) == "try-error") c(NA, NA) if(class(res) == "try-error") warning("Deu erro seu jaguara") } er3 <- fn(0, 2, 1) ``` ``` # Error in baskara(...) : Argumento `a` não pode ser zero. ``` ``` # Warning in fn(0, 2, 1): Deu erro seu jaguara ``` ```r er3 ``` ``` # [1] "Deu erro seu jaguara" ``` ] .pull-right-40[ Com `tryCatch()` podemos definir previamente o resultado quando ocorrer um erro: ```r tryCatch(baskara(0, 2, 1), error = function(cmd) c(NA, NA)) ``` ``` # [1] NA NA ``` ```r tryCatch(baskara(3, 2, 1), error = function(cmd) c(NA, NA)) ``` ``` # [1] NA NA ``` ] --- # Ignorando erros Isso é útil quando se deseja que a função não pare durante uma execução .pull-left[ ```r baskara2 <- function(a, b, c) { * if(a == 0) stop("Argumento `a` não pode ser zero.") denom <- 2 * a delta <- b^2 - 4 * a * c * ## if(delta < 0) stop("Delta é negativo.") * sqrt_delta <- tryCatch(sqrt(delta), * warning = function(cmd) NA) x1 <- (-b - sqrt_delta)/denom x2 <- (-b + sqrt_delta)/denom return(c(x1, x2)) } ``` ] .pull-right[ ```r baskara(0, 2, 1) ``` ``` # Error in baskara(0, 2, 1): Argumento `a` não pode ser zero. ``` ```r baskara2(0, 2, 1) ``` ``` # Error in baskara2(0, 2, 1): Argumento `a` não pode ser zero. ``` ```r baskara(3, 2, 1) ``` ``` # Error in baskara(3, 2, 1): Delta é negativo. ``` ```r baskara2(3, 2, 1) ``` ``` # [1] NA NA ``` ] --- class: center, middle, inverse # Desafio --- # Desafio - Analise o resultado da função `profvis()` do slide 14. - Verifique os pontos críticos da execução e proponha mudanças para otimizar o código. - Use também a função `microbenchmark()` para avaliar suas alternativas propostas.