Serie Nacional de Cuba III: Matriz de Expectativa de Carrera

Introducción

agradecer el apoyo del Lic. Alberto Salazar

La matriz de expectativa de carrera presenta el número esperado de carreras anotadas entre un punto dado y el final de una entrada basado en el número de outs, la ubicación de los corredores y la cantidad de carreras anotadas de forma general.  De la información obtenida en los dos primeros post de esta serie debemos extraer los eventos/sucesos de los partidos, conocer el estado base-out de cada jugada, así como las veces en que se repitió ese estado y cuántas carreras se anotaron a partir de ahí hasta el final de la entrada.

Los datos con los que trabajaremos tienen múltiples variables que explican el entorno en el que se llevó a cabo cada jugada, sin embargo, la mayor parte de la información útil está expresada en lenguaje natural dentro de la variable evento. Por ejemplo, así se ve la parte alta de la primera entrada de un juego en nuestros datos:

Para obtener la expectativa de carrera, el reto está en extraer de la variable evento la siguiente información:

  • El número de outs registrados antes de la jugada.
  • El número de outs efectuados durante la jugada.
  • El número de outs efectuados después de la jugada.
  • Los corredores en base al iniciar la jugada.
  • Los corredores en base al terminar la jugada.
  • Las carreras anotadas antes de la jugada.
  • Las carreras anotadas durante la jugada.
  • Las carreras anotadas después de la jugada.
  • Las carreras anotadas durante la mitad inning.

Luego de analizar las jugadas expresadas en la variable evento identificamos que estas siguen un patrón según el tipo de jugada. Por ejemplo, llamémosle jugadas ofensivas a aquellas que deciden el turno al bate de un jugador, en estas, siempre se comienza expresando el nombre del jugador al bate, el resultado final del turno ofensivo, el tipo de conexión y la dirección a donde fue la pelota en caso de hacer contacto. De forma general todas las jugadas relatadas en la variable entorno siguen algún patrón de acuerdo al evento que la inicia.

Nuestra Solución: ANTLR (ANother Tool for Language Recognition)

ANTLR es un potente generador de analizadores sintácticos. Un analizador toma un fragmento de texto y lo transforma en una estructura organizada, como un árbol de sintaxis abstracta (AST). La idea general es usar ANTLR para analizar una jugada e identificar a qué evento pertenece (base por bolas, robo de base, hit, home run, etc…) para traducirla a un formato en el que podamos calcular la expectativa de carrera.

El primer paso es crear una gramática para representar una jugada. Las gramáticas consisten en un conjunto de reglas que describen la sintaxis del lenguaje. Hay reglas para la estructura sintáctica, así como reglas para los símbolos de vocabulario (tokens) como identificadores y números enteros.

SO: 'Poncha';       
ONBASE: 'embasa por';
STEAL: 'roba';       
CS: 'cogido robando';       
BAT: 'bateando';    
SCORE: 'anota'|'Anota';
HIT: 'batea';
CONECTA: 'conecta';       
             
PRIMERA: 'Primera Base'; 
SEGUNDA: 'Segunda Base';   
TERCERA: 'Tercera Base';   
HOME: 'Home';

Se puede apreciar que los tokens no son más que las palabras claves en cada jugada y la presencia de una o varias de ellas en el texto de la jugada indica la ocurrencia de un evento determinado. Una vez identificadas las palabras claves es necesario escribir las reglas, es decir, la estructura en que aparecen en el texto los tokens. Por ejemplo, si la jugada es: “H. Bravo batea Sencillo de Fly al jardín central”, los tokens son “batea”, “Sencillo”, “Fly” y “jardín central” identificando la acción, el tipo de hit, el tipo de conexión y la dirección respectivamente, mientras que la regla define el orden en que aparecen. 

STRING : ('a'..'z'|'A'..'Z')+;       
INT :   [0-9]+;       
FLOAT: INT POINT INT;   
WS : [ \t\r\n]+ -> channel(HIDDEN);       
                    
name: STRING POINT STRING STRING*; 
stat: STRING COLON (FLOAT|INT) COMA*;
act: stat+;       
plays : play+;    
play : name GET recibeType POINT
    |   name GET recibeType 'de error' POINT 
    |   name SE SO POINT       
    |   name SE SO 'de error' endlineType*       
    |   name SE SO GETFIRST 'por WP o FCH' POINT     
    |   name SE SO GETFIRST 'por Pass Ball o Error probabilidad de Out' POINT;

Una vez terminada la gramática ANTLR nos permite generar un lexer y un parser mediante la línea de comandos para varios lenguajes (Python en nuestro caso)

antlr-4.8-complete.jar -Dlanguage=Python3 Baseball2020.g4

Esto crea varios ficheros: Baseball2020Lexer, Baseball2020Parser y Baseball2020Listener así como otros archivos relacionados con los tokens. El lexer y parser manejarán la mayor parte del reconocimiento de sintaxis y el orden del análisis por nosotros, pero en realidad necesitamos crear un AST para poder extraer la información que necesitamos. Aquí es donde entra Baseball2020Listener. El listener recibe una notificación cuando se activa alguna de nuestras reglas de análisis, de este modo podemos conocer en qué tipo de jugado estamos, el jugador involucrado, las carreras anotadas y la cantidad de outs  en la jugada.

