//styles, look here: https://cdnjs.com/libraries/highlight.js/9.12.0

January 13, 2022

1743 palavras 9 mins

Tratamento Sequencial I

Neste post, eu vou fingir que eu sou um microeconomista aplicado ou um médico e a minha pergunta é se um dado tratamento - no sentido literal se você for um médico - é mais eficiente que o outro.

Nós estamos acostumados a pensar testes de hipótese da seguinte forma: nós temos \(N\) observações, e algumas foram tratadas - talvez aleatoriamente, talvez selecionado em observáveis ou não observáveis. Nós então formulamos o teste de hipótese e buscamos cometer um erro do tipo I com uma probabilidade no máximo \(\alpha\).

Mas nós podemos ter um outro cenário: primeiro, uma unidade chega na nossa mão e nós escolhemos se alocamos essa unidade para o tratamento A ou B (o tratamento B pode muito bem ser um placebo). Nós observamos o resultado da unidade. Chega uma nova unidade e nós precisamos decidir se alocamos ela para tratamento A e B.

Além de médicos e microeconomistas, esse cenário é interessante em vários outros contextos. Por exemplo, você é dono de um site que faz vendas online e precisa decidir onde vai colocar o botão de comprar de forma a maximizar a chance do usuário comprar o produto.

Este post não vai ser nada estruturado, eu vou simplesmente fazer umas simulações com ideias razoáveis e deixar qualquer formalização da solução do problema pro post seguinte.

O problema

Dada a ideia geral, vamos definir os detalhes. Eu vou simplificar a vida colocando muita estrutura:

  1. Existem dois tratamentos possíveis, tratamento zero e tratamento um
  2. Cada tratamento tem distribuição binomial - ou o tratamento é um sucesso ou é um fracasso - com probabilidade de sucesso \(p_0\) e \(p_1\), respectivamente
  3. As probabilidades são desconhecidas por quem vai escolher o tratamento
  4. O objetivo de quem escolhe o tratamento é meramente maximizar o valor esperado
  5. Nós vamos ter \(N\) observações
  6. Em cada período, a gente pode escolher entre alocar o sujeito pro tratamento 0 ou pro tratamento 1

Soluções?

Segue uma descrição da solução mais simples que eu consigo pensar pro problema: durante \(n < N\) períodos, aloque alternadamente entre tratamento 0 ou tratamento 1. Calcule a média de sucessos de cada tratamento. Para períodos depois de \(n\), aloque todo mundo para o tratamento com maior média.

Eu vou escrever uma função que implementa isso. A função retorna pra qual dos tratamentos o sujeito deve ser alocado:

set.seed(06012022)

simple_rule1 <- function(periodo,cutoff,media0,media1){
  
  if (periodo < cutoff){
    if(periodo %% 2 == 0){
      return(c(0,TRUE))
    } else{
      return(c(1,TRUE))
    }
    
  } else {
    if(media1 > media0){
      return(c(1,FALSE))
    } else{
      return(c(0,FALSE))
    }
  }
}

Veja que periodo %% 2 testa se o período é par (%% é a operação módulo, ou resto da divisão). Isso faz com que nos períodos par, a gente aloque para o tratamento zero e nos períodos ímpares a gente aloque pro tratamento um. A função retorna qual é o tratamento escolhido e também um TRUE ou FALSE. O motivo: eu quero que, uma vez que a função tome a decisão de qual é o melhor tratamento, ela nunca mais mude o valor da média. Isso não é necessariamente realista: você poderia sempre atualizar a sua média depois de tomar a decisão e no caso improvável que a média do tratamento que você escolheu fique pior que a média do outro tratamento, você muda o tratamento. Eu vou criar uma função é que o processo gerador de dados e nos dá tanto o sucesso ou fracasso nesta rodada quanto faz todas as atualizações de média necessárias:

dgp <- function(prob0,prob1,n0,n1,media0,media1,decision,update){
  
  if(decision == 0){
    
    suc <- rbinom(1,1,prob0)
    
    if(update){
    
      media0 <- (n0*media0 + suc)/(n0+1)
      n0 <- n0 + 1
    }
    
    return(c(suc,n0,n1,media0,media1))
    
  } else if(decision == 1){
    
    suc <- rbinom(1,1,prob1)
    
    if(update){
      
      media1 <- (n1*media1 + suc)/(n1+1)
      n1 <- n1 + 1
    }
    
    return(c(suc,n0,n1,media0,media1))
    
  }
  
}

Agora, vamos escrever a função que retorna uma simulação dado uma regra. Ela vai me retornar o payoff obtido pelo agente e qual é a decisão dele no último período:

