if(!require(pacman)) install.packages("pacman")
library(pacman)

pacman::p_load(cem, tidyverse, dplyr, ggplot2, corrplot, tibble, kableExtra, fitdistrplus, outliers)

A intenção deste post é tratarmos de um assunto que está presente na grande maioria das métricas de lead time medidas pelos times ágeis. Não é raro encontramos distribuições de lead time com uma cauda longa a direita, representando itens que ficam bloqueados ou aguardando um item dependente em nosso fluxo de trabalho.

E o que fazer quando tais valores aparecem nas distribuições? A grande maioria dos times ágeis com que eu trabalho me fazem este questionamento. Devemos retirar o valor e tratá-lo a parte? Se deixarmos, como ele afetará nossa média e desvio padrão? Como ficam as estimativas? Vou procurar trabalhar com estes questionamentos neste post, sem é claro esgotar o assunto. O tema é amplo e de antemão afirmo que depende de vários fatores e do objetivo de quem estiver fazendo análise, a melhor forma de tratar estes valores.

O que são os outliers

Damos o nome de outliers aos valores discrepantes de uma variável. Imagine por exemplo que você tem uma tarefa que depende de uma definição de um outro setor, ou que depende uma outra tarefa prévia que demora mais do que o normal que a média das tarefas do seu fluxo para acontecer. Dependendo, em ambos os casos, se a demora for em excesso certamente essa sua entrega terá um lead time muito maior que a maioria de suas tarefas padrão.

A presença de outliers pode causar perda substancial de precisão, uma vez que não podemos incorporar efetivamente o impacto dos outliers nas previsões. Chen and Liu (1993) Este fato vai impactar diretamente nos times que estimam por meio de um intervalo (range): sua demanda tem 85% de chances de ser atendida em até 8 dias por exemplo.

Em 1972, A. J. Fox definiu dois tipos de outliers que poderiam aparecer em uma amostra: os do Tipo 1 que corresponde a uma situação em que ocorre erro grosseiro de observação ou erro de registro e os do Tipo 2 que representam algo como uma grande variabilidade, objetivado uma inovação. Fox (1972) Entendo que a variabilidade na entrada é extremamente importante para a entrega de valor.

Ao que me parece, a presença de outliers nas medições de Lead Time pouco de enquadram nas duas descrições apresentadas acima. Hillmer em 1984 ainda coloca que os outliers que sejam “inovadores” devem sim ser incluídos no tratamento da amostra Hillmer (1984), entretanto a maioria dos casos que presencio atualmente a presença destes números se dá por erro de planejamento e descuido das equipes.

Martin, Samarov e Vandacle (1981), argumentaram que, se os dados forem contaminados por observações atípicas (outliers), as previsões baseadas nesses dados históricos podem ser enviesadas. Martin, Samarov, and Vandaele (1981) Portanto, uma parte de qualquer programa de monitoramento de previsão deve ser direcionada à detecção de outliers. Os times devem ter total atenção quando esses valores aparecem e principalmente observar e estudar suas causas para que não voltem a acontecer, caso sejam por descuido na forma como trabalham, repriorizações constantes e indefinição do que deve ser feito.

Quanto mais tempo um item fica no seu quadro, mais valor ele perde e deixa de entregar.

Podemos prever que uma tarefa ao entrar em nosso board será um outlier?

Essa é uma pergunta difícil de ser respondida. Prever se um item ou não será um ponto fora da curva envolve vários fatores. A questão aqui é que o time deve ter maturidade suficiente para conversar sobre cada tarefa, antes e durante a permanência dela no quadro. Reuniões de upstream são extremamente importantes para detectar anomalias. Elas fazem a triagem e expõem os problemas das tarefas e consequentemente do produto.

Uma tarefa que dependa de outra requer atenção, aliais qualquer nível de dependência no desenvolvimento de um produto requer atenção da equipe. Dentre os impactos que as dependências podem causar estão Scheerer et al. (2015):

