English Русский Deutsch 日本語
preview
Codificación ordinal para variables nominales

Codificación ordinal para variables nominales

MetaTrader 5Ejemplos | 5 junio 2025, 11:34
110 0
Francis Dube
Francis Dube

Introducción

Al trabajar con datos categóricos en el aprendizaje automático, es común encontrar variables nominales. Si bien estas variables pueden ser fuentes valiosas de información para el modelado, muchos algoritmos de aprendizaje automático (especialmente aquellos que operan exclusivamente con datos numéricos) no pueden procesarlas directamente. Para abordar esto, a menudo convertimos las variables nominales en variables ordinales. En este artículo, profundizamos en las complejidades de convertir variables nominales en variables ordinales. Exploraremos la lógica detrás de tales conversiones, discutiremos varias técnicas para asignar valores ordinales y destacaremos los posibles beneficios y desventajas de cada enfoque. Además, demostraremos estos métodos utilizando principalmente código Python, mientras también implementamos dos métodos de transformación versátiles en MQL5 puro.


Comprensión de las variables nominales y ordinales

Las variables nominales representan datos categóricos donde no existe ningún orden o clasificación inherente entre las categorías. Algunos ejemplos específicos de conjuntos de datos de series temporales financieras podrían incluir:

  • Tipos de barras de precios (por ejemplo, pin bar, spinning top, hammer)
  • Días de la semana (por ejemplo, lunes, martes, miércoles)

Estas variables son puramente cualitativas, lo que significa que no existe ninguna jerarquía o secuencia implícita entre las categorías. Por ejemplo, una formación de pin bar no es inherentemente superior a una spinning top, ni una barra alcista es mejor que una bajista.

En computación numérica, es una práctica común asignar números enteros arbitrarios a categorías distintas. Sin embargo, si estos números enteros se utilizan como entradas para un algoritmo de aprendizaje automático, existe el riesgo de que los valores asignados distorsionen la información transmitida por los datos originales. El algoritmo podría inferir incorrectamente que valores mayores implican una cierta relación o clasificación, aun cuando no fuera esa la intención.

Por otro lado, las variables ordinales son datos categóricos con un orden o clasificación inherente entre categorías. Los ejemplos incluyen:

  • Intensidad de la tendencia (por ejemplo, tendencia fuerte, tendencia leve, tendencia débil)
  • Volatilidad (por ejemplo, alta volatilidad, baja volatilidad)

Entender esta distinción deja claro por qué simplemente asignar números enteros a categorías nominales puede no ser siempre apropiado. Para obtener una mejor comprensión, crearemos un conjunto de datos que contenga variables categóricas que se convertirán al formato ordinal utilizando diferentes métodos en las próximas secciones. Recopilaremos datos de barras diarias (apertura, máximo, mínimo, cierre) para Bitcoin y generaremos variables nominales que se utilizarán para predecir los retornos del día siguiente. La primera variable nominal clasifica las barras como alcistas o bajistas. La segunda variable nominal contiene cuatro categorías distintas y agrupa las barras según la relación entre el tamaño del cuerpo de la vela y su tamaño completo. Para la última variable nominal se crean tres categorías:

  • Cuando tanto la barra actual como la anterior son alcistas, y el mínimo y el máximo de la barra actual están por encima del mínimo y el máximo de la barra anterior, designamos la barra actual como un "máximo superior".

Patrón de 2 barras HigherHigh

  • El escenario opuesto se denomina "mínimo más bajo".

Patrón de 2 barras LowerLow

  • Cualquier otro patrón de dos barras cae en la tercera categoría.

El código Python que genera este conjunto de datos se proporciona a continuación.

# Copyright 2024, MetaQuotes Ltd.
# https://www.mql5.com
# imports 
from datetime import datetime
import MetaTrader5 as mt5
import pandas as pd
import numpy  as np
import pytz
import os
from category_encoders import OrdinalEncoder, OneHotEncoder, BinaryEncoder,TargetEncoder, CountEncoder, HashingEncoder, LeaveOneOutEncoder,JamesSteinEncoder

if not mt5.initialize():
    print("initialize() failed ")
    mt5.shutdown()
    exit()
 
#set up timezone infomation   
tz=pytz.timezone("Etc/UTC")

#use time zone to set correct date for history data extraction
startdate = datetime(2023,12,31,hour=23,minute=59,second=59,tzinfo=tz)
stopdate = datetime(2017,12,31,hour=23,minute=59,second=59,tzinfo=tz)

#list the symbol 
symbol = "BTCUSD"

#get price history
prices = pd.DataFrame(mt5.copy_rates_range(symbol,mt5.TIMEFRAME_D1,stopdate,startdate))

if len(prices) < 1:
    print(" Error downloading rates history ")
    mt5.shutdown()
    exit()

#shutdown mt5 tether
mt5.shutdown()

#drop unnecessary columns
prices.drop(labels=["time","tick_volume","spread","real_volume"],axis=1,inplace=True)

#initialize categorical features
prices["bar_type"] = np.where(prices["close"]>=prices["open"],"bullish","bearish")
prices["body_type"] = np.empty((len(prices),),dtype='str')
prices["bar_pattern"] = np.empty((len(prices),),dtype='str')

#set feature values
for i in np.arange(len(prices)):
    bodyratio = np.abs(prices.iloc[i,3]-prices.iloc[i,0])/np.abs(prices.iloc[i,1]-prices.iloc[i,2])
    if bodyratio >= 0.75:
        prices.iloc[i,5] = ">=0.75"
    elif bodyratio < 0.75 and bodyratio >= 0.5:
        prices.iloc[i,5]=">=0.5<0.75"
    elif bodyratio < 0.5 and bodyratio >= 0.25:
        prices.iloc[i,5]=">=0.25<0.5"
    else:
        prices.iloc[i,5]="<0.25"
    if i < 1:
      prices.iloc[i,6] = None
      continue
    if(prices.iloc[i,4]=="bullish" and prices.iloc[i-1,4]=="bullish") and (prices.iloc[i,1]>prices.iloc[i-1,1]) and (prices.iloc[i,2]>prices.iloc[i-1,2]):
        prices.iloc[i,6] = "higherHigh"
    elif(prices.iloc[i,4]=="bearish" and prices.iloc[i-1,4]=="bearish") and (prices.iloc[i,2]<prices.iloc[i-1,2]) and (prices.iloc[i,1]<prices.iloc[i-1,1]):
        prices.iloc[i,6] = "lowerLow"
    else :
        prices.iloc[i,6] = "flat"
 
#calculate target
look_ahead = 1
prices["target"] = np.log(prices["close"])
prices["target"] = prices["target"].diff(look_ahead)
prices["target"] = prices["target"].shift(-look_ahead)              

#drop rows with NA values
prices.dropna(axis=0,inplace=True,ignore_index=True)

print("Full feature matrix \n",prices.head())

Ten en cuenta que en Python se asignan nombres literales a las categorías, mientras que en los listados de código en MQL5 que siguen, se utilizan enteros para distinguir entre las categorías.

//get relative shift of is and oos sets
   int trainstart,trainstop;
   trainstart=iBarShift(SetSymbol!=""?SetSymbol:NULL,tf,TrainingSampleStartDate);
   trainstop=iBarShift(SetSymbol!=""?SetSymbol:NULL,tf,TrainingSampleStopDate);
