É tudo uma questão de confiança.

No primeiro artigo Além das médias utilizei simulação de Monte Carlo para demonstrar experimentalmente a probabilidade teórica de 31.25% do evento tirar 3 caras ao jogar 5 vezes uma moeda.

Na implementação utilizei somente um input, o totalSimulacoes porque o total de simulações afeta a confiança estatistica da simulação.

E também não utilizei nenhum método para definir o total de simulações que foi executado. Naquela demonstração foram utilizadas quantidades aleatórias de simulações.

  • Um quantidade de simulações muito baixa: 50 simulações
  • Uma extremamente alto: 1 bilhão de simulações.
  • Uma alta: 200 milhões.

Os resultados estão na caixa abaixo.

#Poucas Rodadas não trazem uma boa estimativa, por exemplo, com 1 rodada ou srá Probabilidade de 0% ou de 100%.

./flipCoin3on5 1
Simulou 1 rodadas de 5 lançamentos...
Casos com exatamente 3 caras: 0
Probabilidade Estimada: 0.0000 (0.00%)

./flipCoin3on5 1
Simulou 1 rodadas de 5 lançamentos...
Casos com exatamente 3 caras: 1
Probabilidade Estimada: 1.0000 (100.00%)

#Com uma quantidade de Rodadas insuficiente o dado não é confiavel.
./flipCoin3on5 50
Casos com exatamente 3 caras: 17
Probabilidade Estimada: 0.3400 (34.00%)

./flipCoin3on5 50
Casos com exatamente 3 caras: 17
Probabilidade Estimada: 0.3400 (34.00%)

#Com rodadas demais temos uma ótima estimativa mas desperdiçamos tempo.
# 1 Bilhão rodadas => 1 minuto
time ./flipCoin3on5 1000000000   
Simulou 1000000000 rodadas de 5 lançamentos...
Casos com exatamente 3 caras: 312500797
Probabilidade Estimada: 0.3125 (31.25%)
real    1m8,066s
user    1m8,147s
sys    0m0,080s

#Com rodas o suficiente temos uma ótima estimativa 
#com 8/10 execuções com Probabilidade estimativa identica a Probabilidade teórica.
# Duzentos Milhões de rodadas => 13 sec
time ./flipCoin3on5 200000000 
Simulou 200000000 rodadas de 5 lançamentos...
Casos com exatamente 3 caras: 62506172
Probabilidade Estimada: 0.3125 (31.25%)
real    0m13,721s
user    0m13,732s
sys    0m0,012s

time ./flipCoin3on5 200000000
Simulou 200000000 rodadas de 5 lançamentos...
Casos com exatamente 3 caras: 62489409
Probabilidade Estimada: 0.3124 (31.24%)
real    0m13,547s
user    0m13,557s
sys    0m0,024s

Nessas simulações, com a probabilidade teórica em mãos (31.25%) me permiti variar o totalSimulacoes até encontrar um resultado aceitavel, como é o caso de 200 milhoes de simulações, para ter uma precisão de ±0,01% (ou 0,0001).

Também foi por ter a probabilidade teórica que toda a ideia empirica de resultado aceitavel, de previsão e valor ideal para totalSimulacoes pareceu aceitavel. Era só olhar para o resultado calculado corretamente de 31.25% e brincar de tá frio, ta quente.

Assim, a pŕoxima pergunta que acho interessate é mas e se? E se não tivessemos a probabilidade teórica?

Sem a probabilidade teórica para comparar não ia ser possível brincar de frio e quente pensando “a amostra de 50 ficou longe do resultado certo mas a 200 milhões ficou próxima ao número calculado hein… deve estar certo o suficiente.”

Eu teria que abandonar a acurácia, que é relativa(a proximidade de uma medida com seu valor verdadeiro/real) por falta do que comparar(probabilidade teórica) e buscar outra alternativa para atender a necessidade de querer confiar na estimativa.

Pode ser precisão…
A primeira ideia seria começar a pensar em precisão. Precisão é a proximidade entre múltiplas medições repetidas (repetibilidade do resultado), mesmo que estejam erradas.

Sera que a probabilidade estimada com um totalSimulacoes de 50 é precisa? Numa serie de 5 estimativas os resultados foram: 0.34, 0.40, 0.37, 0.20, 0.30. Definitivamente não!

Já quando utilizo um totalSimulacoes de 1 bilhão a série de 5 traz uma boa precisão: 0.312497, 0.312493, 0.31254, 0.312496, 0.312497 . Mas cada rodada levou ± 9 minutos com flipCoin.go (e ± 3 minutos com flipCoinBigNumber.go)

… ou confiança.
o Nível de Confiança que você definiu que precisa.

Diferente do que busquei com as ideias acima em a ideia de resultado certo era subjetiva a ideia de nível de confiança é objetiva.

