Blog

Postado em em 30 de novembro de 2023

Previsão do Campeonato Brasileiro com Python – Projeto Completo

Aprenda como realizar a previsão do Campeonato Brasileiro com Python! Vamos criar uma previsão dos resultados por meio da programação!

Caso prefira esse conteúdo no formato de vídeo-aula, assista ao vídeo abaixo ou acesse o nosso canal do YouTube!

Para receber por e-mail o(s) arquivo(s) utilizados na aula, preencha:

Previsão do Campeonato Brasileiro com Python – Projeto Completo

Nesta aula, vou te mostrar como realizar a previsão do Campeonato Brasileiro com Python! Se você é fã de futebol e deseja aprender a desenvolver um projeto completo com Python, esta aula é perfeita para você!

Vou guiar você passo a passo na construção deste projeto abrangente, abordando cada uma das etapas necessárias para criar a previsão do Campeonato Brasileiro com Python.

Esta aula será um pouco mais extensa e envolverá um projeto mais avançado. Portanto, é recomendável que você tenha algum conhecimento prévio em ferramentas como a biblioteca Pandas, Requests, entre outras, que formam a base do Python.

É sempre bom lembrar que este projeto se trata de uma previsão e não garante 100% de certeza nos resultados previstos. Isso ocorre porque estamos utilizando critérios específicos para realizar essa análise, mas você pode escolher e utilizar outros se preferir.

Faça o download do material disponível nesta aula e vamos construir juntos este super projeto de previsão do Campeonato Brasileiro com Python!

Previsão de Jogos de Campeonato – Métricas

A lógica por trás da criação da nossa previsão para o Campeonato Brasileiro com Python é, primeiramente, coletar os dados da tabela atual do campeonato, considerando tudo o que aconteceu até o momento, bem como os jogos que ainda estão por ocorrer.

Se você optar por incorporar resultados de anos anteriores em suas análises, será necessário incluir essas informações em suas avaliações. No entanto, eu escolhi não utilizar resultados de anos anteriores, uma vez que no futebol ocorrem mudanças muito significativas nos times de um ano para o outro.

Portanto, o que utilizaremos essencialmente para criar nossa previsão são os resultados de todos os jogos do Brasileirão de 2023. Além disso, iremos considerar se o jogo é em casa ou fora, um fator crucial para o desempenho de muitos times.

As quatro principais métricas que empregaremos para construir nossa previsão são: média de gols em casa, média de gols fora, média de gols sofridos em casa e média de gols sofridos fora..

Essas quatro métricas, quando combinadas, fornecerão um bom indicativo para o resultado. Por exemplo, se um time com uma boa média de gols em casa enfrentar um time com uma alta média de gols sofridos fora, é provável que o primeiro vença.

Entretanto, é importante ressaltar que essa combinação não é determinante. Não afirmaremos que o time A vencerá o time B apenas com base na média de gols; em vez disso, atribuiremos uma probabilidade a esse cenário. Da mesma forma, teremos uma probabilidade para o empate e para o time B vencer o time A.

Fonte de Informações – Wikipedia

Com as métricas devidamente definidas, é crucial escolhermos a fonte de informações adequada para o projeto. Para este caso, utilizaremos a página da Wikipedia referente ao Campeonato Brasileiro de 2023.

Essa página oferece uma ampla gama de informações sobre os times participantes, os estádios, o regulamento e, especificamente o que nos interessa, a tabela de classificação.

tabela de classificação

A fonte dessa tabela é o próprio site da CBF, garantindo assim constante atualização. Vale ressaltar que você poderia escolher utilizar diretamente o site da CBF, Globo, ou qualquer outra fonte de sua preferência.

Além da tabela do Campeonato Brasileiro, precisaremos acessar a tabela de Confrontos.

tabela de Confrontos

Essa tabela apresenta os resultados de todos os jogos e também exibe informações sobre os jogos que ainda estão por acontecer.

Biblioteca Pandas e Requests – Análise de Dados e Requisição

Neste projeto, faremos uso da biblioteca Pandas para realizar a análise de dados de maneira eficiente, e também da biblioteca Requests, que nos habilita a efetuar requisições e obter informações diretamente da internet.

O ponto de partida para as nossas análises será a instalação e importação dessas duas bibliotecas. Para a instalação, execute o seguinte comando no terminal do seu editor de código:

pip install pandas requests

Ou na célula do Jupyter Notebook:

!pip install pandas requests

Dado que estaremos lidando com a análise e visualização de dados, recomendo o uso do Jupyter Notebook, ou a extensão do Jupyter Notebook para o VSCode.

Após a instalação bem-sucedida das bibliotecas, importe-as para o seu código da seguinte forma:

import pandas as pd
import requests

Requisição dos dados – Tabelas da Wikipedia

Com as bibliotecas devidamente importadas, faremos uma requisição à Wikipedia para obter as informações desejadas. Para isso, realizaremos uma requisição do tipo GET.

import pandas as pd
import requests

requisicao = requests.get("https://pt.wikipedia.org/wiki/Campeonato_Brasileiro_de_Futebol_de_2023_-_S%C3%A9rie_A")

Assim, obteremos as informações contidas na página do Campeonato Brasileiro de 2023 na Wikipedia.

Ao imprimir o texto proveniente dessa requisição, teremos como resposta o código-fonte da página:

código-fonte da página

Se optássemos por lidar manualmente com esse código-fonte, teríamos muito trabalho.