//check for errors from ibarshift calls
   if(trainstart<0 || trainstop<0)
     {
      Print(ErrorDescription(GetLastError()));
      return;
     }
//---set the size of the sample sets
   size_insample=(trainstop - trainstart) + 1;
//---check for input errors
   if(size_insample<=0)
     {
      Print("Invalid inputs ");
      return;
     }
//---
   if(!predictors.Resize(size_insample,3))
     {
      Print("ArrayResize error ",ErrorDescription(GetLastError()));
      return;
     }
//---
   if(!prices.CopyRates(SetSymbol,tf,COPY_RATES_VERTICAL|COPY_RATES_OHLC,TrainingSampleStartDate,TrainingSampleStopDate))
     {
      Print("Copyrates error ",ErrorDescription(GetLastError()));
      return;
     }
//---
   targets = log(prices.Col(3));
   targets = np::diff(targets);
//---
   double bodyratio = 0.0;
   for(ulong i = 0; i<prices.Rows(); i++)
     {
      if(prices[i][3]<prices[i][0])
         predictors[i][0] = 0.0;
      else
         predictors[i][0] = 1.0;

      bodyratio = MathAbs(prices[i][3]-prices[i][0])/MathAbs(prices[i][1]-prices[i][2]);

      if(bodyratio >=0.75)
         predictors[i][1] = 0.0;
      else
         if(bodyratio<0.75 && bodyratio>=0.5)
            predictors[i][1] = 1.0;
         else
            if(bodyratio<0.5 && bodyratio>=0.25)
               predictors[i][1] = 2.0;
            else
               predictors[i][1] = 3.0;

      if(i<1)
        {
         predictors[i][2] = 0.0;
         continue;
        }

      if(predictors[i][0]==1.0 && predictors[i-1][0]==1.0 && prices[i][1]>prices[i-1][1] && prices[i][2]>prices[i-1][2])
         predictors[i][2] = 2.0;
      else
         if(predictors[i][0]==0.0 && predictors[i-1][0]==0.0 && prices[i][2]<prices[i-1][2] && prices[i][1]>prices[i-1][1])
            predictors[i][2] = 1.0;
         else
            predictors[i][2] = 0.0;
     }

   targets = np::sliceVector(targets,1);

   prices = np::sliceMatrixRows(prices,1,predictors.Rows()-1);

   predictors = np::sliceMatrixRows(predictors,1,predictors.Rows()-1);

   matrix fullFeatureMatrix(predictors.Rows(),predictors.Cols()+prices.Cols());

   if(!np::matrixCopyCols(fullFeatureMatrix,prices,0,prices.Cols()) ||
      !np::matrixCopyCols(fullFeatureMatrix,predictors,prices.Cols()))
     {
      Print("Failed to merge matrices");
      return;
     }

A continuación se muestra un fragmento del conjunto de datos.

Matriz de características completa



¿Por qué convertir variables nominales a ordinales?

Algunos algoritmos de aprendizaje automático, como los árboles de decisión, pueden manejar datos nominales directamente. Sin embargo, otros, especialmente los modelos lineales como la regresión logística o las redes neuronales, requieren entradas numéricas. La conversión de variables nominales en variables ordinales puede hacer que estos modelos puedan interpretarlas, lo que permite que los algoritmos aprendan de los datos de manera más efectiva. Si bien las variables ordinales representan categorías, también proporcionan una progresión o clasificación clara, lo que da a los algoritmos más contexto para comprender las relaciones. Cuando una variable nominal comparte información sustancial con la variable objetivo, a menudo puede ser ventajoso elevar su nivel de medición. Si la variable nominal tiene valores numéricos significativos, estos pueden usarse directamente como entradas para el modelo. Sin embargo, incluso cuando los valores carecen de significado numérico intrínseco, a menudo podemos asignar valores ordinales en función de su relación con la variable objetivo.

Al elevar una variable nominal a una escala ordinal, introducimos un sentido de orden o clasificación entre las categorías. Esto puede mejorar la capacidad del modelo para capturar patrones subyacentes y relaciones entre la variable y el objetivo. Si bien en teoría es posible elevar la variable nominal al mismo nivel de medición que la variable objetivo, en la práctica, convertirla a una escala ordinal suele ser suficiente. Este enfoque logra un equilibrio entre preservar el contenido de información de la variable y minimizar el ruido. En las siguientes secciones, exploramos técnicas comunes para convertir variables nominales en formas ordinales, junto con consideraciones clave para mantener la integridad de los datos.



Técnicas de conversión de variables nominales

Comenzamos con el método más simple: la codificación ordinal. En este método, simplemente asignamos un valor entero a cada categoría. Como se mencionó anteriormente, esto impone una clasificación de las categorías. Si el profesional está familiarizado con los datos y sabe de antemano cómo se relacionan las categorías con la variable objetivo, este método debería ser suficiente. Sin embargo, la codificación ordinal no se debe utilizar en el aprendizaje no supervisado, ya que puede introducir fácilmente sesgos al implicar un orden donde no existe ninguno.

Para convertir nuestro conjunto de datos de variables nominales en Python, usaremos el paquete "category_encoders". Este paquete proporciona una amplia gama de implementaciones de transformación categórica, lo que lo hace adecuado para la mayoría de las tareas. Los lectores pueden encontrar más información sobre el proyecto aquí: Repositorio de GitHub.

La conversión de variables a formato numérico ordinal requiere el objeto OrdinalEncoder.

#Ordinal encoding
ord_encoder = OrdinalEncoder(cols = ["bar_type","body_type","bar_pattern"])
ordinal_data = ord_encoder.fit_transform(prices)

print(" ordinal encoding\n ", ordinal_data.head())

Los datos transformados:

Datos codificados ordinales

Las técnicas de conversión apropiadas para algoritmos de aprendizaje no supervisado incluyen codificación binaria, codificación One-Hot y codificación de frecuencia. La codificación One-Hot transforma cada categoría en una columna binaria, donde la presencia de una categoría se marca con un 1 y su ausencia con un 0. La principal desventaja de este método es que aumenta significativamente el número de variables de entrada. Se crea una nueva variable para cada categoría de una variable categórica. Por ejemplo, si codificáramos los meses del año, terminaríamos con 11 variables de entrada adicionales.

El objeto OneHotEncoder gestiona la codificación One-Hot en el paquete "category_encoders".

#One-Hot encoding
onehot_encoder = OneHotEncoder(cols = ["bar_type","body_type","bar_pattern"])
onehot_data = onehot_encoder.fit_transform(prices)

print(" ordinal encoding\n ", onehot_data.head())

Los datos transformados:

Datos codificados de One-Hot

La codificación binaria es una alternativa más eficiente, especialmente cuando se trata de numerosas categorías. En este método, cada categoría se convierte primero en un entero único y luego el entero se representa como un número binario. Esta representación binaria se distribuye en varias columnas, lo que generalmente da como resultado menos columnas en comparación con la codificación One-Hot. Por ejemplo, para codificar 12 meses, solo se necesitarían 4 columnas binarias. La codificación binaria funciona bien en escenarios donde la variable categórica tiene muchas categorías únicas y desea limitar la cantidad de variables de entrada.

Codificación binaria nuestro conjunto de datos BTCUSD.

#Binary encoding
binary_encoder = BinaryEncoder(cols = ["bar_type","body_type","bar_pattern"])
binary_data = binary_encoder.fit_transform(prices)

