Prevendo a pontuação final do Campeonato Brasileiro — Parte I
Outras partes dessa série:
Prevendo a pontuação final do Campeonato Brasileiro — Parte II
Prevendo a pontuação final do Campeonato Brasileiro — Parte III
Está chegando a época do ano mais esperada. Provavelmente você pensou no natal ou virada do ano, mas eu estou me referindo mesmo ao final do campeonato brasileiro. Diferentemente de grande parte das grandes ligas de futebol no mundo, aqui no Brasil o campeonato se encerra no final do ano. E essa é uma época que torcedores ávidos começam a fazer as contas para saber se o seu time vai ser campeão, conseguir uma vaguinha na Libertadores do próximo ano, e tem aqueles que estão na sofrência invocando os deuses da matemática e do bom futebol esperando que o time saia da temida região da degola que leva à série B do ano seguinte.
Se você gosta de futebol e também estatística (assim como eu) deve ficar atento às probabilidades que os especialistas fazem nessa época para o campeonato brasileiro. Na busca de verificar se os mágicos dos números irão realmente revelar o que vai acontecer no mês de dezembro findada a competição.
Existem alguns sites que fazem a muito tempo esses cálculos. Mas atualmente temos ferramentas sofisticadas que nos ajudam nessa tarefa. Softwares, linguagens de programação e técnicas de inteligência computacional vieram para ficar e o uso de tais ferramentas cresceu muito nos últimos anos. Elas são utilizadas no nosso cotidiano sem que a gente perceba e claro o meio futebolístico não ia ficar sem aproveitar as benesses que ela proporciona.
Para quem não sabe eu trabalho como cientista de dados no Clube da Aposta, e desenvolvemos recentemente um algoritmo para calcular essas probabilidades que faz os olhos dos torcedores brilharem (ou lacrimejarem) no final do ano.
O artigo com os resultados desses cálculos para o campeonato brasileiro pode ser analisado no blog no clube: Probabilidade Brasileirão Série A. E não paramos por aí! Analisamos as probabilidades de rebaixamento de 14 diferentes campeonatos, você pode ver os resultados no seguinte link: probabilidades de rebaixamento.
Mas chega de papo, vem comigo nessa jornada que eu vou falar tudo sobre como nós fizemos isso…
1 — Os dados
Alguns dizem que os dados são tudo em data science e machine learning. E essas pessoas não estão erradas. Os algoritmos que utilizamos nessa área nada mais são do que técnicas estatísticas, lidam com números, probabilidades, matemática, assim qualquer ruído pode atrapalhar todo o trabalho. Hoje já existem técnicas mais sofisticadas que lidam melhor com essas situações, mas se for trabalhar com isso tenha essa máxima em mente: a qualidade dos dados é tudo para algoritmos de machine learning. Usando uma frase que vi não sei em que lugar: “Trash in, trash out”. Se a sua entrada é um lixo, sua saída também vai ser.
Agora você deve estar se perguntando: beleza Gabriel, mas o que fazer então para melhorar os dados. Bem, aí vai depender de muitos fatores, quais os tipos dos dados, de onde eles foram extraídos, qual a quantidade etc. Mas tudo isso é tema para um outro artigo. O meu objetivo aqui é falar sobre os dados que eu utilizei, e para a minha sorte são dados mais ou menos estruturados.
Primeiramente faça um exercício mental, qual o nosso objetivo? Queremos prever probabilidades: do time ser campeão, ficar no G4 ou ser rebaixado. Com isso em mente o que você acha que precisamos? Podemos partir do pressuposto que precisamos de dados históricos, a pontuação dos times em diversos campeonatos, mas não só isso…
Não adiantaria ter só a pontuação final do time na última rodada, nós queremos ver a trajetória, por exemplo, o campeonato está na rodada 30, certo time tem uma determinada quantidade de vitórias, derrotas, empates, gols, pontos etc. O algoritmo precisa perceber que existe um certo padrão para então prever qual vai ser a pontuação do time na última rodada.
Mas então como conseguimos os dados? Bem essa não parecia ser uma tarefa fácil, não conhecia nenhum site que mostrava uma tabela do campeonato rodada a rodada. Até que encontrei a galinha dos ovos de ouros, um site chamado WordFootball que disponibiliza exatamente isso que precisava para diversos campeonatos.
Bem, já sei de onde pegar os dados, mas como posso extrair eles? Agora vem a parte divertida, hora do código!
2 — Pegando as tabelas e criando novas variáveis
Eu não quero ser muito exaustivo sobre todos os pontos do código para você achar esse artigo extremamente chato. Então, todos os código que citar aqui estão nesse repositório: https://github.com/gabriel19913/soccer_probability. Para a extração e manipulação de dados os arquivos são: get_data_league_github.ipynb
e get_data_league_github.py
. O código nos dois é o mesmo, mas um é um arquivo jupyter notebook e o outro é um arquivo em Python propriamente dito. Mas chega de enrolação e vamos colocar a mão na massa, ou melhor, no teclado.
A ideia geral para formar esse banco de dados é bastante simples. Selecionei alguns campeonatos e certas ligas, o código deve fazer um loop rodada a rodada em cada um dos campeonatos e temporadas e concatenar todos para no final criar um grande DataFrame (que nada mais é do que uma tabela que facilita a forma da gente realizar cálculos e operações com dados em Python).
Para fazer isso eu criei algumas funções, mas antes é importante destacar aqui um método da biblioteca Pandas (essa é uma biblioteca muito usada na manipulação de dados em Python) chamado read_html
. Esse método é incrível, basta passar uma url para ele que é retornada uma lista com todas as tabelas da página. Isso é lindo, e facilita bastante o trabalho (pode acreditar em mim).
Vamos então falar da primeira função que eu criei: get_table()
. Pelo o nome você deve ter percebido que ela é responsável por pegar a tabela do campeonato propriamente dito.
Observe que essa função recebe os seguintes parâmetros:
current_round
: rodada atualmax_rounds
: número máximo de rodadasleague
: campeonatoseason
: temporadaflag
: uma flag porque em um determinado campeonato e temporada o final da url muda
A primeira coisa que essa função faz é usar o read_html
para pegar a tabela do campeonato segundo os parâmetros que foram passados para a função. Depois disso, as colunas são renomeadas para ficarem mais legíveis. A coluna de gols obtidas possui os gols feitos e sofridos separados por ‘:’ na mesma coluna, então a função coloca eles em colunas distintas.
A seguir são feitas diversas manipulações e geradas novas variáveis a partir das disponíveis. Mas resumidamente:
1 — são criadas as variáveis de gols feitos (goals_for
) e gols sofridos (goals_against
);
2 — a coluna pos
é alterada para iniciar do 1;
3 — é criada uma variável para indicar o número máximo de rodadas que o campeonato possui (max_rounds
);
4 — é criada uma variável que indica quantas rodadas faltam para terminar o campeonato (rounds_left
), que nada mais é: max_rounds — current_round
;
5 — a variável possible_points
indica quantos pontos o time poderia ter obtido se tivesse ganhado todos os jogos até aquela rodada;
6 — criada variávelperformance = (points_for / possible_points)*100
, que indica o desempenho do time calculando um percentual de quantos pontos o time obteve em relação a todos disputados.
7 — a variável total_possible_points
indica quantos pontos o time ainda pode obter se a partir da rodada atual ele ganhar todas as partidas;
8 — a variável goals_for_against_ratio
é uma métrica da efetividade de um time fazer e levar gols, assim é a razão entre goals_for
e goals_against
, se maior que 1 o time faz mais gols que leva, caso contrário ele mais leva do que faz gols;
9 — por fim é adicionada a liga e a temporada daquela tabela específica.
A função get_table()
é, portanto, o coração de todo o código, pois é ela que vai pegar pra gente cada uma das tabelas que precisamos pra gerar o nosso grande banco de dados.
3 — Loopando nos campeonatos e temporadas
Vou mostrar agora a função responsável por realizar um loop em todas as ligas, temporadas e rodadas que você quiser. Essa função é get_all_leagues()
.
Observe, que ela recebe um único parâmetro que é um dicionário cujas chaves são o nome das ligas e os valores são listas nas quais o primeiro item são a temporada inicial e o segundo o número máximo de rodadas, como é possível analisar a seguir.
Dessa maneira, na função get_all_leagues()
é inicialmente realizada uma decisão para verificar se a liga buscada é brasileira ou européia, já que nesse caso muda a forma de tratar a temporada (no Brasil é comum um ano, por exemplo 2005, enquanto na Europa o padrão é 2004–2005). Para ambos os casos é realizado um loop até a temporada na qual eu desejo pegar as informações e são utilizadas outras duas funções: get_league()
e increment_league()
.
A primeira recebe o número máximo de rodadas, o campeonato, a temporada e a flag (como explicado na seção anterior) e retorna um DataFrame com todas as informações para todas rodadas daquele campeonato e temporada especificados. Vale destacar que utilizei o processamento paralelo para que a função fosse executada mais rapidamente, aqui não vou aprofundar mais sobre o assunto, caso tenha curiosidade você pode pesquisar mais sobre a biblioteca joblib
.
A outra função utilizada dentro do loop (increment_season()
) é bem simples, ela recebe somente qual a temporada atual, transforma o valor de string para inteiro para poder incrementar para a próxima temporada.
Assim, os loops da função get_all_leagues()
geram listas com diversos DataFrames que representam as tabelas dos campeonatos com as temporadas e todas as sua rodadas. Ao final dessa função, todos esses DataFrames são então concatenados gerando a tabela final.
4 — Adicionando a pontuação final
Uma coisa que talvez você tenha notado é que até agora o que temos é um grande DataFrame com as tabela rodada a rodada de diversos campeonatos. Mas e a pontuação final que é o que queremos prever?
Pra isso eu criei a seguinte função:
As duas primeiras linhas da função apresentam duas operações necessárias para tratar o nome de um time que não estava consistente em todo DataFrame e algumas situações nas quais as variáveiscurrent_round
,rounds_left
e loss
não estavam com valores corretos.
Após isso, foram utilizados dois laços: o primeiro percorre todos os campeonatos únicos e o segundo percorre as temporadas únicas. Assim, é criado um novo DataFrame derivado do original mas selecionando a temporada e o campeonato específicos.
Em seguida, é criado um novo DataFrame baseado no anterior, mas contendo a pontuação na última rodada do campeonato. Assim, é gerado um novo DataFrame a partir do merge
entre esses dois e é adicionado em uma lista. Por fim, é realizada a concatenação dos DataFrame da lista é realizada e temos o conjunto de dados final, com todas as informações que geramos e a coluna com a pontuação final para cada campeonato e temporada. Uma pequena amostra dele pode ser vista abaixo.
Para a primeira parte desse tutorial é isso pessoal! Na segunda parte vou mostrar os modelos que geram as predições e probabilidades. Peço perdão se você achou que ficou muito longo, tentei ser explicativo, mas mesmo assim não teve como entrar a fundo em tudo. Até a próxima!
Se tiver dúvidas, questões ou críticas pode me chamar nas redes sociais:
Twitter: https://twitter.com/gabriel1991
Instagram: https://www.instagram.com/gabrielfonseca1991/
Linkedin: https://www.linkedin.com/in/gabrielfonseca91/
GitHub: https://github.com/gabriel19913