No entanto, podemos criar uma variável chamada tabelas para armazenar todas as tabelas presentes na página. O código-fonte de uma página é um código HTML, então podemos utilizar a função read_html do Pandas, passando requisicao.text como argumento.

import pandas as pd
import requests

requisicao = requests.get("https://pt.wikipedia.org/wiki/Campeonato_Brasileiro_de_Futebol_de_2023_-_S%C3%A9rie_A")

tabelas = pd.read_html(requisicao.text)

Podemos visualizar essas tabelas iterando sobre elas com um loop for:

import pandas as pd
import requests

requisicao = requests.get("https://pt.wikipedia.org/wiki/Campeonato_Brasileiro_de_Futebol_de_2023_-_S%C3%A9rie_A")

tabelas = pd.read_html(requisicao.text)

for tabela in tabelas:
    display(tabela)

Assim, conseguimos visualizar todas as tabelas presentes dentro da página:

tabelas presentes dentro da página

Entre elas, as tabelas que iremos utilizar:

tabelas que iremos utilizar
tabelas que iremos utilizar

Ou seja, nossa variável tabelas está armazenando uma lista de tabelas presentes na página do Campeonato Brasileiro 2023 da Wikipedia. Dentre elas, escolheremos as tabelas de índice [6] e [7], a sétima e a oitava tabela, respectivamente, para utilizar em nosso projeto:.

import pandas as pd
import requests

requisicao = requests.get("https://pt.wikipedia.org/wiki/Campeonato_Brasileiro_de_Futebol_de_2023_-_S%C3%A9rie_A")

tabelas = pd.read_html(requisicao.text)

tabela_classificacao = tabelas[6]
tabela_jogos = tabelas[7]
display(tabela_classificacao)
display(tabela_jogos)

Dessa forma, armazenamos as tabelas nas variáveis tabela_classificacao e tabela_jogos e as visualizamos novamente com o display.

Tratamento de Dados – Ajuste da Tabela de Jogos

Agora, vamos começar a tratar as informações obtidas por meio das tabelas, começando pela tabela de jogos.

tabela de jogos

Nesta tabela, as linhas representam os times jogando em casa, enquanto as colunas indicam os times jogando fora de casa. No entanto, nas linhas, temos os nomes dos times escritos por extenso, enquanto nas colunas temos suas siglas.

Para padronizar, faremos com que ambos os lados sejam escritos por extenso. Inicialmente, precisamos obter os nomes dos times por extenso. Faremos isso através da função list, pegando a coluna “Casa \ Fora” de nossa tabela_jogos.

nomes_times = list(tabela_jogos["Casa \ Fora"])
print(nomes_times)

função list, pegando a coluna "Casa \ Fora"

Assim, teremos uma lista com os nomes de todos os times por extenso. Agora, precisamos obter as siglas dos times, que estão nas colunas de nossa tabela_jogos.

apelidos = list(tabela_jogos.columns)
apelidos.pop(0)
print(apelidos)
siglas dos times

Como a primeira coluna da tabela é “Casa \ Fora”, podemos usar o método pop(0) para remover o primeiro objeto da nossa lista, que é o nome “Casa \ Fora” da coluna.

Finalmente, podemos gerar um dicionário Python que relacionará as duas listas, tendo as siglas como chave e os nomes por extenso dos times como valor.

Para isso, basta usar a função zip para combinar as duas listas e depois utilizar a função dict para criar o dicionário.

nomes_times = list(tabela_jogos["Casa \ Fora"])
apelidos = list(tabela_jogos.columns)
apelidos.pop(0)
de_para_times = dict(zip(apelidos, nomes_times))
print(de_para_times)

função dict para criar o dicionário

Com nosso dicionário pronto, podemos padronizar a nossa tabela. O resultado desejado será algo assim:

 O resultado desejado

Em que teremos, em uma mesma linha, o time que jogou em casa, o time que jogou fora de casa, a quantidade de gols marcados pelo time que jogou em casa e a quantidade de gols marcados pelo time que jogou fora.

Ao invés de ter uma coluna para cada time, teremos apenas essas 4 colunas.

Para realizar esse processo, vamos primeiro pegar a coluna “Casa/Fora” e transformá-la no índice da nossa tabela.

tabela_jogos_ajustada = tabela_jogos.set_index("Casa \ Fora")
display(tabela_jogos_ajustada)
"Casa/Fora" índice da nossa tabela

Em seguida, vamos pegar as nossas colunas e transformá-las em linhas. Utilizaremos a função unstack para isso, pegando as colunas com os nomes de cada time e trazendo-as para as linhas correspondentes.

tabela_jogos_ajustada = tabela_jogos.set_index("Casa \ Fora")
tabela_jogos_ajustada = tabela_jogos_ajustada.unstack()
display(tabela_jogos_ajustada)
função unstack

A tabela, a princípio, ficará com uma formatação esquisita, mas já conseguimos visualizar todas as informações em uma única linha, tendo a sigla do time que jogou fora de casa, depois o nome por extenso do time que jogou em casa e, por fim, o resultado.

Para ajustar a tabela, podemos, após fazer o unstack, resetar o índice da tabela.

tabela_jogos_ajustada = tabela_jogos.set_index("Casa \ Fora")
tabela_jogos_ajustada = tabela_jogos_ajustada.unstack().reset_index()
display(tabela_jogos_ajustada)
tabela após fazer o unstack, resetar o índice

