Artículo
· 5 sep, 2023 Lectura de 12 min

QuinielaML - Preparación de datos y gestión de modelo predictivo

Continuamos con la serie de artículos relativos a la aplicación de QuinielaML. En este artículo vamos a tratar cómo preparamos los datos en crudo que hemos capturado mediante la funcionalidad de Embedded Python.

¡Bienvenidos a todos!

Happy Artist GIF by VIRTUTE - Find & Share on GIPHY

Introducción

Si recordáis el artículo anterior hemos capturado mediante Embedded Python los datos de los resultados de fútbol de Primera y Segunda División de los últimos 23 años desde una página web externa. Pues bien, ahora que tenemos los datos en crudo deberemos transformarlos y prepararlos para ayudar tanto al mantenimiento de la aplicación como a nuestro modelo de predicción para trabajar con ellos.

QUINIELA_Object.RawMatch

Veamos que forma tienen los datos que hemos capturado en nuestra base de datos de IRIS:

Como podéis ver en la siguiente captura, no difieren mucho de la información presente en la web de BDFutbol:

 

Tablas maestras:

De cara a facilitar un posterior mantenimiento de los datos comunes así como mejorar el desempeño del modelo de predicción hemos definido 2 tablas maestras en las que almacenaremos los equipos y los árbitros. Estas tablas únicamente constarán de la columna con el identificador del registro y el nombre. Echemos un vistazo a ambas:

QUINIELA_Object.Referee:

Correspondiente a la tabla maestra de árbitros.

QUINIELA_Object.Team

Correspondiente a la tabla maestra de árbitros.

 

Preparación de los datos

Perfecto, con nuestras tablas maestras y nuestros datos en crudo ya podemos comenzar el proceso de preparación de los datos para entrenar nuestro modelo. Aquí tenemos el Business Operation que se va a encargar de la preparación:

Class QUINIELA.BO.PrepareBO Extends Ens.BusinessOperation
{

Parameter INVOCATION = "Queue";
Method PrepareData(pRequest As QUINIELA.Message.PrepareRequest, pResponse As QUINIELA.Message.PrepareResponse) As %Status
{
    Set sc = $$$OK
    set pResponse = ##class(QUINIELA.Message.PrepareResponse).%New()
    set pResponse.Operation = pRequest.Operation
    
    set sqlTruncateTrain = "TRUNCATE TABLE QUINIELA_Object.MatchTrain"
    set statementTruncateTrain = ##class(%SQL.Statement).%New()
    set statusTruncateTrain = statementTruncateTrain.%Prepare(sqlTruncateTrain)
    if ($$$ISOK(statusTruncateTrain)) {
        set resultSetTruncateTrain = statementTruncateTrain.%Execute()
        if (resultSetTruncateTrain.%SQLCODE = 0) {
            set sqlMatchTrain = "INSERT INTO QUINIELA_Object.MatchTrain (Day, Division, Journey, LocalTeam, Referee, Result, VisitorTeam, IntDay) "_
                "SELECT "_
                "TO_DATE(RM.Day,'DD/MM/YYYY') AS DayTransformed, "_
                "RM.Division, "_
                "RM.Journey, "_
                "LT.ID as LocalTeam, "_
                "R.ID as Referee, "_
                "CASE WHEN CAST(RM.GoalsLocal As INTEGER) > CAST(RM.GoalsVisitor As INTEGER) THEN 1 WHEN CAST(RM.GoalsLocal As INTEGER) < CAST(RM.GoalsVisitor As INTEGER) THEN 2 ELSE 0 END as Result, "_
                "VT.ID as VisitorTeam, "_
                "CAST({fn CONCAT({fn CONCAT(SUBSTR(RM.Day,7,4),SUBSTR(RM.Day,4,2))},SUBSTR(RM.Day,1,2))} As INTEGER) as IntDay "_
                "FROM "_
                "QUINIELA_Object.RawMatch RM "_
                "LEFT JOIN QUINIELA_Object.Team LT ON UPPER(RM.LocalTeam) = UPPER(LT.Name) "_
                "LEFT JOIN QUINIELA_Object.Team VT ON UPPER(RM.VisitorTeam) = UPPER(VT.Name) "_
                "LEFT JOIN QUINIELA_Object.Referee R ON UPPER(RM.Referee) = UPPER(R.Name)"
            set statementMatchTrain = ##class(%SQL.Statement).%New()
            set statusMatchTrain = statementMatchTrain.%Prepare(sqlMatchTrain)
            if ($$$ISOK(statusMatchTrain)) {
                set resultSetMatchTrain = statementMatchTrain.%Execute()
                if (resultSetMatchTrain.%SQLCODE = 0) {
                    set sqlUpdateLocalStreak = "UPDATE QUINIELA_Object.MatchTrain SET QUINIELA_Object.MatchTrain.LocalStreak = "_
                        "(SELECT SUM(CASE WHEN IsVictory = 1 THEN 4-%VID ELSE 0 END) FROM "_
                        "(SELECT TOP 3 SubMatch.IntDay, "_
                        "CASE WHEN Result = 1 THEN 1 ELSE 0 END AS IsVictory "_
                        "FROM QUINIELA_Object.MatchTrain AS SubMatch "_
                        "WHERE "_
                        "UPPER(SubMatch.LocalTeam) = UPPER(QUINIELA_Object.MatchTrain.LocalTeam) "_
                        "AND SubMatch.IntDay < QUINIELA_Object.MatchTrain.IntDay "_
                        "ORDER BY SubMatch.IntDay DESC)) "
                    set statementUpdateLocalStreak = ##class(%SQL.Statement).%New()
                    set statusUpdateLocalStreak = statementUpdateLocalStreak.%Prepare(sqlUpdateLocalStreak)
                    if ($$$ISOK(statusUpdateLocalStreak)) {
                        set resultSetUpdateLocalStreak = statementUpdateLocalStreak.%Execute()
                        if (resultSetUpdateLocalStreak.%SQLCODE = 0) {
                            set sqlUpdateVisitorStreak = "UPDATE QUINIELA_Object.MatchTrain SET QUINIELA_Object.MatchTrain.VisitorStreak = "_
                                "(SELECT SUM(CASE WHEN IsVictory = 1 THEN 4-%VID ELSE 0 END) FROM "_
                                "(SELECT TOP 3 SubMatch.IntDay, "_
                                "CASE WHEN Result = 2 THEN 1 ELSE 0 END AS IsVictory "_
                                "FROM QUINIELA_Object.MatchTrain AS SubMatch "_
                                "WHERE "_
                                "UPPER(SubMatch.VisitorTeam) = UPPER(QUINIELA_Object.MatchTrain.VisitorTeam) "_
                                "AND SubMatch.IntDay < QUINIELA_Object.MatchTrain.IntDay "_
                                "ORDER BY SubMatch.IntDay DESC)) "
                            set statementUpdateVisitorStreak = ##class(%SQL.Statement).%New()
                            set statusUpdateVisitorStreak = statementUpdateVisitorStreak.%Prepare(sqlUpdateVisitorStreak)
                            if ($$$ISOK(statusUpdateVisitorStreak)) {
                                set resultSetUpdateVisitorStreak = statementUpdateVisitorStreak.%Execute()
                                set sc = statusUpdateVisitorStreak
                            }
                            else {
                                set sc = statusUpdateVisitorStreak
                            }
                        }
                    }
                    else {
                        set sc = statusUpdateLocalStreak
                    }
                }
            }
            else {
                set sc = statusMatchTrain
            }
        }
    }
    
    set pResponse.Status = "Finished"
    Return sc
}

XData MessageMap
{
<MapItems>
  <MapItem MessageType="QUINIELA.Message.PrepareRequest">
    <Method>PrepareData</Method>
  </MapItem>
</MapItems>
}

}

