<jupyter_start><jupyter_text>Simulación de los Resultados de las Eliminatorias usando PythonA falta de 2 fechas para que se termine de jugar la eliminatoria sudamericana para el mundial de Catar 2022, en los medios de comunicación se han barajado múltiples escenarios de las posibilidades que tiene Ecuador de llegar a la cita mundialista. Con eso en mente, desarrollé un tutorial donde se muestra como podemos realizar una simulación de los resultados de los partidos faltantes y analizar las posibilidades que tiene la tricolor de obtener un cupo directo o de llegar al repechaje.En este tutorial te mostraré como descargar la tabla de posiciones y el calendario de juegos restantes desde una página web, simular los resultados de los partidos y presentar los resultados finales. Descripción del ProcesoPara determinar que equipos clasficarán al mundial de Catar debemos considerar las posiciones en la tabla luego de jugadas las dos fechas finales. Por tanto, simularemos cada uno de los partidos faltantes y computaremos el puntaje final y el gol diferencia para definir las ubicaciones. Luego, repetiremos múltiples ocasiones las simulaciones para determinar la probabilidad final de cada selección.Este tutorial está dirigido a personas con conocimientos básicos o intermedios de Python. Puedes seguir el tutorial paso a paso descargando el Notebook directamente de Github. En caso de que tengas dudas, preguntas o sugerencias, puedes escribirme a <EMAIL>  Paquetes requeridosPara llevar a cabo nuestra simulación, necesitaremos herramientas para hacer web scrapping, simulaciones y análisis y visualización de datos.Los paquetes que utilizaremos son requests para solictar información a una página web, Beautiful Soup para procesar el contenido de la página web, pandas para la manipulación de datos, numpy para generar muestras aleatorias y matplotlib para las visualizaciones.<jupyter_code>import requests
from bs4 import BeautifulSoup
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline<jupyter_output><empty_output><jupyter_text>Descargar datos de internetEl primer paso es determinar el estado inicial de la cuestión en análisis, para después entender como las simulaciones modificarán la situación en el futuro. En nuestro ejemplo, queremos saber como se encuentra actualmente la tabla de posiciones, y simular como terminará despues de "jugadas" las fechas 17 y 18.Iniciaremos descargando la tabla actual de posiciones. Recomiendo buscar una página web que muestre los resultado de la manera más sencilla posible para facilitar el trabajo posterior. Luego de pocos minutos de navegación, encontré que la página Fútbol Peruano es precisamente lo que estamos buscando.Solicitaremos la información a la página web usando requests, procesaremos el texto usando beautiful soup, para encontrar la tabla de posiciones que está identificada con la etiqueta class : table table-sm mb-0. Esto último se puede determinar usando Inspección de Elemento o su similar en tu navegador web. Finalmente, leeremos los resultados en forma de un dataframe de pandas.<jupyter_code>url = 'https://www.futbolperuano.com/eliminatorias/tabla-de-posiciones'
page = requests.get(url)
soup = BeautifulSoup(page.text, 'lxml')
table = soup.find('table', attrs = {'class' : 'table table-sm mb-0'})
# Convertiremos la tabla en texto y dado que pd.read_html arroja como resultado una lista, tomaremos únicamente el primer elemento.
posiciones = pd.read_html(str(table))[0]
posiciones<jupyter_output><empty_output><jupyter_text>Como se aprecia, tenemos la misma información que consta en la página web. Sin embargo, los logos de las banderas de cada país fueron interpretados como texto, lo que generó que el nombre de cada país se duplique. A continuación, resolveremos ese inconveniente y eliminaremos las columnas que no usaremos en la simulación<jupyter_code>posiciones['Equipo'] = posiciones.Equipo.apply(lambda x : (x[:(len(x)//2)]))
posiciones.drop(['Pos', 'PJ', 'G', 'E', 'P', 'GF', 'GC'], axis=1, inplace=True)<jupyter_output><empty_output><jupyter_text>Lo que sigue es crear una lista de los partidos faltantes, que la obtendremos de la misma página web. Al desarrollar el tutorial, percibí que la información de los partidos estaba bastante más anidada que la tabla de posiciones, por lo que hizo falta crear una función para extraer el listado de países y organizarlo adecuadamente en función de la localía.<jupyter_code>juegos = 'https://www.futbolperuano.com/eliminatorias/resultados'
partidos = BeautifulSoup(requests.get(juegos).text, 'lxml')

def partidos_restantes(partidos):
    # identificador del calendario en la página web
    atr = 'score_contenido_TorneoTabs_collapse1_{}'
    todos = []
    # repetir el proceso para la fecha 17 y 18.
    for a in ['17', '18']:
        # buscar la información de la fecha
        fecha = partidos.find('div', attrs = {'id':atr.format(a)})
        # buscar el listado de paises
        fecha = fecha.findAll('span', attrs={'class':'team-name d-md-none'}, text=True)
        # filtrar los equipos locales usando indices pares
        locales = [x.text for x in fecha if fecha.index(x) in list(range(0, len(fecha), 2))]
        # los equipos visitantes tienen indices impares
        visitantes = [x.text for x in fecha if fecha.index(x) in list(range(1, len(fecha), 2))]
        # emparejar locales y visitantes.
        todos += list(zip(locales, visitantes))
    return todos<jupyter_output><empty_output><jupyter_text>Al ejecutar la función podemos ver la lista de partidos de las fechas 17 y 18<jupyter_code>todos = partidos_restantes(partidos)
todos<jupyter_output><empty_output><jupyter_text>Simular los resultados de los partidosPara determinar el ganador de cada encuentro, realizaremos un muestreo aleatorio de los posibles resultados. Para efectos demostrativos asignaremos la misma probabilidad a la ganancia del local o visitante y al empate (lo que correspondería a una distribución uniforme de probabilidades, tal como lanzar un dado y obtener cualquier valor entre 1 y 6). Esto puede ser facilmente modificado utilizando el argumento p de np.random.choice para asignar diferentes probabilidades a los equipos. Por ejemplo, usar un valor alto como probabilidad de ganancia para Argentina y Brasil, en vista de su desempeño en las eliminatorias. De igual manera, la cantidad de goles será aleatoria, siendo como mínimo 1 y como máximo 6 (el mayor número de goles anotados por un equipo en esta eliminatoria).Luego de conocer el resultado de la simulación de cada partido, modificaremos la tabla de posiciones sumando 3 puntos al ganador y tantos goles como corresponda. En caso de empate sumamos 1 punto a cada equipo.<jupyter_code>goles_max = np.arange(1, 6)
def clasificacion(cotejo, goles_max, posiciones):
    # 0 significa gana loca, 1 visitante y 2 empate. Cada resultado tiene sus puntos correspondientes.
    resultados = {0 : 3, 1 : 3, 2 : 1} 
    # Muestreo aleatorio. Escogemos un número entre 0, 1 y 2. El resultado es una lista, escogemos el primer elemento.
    simulacion = np.random.choice(np.array([0, 1, 2]), 1)[0]
    puntos = resultados[simulacion]
    # En caso de ganancia
    try:
        # cotejo es un emparejamiento (local, visitante), en ese orden.
        # Escojemos al ganador en función de la simulación.
        pais = cotejo[simulacion]
        # Sumamos puntos y goles en la tabla de clasificaciones.
        posiciones.loc[posiciones['Equipo'] == pais , 'Pts'] += puntos
        posiciones.loc[posiciones['Equipo'] == pais , 'DG'] += np.random.choice(goles_max, 1)[0]
    # En caso de empate, el indice será mayor que la cantidad de elementos en el par (local, visitante)
    except IndexError:
        # Sumamos 1 punto a cada equipo
        posiciones.loc[posiciones['Equipo'] == cotejo[0] , 'Pts'] += puntos
        posiciones.loc[posiciones['Equipo'] == cotejo[1] , 'Pts'] += puntos<jupyter_output><empty_output><jupyter_text>Toda vez que hemos simuladas las dos fechas restantes, corresponde evaluar la posición final de cada selección para determinar si obtuvieron un cupo a Catar. Para esto ordenaremos los equipos en forma descendiente (mayor a menor puntaje y gol diferencia) y obtendremos un listado de equipos y posiciones.<jupyter_code>def reporte(posiciones):
    posiciones.sort_values(by=['Pts', 'DG'], ascending=False, inplace=True)
    posiciones.set_index(pd.Index(np.arange(1, 11)), inplace=True)
    final = dict(zip(posiciones.Equipo, posiciones.index))
    return final<jupyter_output><empty_output><jupyter_text>Finalmente, repetiremos este proceso n veces (2500 en nuestro ejemplo) para observar la variabilidad de los resultados en una muestra mayor. En el computo final, contaremos cuantas veces un equipo terminó en una posición determinada (tabla de frecuencias). Es importante señalar que para el inicio de cada simulación debemos tomar como base la tabla actual de posiciones, es por eso que en cada iteración de la simulación, haremos una copia de la tabla original.<jupyter_code>l = []
for i in range(2500):
    df = posiciones.copy()
    for p in todos:
        clasificacion(p, goles_max, df)
    l.append(reporte(df))

ok = pd.DataFrame.from_records(l)

ok.apply(pd.value_counts)<jupyter_output><empty_output><jupyter_text>Podemos apreciar que Ecuador terminó en la 3ra posición 2224 veces en las 2500 simulaciones, y 276 en 4to. En ningún caso terminó en 5to puesto o más atrás. Es decir, lo más probable es que debamos hacer las maletas para CatarPara una mejor comprensión, expresaremos los resultados como porcentaje y remplazaremos los valores nulos por zeros.<jupyter_code>g = ok.apply(pd.value_counts, normalize=True)
g.set_index(pd.Index(list(range(1,11))), inplace=True)
g.fillna(0, inplace=True)
g = g.apply(lambda x: x*100)
g<jupyter_output><empty_output><jupyter_text>Resulta más didáctico apreciar los resultados en forma gráfica. Seleccionamos únicamente los países que junto a Ecuador disputan el 3ro, 4to y 5to puesto.<jupyter_code>plt.style.use('fivethirtyeight')
g.loc[[3, 4, 5], ['Ecuador', 'Uruguay', 'Perú', 'Chile', 'Colombia']].plot.bar(rot=0, 
                                                                               figsize=(4, 5), 
                                                                               xlabel='Posición final', 
                                                                               ylabel='Probabilidad %')
with plt.xkcd():
    g.loc[[3, 4, 5], ['Ecuador', 'Uruguay', 'Perú', 'Chile', 'Colombia']].plot.bar(rot=0, 
                                                                               figsize=(4, 5), 
                                                                               xlabel='Posición final', 
                                                                               ylabel='Probabilidad %')<jupyter_output><empty_output>