Reproduzindo uma Distribuição Normal Jogando Cara ou Coroa

Fernando Marcos Wittmann
Data Science BR
Published in
6 min readNov 11, 2019

--

Fonte da imagem

Neste post vamos modelar alguns conceitos básicos de estatística através de um dos jogos aleatórios mais clássicos: cara ou coroa. Funções aleatórias são extremamente importantes para algoritmos de machine learning. Muitos tomam vantagem de tais funções aleatórias para adicionar um fator exploratório em seu algoritmo. Random Forest fazem uso de seleções aleatórias de atributos e elementos para criar diferentes árvores de decisão e desta forma evitar overfitting. Em redes neurais, overfitting também é evitado graças à aleatoriedade quando utilizamos camadas do tipo Dropout no qual aleatoriamente desativam uma certa porção dos neurônios. Algoritmos de aprendizagem de reforço também fazem uso de algoritmos aleatórios para inicialmente quando estão explorando o ambiente para a escolha de sua próxima ação. E por aí vai. Para começar, vamos criar um modelo bem simples para tirar cara ou coroa:

import random
id_para_moeda = {0: 'coroa', 1: 'cara'}
moeda_para_id = {'coroa': 0, 'cara': 1}
def jogar_moeda():
return id_para_moeda[random.randint(0,1)]

Vamos agora testar:

>>> jogar_moeda()
'coroa'

Vamos agora testar jogar sequencialmente 10 vezes. Como as chances de retornar cara e coroa são as mesmas, temos que quando a moeda é jogada sequencialmente, a tendência é de que metade das vezes sairá cara e metade das vezes sairá coroa:

>>> for i in range(10):
... print(jogar_moeda())
...
cara
coroa
coroa
cara
coroa
cara
cara
cara
coroa
cara

Temos aqui 6 caras e 4 coroas. A pequena discrepância entra dentro do desvio padrão, como vamos discutir em breve. Vamos agora testar jogar esta moeda 100 vezes seguidas e contar o número de caras e coroas:

>>> def jogar_n_vezes(n=100, imprimir=True):
... if imprimir:
... print(f'Jogando moeda {n} vezes\n')
... resultados = []
... for i in range(n):
... resultados.append(moeda_para_id[jogar_moeda()])
...
... n_caras = sum(resultados)
... n_coroas = n-n_caras
... if imprimir:
... print(f'Resultado')
... print(f'cara - {n_caras} vezes')
... print(f'coroa - {n_coroas} vezes')
... return resultados, n_caras, n_coroas
...
>>> _=jogar_n_vezes()
Jogando moeda 100 vezes
Resultado
cara - 47 vezes
coroa - 53 vezes

Novamente temos proximidades à 50 de cada lado. Vamos agora testar jogar estas moedas sequencialmente 100 vezes por 1000 rodadas e plotar em um histograma a frequência de caras destas rodadas:

>>> import matplotlib.pyplot as plt
>>> numero_de_rodadas = 1000
>>> jogadas_por_rodada = 100
>>> n_caras_rodada = []
>>> n_coroas_rodada = []
>>> for rodada in range(numero_de_rodadas):
... _, n_caras, n_coroas = jogar_n_vezes(jogadas_por_rodada, False)
... n_caras_rodada.append(n_caras)
... n_coroas_rodada.append(n_coroas)
...
>>> _ = plt.hist(n_caras_rodada, 100)
>>> plt.title('Frequencia do numero de caras')
>>> plt.show()
>>> _ = plt.hist(n_coroas_rodada, 100)
>>> plt.title('Frequencia do numero de coroas')
>>> plt.show()

Com este histograma, nós chegamos aqui ao que chamamos distribuição normal, muito comum na observação de elementos naturais. O equacionamento de tal distribuição é o seguinte:

Podemos confirmar que a distribuição acima é equivalente à distribuição normal comparando o gráfico anterior com um plot da função normal:

>>> from math import pi, exp, sqrt
>>> import numpy as np
>>> def normal_distr(avg, std, num=50):
... f = lambda x: exp(-(x-avg)**2/(2*std**2))/sqrt(2*pi*std**2)
... X = np.linspace(avg-3*std, avg+3*std, num=num)
... return X, np.array([f(x) for x in X])
...
>>> X, y = normal_distr(50, 5)
>>> _ = plt.hist(n_caras_rodada, 100)
>>> _ = plt.plot(X, y*1000)

Portanto chegamos aqui à uma distribuição normal reproduzida a partir de um jogo de cara ou coroa. Vamos agora calcular algumas estatísticas em cima dos resultados.

Estatísticas