simulacao <- function(N,prob0,prob1,rule,...){
  
  media0 <- 0
  media1 <- 0
  n0 <- 0
  n1 <- 0
  payoff <- 0
  
  for(i in 1:N){
    
    decision <- rule(periodo = i, media0 = media0, media1 = media1,...)
    
    if(decision[1] == 0){
      
      result <- dgp(prob0,prob1,n0,n1,media0,media1,decision[1],decision[2])
      n0 <- result[2]
      media0 <- result[4]
      payoff <- result[1] + payoff
      
    } else if(decision[1] == 1){
      
      result <- dgp(prob0,prob1,n0,n1,media0,media1,decision[1],decision[2])
      n1 <- result[3]
      media1 <- result[5]
      payoff <- result[1] + payoff
      
    } else{
      stop("Decisão deve ser 0 ou 1")
    }
    
  }
  return(c(payoff,decision[1]))
}

Vamos simular umas mil observações do nosso experimento e com a regra que nós temos. Eu vou deixar a probabilidade de sucesso do experimento 0 ser 0.5 e a probabilidade de sucesso do experimento 1 ser 0.7 e deixar ele alternar por metade dos 30 períodos:

library(purrr)

input <- replicate(1000,30,simplify = FALSE)

output <- map(input,simulacao, prob0 = 0.5,prob1 = 0.7,simple_rule1, cutoff = 15)

output <- do.call(rbind,output)
colnames(output) <- c("Payoff","Proporção de decisões 1")

colMeans(output)
##                  Payoff Proporção de decisões 1 
##                  18.589                   0.689

O próximo passo, naturalmente, é se perguntar quanto tempo você deveria alternar durante os dois tratamentos disponíveis. A próxima função vai permitir que a gente simule a regra simples com diferentes valores pro tempo que a gente passa testando as alternativas - ou explorando elas:

simulacao_cutoff <- function(N,prob0,prob1,cutoffs){
  
  cutoffs_list <- as.list(cutoffs)
  foo <- function(x){simulacao(N,prob0,prob1,simple_rule1,cutoff = x)}
  output <- map(cutoffs_list,foo)
  
  output <- do.call(rbind,output)
  
  return(output)
  
}

O código que faz a simulação, eu vou testar apenas 12 períodos de teste, 14 períodos de teste e 20 períodos de teste. É importante que o número de períodos de teste seja par para grantir que nós vamos ter o mesmo número de observações para cada tratamento. Eu vou fazer duas mil replicações:

input2 <- replicate(2000,c(12,14,20),simplify = FALSE)

output <- map(input2,simulacao_cutoff, prob0 = 0.5,prob1 = 0.7, N = 30)

O código a seguir só organiza a lista em um array com linhas, colunas e uma dimensão a mais e tira a média entre as 2000 simulações:

library(abind)

output <- do.call(abind,list(output,along = 3))

result <- apply(output,c(1,2),mean)
result <- cbind(c("12","14","20"),result)

colnames(result) <- c("n","Payoff","Proporção tratamento 1")

knitr::kable(result)
n Payoff Proporção tratamento 1
12 19.1205 0.77
14 19.0655 0.7875
20 18.8195 0.8385

Esse resultado é bem interessante: note que o maior payoff, na média, é quando nós só gastamos 12 períodos explorando. Veja que a nossa taxa de escolher o tratamento com maior probabilidade é um pouco maior com 20 observações. Isso é totalmente natural, uma vez que a gente tem quase o dobro de observações. Mas isso também requer que a gente passe muito mais tempo jogando a alternativa ruim, o que reduz o payoff que a gente recebe.

Veja que a gente não explorou muita a estrutura de ser capaz de de decidir parar arbitrariamente os testes. De fato, a gente fez basicamente o que um Randomized Controlled Trial faria.

Uma regra simples que me ocorre que explora a possibilidade de parar a qualquer momento do tempo é a seguinte: alterne os tratamentos por \(t_1\) períodos; de \(t_1\) a \(t_2\) continue alternando exceto se a diferença de probabilidades ultrapassar um limiar. Depois de \(t_2\) ou de ultrapassar o limiar, jogue a estratégia com maior média.

Vamos escrever a regra como uma função no R. Dessa vez a função retorna tanto a decisão (como a primeira posição no vetor), se nós estamos atualizando a média ou não, e se nós paramos antes da hora::

simple_rule2 <- function(periodo,t_1,t_2,threshold,media0,media1){
  
  if(periodo < t_1){
    
    if(periodo %% 2 == 0){
      return(c(0,TRUE,FALSE))
    } else{
      return(c(1,TRUE,FALSE))
    }
    
  } else if (periodo < t_2){
    
    if(abs(media0 - media1) < threshold){
      
      if(periodo %% 2 == 0){
        return(c(0,TRUE,FALSE))
      } else{
        return(c(1,TRUE,FALSE))
      }
     
    } else{
      
      if(media0 > media1){
        return(c(0,FALSE,TRUE))
      } else{
        return(c(1,FALSE,TRUE))
      }
      
    }
    
    
  } else{
    
    if(media0 > media1){
      return(c(0,FALSE,FALSE))
    } else{
      return(c(1,FALSE,FALSE))
    }
  }
  
}

