Análise dos números da Mega Sena

Projeto adaptado para trabalhar com as versões mais recentes do sorteio.

  • Parte 1: Análise Exploratória dos Dados
  • Parte 2: Testes Estatísticos

site: http://loterias.caixa.gov.br/wps/portal/loterias/landing/megasena/

Análise Exploratória dos Dados

Vamos focar a análise nos números sorteados, desconsiderando as informações de arrecadação e premiações. A ideia é brincar um pouco com os sorteios e encontrar uma lógica para gerar um jogo vencedor!

Será que é possível? Vamos ver!

In [1]:
# Importação dos pacotes
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

# Importando funções criadas para o projeto
from analysis.functions import download_raffle_file
from analysis.functions import transform_html_to_csv
from analysis.functions import pre_process_dataframe
from analysis.functions import get_data_dir
from analysis.functions import print_frequency_report

Nessa primeira etapa, vamos apenar baixar os dados e extrair as colunas de interesse!

In [2]:
# Definindo o caminho do download
DOWNLOAD_PATH = 'http://www1.caixa.gov.br/loterias/_arquivos/loterias/D_megase.zip'
FILENAME = 'raffle.html'

# Acessando o diretório de trabalho
SAVE_PATH = get_data_dir()
In [3]:
# Fazendo download do arquivo atualizado
download_raffle_file(url=DOWNLOAD_PATH, path=SAVE_PATH, filename=FILENAME)

# Transformando a tabela html que vem no arquivo zip para csv
transform_html_to_csv(path=SAVE_PATH, filename=FILENAME)
In [4]:
# Criando e pré-processando o dataframe com os números.
megasena = pre_process_dataframe(filename=FILENAME, drop=['Cidade',
                                                          'UF',
                                                          'Arrecadacao_Total',
                                                          'Ganhadores_Sena',
                                                          'Rateio_Sena',
                                                          'Ganhadores_Quina',
                                                          'Rateio_Quina',
                                                          'Ganhadores_Quadra',
                                                          'Rateio_Quadra',
                                                          'Acumulado',
                                                          'Valor_Acumulado',
                                                          'Estimativa_Prêmio',
                                                          'Acumulado_Mega_da_Virada'])
In [5]:
# Definindo nome das colunas
names = ['Data', '1_n', '2_n','3_n', '4_n', '5_n', '6_n']
In [6]:
# Alterando os nomes para facilitar a manipulação
megasena.columns = names

# Vizualizando as primeiras linhas
megasena.head() 
Out[6]:
Data 1_n 2_n 3_n 4_n 5_n 6_n
Concurso
1 1996-11-03 41 5 4 52 30 33
2 1996-03-18 9 39 37 49 43 41
3 1996-03-25 36 30 10 11 29 47
4 1996-01-04 6 59 42 27 1 5
5 1996-08-04 1 19 46 6 16 2

Agora, vamos tentar buscar um pouco de informação nos dados para gerar alguns jogos

Será que conseguimos acertar os números de algum sorteio dessa forma?

In [7]:
# Vamos extrair os sorteios para verificar se algum dos jogos que foram gerados já foram sorteados em alguma oportunidade.
sorteios = []

for i in range(1, len(megasena) + 1):
    sorteios.append(list(megasena.loc[i, '1_n':'6_n'].values))

# Colocando os números em ordem para facilitar a avaliação
for sorteio in sorteios:
    sorteio.sort()
In [8]:
# Avaliando a distribuição de frêquencia entre os números sorteados em cada dezena
mega_dezenas = megasena.loc[:, '1_n':'6_n']

plt.figure(figsize=(18,9))

for i, n in enumerate(mega_dezenas.columns):
    plt.subplot(231+i)
    plt.hist(mega_dezenas[n], bins=60)
    plt.xticks(range(0, 61, 5))
    plt.yticks(range(20, mega_dezenas[n].value_counts().max()+5, 5))
    plt.ylim(20, mega_dezenas[n].value_counts().max()+5)
    plt.title(f'{i+1}ª Dezena')

plt.tight_layout()
In [9]:
# Agora, vamos criar alguns jogos baseados na frequencia de sorteio de cada número.
# Jogos com os números mais frequentes:

jogos_mais_frequentes = []

print()
for i, col in enumerate(mega_dezenas.columns):
    jogos_mais_frequentes.append(mega_dezenas[col].value_counts().head(6).index.to_list())
    print(f'{i+1}ª Dezena: {mega_dezenas[col].value_counts().head(6).index.to_list()}', end='\n\n')
    print('-=' * 20, end='\n\n')

# Colocando os números em ordem para facilitar a avaliação
for jogo in jogos_mais_frequentes:
    jogo.sort()
1ª Dezena: [28, 4, 49, 47, 30, 35]

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

2ª Dezena: [5, 17, 10, 53, 32, 39]

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

3ª Dezena: [27, 18, 58, 56, 54, 24]

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

4ª Dezena: [37, 29, 60, 36, 18, 54]

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

5ª Dezena: [35, 44, 28, 45, 16, 52]

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

6ª Dezena: [23, 33, 17, 30, 5, 16]

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

In [10]:
# Jogos com os números mais frequentes por dezena:
jogos_menos_frequentes = []

print()
for i, col in enumerate(mega_dezenas.columns):
    jogos_menos_frequentes.append(mega_dezenas[col].value_counts(ascending=True).head(6).index.to_list())
    print(f'{i+1}ª Dezena: {mega_dezenas[col].value_counts(ascending=True).head(6).index.to_list()}', end='\n\n')
    print('-=' * 20, end='\n\n')
    
# Colocando os números em ordem para facilitar a avaliação
for jogo in jogos_menos_frequentes:
    jogo.sort()
1ª Dezena: [18, 15, 58, 3, 26, 48]

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

2ª Dezena: [60, 38, 14, 25, 24, 55]

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

3ª Dezena: [9, 26, 22, 49, 60, 3]

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

4ª Dezena: [3, 12, 48, 26, 47, 22]

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

5ª Dezena: [55, 26, 21, 22, 1, 39]

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

6ª Dezena: [21, 29, 28, 60, 55, 35]

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

In [11]:
# Avaliando a distribuição das médias entre os 6 números sorteados em cada sorteio.

plt.figure(figsize=(16,8))
plt.hist(mega_dezenas.agg(func=['mean'], axis=1)['mean'], bins=30, color='gray')
plt.xticks(range(10, 58, 4))
plt.title('Distribuição das Médias de cada Sorteio')
plt.xlabel('Médias')
plt.ylabel('Frequência')
plt.tight_layout()
plt.show()
In [12]:
# Preparando o dataset para avaliação geral dos números
dezenas = mega_dezenas.reset_index(level=0)

dezenas = dezenas.melt(id_vars='Concurso',
                       value_vars=['1_n', '2_n', '3_n', '4_n', '5_n', '6_n'],
                       var_name='Dezena',
                       value_name='Numero')

dezenas.sort_values(['Concurso', 'Dezena']).reset_index(drop=True).head(12)
Out[12]:
Concurso Dezena Numero
0 1 1_n 41
1 1 2_n 5
2 1 3_n 4
3 1 4_n 52
4 1 5_n 30
5 1 6_n 33
6 2 1_n 9
7 2 2_n 39
8 2 3_n 37
9 2 4_n 49
10 2 5_n 43
11 2 6_n 41
In [13]:
# Avaliando a distribuição de frequência geral dos números, descartando a dezena em que foi sorteado.
y_lim = dezenas.Numero.value_counts().max() + 5

plt.figure(figsize=(18,9))
plt.hist(dezenas['Numero'], bins=60, color='green')
plt.xticks(range(0, 61, 5))
plt.ylim(170, y_lim)
plt.title('Frequência Geral dos Números')
plt.xlabel('Números')
plt.ylabel('Frequência')
plt.tight_layout()
plt.show()
In [14]:
# Organizando o gráfico em ordem crescente de frequência

freq = dezenas.groupby('Numero').count().iloc[:,0]

plt.figure(figsize=(16,8))
freq.sort_values().plot(kind='bar')
plt.title('Frequência dos números em ordem crescente')
plt.ylim(170, y_lim)
plt.tight_layout()
plt.show()
In [15]:
print_frequency_report(frequency_dataframe=freq)
OS NÚMEROS QUE MAIS APARECERAM FORAM:

10 que apareceu em 263 sorteios
53 que apareceu em 261 sorteios
5 que apareceu em 255 sorteios
23 que apareceu em 254 sorteios
4 que apareceu em 251 sorteios
33 que apareceu em 251 sorteios

OS NÚMEROS QUE MENOS APARECERAM FORAM:

26 que apareceu em 190 sorteios
55 que apareceu em 195 sorteios
22 que apareceu em 202 sorteios
21 que apareceu em 203 sorteios
15 que apareceu em 206 sorteios
3 que apareceu em 207 sorteios

Jogo com números mais frequentes: [4, 5, 10, 23, 33, 53]
Jogo com números menos frequentes: [3, 15, 21, 22, 26, 55]
In [16]:
# Finalizamos então com 14 jogos,
# 12 para a maior e menor frequencia dos números em cada dezena
# Além do maior e menor descartando a ordem que apareceu.

geral = [[4, 5, 10, 23, 33, 53], [3, 15, 21, 22, 26, 55]]


# Vamos juntar todos os jogos!
# Será que algum deles já foi sorteado?

todos_os_jogos = [jogos_mais_frequentes, jogos_menos_frequentes, geral]
In [17]:
# Checando todos os jogos
for jogos in todos_os_jogos:
    for jogo in jogos:
        if jogo in sorteios:
            print('Temos um vencedor!!!', jogo)
            break
        else:
            print('Esse jogo nunca foi sorteado:', jogo)          
Esse jogo nunca foi sorteado: [4, 28, 30, 35, 47, 49]
Esse jogo nunca foi sorteado: [5, 10, 17, 32, 39, 53]
Esse jogo nunca foi sorteado: [18, 24, 27, 54, 56, 58]
Esse jogo nunca foi sorteado: [18, 29, 36, 37, 54, 60]
Esse jogo nunca foi sorteado: [16, 28, 35, 44, 45, 52]
Esse jogo nunca foi sorteado: [5, 16, 17, 23, 30, 33]
Esse jogo nunca foi sorteado: [3, 15, 18, 26, 48, 58]
Esse jogo nunca foi sorteado: [14, 24, 25, 38, 55, 60]
Esse jogo nunca foi sorteado: [3, 9, 22, 26, 49, 60]
Esse jogo nunca foi sorteado: [3, 12, 22, 26, 47, 48]
Esse jogo nunca foi sorteado: [1, 21, 22, 26, 39, 55]
Esse jogo nunca foi sorteado: [21, 28, 29, 35, 55, 60]
Esse jogo nunca foi sorteado: [4, 5, 10, 23, 33, 53]
Esse jogo nunca foi sorteado: [3, 15, 21, 22, 26, 55]

Nem com toda essa lógica conseguimos acertar um jogo.
E ainda, tentamos em mais de 2300 sorteios.

É tão díficil assim ganhar na Megasena? Vamos fazer alguns testes.

Testes Estatíscos:

  • Vamos tentar entender as dificuldades em conseguir sair com o prêmio principal?
  • Vamos validar a tabela de probabilidades da Caixa Econômica Federal?
In [18]:
# Importando a classe "Gambler" criada para esse projeto
# Essa classe simula um apostador.

from statistics.gambler import Gambler

Para criarmos a classe apostador, precisamos definir alguns parametros:

  • intervalo_numeros = Intevalo de números aleatórios(Ex: MegaSena = 60)
  • quantidade_numeros_sorteados = Quantidade de números a serem sorteados (Ex: MegaSena = 6)
  • quantidade_numeros_jogados = Quantidade de números escolhidos para jogar (Ex: MegaSena de 6 até 15)
  • jogos_por_jogador = Quantidade de jogos aleatórios para tentar acertar o sorteio

Dessa forma, podemos validar outros tipos de jogos e sorteios.
Nesse caso, vamos focar apenas na megasena.