print(" binary encoding\n ", binary_data.head())

Los datos transformados:

Datos codificados en binario

La codificación de frecuencia transforma una variable categórica reemplazando cada categoría con la frecuencia de su aparición en el conjunto de datos. En lugar de crear varias columnas, cada categoría se reemplaza por la proporción o el recuento de la frecuencia con la que aparece. Este enfoque es útil cuando existe una relación significativa entre la frecuencia de una categoría y la variable objetivo, ya que preserva información valiosa en una forma más compacta. Sin embargo, puede introducirse sesgo si ciertas categorías dominan en el conjunto de datos. A menudo se utiliza como un primer paso en un proceso de ingeniería de características más complejo, en escenarios de aprendizaje no supervisado.

Aquí utilizamos el objeto 'CountEncoder'.

#Frequency encoding
freq_encoder = CountEncoder(cols = ["bar_type","body_type","bar_pattern"])
freq_data = freq_encoder.fit_transform(prices)

print(" frequency encoding\n ", freq_data.head())

Los datos transformados:


Datos codificados por frecuencia

La codificación binaria, One-Hot y de frecuencia son técnicas versátiles que se pueden aplicar a la mayoría de los datos categóricos sin el riesgo de introducir efectos secundarios injustificados que puedan afectar el resultado del aprendizaje. Estas transformaciones comparten la característica importante de ser independientes de la variable objetivo.

Sin embargo, en algunos casos, un algoritmo de aprendizaje automático puede beneficiarse de transformaciones que reflejen la asociación de una variable con el objetivo. Estos métodos aprovechan la variable objetivo para convertir datos categóricos en valores numéricos que imparten cierto nivel de asociatividad, mejorando potencialmente el poder predictivo del modelo.

Uno de estos métodos es la codificación de objetivos, también conocida como codificación media. Este enfoque reemplaza cada categoría con la media de la variable objetivo para esa categoría. Por ejemplo, si estamos prediciendo la probabilidad de que una acción cierre al alza (objetivo binario), podríamos reemplazar cada categoría en una variable nominal (como el rango de volumen de operaciones) con la probabilidad de cierre promedio para cada rango. La codificación por objetivo (target encoding) puede ser especialmente poderosa para variables categóricas de alta cardinalidad, ya que consolida información útil sin aumentar la dimensionalidad del conjunto de datos. Esta técnica ayuda a capturar las relaciones entre la variable categórica y el objetivo, lo que la hace efectiva para tareas de aprendizaje supervisado. Es más eficaz cuando las categorías tienen una fuerte correlación con el objetivo. Pero también deben combinarse con procedimientos de mitigación de sobreajuste para garantizar una mejor generalización.

#Target encoding
target_encoder = TargetEncoder(cols = ["bar_type","body_type","bar_pattern"])
target_data = target_encoder.fit_transform(prices[["open","high","low","close","bar_type","body_type","bar_pattern"]], prices["target"])

print(" target encoding\n ", target_data.head())

Los datos transformados:

Datos codificación por objetivo

Otro método que depende del objetivo es la codificación leave-one-out (dejar uno fuera). Funciona de manera similar a la codificación de destino, pero ajusta la codificación excluyendo el valor de destino de la fila actual al calcular la media para esa categoría. Esto ayuda a reducir el sobreajuste, especialmente cuando el conjunto de datos es pequeño o cuando ciertas categorías están sobrerrepresentadas. La codificación leave-one-out garantiza que la transformación sea independiente de la fila que se está procesando, manteniendo así la integridad del proceso de aprendizaje.

#LeaveOneOut encoding
oneout_encoder = LeaveOneOutEncoder(cols = ["bar_type","body_type","bar_pattern"])
oneout_data = oneout_encoder.fit_transform(prices[["open","high","low","close","bar_type","body_type","bar_pattern"]], prices["target"])

print(" LeaveOneOut encoding\n ", oneout_data.head())

Los datos transformados:

Datos codificados de LeaveOneOut

La codificación James-Stein es un enfoque bayesiano de codificación que ajusta (o reduce) la estimación de la media objetivo de una categoría hacia la media general, dependiendo de la cantidad de datos disponibles para cada categoría. Esta técnica es particularmente útil para conjuntos de datos con baja cardinalidad, donde métodos tradicionales como la codificación por objetivo o la codificación leave-one-out pueden llevar al sobreajuste, especialmente en conjuntos de datos pequeños o al tratar con categorías que tienen una distribución muy desigual. Al ajustar las medias de las categorías en función de la media general, la codificación James-Stein mitiga el riesgo de que los valores extremos influyan indebidamente en el modelo. Esto da como resultado estimaciones más estables y robustas, lo que lo convierte en una alternativa eficaz cuando se trabaja con datos escasos o categorías con pocas observaciones.

#James Stein encoding
james_encoder = JamesSteinEncoder(cols = ["bar_type","body_type","bar_pattern"])
james_data = james_encoder.fit_transform(prices[["open","high","low","close","bar_type","body_type","bar_pattern"]], prices["target"])

print(" James Stein encoding\n ", james_data.head())

Datos transformados:

Datos codificados de James-Stein

La biblioteca category_encoders ofrece una amplia variedad de técnicas de codificación adaptadas a diferentes tipos de datos categóricos y tareas de aprendizaje automático. El método de codificación adecuado depende de la naturaleza de los datos, el algoritmo de aprendizaje automático utilizado y los requisitos específicos de la tarea en cuestión. En resumen, la codificación One-Hot es un método versátil adecuado para muchos casos de uso, particularmente cuando se trata con variables nominales. La codificación por objetivo, la codificación leave-one-out y la codificación James-Stein deben emplearse cuando se necesita enfatizar la relación entre una variable y el objetivo. Finalmente, la codificación binaria y la codificación hash son técnicas útiles cuando el objetivo es reducir la dimensionalidad y al mismo tiempo conservar información significativa.



Conversión de nominal a ordinal en MQL5

En esta sección, implementamos dos métodos de codificación de variables nominales en MQL5, ambos encapsulados en clases declaradas en el archivo de encabezado "nom2ord.mqh". La clase COneHotEncoder implementa codificación One-Hot para conjuntos de datos y presenta dos métodos principales con los que los usuarios deben estar familiarizados.

