Análisis Liga Santander 2019/2020 con Qlik Sense

11/05/2020 – by Pedro Mas

Introducción

A todos los aficionados al deporte y al fútbol ⚽️ se nos ha pasado por la cabeza alguna vez lo mucho que nos gustaría tener todas las estadísticas de una liga o competición a nuestro alcance para, más allá de consultarlas, llevar a cabo nuestras propias investigaciones como, por ejemplo, analizar el rendimiento de nuestro jugador favorito o estudiar la correlación entre la precisión en los pases y los puntos conseguidos por equipo.

Existen multitud de sitios web en los que podemos consultar la plantilla de los diferentes equipos de una liga, las estadísticas de cada jugador… pero ¿cómo unificar un contenido que se encuentra segmentado en cientos de URL distintas?, ¿cómo transformar todos estos datos en información de la que extraer conocimiento?

Guiado por una pasión hacia el fútbol y los datos de la que me declaro culpable ☝️ , voy a mostraros un ejemplo práctico con la Liga Santander 2019/2020 que llevé a cabo hace unos días con Qlik Sense.

Fuente de Datos y estrategia de extracción y modelado

Dentro del universo de páginas web que ofrecen estadísticas de la Liga Santander, para este proyecto hemos seleccionado el sitio web de Superdeporte. Tras investigar y bucear un tiempo por este portal, nos encontramos con que la información que nos interesa para nuestro estudio está organizada en tres niveles:

  • ⚽️ Nivel 1: https://www.superdeporte.es/deportes/futbol/primera-division/estadisticas/, en el que podremos encontrar la clasificación actualizada de la Liga Santander.
  • ⚽️ Nivel 2: https://www.superdeporte.es/deportes/futbol/primera-division/[NombreEquipo]/, en el que podremos encontrar la plantilla de cada equipo y la información principal de cada jugador.
  • ⚽️ Nivel 3: https://www.superdeporte.es/deportes/futbol/primera-division/[NombreEquipo]/[NombreJugador].html, en el que podremos encontrar las estadísticas individuales de cada jugador.

*Por ejemplo:  https://www.superdeporte.es/deportes/futbol/primera-division/real-madrid/toni-kroos.html te lleva a las estadísticas de Toni Kross.

Por lo tanto, para cada URL de nivel 2 tendremos N URL de nivel 3, siendo N el número de integrantes de la plantilla de cada URL de nivel 2 (equipo).  Este tipo de procesos en los que se extrae información de múltiples páginas web se suele denominar Web Scraping y, como veremos a lo largo de este artículo, suele incluir una combinación de variables y bucles de tipo for.

Una vez entendida la forma en la que están organizados los datos en nuestra fuente, estamos ya en condiciones de pensar en la estrategia que seguiremos para llevar a cabo su extracción. ¡Dedicad unos minutos a pensar cómo lo haríais vosotros!