In [19]:
# Definindo as regras do sorteio
intervalo_numeros = 60              
quantidade_numeros_sorteados = 6    
In [20]:
# Definindo como serão nossos jogos
quantidade_numeros_jogados = 6      
jogos_por_jogador = 1
In [21]:
# Instanciando a classe com os parametros definidos anteriormente
gambler = Gambler(numbers_range=intervalo_numeros,
                  numbers_amount=quantidade_numeros_sorteados,
                  numbers_played=quantidade_numeros_jogados,
                  trials=jogos_por_jogador)
In [22]:
# Com a classe instanciada, conseguimos acessar algumas informações, como o número sorteado.
# Prodemos sobrescrever esse número se for necessário.

# Nesse exemplo, temos o sorteio, os jogos feitos pelo apostador e a quantidade de acertos por jogo.
# Nesse caso, temos apenas um jogo, mas podemos aumentar para quantos quisermos.

print(f'Sorteio: {gambler.raffle_numbers}')
print(f'Aposta: {gambler.gamble()[0]}')
print(f'Acertos: {gambler.check_hits()}')
Sorteio: [56, 7, 23, 39, 10, 3]
Aposta: [19, 2, 47, 10, 17, 26]
Acertos: [1]
In [23]:
# Agora, vamos tentar 100 vezes.
jogos_por_jogador = 100
In [24]:
# Vamos instanciar a classe novamente, agora usando 100 jogos por jogador.
gambler = Gambler(numbers_range=intervalo_numeros,
                  numbers_amount=quantidade_numeros_sorteados,
                  numbers_played=quantidade_numeros_jogados,
                  trials=jogos_por_jogador)
In [25]:
# Como o relatório ficaria muito grande, criamos uma função que avalia todos os jogos e retorna nosso resultado.
gambler.play()
That´s SAD! We tried 100 time(s), but we didn't hit all the 6 numbers!
Our best game just hit 3 number(s)!

Não conseguimos acertar nem com 100 tentativas.
Será que conseguimos com 1 milhão?

In [26]:
# Vamos tentar novamente, agora com 1.000.000 de tentativas.
jogos_por_jogador = 1_000_000

gambler = Gambler(numbers_range=intervalo_numeros,
                  numbers_amount=quantidade_numeros_sorteados,
                  numbers_played=quantidade_numeros_jogados,
                  trials=jogos_por_jogador)

gambler.play()
That´s SAD! We tried 1,000,000 time(s), but we didn't hit all the 6 numbers!
Our best game just hit 5 number(s)!

Já chegamos mais perto, mas mesmo assim não foi o suficiente.
Vamos pedir ajuda ao reporter especializado em jogos e a um clube de apostadores para ver qual a real dificuldade de se vencer nesse jogo

A ideia aqui foi criar uma classe espefícia para gerar os relatórios e outras, para criar uma amostragem significativa o suficiente para validar os resultados que tivermos.

Na "GamblersClub" (Clube de Apostadores) foi adicionado um argumento para definir o número de jogadores (número de amostras) mas a feature mais interessante é que a classe foi otimizada para realizar o processamento das amostras em pararelo, com um ganho bem expressivo de performance:

  • 6 horas: Nenhum tipo de otimização
  • 2 horas: Paralelizado com CPU de 4 núcleos
  • menos de 1 hora: Paralelizado com CPU de 8 núcleos

Esses foram os tempos de processamento para gerar 30 amostras com 100.000.000 de tentativas cada.

In [27]:
# Importando a classe Reporter criada para esse projeto
from magazine.reporter import Reporter
In [28]:
# Instanciando a classe
reporter = Reporter()
In [29]:
# Checando a tabela de probabilidades divulgado pela CEF
reporter.show_probabilities_image()

Para cada quantidade de números jogados, nós geramos 30 amostras com uma quantidade de tentativas alinhada com as chances de acerto. A ideia era ter um número grande o suficiente para ter ao menos 2 acertos na sena.

Mesmo com o processo otimizado para processar os jogos paralelamente, o tempo de processamento é relativamente alto, por isso, salvei as listas para facilitar o acesso.

todos as 15 amostras estão disponíveis através do link: SAMPLES

