Desenmascarando el baloncesto usando Python

Jan 24 2023
Parte 1: raspado con sopa hermosa
En esta serie de blogs, se aprovechan las poderosas bibliotecas de Python para ayudar a descubrir algunas verdades estadísticas ocultas en el baloncesto. El primer paso en cualquier enfoque basado en datos es identificar y recopilar los datos necesarios.

En esta serie de blogs, se aprovechan las poderosas bibliotecas de Python para ayudar a descubrir algunas verdades estadísticas ocultas en el baloncesto. El primer paso en cualquier enfoque basado en datos es identificar y recopilar los datos necesarios.

Afortunadamente para nosotros, Basketball-Reference.com alberga páginas de datos de baloncesto que se pueden extraer fácilmente. Los procesos de este tutorial se pueden aplicar fácilmente a cualquier cantidad de sus páginas, pero para este caso, planeamos recopilar estadísticas estacionales de varias clases de novatos.

Los objetivos

  1. Identificar la fuente de datos
  2. Descarga la Pagina
  3. Identificar elementos importantes de la página
  4. Pre-Limpieza y Extracto
  5. Archivo
  • Paquete de solicitudes
  • Hermoso paquete de sopa
  • paquete pandas

Identificación de la fuente de datos

Basketball-Reference.com alberga cientos de páginas seleccionadas sobre estadísticas de baloncesto que van desde promedios estacionales de categorías típicas de puntajes de cuadro como puntos, rebotes y porcentajes de tiro, hasta la acción jugada por jugada de cada juego jugado en el último 20 años más o menos. Uno puede perderse fácilmente en este tsunami estadístico si no hay un objetivo claro establecido sobre qué buscar exactamente.

El objetivo aquí en esta publicación es simple: obtener datos de novatos que ayudarán a evaluar el verdadero valor y potencial de un jugador joven.

El siguiente enlace es una de esas páginas. Enumera todas las estadísticas relevantes de los novatos en una temporada en particular.

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

Para acumular suficientes datos para hacer inferencias estadísticas sólidas sobre los jugadores, un año de datos no es suficiente. Es necesario recopilar datos de decenas de años para ayudar a filtrar el ruido y llegar a una conclusión sobre el potencial futuro de un jugador.

Si una acción se puede repetir manualmente, se convierte en un gran candidato para la automatización. En este caso, el número en la URL anterior corresponde al año respectivo de esa clase de novatos. Con ese conocimiento, comencemos a armar nuestras primeras líneas 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. Los paquetes apropiados son importados
  2. La yearsvariable de lista especifica el rango de los años deseados - 1990 hasta 2017
  3. url_basesirve para almacenar la variable de cadena con formato previo de la URL de destino

Al raspar páginas web, es imperativo eliminar la mayor cantidad posible de gastos generales. Dado que el sitio almacena toda su información en la interfaz HTML, la página se puede descargar fácilmente y almacenar localmente en su totalidad.

#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. Las llaves que se encuentran dentro de la urlcadena servirán para permitir que formatla sustituya con el año iterado actualmente
    . Por ejemplo, en su primera iteración, el urlvalor seráhttps://www.basketball-reference.com/leagues/NBA_1990_rookies-season-stats.html
    — En su segunda iteración, se haría referencia al año siguiente en su lugar:https://www.basketball-reference.com/leagues/NBA_1991_rookies-season-stats.html
  2. La variable de datos actúa como un marcador de posición para la requests.get()función y las referencias del urlvalor de cadena iterado actualmente
  3. Luego, el requestsmétodo usa la cadena de URL recién formateada para recuperar la página en cuestión.
  4. El subsiguiente with open()lee y escribe ( w+) los datos de la página de nuestro requests.get( data.text) y almacena localmente los archivos HTML recién creados.

Para evitar un dolor de crecimiento común en el raspado de sitios, almacenamos estas páginas como archivos HTML locales. Verá, cuando realiza una visita al sitio de una página, el servidor que aloja dicha página acepta su solicitud y envía los datos apropiados a su navegador. Pero tener un cliente específico que solicita la misma información una y otra vez ejerce una presión indebida sobre el servidor. El administrador del servidor tiene todo el derecho de bloquear estas solicitudes persistentes para poder brindar este servicio de manera óptima a otros en línea.

Al descargar estos archivos HTML en su máquina local, evita dos cosas:

  1. Tener que esperar más de lo habitual para recopilar los mismos datos
  2. Ser bloqueado para visitar la página, detener la recopilación de datos por completo

Hay algunas ventajas en el raspado de datos de formatos HTML. Uno de los más importantes es cómo las propiedades de los elementos se pueden identificar de forma única. Si se pueden identificar de forma única, se pueden raspar con Beautiful Soup. Pero para hacer eso, uno debe saber cómo inspeccionar estos elementos HTML.

Cómo inspeccionar

Tendremos que profundizar más en el funcionamiento interno de este documento, pero prometo que no haré de esto un ejercicio para aprender HTML.