Visualmente, temos que o número mais frequente de vezes que caiu cara está em torno de 50 (centro do gráfico). Temos também que nas regiões mais extremas e improváveis, o número de caras/coroas diminui. Temos que das 1000 rodadas, o menor número de coroas ocorridas foi um pouco menos de 35 e o maior número foi um pouco acima de 65. Vamos agora calcular algumas estatísticas:

>>> import numpy as np
>>> caras_avg = np.mean(n_caras_rodada)
>>> caras_std = np.std(n_caras_rodada)
>>> coroas_avg = np.mean(n_coroas_rodada)
>>> coroas_std = np.std(n_coroas_rodada)
>>> print(f'Média de caras: {caras_avg}')
>>> print(f'Média de coroas: {coroas_avg}')
>>> print(f'Desvio padrão de caras {caras_std:.2f}')
>>> print(f'Desvio padrão de coroas {coroas_std:.2f}')
Média de caras: 50.172
Média de coroas: 49.828
Desvio padrão de caras 5.04
Desvio padrão de coroas 5.04

Conforme esperado, a média do número de coroas retornadas destas 1000 rodadas foi 50 vezes. Quanto ao desvio padrão, vamos recapitular um pouco a partir da seguinte imagem (do wikipedia):

Temos que em uma distribuição normal, 68.2% dos casos estará dentro da região de um desvio padrão em torno da média (média ± sigma). Isso quer dizer que 68% das vezes o número de vezes que retornou cara/coroa está entre 45 e 55 (ou seja, 50–5 e 50+5). Podemos também dizer que temos 68% de confiança de que o número de vezes que caiu cara está entre 45 e 55. Ou que pelo menos em 68% dos casos, saiu cara/coroa entre 45 e 55. Para uma precisão maior, temos 2 sigmas, que representam 13.6% adicionais em cada extremo. Ou seja, 68.2+2*13.6=95.4% das vezes, o número de vezes que retornou cara/coroa está entre 40 e 60. Em outras palavras, podemos dizer que temos 95% de confiança de que o número de vezes que caiu cara ou coroa está entre 40 e 60. Vamos confirmar estas informações:

>>> cara_entre_45_e_55 = sum(45<=n<=55 for n in n_caras_rodada)
>>> cara_entre_40_e_60 = sum(40<=n<=60 for n in n_caras_rodada)
>>> coroa_entre_45_e_55 = sum(45<=n<=55 for n in n_coroas_rodada)
>>> coroa_entre_40_e_60 = sum(40<=n<=60 for n in n_coroas_rodada)
>>> print(f'Número de rodadas: {numero_de_rodadas}')
>>> print(f'Número de jogadas por rodada {jogadas_por_rodada}')
>>> print(f'Quantidade de rodadas que caíram cara entre 45 e 55 vezes: {cara_entre_45_e_55}')
>>> print(f'Quantidade de rodadas que caíram cara entre 40 e 60 vezes: {cara_entre_40_e_60}')
>>> print(f'Quantidade de rodadas que caíram coroa entre 45 e 55 vezes: {coroa_entre_45_e_55}')
>>> print(f'Quantidade de rodadas que caíram coroa entre 40 e 60 vezes: {coroa_entre_40_e_60}')
Número de rodadas: 1000
Número de jogadas por rodada 100
Quantidade de rodadas que caíram cara entre 45 e 55 vezes: 725
Quantidade de rodadas que caíram cara entre 40 e 60 vezes: 968
Quantidade de rodadas que caíram coroa entre 45 e 55 vezes: 725
Quantidade de rodadas que caíram coroa entre 40 e 60 vezes: 968

Portanto, nossa experiência superou o grau de confiança previamente definido: 72% das vezes caiu cara dentro do intervalo de mais ou menos um desvio padrão e 96% das vezes caiu dentro do intervalo de mais ou menos 2 desvios padrões. Portanto está acima da confiança definida acima.

Conclusão

Sendo assim, concluímos a revisão de alguns conceitos básicos de estatística através de um jogo de cara ou coroa. Tais conceitos básicos como desvio padrão e média são popularmente vistos em machine learning. Por exemplo, uma das técnicas mais populares para normalização de dados é a estandardização: subtração dos dados pela sua média seguido de uma divisão pelo seu desvio padrão. Tal normalização fará com que os dados possuam média zero e desvio padrão unitário. O objetivo foi verificar como tais elementos surgem naturalmente para eventos aleatórios. Para qualquer feedback ou sugestão, deixe seu comentário!

--

--

Fernando Marcos Wittmann
Data Science BR

Head of Data Science @ Awari | Machine Learning Expert | E-Learning