Ao executar 2 bilhoes de rodadas do jogo “jogar 5 vezes a moeda pra ver quantas vezes acerto 3 caras” o resultado a variação na estimativa reduzia. Eu poderia aumentar ainda mais o numero de rodadas, mas até quando deveria continuar aumentando esse numero para ter certeza?

Usar 1 milhão de rodadas me deu uma variação pequena, mas que não atendia meu objetivo de provar experimentalmente a resposta teórica de 31.25% porque ainda faltava acertar o centésimo.

Na tabela abaixo você pode ver a variação diminuindo(numero de casas decismais estaveis aumentando) ou seja, a precisão da resposta aumentando.

Numero de RodadasEstimativa
50
0.2600000
0.3400000
0.2800000
1 milhão
0.3125800
0.3120200
0.3128260
0.3124460
42 milhões0.3125884
0.3125093
0.3124413
400 Milhões
0.31250427
0.31249493

Essa percepção de melhoria carece de uma prova e para isso precisamos de 2 ferramentas. A formula de erro padrão e o zScore.

O Erro Padrão (SE de standard error)
O erro padrão mede a disperção em torno da média amostral(soma de todos os valores da amostra dividida pelo número de valores).

erro = sqrt( p * (1 – p) / n )

// variancia: p * (1 - p). 
// No caso de uma moeda justa. A chance de cara ou coroa é igual a 50%=0.5
// 0.5 *( 1 - 0.5) = 0.5 * 0.5 = 0.25

// / n: numero de tentativas. 
erro := math.Sqrt( p* (1-p) / float64(n))

Uma pergunta interessante de responder é: Por que estamos usando um p de 0.5? Tem um artigo tratando desse tema aqui: [[Como lidar com o Erro Padrão se não sei a probabilidade teórica?]] A resposta, por enquanto, é que escolhemos ignorar a probabilidade teórica de 0.3125 a favor de uma análise totalmente experimental.

E de acordo com os experimentos temos os seguintes erros padrão:

Numero de RodadasErro Padrão
500.0707 => 7.07%
1 milhão0.0005 => 0.05%
42 milhões0.000077 => 0.0077%
400 Milhões0.000025 => 0.0025%

“A precisão em Monte Carlo é cara: o custo computacional cresce ao quadrado da precisão desejada.”

A essa altura dá pra ver o quanto custa reduzir o erro. Esse é um ponto importante do problema. A precisão não aumenta de forma linear; você precisa quadruplicar o tamanho da amostra para dobrar a precisão! Essa é a Lei dos Grandes Números

Vamos analisar os dados:

Régua de Confiança
Na prática, o erro padrão é a unidade de uma regua de confiança. Então, o valor de 7.07%, por exemplo, é o tamanho típico do desvio que esperamos ver em 50 jogadas. Cada passo na reguá de confiança do experimento de 50 rodadas representa 7.07%.

“Mas porcentagem é uma medida relativa. 7.07% relativo a que?”

Ao Normal! 🙂

Usando o exemplo da moeda, ao jogar uma moeda não viciada(alterada para cair mais de um lado que outro) a probabilidade teórica(o normal) diz que cada lado tem 50% de chance. Se você jogar 50 vezes essa moeda o resultado teórico é que 25 das vezes voce terá uma cara e outras 25 a coroa.

Mas o mundo real tem aleatoriedade. A realidade é que as vezes vai ter 28 caras x 22 coras, 29 coroas x 21 caras e assim aleatoriamente. E você vai achar normal essa variação; é uma questão de sorte.

Mas se o resultado for 45 caras e 5 coras você vai desconfiar.

O erro padrão é uma ferramenta estatistica objetiva que diz se uma variação é normal ou estranha como 45caras e 5 coroas parece ser.

Somente com o erro padrão ainda não é possivel encontrar o número ideal de simulações para o experimento, mas já se aprende alguma coisa sobre ele.

  1. Arredonde sua estimativa para a mesma ordem de grandeza do seu Erro Padrão.
   // se SE = 0.0005 e estimativa = 0.31256789
   // a linha abaixo trata o ruido como precisão
   fmt.printf("Probabilidade de %.8f\n") 
   // a linha abaixo fala a verdade sobre a precisão pq ignora o ruido do experimento
   fmt.printf("Probabilidade de %.4f\n")    
   // a linha abaixo fala a verdade e adiciona uma informação importante sobre o comportamento de arredondamento. 
   fmt.printf("Probabilidade de %.5f\n")    
**Lembra da ideia da SE ser uma unidade numa regua?** Então, se sua regua tem um risquinho a cada 0.0005 como é que voce vai medir algo menor que isso? Use essa ideia para remover o ruido.  
  1. Economize poder computacional.
    Utilize a SE como uma condição de parada de sua simulação.
    Ao afirmar que você quer que a simulação pare ao atingir uma SE de 0.0005 sua simulação passa a variar de acordo com a incerteza do evento e ao invés de um loop de tamanho fixo. Não se esqueça de iniciar a amostra com um numero significativo ou voce pode ter um falso positivo com amostragens pequenas(veja a tabela para 50 rodadas).

