Démasquer le basket-ball à l'aide de Python

Jan 24 2023
Partie 1 : Gratter avec une belle soupe
Dans cette série de blogs, de puissantes bibliothèques Python sont exploitées pour aider à découvrir certaines vérités statistiques cachées dans le basket-ball. La première étape de toute approche axée sur les données consiste à identifier et à collecter les données nécessaires.

Dans cette série de blogs, de puissantes bibliothèques Python sont exploitées pour aider à découvrir certaines vérités statistiques cachées dans le basket-ball. La première étape de toute approche axée sur les données consiste à identifier et à collecter les données nécessaires.

Heureusement pour nous, Basketball-Reference.com héberge des pages de données de basket-ball qui peuvent être facilement récupérées. Les processus de cette procédure pas à pas peuvent être facilement appliqués à n'importe quel nombre de leurs pages, mais pour ce cas, nous prévoyons de supprimer les statistiques saisonnières de plusieurs classes de recrues.

Les objectifs

  1. Identifier la source de données
  2. Télécharger la page
  3. Identifier les éléments de page importants
  4. Pré-nettoyage et extraction
  5. Archive
  • Paquet de demandes
  • Beau paquet de soupe
  • Forfait Pandas

Identification de la source de données

Basketball-Reference.com héberge des centaines de pages organisées sur les statistiques de basket-ball qui vont des moyennes saisonnières des catégories de scores de boîte typiques comme les points, les rebonds et les pourcentages de tir, jusqu'à l'action play-by-play de chaque jeu joué dans le dernier 20 ans ou plus. On peut facilement se perdre dans ce tsunami statistique s'il n'y a pas d'objectif clair sur ce qu'il faut rechercher exactement.

L'objectif ici dans cet article est simple : obtenir des données sur les recrues qui aideront à évaluer la véritable valeur et le potentiel d'un jeune joueur.

Le lien suivant est une de ces pages. Il répertorie toutes les statistiques pertinentes des recrues d'une saison particulière.

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

Afin d'accumuler suffisamment de données pour faire des inférences statistiques solides sur les joueurs, une année de données ne suffira pas. Il doit y avoir des dizaines d'années de données collectées pour aider à filtrer le bruit et à tirer une conclusion sur le potentiel futur d'un joueur.

Si une action peut être répétée manuellement, elle devient un excellent candidat pour l'automatisation. Dans ce cas, le numéro dans l'URL ci-dessus correspond à l'année respective de cette classe de recrue. Forts de ces connaissances, commençons à assembler nos premières lignes de code.

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. Les packages appropriés sont importés
  2. La yearsvariable de liste spécifie la plage des années souhaitées - 1990 à 2017
  3. url_basesert à stocker la variable de chaîne pré-formatée de l'URL cible

Lors du grattage de pages Web, il est impératif de supprimer autant de frais généraux que possible. Étant donné que le site stocke toutes ses informations sur le frontal HTML, la page peut être facilement téléchargée et stockée localement dans son intégralité.

#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. Les accolades trouvées dans la urlchaîne de serviront à permettre à la formatde la remplacer par l'année actuellement itérée
    — Par exemple, dans sa première itération, la urlvaleur serahttps://www.basketball-reference.com/leagues/NBA_1990_rookies-season-stats.html
    — Lors de sa deuxième itération, l'année suivante serait référencée à la place :https://www.basketball-reference.com/leagues/NBA_1991_rookies-season-stats.html
  2. La variable de données agit comme un espace réservé pour la requests.get()fonction et les références de la urlvaleur de chaîne actuellement itérée
  3. La requestsméthode utilise ensuite la chaîne d'URL nouvellement formatée pour récupérer la page en question
  4. La suite with open()lit et écrit ( w+) les données de la page à partir de notre requests.get( data.text), et stocke localement les fichiers HTML nouvellement créés

Pour éviter une douleur croissante commune dans le grattage de site, nous stockons ces pages sous forme de fichiers HTML locaux. Voyez, lors d'une visite sur un site de page, le serveur hébergeant ladite page honore votre demande et renvoie les données appropriées à votre navigateur. Mais le fait qu'un client spécifique demande sans cesse les mêmes informations exerce une pression excessive sur le serveur. L'administrateur du serveur a tout à fait le droit de bloquer ces demandes persistantes afin de pouvoir fournir ce service de manière optimale aux autres en ligne.

En téléchargeant ces fichiers HTML sur votre machine locale, vous évitez deux choses :

  1. Devoir attendre plus longtemps que d'habitude pour collecter les mêmes données
  2. Être bloqué de visiter la page, arrêtant complètement la collecte de données

Il y a quelques avantages à extraire les données des formats HTML. L'un des plus importants est la manière dont les propriétés des éléments peuvent être identifiées de manière unique. S'ils peuvent être identifiés de manière unique, ils peuvent être grattés à l'aide de Beautiful Soup. Mais pour cela, il faut savoir inspecter ces éléments HTML eux-mêmes.

Comment inspecter

Nous devrons approfondir les rouages ​​de ce document, mais je promets de ne pas en faire un exercice d'apprentissage du HTML.