//+------------------------------------------------------------------+
//| one hot encoder class                                            |
//+------------------------------------------------------------------+
class COneHotEncoder
  {
private:
   vector            m_mapping[];
   ulong             m_cat_cols[];
   ulong             m_vars,m_cols;
public:
   //+------------------------------------------------------------------+
   //|  Constructor                                                     |
   //+------------------------------------------------------------------+
                     COneHotEncoder(void)
     {
     }
   //+------------------------------------------------------------------+
   //|  Destructor                                                      |
   //+------------------------------------------------------------------+
                    ~COneHotEncoder(void)
     {
      ArrayFree(m_mapping);
     }
   //+------------------------------------------------------------------+
   //| map categorical features of a training dataset                   |
   //+------------------------------------------------------------------+
   bool              fit(matrix &in_data,ulong &cols[])
     {
      m_cols = in_data.Cols();
      matrix data = np::selectMatrixCols(in_data,cols);

      if(data.Cols()!=ulong(cols.Size()))
        {
         Print(__FUNCTION__, " invalid input data ");
         return false;
        }

      m_vars = ulong(cols.Size());

      if(ArrayCopy(m_cat_cols,cols)<=0 || !ArraySort(m_cat_cols))
        {
         Print(__FUNCTION__, " ArrayCopy or ArraySort failure ", GetLastError());
         return false;
        }

      if(ArrayResize(m_mapping,int(m_vars))<0)
        {
         Print(__FUNCTION__, " Vector array resize failure ", GetLastError());
         return false;
        }

      for(ulong i = 0; i<m_vars; i++)
        {
         vector unique = data.Col(i);
         m_mapping[i] = np::unique(unique);
        }

      return true;
     }
   //+------------------------------------------------------------------+
   //| Transform abitrary feature matrix to learned category m_mapping  |
   //+------------------------------------------------------------------+

   matrix            transform(matrix &in_data)
     {
      if(in_data.Cols()!=m_cols)
        {
         Print(__FUNCTION__," Column dimension of input not equal to ", m_cols);
         return matrix::Zeros(1,1);
        }

      matrix out,input_copy;
      matrix data = np::selectMatrixCols(in_data,m_cat_cols);

      if(data.Cols()!=ulong(m_cat_cols.Size()))
        {
         Print(__FUNCTION__, " invalid input data ");
         return matrix::Zeros(1,1);
        }

      ulong unchanged_feature_cols[];

      for(ulong i = 0; i<in_data.Cols(); i++)
        {
         int found = ArrayBsearch(m_cat_cols,i);
         if(m_cat_cols[found]!=i)
           {
            if(!unchanged_feature_cols.Push(i))
              {
               Print(__FUNCTION__, " Failed array insertion ", GetLastError());
               return matrix::Zeros(1,1);
              }
           }
        }

      input_copy = unchanged_feature_cols.Size()?np::selectMatrixCols(in_data,unchanged_feature_cols):input_copy;
      ulong numcols = 0;
      vector cumsum = vector::Zeros(ulong(MathMin(m_vars,data.Cols())));

      for(ulong i = 0; i<cumsum.Size(); i++)
        {
         cumsum[i] = double(numcols);
         numcols+=m_mapping[i].Size();
        }

      out = matrix::Zeros(data.Rows(),numcols);

      for(ulong i = 0;i<data.Rows(); i++)
        {
         vector row = data.Row(i);
         for(ulong col = 0; col<row.Size(); col++)
           {
            for(ulong k = 0; k<m_mapping[col].Size(); k++)
              {
               if(MathAbs(row[col]-m_mapping[col][k])<=1.e-15)
                 {
                  out[i][ulong(cumsum[col])+k]=1.0;
                  break;
                 }
              }
           }
        }

      matrix newfeaturematrix(out.Rows(),input_copy.Cols()+out.Cols());

      if((input_copy.Cols()>0 && !np::matrixCopyCols(newfeaturematrix,input_copy,0,input_copy.Cols())) || !np::matrixCopyCols(newfeaturematrix,out,input_copy.Cols()))
        {
         Print(__FUNCTION__, " Failed matrix copy ");
         return matrix::Zeros(1,1);
        }

      return newfeaturematrix;

     }

  };
//+------------------------------------------------------------------+

El primer método es fit(), que debe llamarse después de crear una instancia de la clase. Este método requiere dos entradas: una matriz de características (datos de entrenamiento) y una matriz. La matriz de características puede ser el conjunto de datos completo, incluidas variables categóricas y no categóricas. Si este es el caso, la matriz debe contener los índices de columna de las variables nominales dentro de la matriz. Si se proporciona una matriz vacía, se supone que todas las variables son nominales. Después de que el método fit() se ejecuta exitosamente y devuelve un valor verdadero, se puede llamar al método transform() para obtener el conjunto de datos transformado. Este método requiere una matriz que tenga el mismo número de columnas que la matriz suministrada al método fit(). Si las dimensiones no coinciden, se marcará un error.

Examinemos cómo funciona la clase COneHotEncoder a través de una demostración utilizando el conjunto de datos BTCUSD preparado anteriormente en este texto. El siguiente fragmento ilustra el proceso de conversión. Este código proviene del script MetaTrader 5 "OneHotEncoding_demo.mq5".

//+------------------------------------------------------------------+
//|                                          OneHotEncoding_demo.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs
#include<np.mqh>
#include<nom2ord.mqh>
#include<ErrorDescription.mqh>
//--- input parameters
input datetime TrainingSampleStartDate=D'2023.12.31';
input datetime TrainingSampleStopDate=D'2017.12.31';
input ENUM_TIMEFRAMES tf = PERIOD_D1;
input string   SetSymbol="BTCUSD";
//+------------------------------------------------------------------+
//|global integer variables                                          |
//+------------------------------------------------------------------+
int size_insample,                 //training set size
    size_observations,             //size of of both training and testing sets combined
    price_handle=INVALID_HANDLE;   //log prices indicator handle
//+------------------------------------------------------------------+
//|double global variables                                           |
//+------------------------------------------------------------------+

matrix       prices;                   //array for log transformed prices
vector       targets;                  //differenced prices kept here
matrix       predictors;               //flat array arranged as matrix of all predictors ie size_observations by size_predictors
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//get relative shift of is and oos sets
   int trainstart,trainstop;
   trainstart=iBarShift(SetSymbol!=""?SetSymbol:NULL,tf,TrainingSampleStartDate);
   trainstop=iBarShift(SetSymbol!=""?SetSymbol:NULL,tf,TrainingSampleStopDate);
//check for errors from ibarshift calls
   if(trainstart<0 || trainstop<0)
     {
      Print(ErrorDescription(GetLastError()));
      return;
     }
//---set the size of the sample sets
   size_insample=(trainstop - trainstart) + 1;
//---check for input errors
   if(size_insample<=0)
     {
      Print("Invalid inputs ");
      return;
     }
//---
   if(!predictors.Resize(size_insample,3))
     {
      Print("ArrayResize error ",ErrorDescription(GetLastError()));
      return;
     }
//---
   if(!prices.CopyRates(SetSymbol,tf,COPY_RATES_VERTICAL|COPY_RATES_OHLC,TrainingSampleStartDate,TrainingSampleStopDate))
     {
      Print("Copyrates error ",ErrorDescription(GetLastError()));
      return;
     }
//---
   targets = log(prices.Col(3));
   targets = np::diff(targets);