As nossas colunas ainda estão com os nomes errados, e o resultado não está dividido entre gols fora e gols em casa.

Para corrigir isso, vamos ajustar os nomes das colunas utilizando a função rename e passando para ela um dicionário com o nome antigo da coluna e o nome novo.

tabela_jogos_ajustada = tabela_jogos.set_index("Casa \ Fora")
tabela_jogos_ajustada = tabela_jogos_ajustada.unstack().reset_index()
tabela_jogos_ajustada = tabela_jogos_ajustada.rename(columns={"level_0": "fora", "Casa \ Fora": "casa", 0: "resultado"})
display(tabela_jogos_ajustada)
ajustar os nomes das colunas utilizando a função rename

Feito isso, podemos ajustar os nomes para ficarem exibidos da mesma forma, por extenso nas duas colunas, utilizando a função ajustar_apelido_time.

Essa função vai receber uma linha da tabela, extrair a sigla (apelido) do time na coluna “fora” dessa linha e, utilizando o dicionário criado anteriormente, buscar o nome correspondente, retornando esse nome.

def ajustar_apelido_time(linha):
    apelido = linha["fora"]
    nome = de_para_times[apelido]
    return nome

Agora, podemos aplicar essa função à nossa tabela, pegando a coluna “fora” da tabela tabela_jogos_ajustada e aplicando a função a ela no eixo 1, referente ao eixo das linhas.

tabela_jogos_ajustada["fora"] = tabela_jogos_ajustada.apply(ajustar_apelido_time, axis=1)

Dessa forma, percorreremos cada linha da nossa tabela, pegaremos a sigla contida nela na coluna “fora” e substituiremos pelo nome por extenso correspondente no nosso dicionário de_para_times.

Subistituindo sigla por nome por extenso

Em seguida, podemos excluir as linhas em que o time “fora” é igual ao time “casa”.

tabela_jogos_ajustada = tabela_jogos.set_index("Casa \ Fora")
tabela_jogos_ajustada = tabela_jogos_ajustada.unstack().reset_index()
tabela_jogos_ajustada = tabela_jogos_ajustada.rename(columns={"level_0": "fora", "Casa \ Fora": "casa", 0: "resultado"})

def ajustar_apelido_time(linha):
    apelido = linha["fora"]
    nome = de_para_times[apelido]
    return nome

tabela_jogos_ajustada["fora"] = tabela_jogos_ajustada.apply(ajustar_apelido_time, axis=1)
tabela_jogos_ajustada = tabela_jogos_ajustada[tabela_jogos_ajustada["fora"]!=tabela_jogos_ajustada["casa"]]
display(tabela_jogos_ajustada)

Dessa forma, definimos que a tabela_jogos_ajustada receberá a tabela_jogos_ajustada em que o valor na coluna “fora” é diferente do valor na coluna “casa”.

Tabela sem o time aparecendo contra ele mesmo

A seguir, vamos dividir essa tabela em duas: a tabela_jogos_realizados e a tabela_jogos_faltantes.

Primeiro, vamos tratar os valores vazios da nossa tabela, substituindo-os pelo texto “a jogar”. Para isso, utilizamos o método fillna para preencher os valores vazios com o texto desejado.

tabela_jogos_ajustada["resultado"] = tabela_jogos_ajustada["resultado"].fillna("a jogar")

Agora que tratamos os valores vazios, podemos começar a filtrar e dividir a nossa tabela. Os jogos que já aconteceram terão na coluna resultado a sintaxe número–número. Portanto podemos pegar o traço () como padrão para filtrar essas linhas.

Importante: Repare que o traço que estamos utilizando não é um hífen normal: – (traço) e – (hífen). Portanto, para evitar problemas na hora de utilizá-lo em seu código, é aconselhável copiá-lo diretamente da tabela.

tabela_jogos_realizados = tabela_jogos_ajustada[tabela_jogos_ajustada["resultado"].str.contains("–")]

display(tabela_jogos_realizados)

Com isso, criamos a tabela_jogos_realizados, pegando a tabela_jogos_ajustada em que na coluna “resultado” contenha (contains) o símbolo de traço ().

tabela_jogos_realizados

Já a tabela de jogos faltantes será similar à tabela de jogos realizados, mas pegando apenas as linhas da tabela_jogos_ajustada em que não haja o traço na coluna resultado.

Como queremos pegar o oposto do que utilizamos para a tabela_jogos_realizados, basta adicionar um til (~) antes da condição que queremos negar.

Ou seja, antes queríamos a tabela_jogos_ajustada em que na coluna resultado tivéssemos o traço. Para negar essa condição, basta adicionar um til antes dela e assim estaremos pegando as linhas em que não existam o traço na coluna resultado.

Como a coluna resultado não terá nenhum valor que nos interessa analisar, podemos descartá-la fazendo um drop dela.

tabela_jogos_faltantes = tabela_jogos_ajustada[~tabela_jogos_ajustada["resultado"].str.contains("–")]

tabela_jogos_faltantes = tabela_jogos_faltantes.drop(columns=["resultado"])

display(tabela_jogos_faltantes)

tabela_jogos_faltantes

Cálculo dos Indicadores para a Previsão

Agora que já temos as nossas duas tabelas prontas, precisamos definir o cálculo dos indicadores que eu havia mencionado anteriormente.

Para isso, precisaremos dividir a coluna resultado em gols em casa (gols_casa) e gols fora de casa (gols_fora).

