Desmascarando o basquete usando Python

Jan 24 2023
Parte 1: Raspando com Bela Sopa
Nesta série de blogs, poderosas bibliotecas Python são aproveitadas para ajudar a descobrir algumas verdades estatísticas ocultas no basquete. O primeiro passo em qualquer abordagem baseada em dados é identificar e coletar os dados necessários.

Nesta série de blogs, poderosas bibliotecas Python são aproveitadas para ajudar a descobrir algumas verdades estatísticas ocultas no basquete. O primeiro passo em qualquer abordagem baseada em dados é identificar e coletar os dados necessários.

Felizmente para nós, Basketball-Reference.com hospeda páginas de dados de basquete que podem ser facilmente copiadas. Os processos deste passo a passo podem ser facilmente aplicados a qualquer número de suas páginas, mas, para este caso, planejamos extrair estatísticas sazonais de várias classes novatas.

Os objetivos

  1. Identifique a fonte de dados
  2. Baixe a página
  3. Identificar elementos importantes da página
  4. Pré-limpeza e extração
  5. Arquivo
  • pacote de pedidos
  • Belo pacote de sopa
  • pacote de pandas

Identificando a fonte de dados

O Basketball-Reference.com hospeda centenas de páginas com curadoria de estatísticas de basquete que variam de médias sazonais de categorias típicas de pontuação de caixa, como pontos, rebotes e porcentagens de arremessos, até a ação jogada a jogada de cada jogo disputado no último 20 ou mais anos. Alguém pode facilmente se perder nesse tsunami estatístico se não houver um objetivo claro definido sobre o que exatamente procurar.

O objetivo aqui neste post é simples: obter dados de novatos que ajudarão a avaliar o verdadeiro valor e potencial de um jovem jogador.

O link a seguir é uma dessas páginas. Ele lista todas as estatísticas relevantes dos novatos em uma determinada temporada.

https://www.basketball-reference.com/leagues/NBA_1990_rookies-season-stats.html

Para acumular dados suficientes para fazer inferências estatísticas sólidas sobre os jogadores, um ano de dados não será suficiente. São necessários dezenas de anos de dados coletados para ajudar a filtrar o ruído e chegar a uma conclusão sobre o potencial futuro de um jogador.

Se uma ação puder ser repetida manualmente, ela se torna uma ótima candidata à automação. Nesse caso, o número na URL acima corresponde ao respectivo ano dessa classe de novato. Com esse conhecimento, vamos começar a montar nossas primeiras linhas de código.

import requests
from bs4 import BeautifulSoup
import pandas as pd

years = list(range(1990, 2017))
url_base = "https://www.basketball-reference.com/leagues/NBA_{}_rookies-season-stats.html"

  1. Os pacotes apropriados são importados
  2. A yearsvariável list especifica o intervalo dos anos desejados - 1990 até 2017
  3. url_baseserve para armazenar a variável de string pré-formatada da URL de destino

Ao raspar páginas da Web, é imperativo remover o máximo de sobrecarga possível. Visto que o site armazena todas as informações no front-end HTML, a página pode ser facilmente baixada e armazenada localmente em sua totalidade.

#loop iterates through years
for year in years:
    url = url_base.format(year)
    data = requests.get(url)
    #each page year is locally stored as an HTML file
    with open("notebooks/Rookies/{}.html".format(year), "w+") as f:
        f.write(data. Text)

  1. As chaves encontradas dentro da urlstring ' servirão para permitir que formato substitua pelo ano iterado atual
    — Por exemplo, em sua primeira iteração, o urlvalor seráhttps://www.basketball-reference.com/leagues/NBA_1990_rookies-season-stats.html
    — Em sua segunda iteração, o ano subsequente seria referenciado:https://www.basketball-reference.com/leagues/NBA_1991_rookies-season-stats.html
  2. A variável de dados atua como um espaço reservado para a requests.get()função e as referências do urlvalor de string iterado atualmente
  3. O requestsmétodo então usa a string de URL recém-formatada para recuperar a página em questão
  4. O subseqüente with open()lê e grava ( w+) os dados da página de nosso requests.get( data.text) e armazena localmente os arquivos HTML recém-criados