//---
   double bodyratio = 0.0;
   for(ulong i = 0; i<prices.Rows(); i++)
     {
      if(prices[i][3]<prices[i][0])
         predictors[i][0] = 0.0;
      else
         predictors[i][0] = 1.0;

      bodyratio = MathAbs(prices[i][3]-prices[i][0])/MathAbs(prices[i][1]-prices[i][2]);

      if(bodyratio >=0.75)
         predictors[i][1] = 0.0;
      else
         if(bodyratio<0.75 && bodyratio>=0.5)
            predictors[i][1] = 1.0;
         else
            if(bodyratio<0.5 && bodyratio>=0.25)
               predictors[i][1] = 2.0;
            else
               predictors[i][1] = 3.0;

      if(i<1)
        {
         predictors[i][2] = 0.0;
         continue;
        }

      if(predictors[i][0]==1.0 && predictors[i-1][0]==1.0 && prices[i][1]>prices[i-1][1] && prices[i][2]>prices[i-1][2])
         predictors[i][2] = 2.0;
      else
         if(predictors[i][0]==0.0 && predictors[i-1][0]==0.0 && prices[i][2]<prices[i-1][2] && prices[i][1]>prices[i-1][1])
            predictors[i][2] = 1.0;
         else
            predictors[i][2] = 0.0;
     }

   targets = np::sliceVector(targets,1);

   prices = np::sliceMatrixRows(prices,1,predictors.Rows()-1);

   predictors = np::sliceMatrixRows(predictors,1,predictors.Rows()-1);

   matrix fullFeatureMatrix(predictors.Rows(),predictors.Cols()+prices.Cols());

   if(!np::matrixCopyCols(fullFeatureMatrix,prices,0,prices.Cols()) ||
      !np::matrixCopyCols(fullFeatureMatrix,predictors,prices.Cols()))
     {
      Print("Failed to merge matrices");
      return;
     }

   if(predictors.Rows()!=targets.Size())
     {
      Print(" Error in aligning data structures ");
      return;
     }

   COneHotEncoder enc;

   ulong selectedcols[] = {4,5,6};

   if(!enc.fit(fullFeatureMatrix,selectedcols))
      return;

   matrix transformed = enc.transform(fullFeatureMatrix);

   Print(" Original predictors \n", fullFeatureMatrix);
   Print(" transformed predictors \n", transformed);
  }
//+------------------------------------------------------------------+

Matriz de características anterior:

RQ      0       16:40:41.760    OneHotEncoding_demo (BTCUSD,D1)  Original predictors 
ED      0       16:40:41.761    OneHotEncoding_demo (BTCUSD,D1) [[13743,13855,12362.69,13347,0,2,0]
RN      0       16:40:41.761    OneHotEncoding_demo (BTCUSD,D1)  [13348,15381,12535.67,14689,1,2,0]
DG      0       16:40:41.761    OneHotEncoding_demo (BTCUSD,D1)  [14232.48,15408,14110.57,15130,1,1,2]
HH      0       16:40:41.761    OneHotEncoding_demo (BTCUSD,D1)  [15114,15370,13786.18,15139,1,3,0]
QN      0       16:40:41.761    OneHotEncoding_demo (BTCUSD,D1)  [15055.8,16894,14349.84,16725,1,1,2]
RP      0       16:40:41.761    OneHotEncoding_demo (BTCUSD,D1)  [15699.53,16474,15672.99,16186,1,1,0]
OI      0       16:40:41.761    OneHotEncoding_demo (BTCUSD,D1)  [16187,16258,13639.83,14900,0,2,0]
ML      0       16:40:41.761    OneHotEncoding_demo (BTCUSD,D1)  [14884,15334,13777.33,14405,0,2,0]
GS      0       16:40:41.761    OneHotEncoding_demo (BTCUSD,D1)  [14405,14876,12969.58,14876,1,3,0]
KF      0       16:40:41.761    OneHotEncoding_demo (BTCUSD,D1)  [14876,14927,12417.22,13245,0,1,0]
ON      0       16:40:41.761    OneHotEncoding_demo (BTCUSD,D1)  [12776.79,14078.5,12355.38,13681,1,1,0…]]

Matriz de características después:

PH      0       16:40:41.762    OneHotEncoding_demo (BTCUSD,D1)  transformed predictors 
KP      0       16:40:41.762    OneHotEncoding_demo (BTCUSD,D1) [[13743,13855,12362.69,13347,1,0,1,0,0,0,1,0,0]
KP      0       16:40:41.762    OneHotEncoding_demo (BTCUSD,D1)  [13348,15381,12535.67,14689,0,1,1,0,0,0,1,0,0]
NF      0       16:40:41.762    OneHotEncoding_demo (BTCUSD,D1)  [14232.48,15408,14110.57,15130,0,1,0,1,0,0,0,1,0]
JI      0       16:40:41.762    OneHotEncoding_demo (BTCUSD,D1)  [15114,15370,13786.18,15139,0,1,0,0,1,0,1,0,0]
CL      0       16:40:41.762    OneHotEncoding_demo (BTCUSD,D1)  [15055.8,16894,14349.84,16725,0,1,0,1,0,0,0,1,0]
RL      0       16:40:41.762    OneHotEncoding_demo (BTCUSD,D1)  [15699.53,16474,15672.99,16186,0,1,0,1,0,0,1,0,0]
IS      0       16:40:41.762    OneHotEncoding_demo (BTCUSD,D1)  [16187,16258,13639.83,14900,1,0,1,0,0,0,1,0,0]
GG      0       16:40:41.762    OneHotEncoding_demo (BTCUSD,D1)  [14884,15334,13777.33,14405,1,0,1,0,0,0,1,0,0]
QK      0       16:40:41.762    OneHotEncoding_demo (BTCUSD,D1)  [14405,14876,12969.58,14876,0,1,0,0,1,0,1,0,0]
PL      0       16:40:41.762    OneHotEncoding_demo (BTCUSD,D1)  [14876,14927,12417.22,13245,1,0,0,1,0,0,1,0,0]
GS      0       16:40:41.762    OneHotEncoding_demo (BTCUSD,D1)  [12776.79,14078.5,12355.38,13681,0,1,0,1,0,0,1,0,0.]]

El segundo método de conversión implementado en MQL5 opera en dos modos, ambos son variaciones de la codificación de destino modificadas para reducir los efectos del sobreajuste. Esta técnica está encapsulada en la clase CNomOrd, definida en "nom2ord.mqh". La clase utiliza métodos familiares, fit() y transform(), para convertir variables sin reducir la dimensionalidad de las entradas categóricas.