Veamos ahora en detalle cada una de las instrucciones de SQL que estamos lanzando:

  1. Limpiamos la tabla de datos de entrenamiento:
    TRUNCATE TABLE QUINIELA_Object.MatchTrain
  2. Lanzamos una inserción masiva en nuestra tabla de entrenamiento QUINIELA_Object.MatchTrain
    INSERT INTO QUINIELA_Object.MatchTrain (Day, Division, Journey, LocalTeam, Referee, Result, VisitorTeam, IntDay) 
        SELECT
            TO_DATE(RM.Day,'DD/MM/YYYY') AS DayTransformed, 
            RM.Division, 
            RM.Journey, 
            LT.ID as LocalTeam, 
            R.ID as Referee, 
            CASE WHEN CAST(RM.GoalsLocal As INTEGER) > CAST(RM.GoalsVisitor As INTEGER) THEN 1 WHEN CAST(RM.GoalsLocal As INTEGER) < CAST(RM.GoalsVisitor As INTEGER) THEN 2 ELSE 0 END as Result, 
            VT.ID as VisitorTeam, 
            CAST({fn CONCAT({fn CONCAT(SUBSTR(RM.Day,7,4),SUBSTR(RM.Day,4,2))},SUBSTR(RM.Day,1,2))} As INTEGER) as IntDay 
        FROM
            QUINIELA_Object.RawMatch RM 
            LEFT JOIN QUINIELA_Object.Team LT ON UPPER(RM.LocalTeam) = UPPER(LT.Name) 
            LEFT JOIN QUINIELA_Object.Team VT ON UPPER(RM.VisitorTeam) = UPPER(VT.Name) 
            LEFT JOIN QUINIELA_Object.Referee R ON UPPER(RM.Referee) = UPPER(R.Name)
    Para ello sustituimos el literal con el nombre del árbitro y de los equipos por su referencia en las tablas maestras. También obtenemos el resultado a partir de los goles del partido, 0 para el empate, 1 para la victoria local y 2 para la visitante. Esta columna Result será la que defina nuestro modelo de predicción como un modelo de clasificación, es decir, cada partido pertenecerá a una de nuestras 3 clases (1, X o 2).  
  3. Calculamos la racha para cada equipo dependiendo si juegan como local o visitante. Hemos añadido estas columnas para mejorar, en la medida posible, el desempeño del modelo predictivo. Hemos supuesto que un equipo que lleva varias victorias consecutivas en casa o fuera es más sencillo que continúe con las victorias al estar "en racha". El cálculo se hace de la siguiente manera, se obtienen los 3 últimos partidos (en casa para el equipo que juega como local o fuera para el que lo hace de visitante), si ha ganado el último partido se le asigna 3 puntos, si ha ganado en penúltimo se le asigna 2 y si ha ganado el antepenúltimo se le asigna 1, finalmente se suman los puntos obtenidos dando un valor numérico a la racha. Racha local:
    UPDATE QUINIELA_Object.MatchTrain SET QUINIELA_Object.MatchTrain.LocalStreak = 
        (SELECT SUM(CASE WHEN IsVictory = 1 THEN 4-%VID ELSE 0 END) FROM
            (SELECT TOP 3 SubMatch.IntDay, 
                CASE WHEN Result = 1 THEN 1 ELSE 0 END AS IsVictory 
            FROM QUINIELA_Object.MatchTrain AS SubMatch 
            WHERE
                UPPER(SubMatch.LocalTeam) = UPPER(QUINIELA_Object.MatchTrain.LocalTeam) 
                AND SubMatch.IntDay < QUINIELA_Object.MatchTrain.IntDay 
            ORDER BY SubMatch.IntDay DESC
            )
        )
    Racha visitante:
    UPDATE QUINIELA_Object.MatchTrain SET QUINIELA_Object.MatchTrain.VisitorStreak = 
        (SELECT SUM(CASE WHEN IsVictory = 1 THEN 4-%VID ELSE 0 END) 
        FROM
            (SELECT TOP 3 SubMatch.IntDay, 
                CASE WHEN Result = 2 THEN 1 ELSE 0 END AS IsVictory 
            FROM QUINIELA_Object.MatchTrain AS SubMatch 
            WHERE
                UPPER(SubMatch.VisitorTeam) = UPPER(QUINIELA_Object.MatchTrain.VisitorTeam) 
                AND SubMatch.IntDay < QUINIELA_Object.MatchTrain.IntDay 
                ORDER BY SubMatch.IntDay DESC
            )
        )
     

Veamos cual es el resultado de este conjunto de inserciones y actualizaciones consultando la tabla QUINIELA_Object.MatchTrain:

Como podéis ver hemos transformado los campos de texto a valores numéricos...¿o no? veamos la definición de la clase:

Class QUINIELA.Object.MatchTrain Extends (%Persistent, %JSON.Adaptor) [ DdlAllowed ]
{

/// Day of the match
Property Day As %Date;
/// Local Team
Property LocalTeam As %String;
/// Visitor Team
Property VisitorTeam As %String;
/// Local Streak
Property LocalStreak As %Integer;
/// Visitor Streak
Property VisitorStreak As %Integer;
/// Referee
Property Referee As %String;
/// Result
Property Result As %String;
/// Division
Property Division As %String;
/// Journey
Property Journey As %String;
/// Integer day
Property IntDay As %Integer;
}

Como veis las referencias a las tablas maestras continúan siendo de tipo %String. ¿Cuál es el motivo de esto? Pues bien, podéis encontrar la explicación en esta página de la documentación, pero en resumen es debido a que, aunque realmente sean numéricos, no corresponden a valores cuantificables, si no a identificadores.

Perfecto, ya lo tenemos todo para crear y entrenar nuestro modelo predictivo.

 

Creación y entrenamiento del modelo de predicción

Gracias a la funcionalidad de IntegratedML este paso es extremadamente sencillo para nosotros, ya que sólo necesitaremos ejecutar 2 sencillas comandos en nuestra base de datos. Echemos un vistazo al Business Operation que hemos creado para ello:

Class QUINIELA.BO.TrainBO Extends Ens.BusinessOperation
{

Parameter INVOCATION = "Queue";
/// Description
Method CreateAndTrainModel(pRequest As QUINIELA.Message.TrainRequest, pResponse As QUINIELA.Message.TrainResponse) As %Status
{
        Set tSC = $$$OK
        set pResponse = ##class(QUINIELA.Message.TrainResponse).%New()
        set pResponse.Operation = pRequest.Operation
        set pResponse.Status = "In Process"
        set sql = "SELECT MODEL_NAME FROM INFORMATION_SCHEMA.ML_MODELS WHERE MODEL_NAME = 'QuinielaModel'"
        set statement = ##class(%SQL.Statement).%New()
        set status = statement.%Prepare(sql)
        $$$TRACE(status)
        if ($$$ISOK(status)) {
            set resultSet = statement.%Execute()
            $$$TRACE(resultSet.%SQLCODE)
            if (resultSet.%SQLCODE = 0) {
                while (resultSet.%Next() '= 0) {
                    if (resultSet.%GetData(1) '= "") {
                        set sqlDrop = "DROP MODEL QuinielaModel"
                        set statementDrop = ##class(%SQL.Statement).%New()
                        set statusDrop = statementDrop.%Prepare(sqlDrop)
                        if ($$$ISOK(statusDrop)) {
                            set resultSetDrop = statementDrop.%Execute()
                            if (resultSetDrop.%SQLCODE = 0) {
                                set tSC = statusDrop                                                                
                            }
                        }
                    }
                }
            }            
        }
        $$$TRACE("Creating model")
        set sqlCreate = "CREATE MODEL QuinielaModel PREDICTING (Result) FROM QUINIELA_Object.MatchTrain"
        set statementCreate = ##class(%SQL.Statement).%New()
        set statusCreate = statementCreate.%Prepare(sqlCreate)
        if ($$$ISOK(statusCreate)) {
            $$$TRACE("Model created")
            set resultSetCreate = statementCreate.%Execute()
            if (resultSetCreate.%SQLCODE = 0) {
                set tSC = statusCreate                                
            }
        }
        else
        {
            set tSC = statusDrop
        }

        $$$TRACE("Training model")
        set sqlTrain = "TRAIN MODEL QuinielaModel"
        set statementTrain = ##class(%SQL.Statement).%New()
        set statusTrain = statementTrain.%Prepare(sqlTrain)
        if ($$$ISOK(statusTrain)) {
            set resultSetTrain = statementTrain.%Execute()
            if (resultSetTrain.%SQLCODE = 0) {
                // VALIDATION OF THE MODEL WITH THE PRE-LOADED MATCHES
                set sqlValidate = "VALIDATE MODEL QuinielaModel FROM QUINIELA_Object.MatchTrain"
                set statementValidate = ##class(%SQL.Statement).%New()
                set statusValidate = statementValidate.%Prepare(sqlValidate)
                set resultSetValidate = statementValidate.%Execute()
                set tSC = statusValidate                                    
            }
        }
        else {
            set tSC = statusTrain
        }
        
        set pResponse.Status = "Finished"
        Return tSC
}

XData MessageMap
{
<MapItems>
  <MapItem MessageType="QUINIELA.Message.TrainRequest">
    <Method>CreateAndTrainModel</Method>
  </MapItem>
</MapItems>
}

}

Analicemos que está haciendo nuestro BO:

  1. Creación del modelo predictivo y definición del valor a predecir indicando la columna de nuestra tabla de entrenamiento correspondiente.
    CREATE MODEL QuinielaModel PREDICTING (Result) FROM QUINIELA_Object.MatchTrain
  2. Entrenamiento de nuestro modelo recien creado.
    TRAIN MODEL QuinielaModel
  3. Validación del modelo creado sobre la tabla de entrenamiento utilizada.
    VALIDATE MODEL QuinielaModel FROM QUINIELA_Object.MatchTrain

Con estos tres sencillísimos pasos ya tendríamos listo nuestro modelo para empezar a generar predicciones. Echemos un vistazo a la calidad de nuestro modelo, para ello ejecutaremos la siguiente consulta:

SELECT * FROM INFORMATION_SCHEMA.ML_VALIDATION_METRICS

Con esta consulta obtendremos las siguientes métricas:

Bueno, ni tan mal, para la victoria local acierta un 52% de las ocasiones, para la victoria visitante un 41% y para los empates un 37%, ¡estamos por encima del 33% de la aleatoriedad!

Boxing Memes on X: "Golovkin's fellow country man reacts to his victory...  #GolovkinGeale http://t.co/MDW6F5eJlz" / X

Comentarios (0)1
Inicie sesión o regístrese para continuar