Si vous savez comment inspecter les objets HTML, n'hésitez pas à aller de l'avant. Sinon, veuillez suivre la procédure d'inspection des éléments de la page.

  1. Cliquez sur les trois points verticaux dans la barre de menu supérieure de Chrome
  2. Choisissez "Plus d'outils"
  3. Sélectionnez Outils de développement.
  1. Faites un clic droit sur la page Web
  2. Choisissez "Inspecter" pour accéder au panneau des outils de développement

Inspection de la page

Étant donné que toutes ces pages sont stockées localement, nous pouvons choisir de les afficher soit en accédant au système de fichiers pour les ouvrir dans le navigateur de notre choix, soit en continuant à créer notre code en implémentant l'extrait de code suivant.

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

Toutes les données souhaitées de cette page sont hébergées dans cet élément de table. Avant d'aspirer à la hâte toutes ces données telles quelles, c'est maintenant le meilleur moment pour déterminer si tout ce qui se trouve sur cette table vaut la peine d'être collecté.

Pré-nettoyage

Le pré-nettoyage n'est peut-être pas un mot fréquent dans votre vocabulaire, mais pour ceux d'entre vous qui se voient régulièrement gratter des données, cela devrait l'être. Si vous voulez éviter la frustration des heures perdues à progresser sur un projet de collecte de données, il est préférable de séparer d'abord l'ivraie du bon grain.

Par exemple, notez les trois éléments encadrés en rouge.

Une ligne sert d'en-tête de tableau « principal ». Les deux autres lignes sont des instances en double des mêmes artefacts trouvés en haut. Ce motif se répète tous les 20 rangs.

Une inspection plus approfondie révèle que toutes ces lignes ont la même trbalise HTML (ligne de tableau). Ce qui distingue chacun de ces éléments des autres, ce sont leurs noms de classe.

  1. Ligne d'en-tête principale
    — Classe =over_header
  2. Répéter les lignes d'en-tête
    — Classe =over_header thead
  3. Ligne de catégorie de statistiques
    — Classe =thead
  4. #array to house list of dataframes
    dfs = []
    #unnecessary table rows to be removed
    classes = ["over_header", "over_header thead", "thead"]
    

  5. L' classesobjet de tableau contiendra tous les noms de classe de l'élément de ligne de tableau non déformé.

La decomposeméthode sert à supprimer ces éléments indésirables dans une page. Selon la documentation officielle de Beautiful Soup ...

decompose()

Tag.decompose()supprime une balise de l'arborescence, puis la détruit complètement ainsi que son contenu.

Vous trouverez ci-dessous un extrait de code dans lequel nous avons pu optimiser la decomposeméthode à l'aide de forboucles.

#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. La withméthode fournit à notre code la structure de la pagevariable pour lire les fichiers HTML stockés localement lorsqu'elle est appelée
  2. Une classe d'analyseur HTML est initialisée en instanciant la classe Beautiful Soup et en transmettant à la fois l' pageobjet string et html.parser.
  3. La deuxième forboucle parcourt les valeurs du classestableau
  4. La troisième forboucle utilise la méthode de Beautiful Soup find_allpour identifier les éléments qui ont à la fois tr des balises et des noms de classe correspondant à ceux declasses
  5. tr.decomposesert à omettre complètement chacun des éléments de ligne de tableau identifiés de la page

Extraction des données

Nous pouvons enfin commencer à travailler sur la partie du code qui extrait réellement les données de la table. N'oubliez pas que le tableau contenant toutes les données pertinentes possède l'ID unique HTML rookies. Les ajouts suivants à notre code serviront à analyser les données de cette table.

#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_tableinstancie la méthode de Beautiful Soup findpour identifier l'élément HTML avec IDrookies
  2. Voyant que Pandas peut lire les tables HTML, la table des recrues est chargée dans Pandas à l'aide de la read_htmlméthode, en y passant rookie_tablesous forme de chaîne
  3. Passer à la fin [0]passe rookiesd'une liste de trames de données à une seule trame de données
  4. Une colonne "Année" est la fin de la rookiestrame de données
  5. dfs.append(rookies)sert à héberger toutes les tables de chaque année de recrue dans l'ordre dans lequel elles ont été itérées dans une liste de trames de données
  6. La méthode Pandas concatest utilisée pour combiner la liste des trames de données en une seule trame de données :all_rookies

Notre dernière étape consiste à prendre toutes ces informations utiles et propres et à les archiver dans un format CSV facilement lisible par l'homme et la machine. Repérer cette ligne jusqu'à la fin de notre code sera utile au moment de décider de revenir et de référencer les données collectées.

#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")

Encore une fois, le processus suivi dans cette procédure pas à pas s'appliquera sans aucun doute à la plupart des autres pages de Basketball-Reference.com . Il y a cinq étapes simples à suivre dans chaque cas.

  1. Identifier l'URL de la page
  2. Télécharger la page
  3. Identifier les éléments
  4. Pré-nettoyage et extraction
  5. Archive

La prochaine étape de cette série utilisera ces données pour mieux comprendre le potentiel des futurs joueurs. Soyez donc à l'affût des prochains versements!