public:
   //+------------------------------------------------------------------+
   //|  constructor                                                     |
   //+------------------------------------------------------------------+

                     CNomOrd(void)
     {
     }
   //+------------------------------------------------------------------+
   //|  destructor                                                      |
   //+------------------------------------------------------------------+

                    ~CNomOrd(void)
     {
     }
   //+------------------------------------------------------------------+
   //| fit mapping to training data                                     |
   //+------------------------------------------------------------------+

   bool              fit(matrix &preds_in, ulong &cols[], vector &target)
     {
      m_dim_reduce = 0;
      mapped = false;
      //---
      if(cols.Size()==0 && preds_in.Cols())
        {
         m_pred = int(preds_in.Cols());
         if(!np::arange(m_colindices,m_pred,ulong(0),ulong(1)))
           {
            Print(__FUNCTION__, " arange error ");
            return mapped;
           }
        }
      else
        {
         m_pred = int(cols.Size());
        }
      //---
      m_rows = int(preds_in.Rows()) ;
      m_cols = int(preds_in.Cols());
      //---
      if(ArrayResize(m_mean_rankings,m_pred)<0 ||
         ArrayResize(m_rankings,m_rows)<0 ||
         ArrayResize(m_indices,m_rows)<0 ||
         ArrayResize(m_mapping,m_pred)<0  ||
         ArrayResize(m_class_counts,m_pred)<0  ||
         !m_median.Resize(m_pred) ||
         !m_class_ids.Resize(m_rows,m_pred) ||
         !shuffle_target.Resize(m_rows,2) ||
         (cols.Size()>0 && ArrayCopy(m_colindices,cols)<0) ||
         !ArraySort(m_colindices))
        {
         Print(__FUNCTION__, " Memory allocation failure ", GetLastError());
         return mapped;
        }
      //---
      for(uint col = 0; col<m_colindices.Size(); col++)
        {
         vector var = preds_in.Col(m_colindices[col]);
         m_mapping[col] = np::unique(var);
         m_class_counts[col] = vector::Zeros(m_mapping[col].Size());
         for(ulong i = 0; i<var.Size(); i++)
           {
            for(ulong j = 0; j<m_mapping[col].Size(); j++)
              {
               if(MathAbs(var[i]-m_mapping[col][j])<=1.e-15)
                 {
                  m_class_ids[i][col]=double(j);
                  ++m_class_counts[col][j];
                  break;
                 }
              }
           }
        }

      m_target = target;

      for(uint i = 0; i<m_colindices.Size(); i++)
        {
         vector cid = m_class_ids.Col(i);
         vector ccounts = m_class_counts[i];
         m_mean_rankings[i] = train(cid,ccounts,m_target,m_median[i]);
        }

      mapped = true;
      return mapped;
     }
   //+------------------------------------------------------------------+
   //|  transform nominal to ordinal based on learned mapping           |
   //+------------------------------------------------------------------+

   matrix            transform(matrix &data_in)
     {
      if(m_dim_reduce)
        {
         Print(__FUNCTION__, " Invalid method call, Use fitTransform() or call fit() ");
         return matrix::Zeros(1,1);
        }

      if(!mapped)
        {
         Print(__FUNCTION__, " Invalid method call, training either failed or was not done. Primero llame a fit(). ");
         return matrix::Zeros(1,1);
        }

      if(data_in.Cols()!=ulong(m_cols))
        {
         Print(__FUNCTION__, " Unexpected input data shape, doesnot match training data ");
         return matrix::Zeros(1,1);
        }
      //---
      matrix out = data_in;
      //---
      for(uint col = 0; col<m_colindices.Size(); col++)
        {
         vector var = data_in.Col(m_colindices[col]);
         for(ulong i = 0; i<var.Size(); i++)
           {
            for(ulong j = 0; j<m_mapping[col].Size(); j++)
              {
               if(MathAbs(var[i]-m_mapping[col][j])<=1.e-15)
                 {
                  out[i][m_colindices[col]]=m_mean_rankings[col][j];
                  break;
                 }
              }
           }
        }
      //---
      return out;
     }

El método fit() requiere una entrada adicional: un vector que representa la variable de destino correspondiente. El esquema de codificación difiere de la codificación de destino estándar para minimizar el sobreajuste, que a menudo surge debido a valores atípicos en la distribución de las variables de destino. Para abordar este problema, la clase emplea una transformación de percentil. Los valores objetivo se convierten en una escala basada en percentiles, donde al valor mínimo se le asigna un rango de percentil de 0, el máximo recibe un rango de 100 y los valores intermedios se escalan proporcionalmente. Este enfoque preserva eficazmente la relación ordinal entre los valores y al mismo tiempo atenúa la influencia de los valores atípicos.

   //+------------------------------------------------------------------+
   //|   test for a genuine relationship between predictor and target   |
   //+------------------------------------------------------------------+

   double            score(int reps, vector &test_target,ulong selectedVar=0)
     {
      if(!mapped)
        {
         Print(__FUNCTION__, " Invalid method call, training either failed or was not done. Primero llame a fit(). ");
         return -1.0;
        }

      if(m_dim_reduce==0 && selectedVar>=ulong(m_colindices.Size()))
        {
         Print(__FUNCTION__, " invalid predictor selection ");
         return -1.0;
        }

      if(test_target.Size()!=m_rows)
        {
         Print(__FUNCTION__, " invalid targets parameter, Does not match shape of training data. ");
         return -1.0;
        }

      int i, j, irep, unif_error ;
      double dtemp, min_neg, max_neg, min_pos, max_pos, medn ;
      dtemp = 0.0;
      min_neg = 0.0;
      max_neg = -DBL_MIN;
      min_pos = DBL_MAX;
      max_pos = DBL_MIN ;

      vector id = (m_dim_reduce)?m_class_ids.Col(0):m_class_ids.Col(selectedVar);
      vector cc = (m_dim_reduce)?m_class_counts[0]:m_class_counts[selectedVar];
      int nclasses = int(cc.Size());

      if(reps < 1)
         reps = 1 ;

      for(irep=0 ; irep<reps ; irep++)
        {

         if(!shuffle_target.Col(test_target,0))
           {
            Print(__FUNCTION__, " error filling shuffle_target column ", GetLastError());
            return -1.0;
           }

         if(irep)
           {
            i = m_rows ;
            while(i > 1)
              {
               j = (int)(MathRandomUniform(0.0,1.0,unif_error) * i) ;
               if(unif_error)
                 {
                  Print(__FUNCTION__, " mathrandomuniform() error ", unif_error);
                  return -1.0;
                 }
               if(j >= i)
                  j = i - 1 ;
               dtemp = shuffle_target[--i][0] ;
               shuffle_target[i][0] = shuffle_target[j][0] ;
               shuffle_target[j][0] = dtemp ;
              }
           }

         vector totrain = shuffle_target.Col(0);
         vector m_ranks = train(id,cc,totrain,medn) ;

         if(irep == 0)
           {
            for(i=0 ; i<nclasses ; i++)
              {
               if(i == 0)
                  min_pos = max_pos = m_ranks[i] ;
               else
                 {
                  if(m_ranks[i] > max_pos)
                     max_pos = m_ranks[i] ;
                  if(m_ranks[i] < min_pos)
                     min_pos = m_ranks[i] ;
                 }
              } // For i<nclasses
            orig_max_class = max_pos - min_pos ;
            count_max_class = 1 ;

           }
         else
           {
            for(i=0 ; i<nclasses ; i++)
              {
               if(i == 0)
                  min_pos = max_pos = m_ranks[i];
               else
                 {
                  if(m_ranks[i] > max_pos)
                     max_pos = m_ranks[i] ;
                  if(m_ranks[i] < min_pos)
                     min_pos = m_ranks[i] ;
                 }
              } // For i<nclasses
            if(max_pos - min_pos >= orig_max_class)
               ++count_max_class ;

           }
        }



      if(reps <= 1)
         return -1.0;

      return double(count_max_class)/ double(reps);

     }

El método score() se utiliza para evaluar la significancia estadística de la relación implícita entre una variable ordinal y un objetivo. Se utiliza una prueba de permutación de Monte Carlo para mezclar repetidamente los datos de la variable objetivo y recalcular la relación observada. Al comparar la relación observada con la distribución de relaciones obtenidas a través de permutaciones aleatorias, podemos estimar la probabilidad de que la relación observada se deba simplemente al azar. Para cuantificar la relación observada, calculamos la diferencia entre los percentiles objetivo medios máximo y mínimo en todas las categorías de la variable nominal. Por lo tanto, el método score() devuelve un valor de probabilidad que sirve como resultado de la prueba de hipótesis, donde la hipótesis nula establece que la diferencia observada podría haber surgido por casualidad a partir de una variable nominal y un objetivo no relacionados.

Veamos cómo funciona esto aplicando la clase a nuestro conjunto de datos BTCUSD. Esta demostración se proporciona en el script MQL5 "TargetBasedNominalVariableConversion_demo.mq5".