Para evitar uma dor de crescimento comum na raspagem de sites, armazenamos essas páginas como arquivos HTML locais. Veja, ao fazer uma visita a um site de página, o servidor que hospeda essa página honra sua solicitação e envia de volta os dados apropriados para seu navegador. Mas ter um cliente específico solicitando as mesmas informações continuamente sobrecarrega o servidor. O administrador do servidor tem o direito de bloquear essas solicitações persistentes para poder fornecer esse serviço de maneira ideal a outras pessoas online.

Ao baixar esses arquivos HTML em sua máquina local, você evita duas coisas:

  1. Ter que esperar mais do que o normal para coletar os mesmos dados
  2. Ser impedido de visitar a página, interrompendo completamente a coleta de dados

Existem algumas vantagens em extrair dados de formatos HTML. Uma das mais importantes é como as propriedades do elemento podem ser identificadas de forma única. Se eles puderem ser identificados de forma única, eles podem ser raspados usando Beautiful Soup. Mas para fazer isso, é preciso saber como inspecionar esses próprios elementos HTML.

Como inspecionar

Precisamos nos aprofundar no funcionamento interno deste documento, mas prometo que não farei disso um exercício de aprendizado de HTML.

Se você sabe como inspecionar objetos HTML, fique à vontade para avançar. Caso contrário, acompanhe como inspecionar os elementos da página.

  1. Clique nos três pontos verticais na barra de menu superior do Chrome
  2. Escolha “Mais ferramentas”
  3. Selecione Ferramentas do desenvolvedor.
  1. Clique com o botão direito do mouse na página da web
  2. Escolha “Inspecionar” para acessar o painel de ferramentas do desenvolvedor

Inspecionando a página

Vendo que todas essas páginas são armazenadas localmente, podemos optar por visualizá-las acessando o sistema de arquivos para abri-las em nosso navegador desejado ou podemos continuar a construir nosso código implementando o seguinte trecho de código.

with open("notebooks/Rookies/2000.html") as f:
    page = f.read()

Todos os dados desejados desta página estão alojados nesse elemento da tabela. Antes de sugar apressadamente todos esses dados como estão, agora é o melhor momento para considerar se vale a pena coletar tudo nesta tabela.

Pré-limpeza

Pré-limpeza pode não ser uma palavra frequente em seu vocabulário, mas para aqueles que se veem coletando dados regularmente, deveria ser. Se você quiser evitar a frustração de horas perdidas de progresso em um projeto de coleta de dados, é melhor primeiro separar o joio do trigo.

Por exemplo, observe os três elementos marcados em vermelho.

Uma linha serve como cabeçalho da tabela “principal”. As outras duas linhas são instâncias duplicadas dos mesmos artefatos encontrados no topo. Esse padrão se repete a cada 20 carreiras.

Uma inspeção mais detalhada revela que todas essas linhas têm a mesma trtag HTML (linha da tabela). O que distingue cada um desses elementos de quaisquer outros são seus nomes de classe.

  1. Linha de Cabeçalho Principal
    — Classe =over_header
  2. Repetir Linhas de Cabeçalho
    — Classe =over_header thead
  3. Linha de Categoria de Estatísticas
    — Classe =thead
  4. #array to house list of dataframes
    dfs = []
    #unnecessary table rows to be removed
    classes = ["over_header", "over_header thead", "thead"]
    

  5. O classesobjeto de matriz conterá todos os nomes de classe do elemento de linha da tabela não autorizado.

O decomposemétodo serve para remover tais elementos indesejados em uma página. De acordo com a documentação oficial do Beautiful Soup ...

decompose()

Tag.decompose()remove uma marca da árvore e a destrói completamente e seu conteúdo.

Abaixo está um trecho de código onde conseguimos otimizar o decomposemétodo com ajuda de forloops.

#for loop to iterate through the years 
for year in years:
    with open("notebooks/Rookies/{}.html".format(year)) as f:
        page = f.read()
    soup = BeautifulSoup(page, "html.parser")
    #for loop cleans up unnecessary table headers from reappearing in rows
    for i in classes:
        for tr in soup.find_all("tr", {"class":i}):
            tr.decompose()

  1. O withmétodo fornece ao nosso código a estrutura para a pagevariável ler arquivos HTML armazenados localmente quando chamado
  2. Uma classe de analisador HTML é inicializada instanciando a classe Beautiful Soup e passando o pageobjeto string e html.parser.
  3. O segundo forloop itera pelos valores na classesmatriz
  4. O terceiro forloop utiliza o método Beautiful Soup find_allpara identificar elementos que possuem tags tr e nomes de classe correspondentes aos declasses
  5. tr.decomposeserve para omitir totalmente cada um dos elementos de linha da tabela identificados da página