Vamos criar essas colunas dentro da tabela_jogos_realizados (como são duas colunas, vamos passar no formato de uma lista) que irão receber a coluna resultado dividida (split) pelo traço. Para atribuir cada um dos valores a uma das colunas, vamos passar o parâmetro expand=True.

Dessa forma, a coluna “gols_casa” irá receber o número antes do traço, e “gols_fora” receberá os valores depois do traço.

Feito isso, podemos remover a coluna “resultado” com um drop e converter os valores das colunas “gols_casa” e “gols_fora” para o tipo inteiro (astype(int)).

tabela_jogos_realizados[["gols_casa", "gols_fora"]] = tabela_jogos_realizados["resultado"].str.split("–", expand=True)

tabela_jogos_realizados = tabela_jogos_realizados.drop(columns=["resultado"])

tabela_jogos_realizados["gols_casa"] = tabela_jogos_realizados["gols_casa"].astype(int)

tabela_jogos_realizados["gols_fora"] = tabela_jogos_realizados["gols_fora"].astype(int)

display(tabela_jogos_realizados)
tabela_jogos_realizados

Agora temos as duas colunas devidamente criadas, preenchidas e com os valores tratados como números inteiros. Desse modo, podemos realizar os nossos cálculos.

Vamos começar com a média de gols feitos em casa. Para isso, iremos agrupar (groupby) a coluna “casa”, porque quando fizermos isso, podemos definir o que irá acontecer com a coluna “gols_casa” (se queremos somá-la, calcular a média, etc.). Nesse caso, iremos calcular a média.

Para calcular a média, iremos utilizar a função mean com o parâmetro numeric_only=True.

media_gols_casa = tabela_jogos_realizados.groupby("casa").mean(numeric_only=True)
display(media_gols_casa)
média de gols feitos

Repare que, além da coluna gols_casa, a tabela também trouxe a coluna gols_fora. No entanto, esse valor não representa a média de gols que cada time marcou fora de casa, mas sim a quantidade de gols que esse time tomou jogando em casa.

Para ajustarmos essas informações, podemos renomear essas colunas:

media_gols_casa = tabela_jogos_realizados.groupby("casa").mean(numeric_only=True)
media_gols_casa = media_gols_casa.rename(columns={"gols_casa": "gols_feitos_casa", "gols_fora": "gols_sofridos_casa"})
display(media_gols_casa)
Ajustar nome das colunas

Agora, podemos fazer o mesmo processo, mas para os jogos fora de casa. Assim, agruparemos a tabela_jogos_realizados pela coluna “fora” e calcularemos a média de gols feitos fora de casa e sofridos fora de casa.

media_gols_fora = tabela_jogos_realizados.groupby("fora").mean(numeric_only=True)
media_gols_fora = media_gols_fora.rename(columns={"gols_casa": "gols_sofridos_fora", "gols_fora": "gols_feitos_fora"})
display(media_gols_fora)

media_gols_fora

Assim, temos a média de gols feitos e tomados fora de casa por cada um dos times. Com essas duas tabelas, conseguimos as quatro métricas que precisávamos.

Estimar o Resultado dos Jogos – Previsão dos Jogos

Com as métricas calculadas, precisamos agora estimar o resultado dos jogos e começar a nossa previsão do Campeonato Brasileiro com Python.

Existem algumas técnicas possíveis para construir essa previsão. Neste projeto, iremos considerar a média de gols que o time marca e sofre dentro e fora de casa, e a partir disso, calcular a probabilidade dos resultados possíveis.

Para não ficarmos calculando indefinidamente, podemos definir um limite máximo de gols marcados ou sofridos. Por exemplo, se definirmos o máximo como sendo 7 gols, iremos calcular a probabilidade de todos os resultados possíveis, indo do 0–0 até o 7–7.

Depois de termos calculado essas probabilidades de resultados, iremos calcular a probabilidade de cada cenário possível entre vitória, derrota ou empate.

Tabela de Estatísticas

Agora vamos agrupar todas as informações de gols feitos e sofridos em casa, assim como gols feitos e sofridos fora de casa, em uma única tabela.

Basicamente, vamos mesclar as duas tabelas através da função merge. Além disso, precisamos passar os parâmetros left_index=True e right_index=True. Dessa forma, estamos informando que apesar de terem nomes diferentes, os índices das duas tabelas são correspondentes.

Podemos resetar o índice da nossa tabela para que ele se torne um índice numérico e pegarmos a coluna “casa” que está com os nomes dos times, renomeando-a para “time”.

tabela_estatisticas = media_gols_casa.merge(media_gols_fora, left_index=True, right_index=True)
tabela_estatisticas = tabela_estatisticas.reset_index()
tabela_estatisticas = tabela_estatisticas.rename(columns={"casa": "time"})
display(tabela_estatisticas)

Assim, nossa tabela ficará da seguinte forma:

tabela_estatisticas

Agora temos todas as estatísticas dos times registradas em uma única tabela, e a partir dela teremos que construir a nossa previsão.

Para isso, teremos que pensar basicamente em como, a partir das médias, calcular a probabilidade de o time ganhar, perder ou empatar. E a partir dessa probabilidade, calcular a pontuação estimada que esse time terá.

Cálculo de Valor Esperado

Para realizarmos esses cálculos, vamos entrar na parte de estatística, fazendo o cálculo de valor esperado.