CNomOrd enc;
   
   ulong selectedcols[] = {4,5,6};
    
   if(!enc.fit(fullFeatureMatrix,selectedcols,targets))
     return;
     
   matrix transformed = enc.transform(fullFeatureMatrix);
   
   Print(" Original predictors \n", fullFeatureMatrix);
   Print(" transformed predictors \n", transformed);
   
   for(uint i = 0; i<selectedcols.Size(); i++)
      Print(" Probability that predicator at ", selectedcols[i] , " is associated with target ", enc.score(10000,targets,ulong(i)));

Los datos transformados:

IQ      0       16:44:25.680    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    transformed predictors 
LM      0       16:44:25.680    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)   [[13743,13855,12362.69,13347,52.28360492434251,50.66453470243147,50.45172139701621]
CN      0       16:44:25.680    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    [13348,15381,12535.67,14689,47.85025875164135,50.66453470243147,50.45172139701621]
IH      0       16:44:25.680    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    [14232.48,15408,14110.57,15130,47.85025875164135,49.77386885151457,48.16372967916465]
FF      0       16:44:25.680    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    [15114,15370,13786.18,15139,47.85025875164135,49.23046392011166,50.45172139701621]
HR      0       16:44:25.680    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    [15055.8,16894,14349.84,16725,47.85025875164135,49.77386885151457,48.16372967916465]
EM      0       16:44:25.680    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    [15699.53,16474,15672.99,16186,47.85025875164135,49.77386885151457,50.45172139701621]
RK      0       16:44:25.680    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    [16187,16258,13639.83,14900,52.28360492434251,50.66453470243147,50.45172139701621]
LG      0       16:44:25.680    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    [14884,15334,13777.33,14405,52.28360492434251,50.66453470243147,50.45172139701621]
QD      0       16:44:25.680    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    [14405,14876,12969.58,14876,47.85025875164135,49.23046392011166,50.45172139701621]
HP      0       16:44:25.680    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    [14876,14927,12417.22,13245,52.28360492434251,49.77386885151457,50.45172139701621]
PO      0       16:44:25.680    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    [12776.79,14078.5,12355.38,13681,47.85025875164135,49.77386885151457,50.45172139701621…]]

Los valores "p", que estiman la relación entre las variables transformadas y el objetivo.

NS	0	16:44:29.287	TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    Probability that predicator at 4 is associated with target 0.0005
IR      0       16:44:32.829    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    Probability that predicator at 5 is associated with target 0.7714
JS      0       16:44:36.406    TargetBasedNominalVariableConversion_demo (BTCUSD,D1)    Probability that predicator at 6 is associated with target 0.749

Los resultados indican que, entre todas las variables categóricas, solo la clasificación alcista/bajista es significativamente relevante para el objetivo. Por el contrario, el patrón de dos barras y el tamaño del cuerpo de la vela no muestran ninguna correspondencia obvia con el objetivo. Esto indica que las variables convertidas tienen poca diferencia con una variable aleatoria.

La demostración final muestra el uso de la clase para convertir un conjunto de variables nominales en combinación con la reducción de dimensionalidad. Esta funcionalidad se logra llamando a fitTransform(), que acepta los mismos parámetros de entrada que el método fit() y devuelve una matriz que contiene una única variable representativa derivada de las variables nominales convertidas. Esta operación reduce consistentemente cualquier número de variables nominales a una variable ordinal.

   //+------------------------------------------------------------------+
   //| categorical conversion with dimensionality reduction             |
   //+------------------------------------------------------------------+
   matrix            fitTransform(matrix &preds_in, ulong &cols[], vector &target)
     {
      //---
      if(preds_in.Cols()<2)
        {
         if(!fit(preds_in,cols,target))
           {
            Print(__FUNCTION__, " error at ", __LINE__);
            return matrix::Zeros(1,1);
           }
         //---
         return transform(preds_in);
        }
      //---
      m_dim_reduce = 1;
      mapped = false;
      //---
      if(cols.Size()==0 && preds_in.Cols())
        {
         m_pred = int(preds_in.Cols());
         if(!np::arange(m_colindices,m_pred,ulong(0),ulong(1)))
           {
            Print(__FUNCTION__, " arange error ");
            return matrix::Zeros(1,1);
           }
        }
      else
        {
         m_pred = int(cols.Size());
        }
      //---
      m_rows = int(preds_in.Rows()) ;
      m_cols = int(preds_in.Cols());
      //---
      if(ArrayResize(m_mean_rankings,1)<0 ||
         ArrayResize(m_rankings,m_rows)<0 ||
         ArrayResize(m_indices,m_rows)<0 ||
         ArrayResize(m_mapping,m_pred)<0  ||
         ArrayResize(m_class_counts,1)<0  ||
         !m_median.Resize(m_pred) ||
         !m_class_ids.Resize(m_rows,m_pred) ||
         !shuffle_target.Resize(m_rows,2) ||
         (cols.Size()>0 && ArrayCopy(m_colindices,cols)<0) ||
         !ArraySort(m_colindices))
        {
         Print(__FUNCTION__, " Memory allocation failure ", GetLastError());
         return matrix::Zeros(1,1);
        }
      //---
      for(uint col = 0; col<m_colindices.Size(); col++)
        {
         vector var = preds_in.Col(m_colindices[col]);
         m_mapping[col] = np::unique(var);
         for(ulong i = 0; i<var.Size(); i++)
           {
            for(ulong j = 0; j<m_mapping[col].Size(); j++)
              {
               if(MathAbs(var[i]-m_mapping[col][j])<=1.e-15)
                 {
                  m_class_ids[i][col]=double(j);
                  break;
                 }
              }
           }
        }

      m_class_counts[0] = vector::Zeros(ulong(m_colindices.Size()));

      if(!m_class_ids.Col(m_class_ids.ArgMax(1),0))
        {
         Print(__FUNCTION__, " failed to insert new class id values ", GetLastError());
         return matrix::Zeros(1,1);
        }

      for(ulong i = 0; i<m_class_ids.Rows(); i++)
         ++m_class_counts[0][ulong(m_class_ids[i][0])];

      m_target = target;

      vector cid = m_class_ids.Col(0);
      m_mean_rankings[0] = train(cid,m_class_counts[0],m_target,m_median[0]);

      mapped = true;

      ulong unchanged_feature_cols[];

      for(ulong i = 0; i<preds_in.Cols(); i++)
        {
         int found = ArrayBsearch(m_colindices,i);
         if(m_colindices[found]!=i)
           {
            if(!unchanged_feature_cols.Push(i))
              {
               Print(__FUNCTION__, " Failed array insertion ", GetLastError());
               return matrix::Zeros(1,1);
              }
           }
        }

      matrix out(preds_in.Rows(),unchanged_feature_cols.Size()+1);
      ulong nfeatureIndex = unchanged_feature_cols.Size();

      if(nfeatureIndex)
        {
         matrix input_copy = np::selectMatrixCols(preds_in,unchanged_feature_cols);
         if(!np::matrixCopyCols(out,input_copy,0,nfeatureIndex))
           {
            Print(__FUNCTION__, " failed to copy matrix columns ");
            return matrix::Zeros(1,1);
           }
        }

      for(ulong i = 0; i<out.Rows(); i++)
        {
         ulong r = ulong(m_class_ids[i][0]);
         if(r>=m_mean_rankings[0].Size())
           {
            Print(__FUNCTION__, " critical error , index out of bounds ");
            return matrix::Zeros(1,1);
           }
         out[i][nfeatureIndex] = m_mean_rankings[0][r];
        }

      return out;
     }

