Python Intermedio para Fútbol

Módulo 2: La táctica del analista - Filtrado y consultas

La lupa del analista: encontrando la jugada clave

En el módulo anterior, aprendimos a cargar un set de datos completo en un DataFrame de Pandas. Es el equivalente a tener la grabación del partido entero. Pero un analista no vuelve a ver los 90 minutos; se enfoca en los momentos cruciales. Nuestra labor ahora es aprender a usar la “lupa” para aislar esas jugadas clave dentro de nuestro mar de datos.

Esta habilidad se llama filtrado, y es, sin lugar a dudas, la técnica que usarás con más frecuencia en tu día a día. El filtrado de datos con Pandas nos permite pasar de tener miles de eventos a enfocarnos solo en los que responden a nuestra pregunta: los tiros de un jugador, los pases en el último tercio del campo, o las recuperaciones de un equipo en un período de tiempo.

El concepto clave: las máscaras booleanas

Para filtrar datos en Pandas, no le decimos directamente “dame los tiros”. En su lugar, adoptamos un enfoque más inteligente. Realizamos una pregunta a nuestra columna de datos que solo puede tener dos respuestas posibles: True (verdadero) o False (falso).

Por ejemplo, recorremos cada evento de nuestro DataFrame y preguntamos: “¿El tipo de este evento es igual a ‘Shot’?”. Pandas evalúa esta pregunta para cada una de las miles de filas y nos devuelve una nueva columna (una Serie de Pandas) que contiene únicamente valores True o False.

Esta serie de verdaderos y falsos se conoce como una máscara booleana.

Imagina que tienes una plantilla de cartón con agujeros recortados (un “stencil”). Si colocas esta plantilla sobre una lista de jugadores, solo podrás ver los nombres que coinciden con los agujeros. Una máscara booleana funciona exactamente igual: es una plantilla que, al aplicarla a nuestro DataFrame, solo deja pasar las filas donde el valor es True, ocultando todas las demás.

Entender este concepto es fundamental. No estamos buscando datos directamente; estamos creando una “plantilla” de condiciones que los datos deben cumplir. Una vez que tenemos la plantilla correcta, aplicarla para obtener el resultado es un paso muy sencillo.

Tu primer filtro: aislando un tipo de evento

Vamos a poner en práctica la teoría. Nuestro primer objetivo será cargar los datos de un partido y aislar todos los eventos que fueron un tiro a puerta. Este es uno de los filtros más comunes y útiles en el análisis de partidos.

Primero, en tu Jupyter Notebook, asegúrate de tener un DataFrame con los datos de un partido. Si no lo tienes, puedes cargarlo usando la librería underdata, como vimos en el curso anterior.

import pandas as pd
from underdata import Understat

# Creamos una instancia para la Premier League
epl = Understat(league="EPL")

# Obtenemos los datos de un partido de ejemplo
# Nota: el match_id puede variar, usamos uno para ilustrar el proceso.
match_data = epl.get_match_data(match_id=23722)

Ahora que match_data contiene todos los eventos del partido, vamos a aplicar nuestro filtro en dos pasos:

Paso 1: Crear la máscara booleana

Realizamos la pregunta a la columna result: “¿El valor de esta fila es igual a ‘Shot’?”. El operador para “es igual a” en Python es == (dos signos de igual).

# Le pedimos a Pandas que nos diga dónde la columna 'result' es exactamente 'Shot'
mascara_tiros = match_data['result'] == 'Shot'

# Para ver cómo se ve la máscara, puedes imprimir las primeras 5 filas
print(mascara_tiros.head())

La salida de ese print será una Serie de Pandas que se ve algo así, mostrando True o False para cada una de las primeras cinco filas del DataFrame original:

0    False
1    False
2    True
3    False
4    False
Name: result, dtype: bool

Paso 2: Aplicar la máscara al DataFrame