O cálculo de valor esperado é realizado multiplicando cada possível resultado pelo seu respectivo peso de probabilidade e somando todos esses produtos. Ou seja, multiplicando a probabilidade de ele acontecer pelo seu valor.

Por exemplo, vamos supor que em um jogo do Vasco contra o Cruzeiro exista 60% de chance de o Vasco ganhar, contra 40% de chance de o Cruzeiro ganhar.

O valor esperado para o Vasco será de 60% * 3, resultando em 1.8 pontos, enquanto o do Cruzeiro será de 40% * 3, resultando em 1.2 pontos.

Por isso, quando chegarmos na tabela final, teremos uma pontuação para cada time com casas decimais, pois pegaremos a pontuação atual deles e somaremos com esses valores esperados.

Vamos considerar um cenário completo em que o Vasco tenha 50% de chance de ganhar, 10% de chance de empatar, e 40% de chance de perder.

Dessa forma o valor esperado para o Vasco será de:

valor_esperado_vasco = 50% * 3 + 10% * 1 = 1.6

Cálculo de Probabilidade – Distribuição de Poisson

Como vimos, para calcular a pontuação que cada time receberá por confronto, temos de calcular primeiro a probabilidade para cada cenário possível: vitória, derrota e empate.

Então, iremos, a partir das médias, calcular a probabilidade para cada um desses cenários, utilizando a distribuição de Poisson.

De forma resumida e simplificada, a distribuição de Poisson é uma distribuição estatística que calcula a probabilidade de um resultado acontecer e de todos os resultados próximos dele acontecerem.

Para a distribuição de Poisson, passamos a média de ocorrência de um evento, e ela calculará a probabilidade de resultados próximos a esse evento.

Ou seja, ao passarmos a média de gols que um time marca em casa e a média de gols que o outro time marca fora de casa, a distribuição de Poisson consegue nos dar a probabilidade de gols que cada time pode marcar nesse cenário.

A partir disso, conseguimos calcular a probabilidade de cada resultado possível acontecer.

Essa distribuição de Poisson é interessante porque ela considera cada gol como um evento independente. Mesmo que o Vasco já tenha feito um gol, a probabilidade dele fazer outro gol é independente desse primeiro.

O que a distribuição de Poisson irá considerar como chave para calcular a probabilidade dos gols será a média que passarmos para ela. Quanto mais próximo da média, maior a probabilidade de acontecer; quanto mais longe, menor.

Em um gráfico, poderíamos visualizar a distribuição de Poisson como o gráfico a seguir.

Distribuição de Poisson

Onde o meio da curva é o valor da média (Lambda).

Vamos pegar o confronto do Vasco com o Bragantino para exemplificar esse conceito. O jogo do Vasco contra o Bragantino será dentro da casa do Vasco.

O primeiro passo será calcular o Lambda para cada um desses times, ou seja, a média de gols que o Vasco faz, jogando em casa, contra o Bragantino jogando fora, e a média de gols que o Bragantino faz, jogando fora, contra o Vasco jogando em casa.

Ou seja, para calcular a lambda_casa, é necessário multiplicar a média de gols que o Vasco faz em casa pela média de gols que o Bragantino sofre fora de casa.

O lambda_fora é semelhante, mas considera a média de gols que o Bragantino faz fora de casa, multiplicada pela média de gols que o Vasco sofre em casa.

time_casa = "Vasco da Gama"
time_fora = " Red Bull Bragantino"

lambda_casa = média de gols do Vasco em casa * média de gols sofridos pelo Bragantino fora
lambda_fora = média de gols do Bragantino fora * média de gols sofridos pelo Vasco em casa

Para transformarmos essa lógica em código Python, vamos precisar usar a função loc para localizar dentro da tabela_estatísticas os valores que precisamos multiplicar para cada um dos lambdas.

Para o lambda_casa, vamos precisar localizar na tabela a linha onde o time é igual ao time_casa e a coluna seja gols_feitos_casa. Como resultado, teremos uma tabela de um único valor, então para acessá-lo, vamos passar o índice [0].

Além disso, precisamos multiplicar esse valor pela tabela que contenha a linha onde o time é igual ao time_fora e a coluna igual aos gols_sofridos_fora.

time_casa = "Vasco da Gama"
time_fora = "Red Bull Bragantino"

lambda_casa = (tabela_estatisticas.loc[tabela_estatisticas["time"]==time_casa, "gols_feitos_casa"].iloc[0]
                * tabela_estatisticas.loc[tabela_estatisticas["time"]==time_fora, "gols_sofridos_fora"].iloc[0])

Já o lambda_fora será calculado de forma diferente, mas pegaremos primeiro a célula em que a linha corresponda ao time_fora e os gols_feitos_fora multiplicado pelo valor da linha em que o time é igual a time_casa e a coluna igual a gols_sofridos_casa.

time_casa = "Vasco da Gama"
time_fora = "Red Bull Bragantino"

lambda_casa = (tabela_estatisticas.loc[tabela_estatisticas["time"]==time_casa, "gols_feitos_casa"].iloc[0]
                    *
tabela_estatisticas.loc[tabela_estatisticas["time"]==time_fora, "gols_sofridos_fora"].iloc[0])

lambda_fora =  (tabela_estatisticas.loc[tabela_estatisticas["time"]==time_fora, "gols_feitos_fora"].iloc[0]
                    * tabela_estatisticas.loc[tabela_estatisticas["time"]==time_casa, "gols_sofridos_casa"].iloc[0])