El script "TargetBasedNominalVariableConversionWithDimReduc_demo" ilustra cómo se implementa este proceso.

CNomOrd enc;
   
   ulong selectedcols[] = {4,5,6};
     
   matrix transformed = enc.fitTransform(fullFeatureMatrix,selectedcols,targets);
   
   Print(" Original predictors \n", fullFeatureMatrix);
   Print(" transformed predictors \n", transformed);
   
   Print(" Probability that predicator  is associated with target ", enc.score(10000,targets));

La variable transformada.

JR      0       16:51:06.137    TargetBasedNominalVariableConversionWithDimReduc_demo (BTCUSD,D1)        transformed predictors 
JO      0       16:51:06.137    TargetBasedNominalVariableConversionWithDimReduc_demo (BTCUSD,D1)       [[13743,13855,12362.69,13347,49.36939702213909]
NS      0       16:51:06.137    TargetBasedNominalVariableConversionWithDimReduc_demo (BTCUSD,D1)        [13348,15381,12535.67,14689,49.36939702213909]
OP      0       16:51:06.137    TargetBasedNominalVariableConversionWithDimReduc_demo (BTCUSD,D1)        [14232.48,15408,14110.57,15130,49.36939702213909]
RM      0       16:51:06.137    TargetBasedNominalVariableConversionWithDimReduc_demo (BTCUSD,D1)        [15114,15370,13786.18,15139,50.64271980734179]
RL      0       16:51:06.137    TargetBasedNominalVariableConversionWithDimReduc_demo (BTCUSD,D1)        [15055.8,16894,14349.84,16725,49.36939702213909]
ON      0       16:51:06.137    TargetBasedNominalVariableConversionWithDimReduc_demo (BTCUSD,D1)        [15699.53,16474,15672.99,16186,49.36939702213909]
DO      0       16:51:06.137    TargetBasedNominalVariableConversionWithDimReduc_demo (BTCUSD,D1)        [16187,16258,13639.83,14900,49.36939702213909]
JN      0       16:51:06.137    TargetBasedNominalVariableConversionWithDimReduc_demo (BTCUSD,D1)        [14884,15334,13777.33,14405,49.36939702213909]
EN      0       16:51:06.137    TargetBasedNominalVariableConversionWithDimReduc_demo (BTCUSD,D1)        [14405,14876,12969.58,14876,50.64271980734179]
PI      0       16:51:06.137    TargetBasedNominalVariableConversionWithDimReduc_demo (BTCUSD,D1)        [14876,14927,12417.22,13245,50.64271980734179]
FK      0       16:51:06.137    TargetBasedNominalVariableConversionWithDimReduc_demo (BTCUSD,D1)        [12776.79,14078.5,12355.38,13681,49.36939702213909…]]
NQ      0       16:51:09.741    TargetBasedNominalVariableConversionWithDimReduc_demo (BTCUSD,D1)        Probability that predicator  is associated with target 0.4981

Al comprobar la relación de la nueva variable con el objetivo se obtiene un valor p relativamente alto, lo que pone de relieve una de las limitaciones de la reducción de dimensionalidad: la pérdida de información. Por lo tanto, este método debe utilizarse con precaución. 



Conclusión

La conversión de variables nominales en variables ordinales para el aprendizaje automático es un proceso poderoso pero matizado. Permite que los modelos trabajen con datos categóricos de manera significativa, pero requiere una consideración cuidadosa para evitar introducir sesgos o tergiversaciones. Al emplear las técnicas de transformación adecuadas (ya sea ordenamiento manual, codificación basada en frecuencia, agrupamiento o codificación de destino), puede garantizar que sus modelos de aprendizaje automático manejen variables nominales de manera eficaz y, al mismo tiempo, preserven la integridad de los datos. Este artículo proporciona una descripción general incompleta de los métodos comunes de conversión de variables nominales, pero es importante tener en cuenta que hay muchas más técnicas avanzadas disponibles. El objetivo principal de este texto es presentar a los profesionales el concepto de codificación ordinal y los factores a considerar al seleccionar el método más apropiado para su caso de uso específico. Al comprender las implicaciones de las diferentes técnicas de codificación, los profesionales pueden tomar decisiones más informadas y mejorar el rendimiento y la interpretabilidad de sus modelos de aprendizaje automático. Todos los archivos de código a los que se hace referencia en el artículo se adjuntan a continuación.

Archivo
Descripción
MQL5/scripts/CategoricalVariableConversion.py
Script de Python con ejemplos de conversión categórica.
MQL5/scripts/CategoricalVariableConversion.ipynb
Notebook de Jupyter del script de Python mencionado anteriormente.
MQL5/scripts/ OneHotEncoding_demo.mq5
Script de demostración para convertir una variable nominal mediante codificación One-Hot en MQL5.
MQL5/scripts/TargetBasedNominalVariableConversion_demo.mq5
Script de demostración para convertir variables nominales mediante un método de codificación personalizado basado en objetivos.
MQL5/scripts/TargetBasedNominalVariableConversionWithDimReduc_demo.mq5
Script de demostración para convertir variables nominales utilizando un método de codificación personalizado basado en objetivos que implementa la reducción de dimensionalidad.
MQL5/include/nom2ord.mqh
Archivo de cabecera que contiene la definición de las clases "CNomOrd" y "COneHotEncoder".
MQL5/include/np.mqh
Archivo de cabecera de funciones utilitarias para vectores y matrices.

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/16056

Simulación de mercado (Parte 03): Una cuestión de rendimiento Simulación de mercado (Parte 03): Una cuestión de rendimiento
Muchas veces, estamos obligados a dar un paso atrás para luego avanzar. En este artículo, mostraré todos los cambios necesarios para que el rendimiento de los indicadores Mouse y Chart Trade no se viera comprometido. Como bono, presentaré otros cambios que ocurrieron en otros archivos de encabezado, los cuales serán muy utilizados en el futuro.
Simulación de mercado (Parte 02): Orden cruzada (II) Simulación de mercado (Parte 02): Orden cruzada (II)
A diferencia de lo que se vio en el artículo anterior, aquí vamos a hacer el control de selección en el Asesor Experto. Aunque esta no es aún una solución definitiva, nos servirá por ahora. Así que acompaña el artículo para entender cómo implementar una de las soluciones posibles.
Características del Wizard MQL5 que debe conocer (Parte 42): Oscilador ADX Características del Wizard MQL5 que debe conocer (Parte 42): Oscilador ADX
El ADX es otro indicador técnico relativamente popular utilizado por algunos traders para medir la fuerza de una tendencia predominante. Actuando como una combinación de otros dos indicadores, se presenta como un oscilador cuyos patrones exploramos en este artículo con la ayuda del asistente de ensamblaje MQL5 y sus clases de soporte.
Simulación de mercado (Parte 01): Orden cruzada (I) Simulación de mercado (Parte 01): Orden cruzada (I)
A partir de este artículo, iniciaremos la segunda fase, que tratará la cuestión del sistema de repetición/simulación de mercado. Entonces, comenzaremos mostrando una posible solución para el cruce de órdenes. Esta solución que presentaré no es definitiva, sino una propuesta para el problema que aún será necesario abordar próximamente.