Troy Magennis ao falar sobre dependências Magennis (2016) expõe o tema da seguinte forma:

Uma dependência corta nossas opções pela metade para se iniciar o trabalho.

ds2 <- tibble(
  A = c("A", "B"),
  B = c("B", "A"),
  AB = c("SIM", "NÃO")
)
ds2 <- rename(ds2, "Tarefa A" = "A", "Tarefa B" = "B", "A antes de B" = "AB")
ds2$`A antes de B` = cell_spec(ds2$`A antes de B`, bold = T, color = "white", background = ifelse(ds2$`A antes de B` == "NÃO", "red", "green"))
kbl(ds2, escape = F) %>%
kable_styling()
Tarefa A Tarefa B A antes de B
A B SIM
B A NÃO

Se a Tarefa B depende de A, necessariamente temos que começar por A e B ficará em espera até que A termine. Isso limita nosso início de trabalho para 50%. Temos duas tarefas para fazer, mas temos que começar por uma em específico. Veja abaixo quando temos três tarefas e suas dependências:

ds3 <- tibble(
  A = c("A", "A", "B", "B", "C", "C"),
  B = c("B", "C", "A", "C", "A", "B"),
  C = c("C", "B", "C", "A", "B", "A"),
  AB = c("SIM", "SIM", "NÃO", "NÃO", "SIM", "NÃO"),
  ABC = c("SIM", "NÃO", "NÃO", "NÃO", "NÃO", "NÃO")
)
ds3 <- rename(ds3, "Tarefa A" = "A", "Tarefa B" = "B", "Tarefa C" = "C", "A antes de B" = "AB", "A antes de B, B antes de C" = "ABC")

ds3$`A antes de B` = cell_spec(ds3$`A antes de B`, bold = T, color = "white", background = ifelse(ds3$`A antes de B` == "NÃO", "red", "green"))
ds3$`A antes de B, B antes de C` = cell_spec(ds3$`A antes de B, B antes de C`, bold = T, color = "white", background = ifelse(ds3$`A antes de B, B antes de C` == "NÃO", "red", "green"))
kbl(ds3, escape = F) %>%
kable_styling()
Tarefa A Tarefa B Tarefa C A antes de B A antes de B, B antes de C
A B C SIM SIM
A C B SIM NÃO
B A C NÃO NÃO
B C A NÃO NÃO
C A B SIM NÃO
C B A NÃO NÃO

Estamos bem limitados neste caso. Não podemos começar nada sem que seja seguida uma sequência. Podemos até começar C, mas vamos ficar bloqueados pela Tarefa B, ou seja, temos 16,6% para concluir o trabalho dada as dependências acima. O ideal é que o time converse com antecedência sobre este cenário para não incorrer em atrasos e altos lead times.

Tarefas com dependências e outliers tem uma relação direta. Importante que isso seja tratado pelo time antes do trabalho começar. Dê papéis claros de quem serão os responsáveis pela tarefa. Chame os times que são responsáveis pelas predecessoras para participarem das reuniões e tomarem ciência das dependências.

Diferença entre tratar uma amostra com e sem outliers

Vou apresentar um cálculo bem simples quando usamos os outliers para os nossos cálculos e quando retiramos da amostra. Vou usar um conjunto real de dados com 40 observações de lead times.

dados <- read.csv2('lead_time.csv') # Carregamento do arquivo csv
as_tibble(dados)
## # A tibble: 40 x 1
##       X3
##    <int>
##  1     4
##  2     3
##  3     6
##  4     5
##  5     4
##  6     3
##  7     2
##  8     4
##  9     3
## 10     2
## # ... with 30 more rows
dados <- dados %>% 
  rename("lead_time" = X3)