print(lambda_casa)
print()
print(lambda_fora)

Dessa forma, teremos que a média (lambda) para cada um desses times será de 1.29 para o Vasco da Gama e 1.35 para o Bragantino.

Agora precisamos transformar essas médias em probabilidade, e vamos fazer isso através do Poisson, que irá calcular a probabilidade de cada cenário acontecer. A princípio, teremos que a probabilidade para todos os cenários será 0.

time_casa = "Vasco da Gama"
time_fora = "Red Bull Bragantino"

lambda_casa = (tabela_estatisticas.loc[tabela_estatisticas["time"]==time_casa, "gols_feitos_casa"].iloc[0]
                    * 
tabela_estatisticas.loc[tabela_estatisticas["time"]==time_fora, "gols_sofridos_fora"].iloc[0])

lambda_fora =  (tabela_estatisticas.loc[tabela_estatisticas["time"]==time_fora, "gols_feitos_fora"].iloc[0]
                    * tabela_estatisticas.loc[tabela_estatisticas["time"]==time_casa, "gols_sofridos_casa"].iloc[0])

#Probabilidade de vitória do time da casa
pv_casa = 0
#Probabilidade de empate
p_empate = 0
#Probabilidade de vitória do time de fora
pv_fora = 0

A partir das médias que passaremos para o Poisson, ele conseguirá calcular a probabilidade do jogo ser 0–0, 1–0, 1–1, e assim sucessivamente. Vamos considerar o cenário em que o menor placar é 0–0, e o maior é 7–7.

Em outras palavras, o Poisson calculará dentro desse intervalo a probabilidade possível para cada um dos resultados. Portanto, os gols do time da casa podem variar de 0 a 7, assim como os gols do time de fora.

Para utilizarmos o Poisson, será necessário instalá-lo. Execute o seguinte comando no terminal do seu editor de códigos:

pip install scipy

Ou na célula do Jupyter Notebook:

!pip install scipy

Feito isso, podemos importar o módulo poisson e calcular a probabilidade de todos os resultados possíveis. Vamos fazer isso dentro de um loop for.

from scipy.stats import poisson

time_casa = "Vasco da Gama"
time_fora = "Red Bull Bragantino"

lambda_casa = (tabela_estatisticas.loc[tabela_estatisticas["time"]==time_casa, "gols_feitos_casa"].iloc[0]
                    *
tabela_estatisticas.loc[tabela_estatisticas["time"]==time_fora, "gols_sofridos_fora"].iloc[0])

lambda_fora = (tabela_estatisticas.loc[tabela_estatisticas["time"]==time_fora, "gols_feitos_fora"].iloc[0]
                    * tabela_estatisticas.loc[tabela_estatisticas["time"]==time_casa, "gols_sofridos_casa"].iloc[0])

#Probabilidade de vitória do time da casa
pv_casa = 0
#Probabilidade de empate
p_empate = 0
#Probabilidade de vitória do time de fora
pv_fora = 0

for gols_casa in range(8):
    for gols_fora in range(8):
        probabilidade_resultado = poisson.pmf(gols_casa, lambda_casa) * poisson.pmf(gols_fora, lambda_fora)

Esse loop for atribuirá a gols_casa um número de 0 a 7, e para cada um desses números, atribuirá a gols_fora um número de 0 a 7 também.

Teremos uma iteração em que gols_casa será 0 e gols_fora será 0, em seguida, teremos gols_casa sendo 0 e gols_fora sendo 1, e assim sucessivamente.

Dentro desse loop, como estamos considerando os eventos de gols como eventos independentes, para calcular a probabilidade de cada um desses resultados, podemos multiplicar a probabilidade do Vasco marcar o placar de gols_casa pela probabilidade do Bragantino marcar os gols_fora.

Na primeira iteração, teremos a probabilidade do Vasco marcar 0 gols, multiplicada pela probabilidade do Bragantino marcar 0 gols, com base nos respectivos lambdas.

São esses os argumentos que passamos para a função pmf do Poisson: o evento que queremos calcular e o lambda correspondente. Então, temos gols_casa com lambda_casa e gols_fora com lambda_fora.

Podemos visualizar a probabilidade para cada um desses resultados executando um print(probabilidade_resultado, gols_casa, gols_fora).

probabilidade_resultado, gols_casa, gols_fora

Entretanto, para o nosso caso, o foco não será a probabilidade de cada resultado específico, mas sim o cenário associado a cada um deles, ou seja, se será vitória do time da casa, empate, ou vitória do time fora.

Isso porque não estamos considerando o saldo de gols, mas apenas a quantidade de pontos que cada time terá ao final do Brasileirão.

Desse modo, podemos calcular a probabilidade do time da casa ganhar (pv_casa), do empate (p_empate), e do time visitante ganhar (pv_fora). Vamos somar a probabilidade de cada resultado possível ao seu cenário correspondente.

Por exemplo, 0 a 0 aumenta a probabilidade de empate em 0.07, 1 a 1 aumenta a probabilidade de empate em 0.12, e assim sucessivamente para cada cenário e resultado possível.

from scipy.stats import poisson

time_casa = "Vasco da Gama"
time_fora = "Red Bull Bragantino"

lambda_casa = (tabela_estatisticas.loc[tabela_estatisticas["time"]==time_casa, "gols_feitos_casa"].iloc[0]
                   *
tabela_estatisticas.loc[tabela_estatisticas["time"]==time_fora, "gols_sofridos_fora"].iloc[0])