Um ponto interessante que surgiu no momento de armazenar os dados, foi que como uma matriz numpy o tamanho das amostras excedeu bastante as minhas expectativas. Como solução, usei a biblioteca joblib para salvar os dados no formato pickle e tivemos uma redução bastante significativa.

In [30]:
import matplotlib.pyplot as plt

numpy = plt.imread('data/numpy.png')
pickle = plt.imread('data/pickle.png')

plt.figure(figsize=(15,10))

plt.subplot(121)
plt.axis('off')
plt.title('Armazenamento no formato Numpy')
plt.imshow(numpy)

plt.subplot(122)
plt.axis('off')
plt.title('Armazenamento no formato Pickle')
plt.imshow(pickle)

plt.tight_layout()
plt.show()

Para exemplificar qual seria o processo para gerar as amostras, vamos exemplicar a utilização da classe GamblersClub. Vamos gerar uma amostra de tamanho médio e medir seu tempo.

In [31]:
from statistics.club import GamblersClub
In [32]:
intervalo_numeros = 60              #Intevalo de números aleatórios(Ex: MegaSena = 60)
quantidade_numeros_sorteados = 6    #Quantidade de números a serem sorteados (Ex: MegaSena = 6)
quantidade_numeros_jogados = 6      #Quantidade de números escolhidos para jogar (Ex: MegaSena de 6 até 15)
jogos_por_jogador = 1_000_000       #Quantidade de jogos aleatórios para tentar acertar o sorteio
jogadores = 10                      #Quantidade de repetições para gerar um amostra estatística
In [33]:
gamblers = GamblersClub(numbers_range=intervalo_numeros,
                        numbers_amount=quantidade_numeros_sorteados,
                        numbers_played=quantidade_numeros_jogados,
                        trials=jogos_por_jogador,
                        players=jogadores)
In [34]:
%time gamblers.play()
Wall time: 54 s

Para finalizar, vamos solicitar ao reporter que verifique as amostras e nos gere um relatório sobre nossos resultados comparados com a tabela de probabilidades.

Também vamos solicitar um intervalo de confiança para avaliar esses números.

In [35]:
path = 'data/trials-15_000_000-samples-30-numbers-7.pkl'
%time reporter.load_hits(path)
Wall time: 12.8 s
In [36]:
%time reporter.hit_report(number=4)
The mean score was approximately 14,433 each 15,000,000 trials:

1 hit each 1,039 games played!
We have played with 7 numbers.

the raffler says: 1,038
Wall time: 1min 43s
In [37]:
%time reporter.confidence_report(number=4, confidence=0.99)
A média da quantidade de acertos foi de 14394 até 14476 considerando 99% de confiança
Wall time: 1min 42s
In [38]:
%time reporter.hit_report(number=5)
The mean score was approximately 337 each 15,000,000 trials:

1 hit each 44,510 games played!
We have played with 7 numbers.

the raffler says: 44,981
Wall time: 1min 43s
In [39]:
%time reporter.confidence_report(number=5, confidence=95)
A média da quantidade de acertos foi de 332 até 343 considerando 95% de confiança
Wall time: 1min 43s
In [40]:
%time reporter.hit_report(number=6)
The mean score was approximately 1 each 15,000,000 trials:

1 hit each 15,000,000 games played!
We have played with 7 numbers.

the raffler says: 7,151,980
Wall time: 1min 42s
In [41]:
%time reporter.confidence_report(number=6, confidence=0.90)
A média da quantidade de acertos foi de 1 até 2 considerando 90% de confiança
Wall time: 1min 42s

CONCLUSÃO:

As quantidades ficaram bem próximas da tabela, com exceção do número 6 que exigiria que mais tentativas fossem realizadas para aferir corretamente a média de jogos para conseguir uma sena.

Outro ponto interessante é que a técnica de bootstrap com uma quantidade grande de dados apresentou um gargalo no processo, onde cada relatório levou quase dois minutos para ser produzido. Essa é uma ótima oportunidade para otimização via processamento paralelo. Na próxima atualização do projeto, será o foco principal.

Agradeço de você acompanhou todo o processo. Qualquer dúvida, estarei a disposição.

Meus contatos estão no site: Samuel Baptista

MUITO OBRIGADO!

FIM!