Eu vou querer saber quantas vezes eu paro antes de \(n_2\), então eu vou escrever uma função de simulação nova (desculpa, don’t repeat yourself):

simulacao2 <- function(N,prob0,prob1,t_1,t_2,threshold){
  
  media0 <- 0
  media1 <- 0
  n0 <- 0
  n1 <- 0
  payoff <- 0
  early_stop <- FALSE
  
  for(i in 1:N){
    
    decision <- simple_rule2(i,t_1,t_2,threshold,media0,media1)
    
    if(decision[1] == 0){
      
      result <- dgp(prob0,prob1,n0,n1,media0,media1,decision[1],decision[2])
      n0 <- result[2]
      media0 <- result[4]
      payoff <- result[1] + payoff
      
      early_stop <- ifelse(early_stop,TRUE,ifelse(decision[3],TRUE,FALSE))
        
    } else if(decision[1] == 1){
      
      result <- dgp(prob0,prob1,n0,n1,media0,media1,decision[1],decision[2])
      n1 <- result[3]
      media1 <- result[5]
      payoff <- result[1] + payoff
      
      early_stop <- ifelse(early_stop,TRUE,ifelse(decision[3],TRUE,FALSE))
      
    } else{
      stop("Decisão deve ser 0 ou 1")
    }
    
  }
  return(c(payoff,decision[1],early_stop))
}

Vamos simular o caso em que nós observamos pelo menos 10 observações, e se a diferença não for grande o bastante, nós observamos até 15 períodos. Eu vou colocar o threshold em 0.5, o que é relativamente alto:

input <- replicate(1000,30,simplify = FALSE)

output <- map(input,simulacao2, prob0 = 0.5,prob1 = 0.7,t_1 = 10, t_2 = 15, threshold = 0.5)

output <- do.call(rbind,output)

colnames(output) <- c("Payoff","Proporção decisão 1", "Proporção Early Stop")

knitr::kable(colMeans(output))
x
Payoff 19.306
Proporção decisão 1 0.836
Proporção Early Stop 0.310

Nós temos agora uns três parâmetros livres: quantas observações nós fazemos no mínimo antes de tomar a decisão, quantas observações nós fazemos no máximo antes de tomar a decisão, e qual a diferença mínima que nós exigimos para tomar uma decisão antes de ver o máximo de observações. Eu vou brincar de mudar só esse último parâmetro. Primeiro, uma função que permite a gente simular, para vários limiares, a escolha que a regra faria:

simulacao_threshold <- function(N,prob0,prob1,t_1,t_2,thresholds){
  
  thresholds_list <- as.list(thresholds)
  results <- map(thresholds_list,simulacao2,N = N,prob0 = prob0,prob1 = prob1,t_1 = t_1,t_2 = t_2)
  results <- do.call(rbind,results)
  rownames(results) <- thresholds
  colnames(results) <- c("Payoff","Proporção Tratamento 1","Proporção Early Stop")
  
  return(results)
  
}

A simulação em si. EU to mantendo tudo como nos problemas anteriores:

input <- replicate(2000,c(0.2,0.5,0.7),simplify = FALSE)
output <- map(input,simulacao_threshold,N = 30,prob0 = 0.5,prob1 = 0.7, t_1 = 10,t_2 = 15)

output <- do.call(abind,list(output,along = 3))
result <- apply(output,c(1,2),mean)

knitr::kable(result)
Payoff Proporção Tratamento 1 Proporção Early Stop
0.2 19.2420 0.8205 0.8075
0.5 19.3550 0.8525 0.3395
0.7 19.2185 0.8510 0.0710

O resultado sugere que, depois de dez observações, mesmo se a diferença entre as probabilidades for só 0.2, o nosso payoff já é maior que com regras que exigem mais evidência de que um tratamento é melhor que o outro. Com 5 simulações em cada tratamento, isso significa que se um tratamento tiver um sucesso a mais que o outro, nós já deveríamos parar e escolher o tratamento com mais sucessos. A diferença de payoff entre a diferença de probabilidade ser 0.2 ou 0.5 ser tão pequena sugere que talvez algum valor intermediário seja melhor que 0.2 e 0.5.

Existem um milhão de maneiras de brincar com esse problema. No próximo post, eu vou tentar formular isso como um problema de otimização.