lambda_fora = (tabela_estatisticas.loc[tabela_estatisticas["time"]==time_fora, "gols_feitos_fora"].iloc[0]
                    * tabela_estatisticas.loc[tabela_estatisticas["time"]==time_casa, "gols_sofridos_casa"].iloc[0])

#Probabilidade de vitória do time da casa
pv_casa = 0
#Probabilidade de empate
p_empate = 0
#Probabilidade de vitória do time de fora
pv_fora = 0

for gols_casa in range(8):
    for gols_fora in range(8):
        probabilidade_resultado = poisson.pmf(gols_casa, lambda_casa) * poisson.pmf(gols_fora, lambda_fora)
        if gols_casa == gols_fora:
            p_empate += probabilidade_resultado
        elif gols_casa > gols_fora:
            pv_casa += probabilidade_resultado
        elif gols_casa < gols_fora:
            pv_fora += probabilidade_resultado

print(pv_casa)
print(p_empate)
print(pv_fora)

pv_casa, p_empate e pv_fora

Desse modo, temos que a probabilidade do time da casa ganhar é de aproximadamente 35%, do empate é de 26%, e do time visitante ganhar é de 38%.

A partir da probabilidade dos resultados, podemos calcular o valor esperado, como visto anteriormente.

O valor esperado para o time da casa será ve_casa = pv_casa * 3 + p_empate, e o valor esperado para o time visitante será ve_fora = pv_fora * 3 + p_empate.

ve_casa = pv_casa * 3 + p_empate
ve_fora = pv_fora * 3 + p_empate

Adicionando a Pontuação aos Times

Com o valor esperado calculado, podemos adicionar a pontuação gerada aos pontos que o time possui na tabela_classificacao, fazendo isso para cada um dos jogos.

Até agora, fizemos os cálculos considerando um único time de casa e de fora para facilitar a visualização do passo a passo na construção da previsão do Campeonato Brasileiro com Python.

Agora, vamos colocar esse código dentro da função calcular_pontuacao_esperada, que pegará uma linha da tabela jogos_faltantes e calculará a pontuação esperada.

Adicionaremos essa pontuação esperada a uma nova coluna da tabela, chamada pontos_casa e pontos_fora. Ao final, retornaremos essa linha atualizada.

from scipy.stats import poisson

def calcular_pontuacao_esperada(linha):
    time_casa = linha["casa"]
    time_fora = linha["fora"]

    lambda_casa = (tabela_estatisticas.loc[tabela_estatisticas["time"]==time_casa, "gols_feitos_casa"].iloc[0]
                *
tabela_estatisticas.loc[tabela_estatisticas["time"]==time_fora, "gols_sofridos_fora"].iloc[0])

    lambda_fora = (tabela_estatisticas.loc[tabela_estatisticas["time"]==time_fora, "gols_feitos_fora"].iloc[0]
                    * tabela_estatisticas.loc[tabela_estatisticas["time"]==time_casa, "gols_sofridos_casa"].iloc[0])

    pv_casa = 0
    p_empate = 0
    pv_fora = 0

    for gols_casa in range(8):
        for gols_fora in range(8):
            probabilidade_resultado = poisson.pmf(gols_casa, lambda_casa) * poisson.pmf(gols_fora, lambda_fora)
            if gols_casa == gols_fora:
                p_empate += probabilidade_resultado
            elif gols_casa > gols_fora:
                pv_casa += probabilidade_resultado
            elif gols_casa < gols_fora:
                pv_fora += probabilidade_resultado

    ve_casa = pv_casa * 3 + p_empate
    ve_fora = pv_fora * 3 + p_empate
    linha["pontos_casa"] = ve_casa
    linha["pontos_fora"] = ve_fora
    return linha

Com a nossa função pronta, podemos aplicá-la à nossa tabela de jogos_faltantes.

tabela_jogos_faltantes = tabela_jogos_faltantes.apply(calcular_pontuacao_esperada, axis=1)
display(tabela_jogos_faltantes)

Como estamos trabalhando com as linhas da tabela, não podemos deixar de passar o parâmetro axis=1.

tabela_jogos_faltantes

Assim, teremos a tabela de jogos faltantes com o valor estimado para os times da casa e os times visitantes.

Agora, vamos pegar a nossa tabela original da classificação para somar os resultados.

tabela de pontos

No entanto, repare que temos diversas colunas nela que não são interessantes para a nossa previsão, e também há essas siglas nos nomes dos times para indicar quais times estão classificados ou qualificados para outras competições.

Então, primeiro vamos criar uma função para tratar o nome desses times, assim como fizemos anteriormente com as siglas da outra tabela.

def ajustar_nome_time(linha):
    for nome in nomes_times:
        if nome in linha["Equipevde"]:
            return nome

Essa função receberá uma linha da nossa tabela, e a partir da lista nomes_times que criamos anteriormente, se o nome do time estiver em alguma linha da coluna Equipevde da tabela de classificação, vamos retornar apenas o nome do time da nossa lista nomes_times.

A partir disso, podemos aplicar essa função à nossa tabela_classificacao, criando uma coluna time e atribuindo os nomes dos times retornados pela função a ela.

Em seguida, podemos criar uma nova tabela chamada tabela_classificacao_atualizada, que conterá apenas as colunas “time” e “Pts”, as duas únicas informações que nos interessam.