summary(dados)
##    lead_time    
##  Min.   : 2.00  
##  1st Qu.: 3.00  
##  Median : 4.00  
##  Mean   : 6.75  
##  3rd Qu.: 6.00  
##  Max.   :42.00
dados %>% ggplot(aes(x=lead_time)) + 
  geom_histogram(binwidth=1, fill="#69b3a2", color="#e9ecef", alpha=0.9) +
  labs(x = "Lead Time", y = "Frequência") +
  scale_x_continuous(breaks = seq(1,42, by = 2)) +
  scale_y_continuous(expand = expansion(add = c(0,1)), breaks = seq(0,12, by = 2))

Repare pela visualização que temos dois possíveis outliers, uma tarefa levou 28 dias e outra 35 e 42 dias. Vamos usar um boxplot para confirmação e logo abaixo vou plotar um outro gráfico, desta vez sem outliers.

boxplot(dados$lead_time, main="Lead Time", 
        xlab = "Lead Time", ylab="Dias")$out

## [1] 28 35 42

Os valores estatísticamente considerados como outliers são: 28, 35 e 42. Vamos ver como calculamos estes valores, mas antes veja abaixo o gráfico sem os valores discrepantes.

boxplot(dados$lead_time, main="Lead Time", 
        xlab = "Lead Time", ylab="Dias", outline=FALSE)



Como encontrar os outliers

Este é um passo importante porque a visualização nem sempre é a maneira mais eficaz de analisar outliers. Nem sempre você pode olhar para uma amostra e dizer: “Este é um outlier porque está longe do resto dos pontos.” Seu conjunto de dados pode ter milhares de observações e é importante ter um corte numérico que diferencie um outlier de um não outlier. Isso permite que você trabalhe com qualquer conjunto de dados, independentemente de seu tamanho.

Vamos usar a função quartile() para encontrar o 25º e o 75º, e a função IQR () que elegantemente nos dá a diferença do 75º e 25º quatil.

Q <- quantile(dados$lead_time, probs = c (.25 , .75) , nd.Rm = FALSE)
Q
## 25% 75% 
##   3   6
iqr <- IQR(dados$lead_time)
iqr
## [1] 3

Vamos calcular os limites:

sup <- round(Q[2]+1.5*iqr) # Limite Superior
inf <- round(Q[1]-1.5*iqr) # Limite Inferior

Pelos limites, os valores que estão acima de 10 e abaixo de 3 (arredondei os valores, pois não fazem sentido neste caso) são considerados outliers por estarem uma vez e meia o IRQ para cima e uma vez e meia o IRQ para baixo.

Vamos verificar agora média e desvio padrão com e sem os outliers. Para ser didático vou criar um outro dataset sem os outiers, entretando existem outras maneiras de se fazer isso que vou apresentar no final do artigo.

medidas_com <- dados %>%
    summarise(Media = mean(lead_time),
             Desvio_Padrao = sd(lead_time))
dados_sem_outliers <- subset(dados, dados$lead_time > (Q[1] - 1.5*iqr) & dados$lead_time < (Q[2]+1.5*iqr))

medidas_sem <- dados_sem_outliers %>%
      summarise(Media = mean(lead_time),
              Desvio_Padrao = sd(lead_time))
medidas <- full_join(medidas_com, medidas_sem)
## Joining, by = c("Media", "Desvio_Padrao")
rownames(medidas) = c("Com Outliers", "Sem Outliers")

kbl(medidas, escape = F) %>%
kable_paper("hover", full_width = F)
Media Desvio_Padrao
Com Outliers 6.75000 8.499623
Sem Outliers 4.45946 1.908964

Pelo óbvio confirmamos acima que a média e o desvio padrão quando excluímos os outliers são menores. Basicamente na média, temos 2 dias de diferença com apenas 3 valores discrepantes. Imagine agora um time com muitos outliers? No desvio padrão a diferença é ainda maior.

Vamos verificar como ficam os quatis com e sem outliers:

quant_com <- data.frame(round(quantile(dados$lead_time)))
colnames(quant_com) = c("Com Outliers")
quant_com <- quant_com %>% 
  t()