O ZScore

O zScore é um multiplicador. Ele diz a quanto erros padrão você esta longe da normalidade. Na imagem anterior você tem uma regua com uma curva normal e algumas marcações nela. O ponto central da curva é a probabilidade teórica, ele marca 0.5 como chance teórica de cair cara ou coroa ao se jogar uma moeda e do centro para as bordas você tem os desvios-padrão.

Abaixo uma tabela de Desvio Padrão e zScore. De uma olhada na regra 68-95-99,7 para saber mais

Desvio PadrãoZInterpretação
050%O dado esta exatamente na média
+- 168%aproximadamente 68% dos dados estão entre -1 e +1
+- 1.64590%aprox. 90% dos dados estão distantes 1.645 desvio padrão da média
+- 1.9695%aprox. 95% dos dados estão distantes 1.96 desvio padrão da média
+-399,7%aprox. 99.7% dos dados entre -3 e +3
![[Pasted image 20251223125519.png]]

Custo do Erro x Custo da Amostra
Na prática o intervalo de confiança de 95% é o mais utilizado nas áreas cientificas e de negócios e isso se deve a uma analise que leva em conta:

  • o custo do erro, ou quão ruim é se a nossa conclusão estiver errada.
  • o custo da amostra, ou quanto custa obter os dados para ter mais certeza.

Quanto maior for o custo do erro, maior é o intervalo de confiança utilizado. Por exemplo, na industria farmaceutica utiliza 95% de confiança na funcionalidade do medicamento perante um placebo, porém, exigem 99% de confiança de que o medicamento é seguro e não toxico ao olhar para seus efeitos colaterais.

“É mais aceitavel um remédio não ter efeito algum do que ter um efeito colateral danoso que pode levar a morte”

Voltando ao nosso exemplo, o intervalo de confiança utilizado é 95%, assim o z é 1.96.

Margem de Erro

MargemErro = zScore x Erro padrão.
ME(50 rodadas) = 1.96 * 0.0707
ME(50 rodadas) = 0.13857 ~= 0.1386 => 13.86%.

Isso significa que ao jogar 50 vezes uma moeda qualquer resultado até 13.86% diferente de 50% é considerado “sorte normal”.

médiaMargem errovalor final%exemplo resultado
Limite Superior0.5+ 0.13860.638663.8631 caras x 19 coroa
Limite inferior0.5– 0.13860.361436.1418 caras x 32 coroas
Numero de RodadasErro PadrãoMargem de Erro
500.0707 => 7.07%± 0.13857
1 milhão0.0005 => 0.05%± 0.00098
42 milhões0.000077 => 0.0077%± 0.00015
400 Milhões0.000025 => 0.0025%± 0.000049

No experimento com 50 rodadas, o erro padrão é de 7.07%, isso significa que se voce repetir o experimento vezes o suficiente vai encontrar uma variação no resultado 7.07% distante da probabilidade teórica de 50% para cada lado. O script flipCoin50xSE707.go executa um experimento para validar esse dado teórico.

Na prática isso significa que não podemos utilizar somente 50 rodadas para validar um evento de lançamento de moedas. Isso ocorre porque com essa quantidade “a sorte” ainda ganha da lei de grandes numeros e por causa disso a precisão da regua é muito baixa.

O exprimento de 50 rodadas não tem a precisão desejada.

Para comprovar experimentalmente a propabilidade teórica precisamos de uma precisão de centéssimo. O resultado teórico é 31.25%.

Para ilustrar, podemos dizer que não dá pra usar uma regua com precisão de 7cm para medir algo quando você quer saber a medida em centéssimo de milimetro, por que você vai ser obrigado a dizer que mede 7cm e alguma coisa. Falta Precisão.

Assim como temos regua, trena, paquimetro, micrometro e outros instrumentos com precisões diferentes dependendo da aplicação nós utilizamos tamanhos de amostra diferentes para diferentes aplicações.

Agora que já conversamos sobre as peças necessárias para validar experimentalmente o resultado teórico. Vamos a implementação.

Precisamos de um código:
a) Com margem de erro que garanta uma precisão como em 0.3125
b) que saiba quando parar.

A ideia é implementar uma regra de parada (b) usando o conceito de (a).

Começando pelo fim, o resultado:

Tamanho Amostragem: 40000000
Estimativa : 0.312592 | Margem de erro : 0.0001 
[NOK] Margem de Erro 0.000144 > 0.000140.

Tamanho Amostragem: 41000000
Estimativa : 0.312556 | Margem de erro : 0.0001 
[NOK] Margem de Erro 0.000142 > 0.000140.