def ajustar_nome_time(linha):
    for nome in nomes_times:
        if nome in linha["Equipevde"]:
            return nome

tabela_classificacao["time"] = tabela_classificacao.apply(ajustar_nome_time, axis=1)
tabela_classificacao_atualizada = tabela_classificacao[["time", "Pts"]]
tabela_classificacao_atualizada["Pts"] = tabela_classificacao_atualizada["Pts"].astype(int)

display(tabela_classificacao_atualizada)

tabela_classificacao_atualizada

Com a nossa tabela de pontos pronta, agora basta adicionarmos os pontos que calculamos com a função calcular_pontuacao_esperada.

Esses pontos estão registrados na tabela de jogos_faltantes. Para simplificarmos a soma deles aos pontos da nossa tabela de classificação atualizada, vamos dividir essa tabela em tabela_pontuacao_casa e tabela_pontuacao_fora.

Assim, teremos quantos pontos esperados cada time irá ganhar dentro e fora de casa. Para fazer esse cálculo, vamos agrupar a tabela_jogos_faltantes para jogos em casa, somando apenas os valores de pontos_casa. E agrupar a tabela_jogos_faltantes para jogos fora, somando apenas os valores de pontos_fora.

tabela_pontuacao_casa = tabela_jogos_faltantes.groupby("casa").sum(numeric_only=True)[["pontos_casa"]]
tabela_pontuacao_fora = tabela_jogos_faltantes.groupby("fora").sum(numeric_only=True)[["pontos_fora"]]
display(tabela_pontuacao_casa)
display(tabela_pontuacao_fora)

Assim, teremos essas duas tabelas contendo a pontuação total que cada time fez dentro e fora de casa:

Pontuação dentro e fora de casa total

Feito isso, vamos definir uma função para atualizar a pontuação da tabela de classificação.

def atualizar_pontuacao_previsao(linha):
    time = linha["time"]
    pontuacao = int(linha["Pts"]) + float(tabela_pontuacao_casa.loc[time, "pontos_casa"]) + float(tabela_pontuacao_fora.loc[time, "pontos_fora"])
    return pontuacao

Essa função recebe a linha da tabela_classificacao_atualizada, em que o time será igual ao nome do time presente na coluna [“time”].

A partir disso, definimos a variável pontuacao, que receberá o valor de pontos presente na coluna [“Pts”] da tabela_classificacao_atualizada, mais o valor da tabela_pontuacao_casa localizando o time e o valor de pontos_casa, mais o valor da tabela_pontuacao_fora localizando o time e o valor de pontos_fora.

Depois de definirmos essa função, podemos aplicá-la à nossa tabela_classificacao_atualizada na coluna [“Pts”].

Além disso, para ajustarmos a visualização da nossa tabela, podemos ordenar os valores pela pontuação final de modo decrescente (ascending=False), resetar o índice e somar 1 a ele, porque por padrão o índice no Python começa em 0, e na tabela queremos que ele comece em 1.

tabela_classificacao_atualizada["Pts"] = tabela_classificacao_atualizada.apply(atualizar_pontuacao_previsao, axis=1)
tabela_classificacao_atualizada = tabela_classificacao_atualizada.sort_values(by="Pts", ascending=False)
tabela_classificacao_atualizada = tabela_classificacao_atualizada.reset_index(drop=True)
tabela_classificacao_atualizada.index = tabela_classificacao_atualizada.index + 1
display(tabela_classificacao_atualizada)

Tabela de classificação do Brasileirão 2023 atualizada

Dessa forma, temos pela nossa previsão do Campeonato Brasileiro com Python que o Palmeiras terminará como campeão do Brasileirão, seguido pelo Atlético Mineiro, Flamengo e Botafogo.

É importante reparar que temos pontuações que ficaram muito próximas, o que significa que são probabilidades muito próximas de acontecer. Portanto, há uma chance de que esses resultados na realidade se alterem ou se reordenem entre esses times com pontuações tão próximas.

Vale lembrar que a cada rodada do Brasileirão, você pode esperar a Wikipedia atualizar as tabelas e executar essa previsão do Campeonato Brasileiro com Python novamente. Assim, você terá uma nova previsão com os resultados atualizados.

Conclusão – Previsão do Campeonato Brasileiro com Python – Projeto Completo

Nessa aula, você aprendeu a fazer a previsão do Campeonato Brasileiro com Python! E não só do Brasileirão, pois a lógica e os procedimentos abordados ao longo dessa aula podem ser replicados para outros campeonatos e esportes que você deseje analisar.

Apesar de ser uma aula extensa, você teve a oportunidade de construir um projeto completo do zero, envolvendo análise de dados e automação em Python. Isso incluiu a obtenção de dados por meio de requisições, tratamento e análise de dados, cálculos de probabilidade e estatística, além da implementação de atualizações automáticas.

É importante ressaltar que este projeto representa uma previsão e não garante 100% de certeza nos resultados. Isso ocorre porque estamos utilizando critérios específicos para essa análise, e é possível explorar outros critérios conforme necessário.

Acima de tudo, esse projeto é uma excelente oportunidade para praticar e aprimorar seus conhecimentos e habilidades em Python. Portanto, não deixe de replicá-lo e estudá-lo a fundo.

Hashtag Treinamentos

Para acessar outras publicações de Python, clique aqui!


Quer aprender mais sobre Python com um minicurso básico gratuito?