Para usar el listener debemos crear una clase que herede de Baseball2020Listener y sobrescribir los métodos que necesitemos usar, cada método corresponde a la llegada o salida de un tipo de nodo del árbol. Los tipos de nodo están definidos según las reglas que declaramos en la gramática, estos pueden ser un nodo jugada, un nodo tipo de hit, un nodo tipo de base, entre otros. Cuando se recorre el árbol y llegamos a un nodo se le hace una llamada a alguno de nuestros métodos según el tipo de nodo, donde extraemos la información relevante de la jugada.

class BaseballListener(Baseball2020Listener) :
     def __init__(self, nombre="", outs=0, first="", second="", third="", runs=0):        
        self.nombre = nombre
        self.outs = outs
        self.manonFirst = first
        self.manonSecond = second
        self.manonThird = third
        self.base = ""
        self.runs = runs
        self.valid = True
        self.sacrihit = False
        self.aux_name = ""
        self.error = False

    def enterName(self, ctx:Baseball2020Parser.NameContext):
        self.nombre = ""
        for palabra in ctx.STRING():
            self.nombre += (palabra.getText() + " ")

En nuestro caso nos interesa saber cuándo salimos de un nodo de tipo jugada. Esto nos permitirá comprobar los tokens presenta en la jugada y ejecutar una acción en dependencia de la presencia o no de determinados tokens. Por ejemplo, si en el recorrido sobre el árbol salimos de un nodo tipo jugada y queremos saber si el bateador se poncha o no, comprobamos la presencia del token “se” y del token “Poncha” en el texto, si están ambos tokens debemos contar un out en la jugada.

def exitPlay(self, ctx:Baseball2020Parser.PlayContext):
        if( len(ctx.SE())>0 and ctx.SO() ):
            self.esOut(self.nombre)
        elif( len(ctx.OUT()) > 0 and len(ctx.ES()) > 0):
            self.esOut(self.nombre)
        elif( len(ctx.SCORE()) > 0 ):
            self.quitarDelaBase(self.nombre)
            self.runs+=1
        elif( ctx.HIT() ):
            if( len(ctx.GOTO()) > 0 ):
                self.moveRunner(self.nombre)
            elif( len(ctx.ONBASE()) > 0 ):
                self.manonFirst = self.nombre
                self.outs-=1
            else:
                self.moveRunner(self.nombre)
        else:
            self.valid = False

Transformando el Conjunto de Datos

Una vez terminada la gramática y ajustado nuestro listener, el siguiente paso es recorrer todas las jugadas en nuestro conjunto de datos, teniendo en cuenta el inning y la parte del inning, para ir transformando la jugada textual en la información que necesitamos conocer para calcular la expectativa de carrera.

        error_listener = MyErrorListener()  
        error_listener.csvline = index
        lexer = Baseball2020Lexer(InputStream(play["evento"]))
        stream = CommonTokenStream(lexer)
        parser = Baseball2020Parser(stream)
        parser.removeErrorListeners()
        parser.addErrorListener(error_listener)
        tree = parser.plays()
        jugada = pd.Series(index=data.columns, name=count)

        jugada["Liga"]="SNB60"
        jugada["Inning"]=play["inning"]
        jugada["Mitad Del Inning"]=play["parte_del_inning"]
        jugada["Evento"]=play["evento"]
        jugada["Outs Antes De Jugada"]=outs
        jugada["Corredores Inicio De Jugada"]=runners
        jugada["Carreras Antes De Jugada"]=runs
        
        bl = BaseballListener(outs=outs,first=mFirst, second=mSecond, third=mThird,runs=runs)
        walker = ParseTreeWalker()
        walker.walk(bl, tree)
        outs = bl.outs
        runs = bl.runs
        manonFirst = bl.manonFirst
        manonSecond = bl.manonSecond
        manonThird = bl.manonThird
        runners = writeRunners(manonFirst,manonSecond,manonThird)

        jugada["Carreras Anotadas En Jugada"] = runs - jugada["Carreras Antes De Jugada"]
        jugada["Outs En Juego"] = outs - jugada["Outs Antes De Jugada"]
        jugada["Outs Despues De Jugada"] = outs
        jugada["Corredores Despues De Jugada"] = runners
        jugada["Carreras Despues De Jugada"] = runs
        if(jugada["Outs Antes De Jugada"] > 2):

Luego de esta transformación, una jugada estaría compuesta por la siguiente información:

  • Liga,
  • Inning,
  • Mitad Del Inning,
  • Evento,
  • Outs Antes De Jugada,
  • Corredores Inicio De Jugada,
  • Carreras Antes De Jugada,
  • Carreras Anotadas En Jugada,
  • Outs En Juego,
  • Outs Después De Jugada,
  • Corredores Después De Jugada,
  • Carreras Después De Jugada,
  • Carreras Final Inning

Todas las jugadas tendrían ahora la siguiente estructura:

SNB60,1,top,H. Bravo  recibe Base por Bolas.,0,---,0,0,0,0,1--,0,1

Calculando la Expectativa de Carrera

Una vez transformados nuestros datos estamos listos para calcular la expectativa de carrera para la 60 serie nacional de béisbol. Para esta tarea usamos el script del post Expectativa de Carrera I: Calculando la Matriz de Expectativa de Carrera.

Al momento de escribir este artículo nuestros datos acumulan todos los juegos de la 60 serie nacional de béisbol hasta el día 24 de octubre. A continuación, nuestra matriz de expectativa de carreras:

El código usado en este artículo lo puede encontrar en el siguiente enlace: https://github.com/junger92/cuban-rem24

Deja un comentario