Si sabe cómo inspeccionar objetos HTML, no dude en adelantarse. De lo contrario, siga las instrucciones sobre cómo inspeccionar los elementos de la página.

  1. Haga clic en los tres puntos verticales en la barra de menú superior de Chrome
  2. Elija "Más herramientas"
  3. Seleccione Herramientas para desarrolladores.
  1. Haga clic derecho en la página web
  2. Elija "Inspeccionar" para acceder al panel de herramientas del desarrollador

Inspeccionar la página

Al ver que todas estas páginas están almacenadas localmente, podemos elegir verlas ingresando al sistema de archivos para abrirlas en nuestro navegador deseado, o podemos continuar compilando nuestro código implementando el siguiente fragmento de código.

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

Todos los datos deseados de esta página están alojados en ese elemento de la tabla. Antes de absorber apresuradamente todos estos datos tal como están, ahora es el mejor momento para considerar si vale la pena recopilar todo lo que hay en esta tabla.

limpieza previa

La limpieza previa puede no ser una palabra frecuente en su vocabulario, pero para aquellos de ustedes que se ven extrayendo datos de forma regular, debería serlo . Si desea evitar la frustración de perder horas de progreso en un proyecto de recopilación de datos, es mejor separar primero la paja del trigo.

Por ejemplo, tome nota de los tres elementos enmarcados en rojo.

Una fila sirve como encabezado de la tabla "principal". Las otras dos filas son instancias duplicadas de los mismos artefactos que se encuentran en la parte superior. Este patrón se repite cada 20 filas.

Una inspección más detallada revela que todas estas filas tienen la misma tretiqueta HTML (fila de la tabla). Lo que distingue a cada uno de estos elementos de cualquier otro son sus nombres de clase.

  1. Fila de encabezado principal
    — Clase =over_header
  2. Repetir filas de encabezado
    — Clase =over_header thead
  3. Categoría de estadísticas Fila
    — Clase =thead
  4. #array to house list of dataframes
    dfs = []
    #unnecessary table rows to be removed
    classes = ["over_header", "over_header thead", "thead"]
    

  5. El classesobjeto de matriz contendrá todos los nombres de clase del elemento de fila de la tabla no verificado.

El decomposemétodo sirve para eliminar dichos elementos no deseados en una página. Según la documentación oficial de Beautiful Soup ...

decompose()

Tag.decompose()elimina una etiqueta del árbol y luego la destruye por completo junto con su contenido.

A continuación se muestra un fragmento de código en el que pudimos optimizar el decomposemétodo con la ayuda de forbucles.

#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. El withmétodo proporciona a nuestro código la estructura para que la pagevariable lea archivos HTML almacenados localmente cuando se le solicite
  2. Una clase de analizador HTML se inicializa instanciando la clase Beautiful Soup y pasando tanto el pageobjeto de cadena como html.parser.
  3. El segundo forciclo itera a través de los valores en la classesmatriz
  4. El tercer forciclo utiliza el método de Beautiful Soup find_allpara identificar elementos que tienen etiquetas tr y nombres de clase que coinciden con los declasses
  5. tr.decomposesirve para omitir cada uno de los elementos de fila de tabla identificados de la página por completo

Extrayendo los datos

Finalmente podemos comenzar a trabajar en la parte del código que realmente extrae datos de la tabla. Recuerde que la tabla con todos los datos relevantes tiene la ID única de HTML rookies. Las siguientes adiciones a nuestro código servirán para analizar los datos de esta tabla.

#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 el método de Beautiful Soup findpara identificar el elemento HTML con IDrookies
  2. Al ver que Pandas puede leer tablas HTML, la tabla novata se carga en Pandas usando el read_htmlmétodo, pasándola rookie_tablecomo una cadena
  3. Virar hasta el final de [0]los turnos rookiesde una lista de marcos de datos en un solo marco de datos
  4. Una columna "Año" es el final del rookiesmarco de datos
  5. dfs.append(rookies)sirve para albergar todas las tablas de cada año de novato en el orden en que se iteraron en una lista de marcos de datos
  6. El método Pandas concatse utiliza para combinar la lista de marcos de datos en un solo marco de datos:all_rookies

Nuestro paso final implica tomar toda esta información útil y limpia y archivarla en un formato CSV fácilmente legible por humanos y máquinas. Agregar esta línea al final de nuestro código será útil cuando decida regresar y hacer referencia a los datos recopilados.

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

Una vez más, el proceso seguido en este tutorial sin duda se aplicará a la mayoría de las demás páginas de Basketball-Reference.com . Hay cinco pasos simples que vale la pena tomar en cada caso.

  1. Identificar la URL de la página
  2. Descarga la Pagina
  3. identificar los elementos
  4. Pre-Limpieza y Extracto
  5. Archivo

El próximo paso en esta serie será usar estos datos para obtener información sobre el potencial de los futuros jugadores. ¡Así que estén atentos a futuras entregas!