Una vez que tenemos nuestra “plantilla”, se la pasamos a nuestro DataFrame original usando corchetes []. Pandas entenderá que solo queremos las filas donde la máscara tiene el valor True.

# Aplicamos la máscara para obtener un nuevo DataFrame solo con los tiros
tiros = match_data[mascara_tiros]

# Exploremos el resultado para confirmar que funcionó
print(f"Total de eventos en el partido: {len(match_data)}")
print(f"Total de tiros encontrados: {len(tiros)}")

# Mostramos las primeras filas de nuestro nuevo DataFrame de tiros
tiros.head()

¡Listo! La variable tiros ahora contiene un DataFrame limpio con todos los tiros del partido. Puedes inspeccionarlo con tiros.head() y verás que en la columna result, todos los valores son Shot.

Has ejecutado tu primer filtro exitosamente. En la siguiente sección, aprenderemos a combinar múltiples condiciones para hacer análisis más complejos y responder preguntas tácticas más específicas.

Combinando condiciones: el análisis táctico empieza aquí

El verdadero poder del filtrado se desata cuando empezamos a hacer preguntas más complejas. Rara vez un análisis se basa en una sola condición. Lo que realmente buscamos son patrones que surgen al combinar varios criterios. Por ejemplo, no nos interesan todos los tiros, sino los tiros de un jugador específico que, además, fueron con la pierna izquierda.

Para lograr esto en Pandas, combinamos nuestras máscaras booleanas usando los operadores lógicos & (que significa “Y”) y | (que significa “O”).

La regla de oro: cuando combinas varias condiciones, cada una de ellas debe ir entre paréntesis (). Esto asegura que Python las evalúe en el orden correcto.

Vamos a aplicarlo. Nuestro objetivo será encontrar todos los tiros realizados por Mohamed Salah durante el partido.

# Asumimos que 'match_data' ya está cargado

# 1. Creamos la primera máscara: solo los eventos que son tiros
mascara_tiros = match_data['result'] == 'Shot'

# 2. Creamos la segunda máscara: solo los eventos del jugador 'Mohamed Salah'
mascara_salah = match_data['player'] == 'Mohamed Salah'

# 3. Combinamos ambas máscaras usando el operador '&' (Y)
# La sintaxis es: df[(condicion_1) & (condicion_2)]
tiros_de_salah = match_data[mascara_tiros & mascara_salah]

# 4. Exploramos el resultado
print(f"Total de tiros de Mohamed Salah: {len(tiros_de_salah)}")

# Mostramos columnas relevantes para verificar
tiros_de_salah[['player', 'xG', 'shotType']].head()

Como puedes ver, el proceso es muy lógico. Creamos una “plantilla” para cada condición que nos interesa y luego las superponemos para obtener un filtro mucho más específico.

Vamos a subir un poco la complejidad. ¿Qué tal si queremos encontrar todos los pases (Pass) que no fueron hechos por el portero, Alisson Becker? Aquí usaríamos el operador !=, que significa “no es igual a”.

# Creamos la máscara para los pases
mascara_pases = match_data['result'] == 'Pass'

# Creamos la máscara para excluir al portero
mascara_no_alisson = match_data['player'] != 'Alisson Becker'

# Combinamos y aplicamos el filtro
pases_de_campo = match_data[mascara_pases & mascara_no_alisson]

print(f"Total de pases de jugadores de campo: {len(pases_de_campo)}")

Esta habilidad para combinar condiciones es el pilar del análisis de datos exploratorio. Te permite segmentar, aislar y comparar cualquier subconjunto de datos que puedas imaginar, abriendo la puerta a insights tácticos mucho más profundos. En la siguiente sección, exploraremos un par de técnicas de filtrado más avanzadas que te harán aún más eficiente.

Técnicas de filtrado avanzadas

Ya dominas el filtrado por una o varias condiciones exactas. Ahora, vamos a añadir dos herramientas a tu arsenal que te permitirán realizar consultas más complejas y eficientes. Estas técnicas son extremadamente comunes en el día a día de un analista.