quant_sem <- data.frame(round(quantile(dados_sem_outliers$lead_time)))
colnames(quant_sem) = c("Sem Outliers")
quant_sem <- quant_sem %>% 
  t()
quartis <- rbind(quant_com, quant_sem)
quartis %>% kbl(escape = F) %>%
  kable_paper("hover", full_width = F)
0% 25% 50% 75% 100%
Com Outliers 2 3 4 6 42
Sem Outliers 2 3 4 5 9

Notem que há uma pequena mudança (1 dia) no quartil 75º. Se você costuma usá-lo para estimar poderá, com o tratamento dos outliers, terá um cenário mais otimista. Sem os outliers, 100% de suas demandas foram entregues em até 9 dias, e isso muda é claro no cenário sem o devido tratamento.

Cada time é único, e a forma com que lida com estes valores pode variar. O importante é você identificar e descobrir as causas destes acontecimentos fora da curva.

Outras formas de identificarmos outliers

Primeiro vamos identificar os outliers:

boxplot(dados$lead_time, plot=FALSE)$out
## [1] 28 35 42

Vamos salvar estes numeros em um vetor

outliers <- boxplot(dados$lead_time, plot=FALSE)$out

Este vetor deve ser excluído de nosso conjunto de dados. A função which() nos diz em quais linhas existem os outliers, essas linhas devem ser removidas de nosso conjunto de dados. No entanto, antes de removê-las, vamos armazenar o conjuntos de dados inicia em uma variável para garantir que esta primeira amostra não seja perdida. Este procedimento é uma precaução que sempre adoto, e recomendo fortemente que vocês também façam isso.

dados_inicial <- dados
dados <- dados [-which(dados$lead_time %in% outliers),]
dados <- data.frame(dados)

Ou você pode fazer de forma direta aproveitando o vetor “outliers” criado anteriormente.

dados_inicial %>%
    filter(!lead_time %in% outliers) %>%
    summarise(Media = mean(lead_time))
##      Media
## 1 4.459459

Um dica de coach é: aproveite os momentos de discussões com sua equipe (reuniões diárias principalmente) e convere sobre dependências e o que pode impactar o trabalho. Chame se possível representantes de outras equipes que tem influência direta sobre o trabalho. A coordenação (FL2) Leopold (2020) é fundamental para evitar o colapso. Por último, qualquer evento do Scrum deve ser usado para inspeção e adaptação.

Grande abraço e até o próximo post!

Referências

Chen, Chung, and Lon-Mu Liu. 1993. “Forecasting Time Series with Outliers.” Journal of Forecasting 12 (1): 13–35. https://doi.org/10.1002/for.3980120103.
Fox, A. J. 1972. “Outliers in Time Series.” Journal of the Royal Statistical Society Series B (Methodological) Vol. 34 Iss. 3 34. https://doi.org/10.2307/2985071.
Hillmer, Steven. 1984. “Monitoring and Adjusting Forecasts in the Presence of Additive Outliers.” Journal of Forecasting 3 (2): 205–15. https://doi.org/10.1002/for.3980030208.
Leopold, Klaus. 2020. Repensando a Agilidade: Por Que Os Times Ágeis Não Têm Nada a Ver Com Business Agility (Portuguese Edition). Translated by Jose JR and Paula Viani. Kindle Edition. LEANability PRESS.
Magennis, Troy. 2016. “Dependencies.” December 19, 2016. https://github.com/FocusedObjective/FocusedObjective.Resources/blob/master/Canvas and Forms/Dependencies.pdf.
Martin, R Douglas, Alexander M Samarov, and Walter Vandaele. 1981. Robust Methods for ARIMA Models. Center for Computational Research in Economics; Management Science ….
Scheerer, Alexander, Saskia Bick, Tobias Hildenbrand, and Armin Heinzl. 2015. “The Effects of Team Backlog Dependencies on Agile Multiteam Systems: A Graph Theoretical Approach,” January. https://doi.org/10.1109/hicss.2015.606.