Motivação
Recentemente encontrei o artigo How to find pi in randomness all around you. Nele, a autora cita um paper intitulado Estimating π with a Coin, no qual o autor propõe como estimar o valor de π utilizando o lançamento de moedas honestas.
Segundo o paper, lançar uma moeda honesta um número grande de vezes é capaz de estimar o valor de π. Segundo o próprio texto,
Toss a coin until the first time the observed proportion of heads exceeds ½ (that is, until the cumulative number of heads exceeds the cumulative number of tails) and record that fraction; for instance, if the tosses proceeded as tails, heads, tails, heads, heads you would record the fraction ⅗. Now perform a second run of coin-tosses, starting afresh and again recording a fraction that exceeds ½ (starting afresh means that you ignore all earlier tosses). Repeat. As you perform more and more such trials, the average of the fractions will approach π⁄4.
Decidi implementar isso computacionalmente para verificar este fato, apesar do partigo demonstrá-lo matematicamente.
Função para Simulação
A função f_lancamento implementa cada sequência particular de lançamentos até a obtenção de uma sequência de caras cuja proporção em relação ao total de lançamentos seja estritamente maior do que 0.5. Neste caso, estou considerando a realização de uma variável aleatória binomial com \(n = 1\) e \(p = 0.5\) em que o resultado 1 representa cara e 0 representa coroa.
f_lancamento <- function(){
lancamento <- rbinom(1, 1, 0.5)
if (lancamento == 1){
razao <- 1
} else {
lancamento <- c(lancamento, rbinom(1, 1, 0.5))
while(mean(lancamento) <= 0.5){
lancamento <- c(lancamento, rbinom(1, 1, 0.5))
}
}
return(lancamento)
}
Abaixo é possível ver quatro realizações desta função:
set.seed(423809)
f_lancamento()
## [1] 1
f_lancamento()
## [1] 0 1 0 1 1
f_lancamento()
## [1] 1
f_lancamento()
## [1] 0 0 0 1 0 1 1 1 0 1 0 0 0 1 0 1 0 1 0 1 1 1 0 0 0 1 0 0 1 0 1 1 1 1 1
Entretanto, simulações grandes deste tipo podem gerar problemas de gerenciamento de memória, já que a sequência inteira de lançamentos das moedas fica armazenada na memória do R. Sendo assim, a função f_lancamento_media já retorna os valores da proporção de caras em relação ao total de lançamentos da moeda.
f_lancamento_media <- function(){
lancamento <- rbinom(1, 1, 0.5)
if (lancamento == 1){
razao <- 1
} else {
lancamento <- c(lancamento, rbinom(1, 1, 0.5))
while(mean(lancamento) <= 0.5){
lancamento <- c(lancamento, rbinom(1, 1, 0.5))
}
}
return(mean(lancamento))
}
Simulação de Fato
Com a função a ser utilizada na simulação implementada, basta rodá-la um número grande de vezes para obter a estimativa de π. Como é algo computacionalmente intenso, opter por repeti-la apenas 500 vezes e paralelizar o código utilizando todos menos um núcleo do meu computador.
library(foreach)
library(doParallel)
# configurar cluster com o numero maximo de núcleos
# em meu computador, menos 1
cl <- makeCluster(detectCores()-1)
registerDoParallel(cl)
n <- 500
set.seed(1)
# loop paralelizado com foreach
system.time(
serie <- foreach(j = 1:n, .combine = c) %dopar% {
mean(f_lancamento_media())
}
)
## user system elapsed
## 0.060 0.016 291.392
mean(serie) * 4
## [1] 3.151777
stopCluster(cl)
Como é possível ver, a estimativa de π ficou igual a 3.1518, uma diferença de -0.0102 em relação ao valor nomimal de π. Não é uma má aproximação, além de ter sido relativamente rápida.
Entretanto, se aumentarmos o número de replicações, o tempo de execução aumenta de maneira não-linear. Afinal, ao fazer isso, aumentamos a chance de termos sequências mais longas, que demoram mais a ter uma proporção maior de caras em relação a coroas.
No fim, esta forma de cálculo de π acaba sendo mais uma curiosidade do que algo a ser utilizado de forma prática. Entretanto, é interessante que apenas em 2025 alguém tenha conseguido pensar nela e propô-la em um artigo científico.