Filtrando por una lista de valores con .isin()

A menudo, no queremos filtrar por un solo jugador, sino por un grupo. Por ejemplo, ¿cómo obtenemos todos los tiros de nuestros delanteros principales? Podríamos escribir una condición larga con múltiples | (o), pero hay una forma mucho más limpia: el método .isin().

.isin() nos permite crear una máscara booleana que es True para todas las filas donde el valor de una columna se encuentra dentro de una lista que le proporcionamos.

Caso práctico: Vamos a encontrar todos los tiros de los delanteros clave del Liverpool, como Mohamed Salah y Darwin Núñez.

# Asumimos que 'match_data' y el DataFrame 'tiros' ya están cargados

# 1. Definimos nuestra lista de jugadores de interés
delanteros_clave = ["Mohamed Salah", "Darwin Núñez"]

# 2. Creamos la máscara usando .isin() sobre el DataFrame que ya contiene solo los tiros
mascara_delanteros_clave = tiros['player'].isin(delanteros_clave)

# 3. Aplicamos la máscara
tiros_de_delanteros_clave = tiros[mascara_delanteros_clave]

print(f"Total de tiros de Salah y Núñez: {len(tiros_de_delanteros_clave)}")
tiros_de_delanteros_clave[['player', 'xG', 'shotType']].head()

Este método es increíblemente útil y legible, especialmente cuando tu lista de interés contiene muchos elementos.

Filtrando por texto parcial con .str.contains()

¿Qué pasa si queremos encontrar todos los eventos que terminaron en gol, pero en nuestros datos algunos se registran como 'Goal' y otros como 'OwnGoal' (gol en propia puerta)? Una condición == 'Goal' no encontraría los goles en propia.

Para resolver esto, podemos filtrar por filas que contengan una palabra o un patrón de texto, en lugar de una coincidencia exacta. Esto se logra accediendo a los métodos de string de una columna a través de .str.

Caso práctico: Vamos a encontrar todos los eventos que resultaron en cualquier tipo de gol.

# 1. Creamos la máscara usando .str.contains()
# Buscamos la palabra 'Goal' en la columna 'result'
# na=False es una buena práctica para manejar posibles valores nulos y evitar errores
mascara_goles = match_data['result'].str.contains('Goal', na=False)

# 2. Aplicamos la máscara
goles = match_data[mascara_goles]

print(f"Total de goles en el partido: {len(goles)}")

# Mostramos columnas relevantes de los goles encontrados
goles[['player', 'result', 'h_a']].head()

Estas dos técnicas, .isin() y .str.contains(), expanden enormemente tu capacidad para segmentar datos de formas complejas, permitiéndote pasar de preguntas simples a un análisis táctico mucho más rico y detallado.

El siguiente paso en tu formación

Has completado uno de los módulos más importantes de este curso. La habilidad para filtrar y consultar datos es, en muchos sentidos, el corazón del análisis de datos. Has aprendido a pasar de tener un océano de información, como son los datos de un partido completo, a aislar las gotas de agua que realmente importan para responder preguntas tácticas.

Dominar las máscaras booleanas, combinar condiciones con & y |, y utilizar métodos eficientes como .isin() y .str.contains() te da un control casi total sobre tus datos. Ya no eres un espectador pasivo de la información; ahora puedes interrogarla, segmentarla y forzarla a revelar los patrones que se esconden en ella.

Sin embargo, a veces, la información que necesitamos no está explícitamente en los datos que recibimos. A veces, tenemos que crearla.

En el próximo módulo, daremos el siguiente paso lógico en nuestro viaje como analistas: aprenderemos a realizar ingeniería de características. Dejaremos de solo seleccionar datos para empezar a crear nuevas métricas y columnas que enriquezcan nuestro análisis y nos permitan descubrir insights que, hasta ahora, permanecían ocultos.