Extraindo os Dados

Podemos finalmente começar a trabalhar na parte do código que realmente extrai os dados da tabela. Lembre-se de que a tabela com todos os dados relevantes possui o ID exclusivo do HTML rookies. As seguintes adições ao nosso código servirão para analisar os dados desta tabela.

#the years we wish to parse for
years = list(range(1990, 2017))
#array to house list of dataframes
dfs = []
#unnecessary table headers to be removed
classes = ["over_header","over_header thead", "thead"]
 
for year in years:
    #url = url_base.format(year)
    #data = requests.get(url)
    ##page is saved as an html and placed in Rookies folder
    #with open("notebooks/Rookies/{}.html".format(year), "w+") as f:
        #f.write(data.text)
     #with open("notebooks/Rookies/2000.html") as f:
    with open("notebooks/Rookies/{}.html".format(year)) as f:
        page = f.read()
      soup = BeautifulSoup(page, "html.parser")   #for loop cleans up unnecessary table headers from reappearing in rows
    for i in classes:
        for tr in soup.find_all("tr", {"class":i}):
        tr.decompose()
        
### Scraping Block ###
    #identifies, scrapes, and loads rookie tables into one dataframe
    rookie_table = soup.find(id="rookies")
    rookies = pd.read_html(str(rookie_table))[0]
    rookies["Year"] = year
    dfs.append(rookies)

#new variable turns list of dataframes into single dataframe
all_rookies = pd.concat(dfs)

  1. rookie_tableinstancia o método do Beautiful Soup findpara identificar o elemento HTML com IDrookies
  2. Vendo que o Pandas pode ler tabelas HTML, a tabela rookie é carregada no Pandas usando o read_htmlmétodo, passando para ela rookie_tablecomo uma string
  3. Aderir ao final [0]transforma rookiesuma lista de quadros de dados em um único quadro de dados
  4. Uma coluna “Ano” é o fim do rookiesquadro de dados
  5. dfs.append(rookies)serve para armazenar todas as tabelas de cada ano de novato na ordem em que foram iteradas em uma lista de quadros de dados
  6. O método Pandas concaté usado para combinar a lista de quadros de dados em um único quadro de dados:all_rookies

Nossa etapa final envolve pegar todas essas informações úteis e limpas e arquivá-las em um formato CSV facilmente legível por humanos e máquinas. Colocar esta linha no final do nosso código servirá para ser útil ao decidir voltar e referenciar os dados coletados.

#a single dataframe archived as a local CSV using Pandas
all_rookies.to_csv("archive/NBA_Rookies_1990-2016.csv")

import requests
import pandas as pd
from bs4 import BeautifulSoup

years = list(range(1990, 2017))
dfs = []
classes = ["over_header","over_header thead", "thead"] #unnecessary table headers to be removed

for year in years:
    with open("C:/Users/cisco/OneDrive/python/Projects/Basketball/Rookies/{}.html".format(year), encoding = "utf-8") as f:
        page = f.read()
    soup = BeautifulSoup(page, "html.parser")
    #second for loop cleans up unnecessary table headers from reappearing in rows
    for i in classes:
        for tr in soup.find_all("tr", {"class":i}):
            tr.decompose()
    #identifies, scrapes, and loads rookie tables into one dataframe
    table_rookies = soup.find(id="rookies")
    rookies = pd.read_html(str(table_rookies))[0]
    rookies["Year"] = year
    dfs.append(rookies)

#new variable turns list of dataframes into single dataframe
all_rookies = pd.concat(dfs)

#dataframe archived as local CSV
all_rookies.to_csv("archive/NBA_Rookies_1990-2016.csv")

Novamente, o processo seguido neste passo a passo sem dúvida se aplicará a quase todas as outras páginas do Basketball-Reference.com . Existem cinco etapas simples que valem a pena seguir em cada instância.

  1. Identifique o URL da página
  2. Baixe a página
  3. Identifique os Elementos
  4. Pré-limpeza e extração
  5. Arquivo

A seguir, nesta série, usaremos esses dados para obter informações sobre o potencial do futuro jogador. Portanto, fique atento às próximas parcelas!