Tamanho Amostragem: 42000000
Estimativa : 0.312404 | Margem de erro : 0.0001 
[NOK] Margem de Erro 0.000140 > 0.000140.

Tamanho Amostragem: 43000000
Estimativa : 0.312398 | Margem de erro : 0.0001 
[OK] Margem de Erro 0.000139 < 0.000140.
Simulou 43000000 rodadas de 5 lançamentos...
Probabilidade Estimada: 0.3124 (31.24%)

Definição das regras.

const lancamentosPorVez = 5                                                     
const alvoCaras = 3                                                             

func main() {                                                                   

    rand.Seed(time.Now().UnixNano())                                            
    // delta aceitavel na estimativa 0.0001                                     
    variacaoAceita := 0.00014
    // para evitar falsos positivos com poucas simulações inicio com 10.000.000
    totalSimulacoes := 10000000
    // se oresultado é insatisfatório o numero de simulacoes aumento em 1.000.000
    stepSize := 1000000
    // define um nivel de confiança de 95%
    nivelConfianca := 1.96  

Execução do evento para coletar resultado.

// Dentro da função rodada lanço 5 vezes a moeda e conto o resultado.
carasNoExperimento := 0
// lancar a moeda 5 vezes.    
for j := 0; j < lancamentosPorVez; j++ {
    if lancaMoeda() == 1 {
        carasNoExperimento++
    }
}

// Se o experimento resultou em exatamente 3 caras, contamos como sucesso
if carasNoExperimento == alvoCaras {
    sucessos++
}

Execução da simulação até satisfazer as regras.

“Eu só ficarei satisfeito com a simulação quando eu tiver 95% de certeza estatística de que o valor real está entre (Estimativa – 0.00014) e (Estimativa + 0.00014).”

// ... variáveis de configuração (margem de erro aceita, nível de confiança 95%)

for {
    // 1. Executa a simulação massiva (ex: começa com 10 milhões)
    probabilidadeEstimada = rodada(totalSimulacoes)

    // 2. Calcula o Erro Padrão para uma proporção binomial
    // Fórmula: sqrt( (p * (1-p)) / n )
    erroPadrao := math.Sqrt( (probabilidadeEstimada * ( 1- probabilidadeEstimada)) / float64(totalSimulacoes))

    // 3. Calcula a Margem de Erro atual (baseado em 95% de confiança, Z=1.96)
    margemErroAtual := nivelConfianca * erroPadrao

    // ... prints ...

    // 4. Verifica se a margem de erro já é pequena o suficiente
    if  margemErroAtual > variacaoAceita {
        // Se o erro ainda é grande, aumenta o tamanho da amostra e tenta de novo
        totalSimulacoes = totalSimulacoes + stepSize
        fmt.Printf("[NOK] Margem de Erro %f > %f.\n", margemErroAtual , variacaoAceita)
    }else {
        // Se o erro já é aceitável, para o loop.
        fmt.Printf("[OK] Margem de Erro %f < %f.\n", margemErroAtual,variacaoAceita)
        break
    }
}

Por que a variacaoAceita tem o valor de 0.00014 ?

  • Se a aceitação fosse 0.01 (1%), o código pararia muito rápido, mas a resposta seria imprecisa (ex: 31% ± 1%).
  • Se a aceitação fosse 0.000001, o código rodaria por horas ou dias para atingir uma precisão extrema e talvez desnecessária.

Por que inicie o numero de rodadas com 10 milhões?

  • Para evitar um falso positivo com poucas rodadas que poderia dar resposta de erro de 0 erro por pura sorte.

Essa foi uma jornada longa para responder uma pergunta aparentemente simples.
Esse exercício de explicar os porquês do código foi uma jornada focada em fixar conceitos de probabilidade, mas não foi uma jornada exaustiva.

Apesar de longa, a explicação é simplificada e deixou varios pontos a serem explicados e que vou abordar em outros textos e linkar com esse texto para permitir uma navegação maior pelos conceitos e aprendizados. E como a curiosidade é a mãe do aprendizado nada melhor que perguntas para exemplificar esses pontos.

  • Por que foi utilizado 0.5 para a probabilidade da moeda?
  • Por que ao aumentar o número de simulações o ruído diminui e me aproximo da probabilidade teórica?
  • Por que foi utilizado o nível de confiança de 95%?
  • Por que na tabela de nível de confiança não tem o nível 2?
  • Por que foi utilizado intervalo de Confiança?
  • Por que o resultado da distribuição binomial da moeda se encaixa no resultado da distribuiçµão normal?
  • Por que é confuso falar de Desvio Padrão e Erro Padrão?

O código todo da implementação você encontra em flipCoin3on5_notheoricalKnowledge.go

repositório no github: https://github.com/iannsp/montecarlo