La estrategia que seguiremos para extraer los datos será la siguiente:

  • Del nivel 1 obtendremos el nombre de todos los equipos.
  • Una vez tengamos el nombre de todos los equipos, construiremos un bucle que recorra todas las URL de nivel 2 (una por equipo) y nos proporcione la plantilla de cada equipo.
  • Una vez tengamos el nombre de todos los equipos y jugadores, construiremos un bucle que recorra, para cada URL de nivel 2, todas las URL de nivel 3 (una por jugador) y nos proporcione las estadísticas individuales de cada jugador.
  • Imagen 1: Esquema de la estrategia de extracción de los datos

    Un punto muy importante es que, en las URL, todos los nombres deben incluirse en un formato característico (sin mayúsculas ni caracteres especiales y sustituyendo espacios por guiones ‘-‘), por lo que cuando extraigamos estos nombres deberemos aplicarles este formato. En Qlik Sense, la forma más eficiente de llevar a cabo este formateo es a través de Mapping Tables o tablas de correspondencia, que se almacenan temporalmente en la memoria y se eliminan automáticamente tras la ejecución del script. En ellas, debemos definir los valores que queremos sustituir en la primera columna y los valores deseados en la segunda.

    Por último, para cargar datos directamente desde una URL, es necesario activar el Legacy Mode de Qlik Sense. Para ello, tenemos que abrir el archivo C:\Users\{user}\Documents\Qlik\Sense\Setting.ini en un editor de texto y cambiar StandardReload = 1 por StandardReload = 0.

    Una vez tenemos la estrategia bien definida, ¡es hora de ponerse con el código!

    Cargando Clasificación y Plantillas

    La carga de la clasificación no tendrá ninguna complicación, simplemente cargaremos en Qlik Sense la tabla correspondiente a la clasificación en la URL del nivel 1. Aunque estemos extrayendo los datos directamente de una URL, es altamente recomendable crear una conexión de tipo “Archivo Web a la dirección web, de forma que podamos explorar todas las tablas presentes en dicha dirección a través de la opción “Seleccionar datos”. Como hemos comentado anteriormente, es necesario aplicar un formateo al nombre de los equipos. Esto se lleva a cabo en la línea 4, en la que utilizamos la función MapSubString() para aplicar la Mapping Table, que hemos denominado “Names_Mapping”, sobre el campo que contiene el nombre de los equipos.

    Imagen 2: Carga de la Clasificación y plantillas

    Ahora que ya tenemos todos los nombres de los equipos correctamente formateados almacenados en el campo “team”, el siguiente paso es almacenar en una tabla la plantilla de todos los equipos de la Liga Santander. Aquí es donde comienzan a entrar en juego las técnicas de Web Scraping. En la Liga Santander participan 20 equipos, por lo que para extraer la plantilla de todos ellos tenemos que acceder a 20 URL diferentes. Sería posible hacer esto definiendo 20 cargas de datos diferentes, pero ¿y si fueran 100 URL? ¿y si fueran 1.000? Vamos a ver cómo se puede automatizar este proceso.

    Como hemos visto en la sección anterior, la URL de los distintos equipos (nivel 2) varía únicamente en el nombre del equipo, por lo que vamos a construir un bucle que en cada iteración vaya cambiando el nombre del equipo de la URL de la que se realiza la carga de los datos. La forma de hacer esto en Qlik Sense es a través de una sentencia de control for each … next (líneas 19-36 de la Imagen 2). Esta construcción lo que hace es ejecutar todas las sentencias incluidas entre for each y next para cada valor existente en una lista. Así, definimos la variable vEquipo, que adquirirá un nuevo valor de la lista (FieldValueList(‘team’) crea una lista con todos los valores del campo team) para cada ejecución del bucle, e incluiremos esta variable en la URL de la que se cargan los datos en cada iteración, de forma que el bucle cargue la plantilla de un equipo diferente en cada iteración hasta que recorra todo el espacio de valores del campo team. Los datos cargados se irán concatenando en una misma tabla, ya que Qlik concatena por defecto las cargas de datos que tienen una misma estructura. Para cada jugador crearemos también una clave team-player que identificará de forma única a cada jugador (dos jugadores pueden tener mismo nombre y apellido). Por último, la claúsula next le dice al bucle que ya puede pasar a la siguiente iteración.

    Imagen 3: Tabla Players una vez cargada

    Cargando las Estadísticas de los Jugadores

    Para cargar las estadísticas de cada jugador, construiremos un bucle anidado, con un bucle for each … next exterior que itere entre todos los equipos y un bucle for each … next interior que recorra todos los jugadores incluidos en la plantilla de cada equipo. La Imagen 4 muestra este bucle anidado, en el que se carga la plantilla correspondiente a cada equipo para poder así iterar entre todos los jugadores.

    Un problema con el que me encontré al realizar las primeras cargas, fue que no todos los jugadores tenían creada una página web con sus estadísticas, por lo que carga de datos daba error al no encontrar la URL y se interrumpía. Este problema se solventa con la sentencia que podéis ver en la línea 1 de la Imagen 4, set errormode = 0, que establece que la carga de datos no se interrumpa al encontrar un error y siga adelante. Es importante borrar la tabla temporal al final de cada iteración del bucle exterior, y reestablecer set errormode = 1 una vez finalizado el bucle anidado.

    Imagen 4: Bucle anidado

    Si le echamos un vistazo a la página de un jugador, por ejemplo Toni Kroos, veremos como las estadísticas están divididas en varias tablas diferentes, por lo que en el proceso de carga iremos concatenando todas las estadísticas en una tabla maestra (Imagen 5). A cada atributo o categoría le añadiremos un prefijo que indique su tabla de origen en la URL, y con la función Match() definiremos las categorías que deseemos cargar de cada tabla. Para definir la URL de carga, utilizaremos el método descrito en la Sección anterior.

    Imagen 5: Carga de estadísticas

    Una vez finalizada esta carga, tendremos una tabla (STATS_TEMP) formada por un campo llave (jugador), un campo de atributo (categoría) y un campo de datos con el valor correspondiente a cada combinación jugador – categoría, lo que no resulta nada eficiente para el tipo de estudio que queremos realizar, por lo que vamos a ver cómo podemos formatear la tabla para tener únicamente un registro (fila) por jugador.

    Lo que necesitamos es pivotar los datos de forma que a cada valor del campo de atributo le corresponda una columna y transformar el campo de datos en los datos de columna. Aunque no hay ninguna función concreta para realizar esta operación en Qlik Sense, existen varias formas de llevarla a cabo.

    Primero, vamos a crear una tabla (Players_Keys) con todas las claves distintas de los jugadores (Imagen 6). Luego, construiremos un nuevo bucle for each … next que recorra todos los valores distintos del campo de atributo. En cada iteración, haremos un JOIN para añadir una nueva columna a la tabla Players_Keys con el atributo como nombre de columna y el valor del campo de datos correspondiente a dicho atributo como dato de columna. De esta forma, tendremos una nueva tabla con un registro (fila) por jugador y con tantas columnas como atributos (categorías) hayamos cargado previamente. Por último, no os olvidéis de eliminar la tabla despivotada.

    Imagen 6: Transformación realizada para pivotar la tabla STATS_TEMP

    Ahora, ya tenemos nuestra tabla objetivo con las estadísticas de cada jugador, pero fijaos que el nombre de cada columna será el nombre de cada atributo tal y como está escrito en la URL, precedido por un prefijo que indica la tabla de origen en la URL, ya que así lo hemos definido en la Imagen 5. Por lo tanto, nuestro último paso será cargar todos los campos de la tabla (Player_Keys) otorgándole a cada campo el nombre que nos plazca (acordaos de eliminarla luego).

    ¡Genial! Ya hemos conseguido extraer las estadísticas individuales y almacenarlas de una forma eficiente para efectuar un buen análisis. Sin embargo, antes de proceder al análisis, hay que tener claro y definir las métricas que vamos a necesitar, ya que normalmente suele ser más útil y eficiente definirlas en el Script.

    Creando métricas

    Vamos a definir nuestras métricas en el último proceso de carga, donde hemos renombrado todos los campos de la tabla final. Podemos crear cualquier métrica que nos interese medir en nuestro estudio. Por ejemplo, nosotros vamos a analizar cuáles son los 25 mejores atacantes de la Liga Santander, pero además de los goles conseguidos queremos tener en cuenta también la precisión y la efectividad. Para ello crearemos una métrica que le otorgue un 60% de peso a los goles marcados, un 20% a la precisión (goles/remate), un 15% a la efectividad (goles/partidos jugados) y un 5% a las asistencias conseguidas (Imagen 7). También almacenaremos los ratios de efectividad y precisión comentados.

    Imagen 7: Creación de métricas en la tabla STATS_AUX

    Para que nuestra visualización de datos sea más eficaz e intuitiva, vamos a normalizar la métrica que hemos creado al intervalo [0,100], y vamos a crear también un ranking de mejores atacantes según esta métrica para que posteriormente nos sea más sencillo filtrar en las visualizaciones. Primero, vamos a cargar en una tabla todos los valores de la métrica calculada ordenados de mayor a menor. Con la función Peek() almacenamos en una variable (vTopStriker) el valor máximo de esta métrica y borramos la tabla creada. Luego, cargamos otra vez todos los valores de la tabla de estadísticas añadiendo uno nuevo con la métrica normalizada (metric_topstriker). Una forma muy sencilla y eficaz de crear rankings es ordenando la tabla por el campo que queremos rankear de mayor a menor, y añadir un nuevo campo con el número de fila a través de la función RowNo(). Con la tabla final ya cargada, podemos eliminar las anteriores tablas auxiliares.

    Imagen 8: Normalización de la métrica y creación de Ranking

    Con los datos estructurados de la forma correcta y con nuestra métrica creada, ya estamos preparados para proceder con el análisis. ¡Vamos a ver un par de visualizaciones!

    Imagen 9: Pre visualización de la tabla final con las estadísticas de los jugadores

    Visualizando los datos

    Para empezar, veamos cómo podemos visualizar el ranking de los mejores 25 atacantes según la métrica que creamos en la sección anterior (si encontráis alguna métrica en la que Messi no destaque por mucho, por favor compartidla con este corazón madridista). Para ello, vamos a crear una medida maestra definida por la siguiente expresión (proporciona la métrica para aquellos jugadores cuyo puesto en el ranking sea igual o menor que 25):

    =sum({<topstriker_ranking = {“<= 25”}>} metric_topstriker)

    En general, la forma más intuitiva de visualizar un ranking es a través de un diagrama de barras. En este caso, hemos elegido un diagrama de barras horizontal (si el número de agrupaciones os lo permite, es buena práctica ponerlo en vertical) para que sean visibles los 25 jugadores, y hemos codificado el color de cada jugador en función de su “puntuación” en nuestra métrica. A la hora de representar datos nominales, acordaos siempre de ordenar las categorías de mayor a menor para facilitar la interpretación del gráfico (no aplicable al trabajar con datos ordinales porque las categorías gozan ya de una jerarquía intrínseca). Por otro lado, intentad usar siempre paletas de colores pastel para garantizar una mejor experiencia de usuario.

    Imagen 10: Ranking de los 25 mejores atacantes según nuestra propia métrica

    Vamos a estudiar ahora la relación entre dos variables cuantitativas, el ratio de pases buenos de un equipo (pases buenos / pases totales) y su número de goles a favor. Al estudiar dos variables independientes, como en este caso, podemos estudiar el grado de correlación entre ambas variables a través de un diagrama de dispersión. Como muestra este diagrama (Imagen 11), parece haber cierta correlación positiva entre ambas variables (curioso el número considerable de goles que ha conseguido el Getafe con el peor porcentaje de pases acertados con diferencia). Tened en cuenta que el estudio visual no es suficiente para determinar si existe una correlación, para ello tendríamos que realizar un análisis estadístico, calculando el coeficiente de correlación y realizando un test de hipótesis para ver si dicho coeficiente es estadísticamente significativo. Es muy importante tener claro que la correlación no implica causalidad (la causalidad es un juicio de valor que requiere de más información), ya que la correlación podría ser totalmente casual o podría existir una tercera variable que hace que nuestras variables de estudio se modifiquen a la vez.

    Es muy importante en los diagramas de dispersión etiquetar los puntos, ya que esto facilita enormemente la interpretación del gráfico. En casos como este, en el que los valores dimensionales son demasiado largos, es una buena práctica abreviarlos. Fijaos como, sin embargo, hay varios nombres de equipos diferentes cuya primera palabra es ‘Real’, por lo que estos nombres no podrán ser abreviados para una correcta identificación. Una forma de solventar este problema es crear la dimensión (o crear un nuevo campo de dimensión en el script) de la siguiente forma:

    =if(left(team_name,4) = ‘Real’, team_name, left(team_name,3))

    Imagen 11: Diagrama de dispersión

    Por último, me pareció interesante tratar de estudiar como influían diversas medidas en la clasificación de los equipos, ¿tiene alguna influencia la edad media de un equipo en su posición? ¿los equipos que “dan más palos” están más abajo o más arriba de la clasificación? Para ello, se me ocurrió representar visualmente la clasificación mediante un gráfico de dispersión y hacer que el color y tamaño de los puntos variara según la medida, pero de una forma que fuese posible cambiar de forma rápida entre unas medidas y otras.

    Esto es posible a través de la creación de una “tabla isla” en el modelo, es decir, una tabla que no tiene relación alguna con las otras tablas del modelo y que únicamente utilizaremos para poder filtrar entre diferentes campos dentro de una misma visualización.

    Imagen 12: Creación de tabla isla

    La Imagen 12 muestra cómo crear una tabla isla. En ella, tenemos que definir un nuevo campo (Size) que incluya el nombre de todos los campos entre los que vamos a querer seleccionar dentro de la visualización. En nuestro caso, dependiendo del campo vamos a querer aplicar una medida diferente (por ejemplo, nos interesa sumar el número de goles, pero promediar la edad), por lo que vamos a crear otra columna en la tabla isla (size_num) para identificar los campos que vayamos a querer sumar o promediar. Ahora viene lo importante, ¿cómo conectamos nuestra visualización con esta tabla isla? Lo haremos creando la siguiente medida maestra:

    =Pick(size_num, sum($(=GetFieldSelections(Size))), Avg($(=GetFieldSelections(Size))))

    Vamos por partes. Primero, la función GetFieldSelections() devuelve una cadena con las selecciones actuales de un campo (GetFieldSelections(Size) nos devuelve la selección actual del campo Size), y la expansión $() evalúa la expresión que introduzcamos dentro.

    Luego, a través de la función Pick() podemos indicar la medida que queremos aplicar sobre el campo seleccionado gracias al identificador numérico que hemos incluido en la tabla isla. Junto a la visualización, incluiremos un panel de filtrado con el campo (Size) de selección que hemos creado en la tabla isla. Asociando el color y tamaño de las burbujas a esta medida maestra, dichas propiedades variarán en función del campo que seleccionemos en el panel de filtrado. Aunque no es necesario, para una mejor experiencia de usuario es recomendable activar la opción Siempre un valor seleccionado para el campo de selección, ya que nos interesa que siempre haya un único valor seleccionado en este campo.

    Para representar visualmente la clasificación, hemos utilizado un diagrama de dispersión con la posición en el eje X y los puntos en el eje Y. Como podéis ver en las Imágenes 13 y 14, hemos revertido el X para que el sentido sea creciente en ambos ejes (menor posición implica más puntos). Para ello hemos introducido el valor negativo de la posición en el eje X y hemos aplicado el siguiente formato numérico personalizado: #,##;#,## Simplemente hemos eliminado el signo menos del formato correspondiente a los números negativos (después del punto y coma).

    Vamos a ver dos ejemplos. En la Imagen 13, hemos fijado el campo de selección en edad, de forma que el tamaño y el color de las burbujas está codificado según la edad media de los equipos. Igual alguien debería decirle al Eibar que tire un poco más de cantera, o al Celta que un par de viejas leyendas siempre vienen bien en cualquier vestuario.

    Imagen 13: Relación entre la clasificación y la edad de los equipos

    En este otro ejemplo (Imagen 14) hemos fijado el campo de selección en faltas cometidas, de forma que el tamaño y el color de las burbujas está codificado según el número total de faltas cometidas por el equipo. Podemos ver que, si te enfrentas al Getafe con la idea de tener posesiones largas, igual no te vuelves muy contento a casa, mientras que seguramente el Valencia te deje jugar más con el balón.

    Imagen 14: Relación entre la clasificación y el número de faltas cometidas de los equipos

    Cierre

    En este artículo, hemos tratado de guiaros a lo largo de todo el proceso de extracción, manipulación y visualización de datos a la hora de enfocar un proyecto de Web Scraping con Qlik Sense, una de las mejores herramientas de Data Discovery gracias a su modelo asociativo. Hemos visto algunos de los problemas que te pueden surgir en un estudio de estas características y cómo enfocarlos, además de un par de consejos y trucos para que vuestras visualizaciones impacten visualmente y aporten valor a vuestro análisis.

    ¡Esperamos que hayáis disfrutado y que os hayan surgido ideas para aplicar a vuestros propios proyectos! 😊

    Si os surge alguna duda no dudéis en contactarme a través de linkedin pinchando aquí.