English Español Deutsch 日本語
preview
Ординальное кодирование номинальных переменных

Ординальное кодирование номинальных переменных

MetaTrader 5Примеры |
294 0
Francis Dube
Francis Dube

Введение

При работе с категориальными данными в машинном обучении часто приходится сталкиваться с номинальными переменными. Хотя эти переменные могут быть ценными источниками информации для моделирования, многие алгоритмы машинного обучения, особенно работающие исключительно с числовыми данными, не могут обрабатывать их напрямую. Для решения этой проблемы мы часто преобразуем номинальные переменные в порядковые. В настоящей статье мы рассмотрим сложности преобразования номинальных переменных в порядковые. Мы рассмотрим обоснование таких преобразований, обсудим различные методы присвоения порядковых значений и выделим потенциальные преимущества и недостатки каждого подхода. Кроме того, мы продемонстрируем эти методы, в основном используя код на Python, а также реализуем два универсальных метода преобразования на чистом MQL5.


Понимание номинальных и порядковых переменных

Номинальные переменные представляют собой категориальные данные, в которых между категориями не существует присущего им порядка или ранга. Примеры, относящиеся к наборам данных финансовых временных рядов, могут включать:

  • Типы ценовых баров (например, пин-бар, волчок, молот)
  • Дни недели (например, понедельник, вторник, среда)

Эти переменные являются чисто качественными, что означает отсутствие подразумеваемой иерархии или последовательности между категориями. Например, формирование пин-бара по своей сути не превосходит волчок, а бычий бар не лучше медвежьего бара.

В числовых вычислениях обычной практикой является отнесение произвольных целых чисел к различным категориям. Однако, если эти целые числа используются в качестве входных данных для алгоритма машинного обучения, существует риск того, что присвоенные значения могут исказить информацию, передаваемую исходными данными. Алгоритм может ошибочно предположить, что большие значения подразумевают определенную взаимосвязь или ранжирование, даже если это не было задумано.

С другой стороны, порядковые переменные - это категориальные данные с присущим им порядком или рангом между категориями. Например:

  • Прочность тренда (например, сильный тренд, умеренный тренд, слабый тренд)
  • Волатильность (например, высокая волатильность, низкая волатильность)

Понимание этого различия позволяет понять, почему простое отнесение целых чисел к номинальным категориям не всегда может быть целесообразным. Для лучшего понимания мы создадим набор данных, содержащий категориальные переменные, которые будут преобразованы в порядковый формат с использованием различных методов в следующих разделах. Мы будем собирать ежедневные данные баров (открытие, максимум, минимум, закрытие) для биткоина и генерировать номинальные переменные, которые будут использоваться для прогнозирования доходности на следующий день. Первая номинальная переменная классифицирует бары либо как бычьи, либо как медвежьи. Вторая номинальная переменная содержит четыре различные категории и группирует бары на основе отношения размера тела свечи к ее полному размеру. Для последней номинальной переменной создаются три категории:

  • Когда и текущий, и предыдущий бары являются бычьими, а минимум и максимум текущего бара выше минимума и максимума предыдущего бара, мы обозначаем текущий бар как "более высокий максимум".

HigherHigh 2 bar pattern

  • Противоположный сценарий обозначается как "более низкий минимум".

LowerLow 2 bar pattern

  • Любой другой двухбаровый паттерн относится к третьей категории.

Ниже приведен код на Python, генерирующий этот набор данных.

# 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())

Обратите внимание, что на языке Python категориям присваиваются буквенные имена, тогда как в приведенных ниже листингах кода MQL5 для различения категорий используются целые числа.

//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;
     }

Ниже приведен фрагмент этого набора данных.

Full Feature Matrix



Зачем преобразовывать номинальные переменные в порядковые

Некоторые алгоритмы машинного обучения, такие как деревья решений, могут напрямую обрабатывать номинальные данные. Однако другие — особенно линейные модели, такие как логистическая регрессия или нейронные сети — требуют численных данных. Преобразование номинальных переменных в порядковые может сделать их доступными для интерпретации с помощью этих моделей, позволяя алгоритмам более эффективно извлекать уроки из данных. Хотя порядковые переменные представляют категории, они также обеспечивают четкую последовательность или ранг, предоставляя алгоритмам больше контекста для понимания взаимосвязей. Когда номинальная переменная делится существенной информацией с целевой переменной, часто бывает выгодно повысить уровень ее измерения. Если номинальная переменная имеет значимые числовые значения, они могут быть непосредственно использованы в качестве входных данных для модели. Однако, даже если значения не имеют внутреннего числового значения, мы часто можем присвоить им порядковые значения на основе их связи с целевой переменной.

Переводя номинальную переменную в порядковую шкалу, мы создаем ощущение порядка или ранга между категориями. Это может повысить способность модели улавливать лежащие в ее основе паттерны и взаимосвязи между переменной и целью. Хотя теоретически возможно повысить номинальную переменную до того же уровня измерения, что и целевую переменную, на практике часто бывает достаточно перевести ее в порядковую шкалу. Такой подход обеспечивает баланс между сохранением информационного содержания переменной и минимизацией шума. В следующих разделах мы рассмотрим распространенные методы преобразования номинальных переменных в порядковые значения, а также основные рекомендации по сохранению целостности данных.



Методы преобразования номинальных переменных

Мы начнем с самого простого метода: порядкового кодирования. В данном методе мы просто присваиваем целочисленное значение каждой категории. Как упоминалось ранее, это устанавливает ранг на категории. Если практикующий специалист знаком с данными и заранее знает, как категории соотносятся с целевой переменной, этого метода должно быть достаточно. Однако порядковое кодирование не следует использовать при обучении без присмотра, поскольку оно может легко привести к искажениям, подразумевая порядок там, где его нет.

Для преобразования нашего набора данных номинальных переменных на языке Python мы будем использовать пакет category_encoders. Данный пакет предоставляет широкий спектр реализаций категориальных преобразований, что делает его подходящим для большинства задач. Читатели могут найти дополнительную информацию о проекте в репозитории GitHub.

Для преобразования переменных в порядковый числовой формат требуется объект 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())

Преобразованные данные:

Ordinal Encoded Data

Подходящие методы преобразования для алгоритмов обучения без контроля включают двоичное кодирование, однократное кодирование и частотное кодирование. Однократное кодирование преобразует каждую категорию в двоичный столбец, где наличие категории помечается цифрой 1, а ее отсутствие - 0. Основным недостатком этого метода является то, что он значительно увеличивает количество входных переменных. Для каждой категории категориальной переменной создается новая переменная. Например, если бы мы кодировали месяцы года, то в итоге получили бы 11 дополнительных входных переменных.

Объект 'OneHotEncoder' обрабатывает однократное кодирование в пакете 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())

Преобразованные данные:

OneHot Encoded Data

Двоичное кодирование является более эффективной альтернативой, особенно при работе с многочисленными категориями. В этом методе каждая категория сначала преобразуется в уникальное целое число, а затем это целое число представляется в виде двоичного числа. Это двоичное представление распределено по нескольким столбцам, что обычно приводит к меньшему количеству столбцов по сравнению с однократным кодированием. Например, для кодирования 12 месяцев потребовалось бы всего 4 двоичных столбца. Двоичное кодирование хорошо работает в сценариях, где категориальная переменная имеет множество уникальных категорий, а нам надо ограничить количество входных переменных.

Двоичное кодирование нашего набора данных 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())

Преобразованные данные:

Binary Encoded Data

Частотное кодирование преобразует категориальную переменную, заменяя каждую категорию частотой ее появления в наборе данных. Вместо создания нескольких столбцов каждая категория заменяется соотношением или отсчетом того, как часто она появляется. Этот подход полезен, когда существует значимая взаимосвязь между частотой использования категории и целевой переменной, поскольку он позволяет сохранить ценную информацию в более компактной форме. Однако если в наборе данных преобладают определенные категории, это может привести к смещению. Он часто используется в качестве первого шага в более сложном конвейере разработки функций в сценариях обучения без контроля.

Здесь мы используем объект 'CountEncoder' object.

#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())

Преобразованные данные:


Frequency Encoded Data

Двоичное, однократное и частотное кодирование - это универсальные методы, которые могут применяться к большинству категориальных данных без риска возникновения нежелательных побочных эффектов, которые могут повлиять на результат обучения. Эти преобразования обладают важной характеристикой независимости от целевой переменной.

Однако в некоторых случаях алгоритм машинного обучения может извлечь выгоду из преобразований, отражающих связь переменной с целевой переменной. Эти методы используют целевую переменную для преобразования категориальных данных в числовые значения, которые придают некоторый уровень ассоциативности, потенциально повышая прогнозную способность модели.

Одним из таких методов является целевое кодирование, также известное как среднее кодирование. При таком подходе каждая категория заменяется средним значением целевой переменной для этой категории. Например, если мы прогнозируем вероятность того, что цена акции закроется выше (бинарная цель), мы могли бы заменить каждую категорию в номинальной переменной, такой как диапазон торгового объема, на среднюю вероятность закрытия для каждого диапазона. Целевое кодирование может быть особенно эффективным для высококардинальных категориальных переменных, поскольку оно объединяет полезную информацию без увеличения размерности набора данных. Этот метод помогает уловить взаимосвязь между категориальной переменной и целью, что делает его эффективным при обучении под руководством преподавателя. Когда категории имеют сильную корреляцию с целью, это наиболее эффективно. Но они также должны сочетаться с процедурами смягчения переобучения для обеспечения лучшего обобщения.

#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())

Преобразованные данные:

Target Encoded Data

Другим методом, зависящим от цели, является кодирование с исключением по одному. Он работает аналогично целевой кодировке, но корректирует кодировку путем исключения целевого значения текущего ряда при вычислении среднего значения для этой категории. Это помогает уменьшить переобучение, особенно когда набор данных невелик или когда некоторые категории чрезмерно представлены.Кодирование с исключением по одному обеспечивает, что преобразование остается независимым от обрабатываемого ряда, тем самым поддерживая целостность процесса обучения.

#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())

Преобразованные данные:

LeaveOneOut Encoded Data

Кодирование по методу Джеймса-Стайна - это байесовский подход к кодированию, который сокращает оценку целевого среднего значения для категории до общего среднего значения в зависимости от объема данных, доступных для каждой категории. Этот метод особенно полезен для наборов данных с низкой мощностью множества, где традиционные методы, такие как целевое кодирование или кодирование с исключением по одному, могут привести к переобучению, особенно в небольших наборах данных или при работе с категориями, имеющими крайне неравномерное распределение. Корректируя средние значения категорий на основе общего среднего значения, кодирование по методу Джеймса-Стайна снижает риск чрезмерного влияния предельных значений на модель. Это приводит к более стабильным и достоверным оценкам, что делает его эффективной альтернативой при работе с разреженными данными или категориями с небольшим количеством наблюдений.

#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())

Преобразованные данные:

James Stein Encoded Data

Библиотека category_encoders предлагает широкий спектр методов кодирования, адаптированных для различных типов категориальных данных и задач машинного обучения. Подходящий метод кодирования зависит от характера данных, используемого алгоритма машинного обучения и конкретных требований поставленной задачи. Таким образом, однократное кодирование - это универсальный метод, подходящий для многих случаев использования, особенно при работе с номинальными переменными. Целевое кодирование, кодирование с исключением по одному, а также кодирование по методу Джеймса-Стайна следует использовать, когда есть необходимость подчеркнуть взаимосвязь между переменной и целевым значением. Наконец, двоичное кодирование и хэш-кодирование являются полезными методами, когда целью является уменьшение размерности при сохранении значимой информации.



Преобразование номинальной переменной в порядковую на MQL5

В данном разделе мы реализуем два метода кодирования номинальных переменных в MQL5. Оба инкапсулированы в классы, объявленные в заголовочном файле nom2ord.mqh. Класс COneHotEncoder реализует однократное кодирование наборов данных и включает в себя два основных метода, с которыми пользователи должны ознакомиться.

//+------------------------------------------------------------------+
//| 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;

     }

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

Первый метод - это fit(), который должен вызываться после создания экземпляра класса. Для этого метода требуются два входных сигнала: матрица признаков (обучающие данные) и массив. Матрица признаков может представлять собой полный набор данных, включающий как категориальные, так и некатегориальные переменные. Если это так, то массив должен содержать индексы столбцов номинальных переменных в матрице. Если указан пустой массив, предполагается, что все переменные являются номинальными. После успешного выполнения метода fit(), а также возвращения значение true, для получения преобразованного набора данных можно вызвать метод transform(). Для этого метода требуется матрица, содержащая то же количество столбцов, что и матрица, переданная методу fit(). Если измерения не совпадают, будет выдано сообщение об ошибке.

Давайте рассмотрим, как работает класс COneHotEncoder, на примере демонстрации с использованием набора данных BTCUSD, подготовленного ранее в нстоящем тексте. В нижеприведенном фрагменте иллюстрируется процесс преобразования. Этот код взят из скрипта 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);
  }
//+------------------------------------------------------------------+

Матрица признаков до:

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…]]

Матрица признаков после:

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.]]

Второй метод преобразования, реализованный на MQL5, работает в двух режимах, оба из которых являются вариациями целевой кодировки, модифицированной для уменьшения последствий переобучения. Данный метод инкапсулирован в классе CNomOrd, определенный в nom2ord.mqh. В классе используются знакомые методы, fit() и transform(), для преобразования переменных без уменьшения размерности категориальных входных данных.

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. Call fit() first. ");
         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;
     }

Метод fit() требует дополнительных входных данных: вектора, представляющего соответствующую целевую переменную. Схема кодирования отличается от стандартной целевой кодировки, чтобы свести к минимуму переобучение, которое часто возникает из-за выбросов при распределении целевых переменных. Для решения проблемы класс использует процентильное преобразование. Целевые значения преобразуются в шкалу, основанную на процентилях, где минимальному значению присваивается процентильный ранг 0, максимальному - 100, а промежуточные значения масштабируются пропорционально. Такой подход эффективно сохраняет порядковую связь между значениями, уменьшая при этом влияние выбросов.

   //+------------------------------------------------------------------+
   //|   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. Call fit() first. ");
         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);

     }

Метод score() используется для оценки статистической значимости подразумеваемой взаимосвязи между порядковой переменной и целью. Тест на перестановку по методу Монте-Карло используется для многократного перетасовывания данных целевой переменной и повторного вычисления наблюдаемой взаимосвязи. Сравнивая наблюдаемую взаимосвязь с распределением взаимосвязей, полученных с помощью случайных перестановок, мы можем оценить вероятность того, что наблюдаемая взаимосвязь является просто случайной. Для количественной оценки наблюдаемой взаимосвязи вычисляем разницу между максимальным и минимальным средним целевым процентилем по всем категориям номинальной переменной. Таким образом, метод score() возвращает значение вероятности, которое служит результатом проверки гипотезы, где нулевая гипотеза утверждает, что наблюдаемая разница могла возникнуть случайно из-за несвязанной номинальной переменной и целевого показателя.

Давайте посмотрим, как это работает, применив класс к нашему набору данных BTCUSD. Эта демонстрация представлена в скрипте на 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)));

Преобразованные данные:

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…]]

P-значения, оценивающие связь между преобразованными переменными и целью.

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

Результаты показывают, что среди всех категориальных переменных только бычья/медвежья классификация имеет существенное отношение к цели. Напротив же, двухбаровый паттерн и размер тела свечи не демонстрируют какого-либо очевидного соответствия цели. Это указывает на то, что преобразованные переменные мало отличаются от случайной величины.

Заключительная демонстрация показывает использование класса для преобразования набора номинальных переменных в сочетании с уменьшением размерности. Эта функциональность достигается вызовом функции fitTransform(), принимающей те же входные параметры, что и метод fit(), и возвращает матрицу, содержащую единственную репрезентативную переменную, полученную из преобразованных номинальных переменных. Эта операция последовательно сокращает любое количество номинальных переменных до одной порядковой переменной.

   //+------------------------------------------------------------------+
   //| 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;
     }

В скрипте TargetBasedNominalVariableConversionWithDimReduc_demo иллюстрируется то, как реализуется этот процесс.

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));

Преобразованная переменная.

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

Проверка связи новой переменной с целью дает относительно высокое p-значение, что указывает на одно из ограничений уменьшения размерности: потерю информации. Поэтому этот метод следует использовать с осторожностью. 



Заключение

Преобразование номинальных переменных в порядковые для машинного обучения - это мощный, но сложный процесс. Он позволяет моделям осмысленно работать с категориальными данными, но требует тщательного рассмотрения, чтобы избежать отклонений или внесения искажений. Используя соответствующие методы преобразования — будь то ручное упорядочение, частотно-ориентированное кодирование, кластеризация или целевое кодирование, — вы можете гарантировать, что ваши модели машинного обучения эффективно обрабатывают номинальные переменные, сохраняя при этом целостность данных. В настоящей статье представлен неполный обзор распространенных методов преобразования номинальных переменных. Важно отметить, что существует множество более продвинутых методов. Основная цель данного текста - познакомить практиков с концепцией порядкового кодирования и факторами, которые следует учитывать при выборе наиболее подходящего метода для конкретного случая их использования. Понимая значение различных методов кодирования, специалисты-практики могут принимать более обоснованные решения и повышать эффективность и интерпретируемость своих моделей машинного обучения. Все файлы кода, на которые есть ссылки в статье, прикреплены ниже.

Файл
Описание
MQL5/scripts/CategoricalVariableConversion.py
Python-скрипт с примерами категориального преобразования
MQL5/scripts/CategoricalVariableConversion.ipynb
Jupyter notebook для скрипта на python, указанного выше
MQL5/scripts/ OneHotEncoding_demo.mq5
демонстрационный скрипт для преобразования номинальной переменной с использованием однократной кодировки на MQL5
MQL5/scripts/TargetBasedNominalVariableConversion_demo.mq5
демонстрационный скрипт для преобразования номинальных переменных с использованием пользовательского целевого метода кодирования
MQL5/scripts/TargetBasedNominalVariableConversionWithDimReduc_demo.mq5
демонстрационный скрипт для преобразования номинальных переменных с использованием пользовательского целевого метода кодирования, реализующий уменьшение размерности
MQL5/include/nom2ord.mqh
заголовочный файл, содержащий определение классов CNomOrd и COneHotEncoder
MQL5/include/np.mqh
заголовочный файл векторных и матричных служебных функций

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/16056

Возможности Мастера MQL5, которые вам нужно знать (Часть 43): Обучение с подкреплением с помощью SARSA Возможности Мастера MQL5, которые вам нужно знать (Часть 43): Обучение с подкреплением с помощью SARSA
SARSA (State-Action-Reward-State-Action, состояние-действие-вознаграждение-состояние-действие) — еще один алгоритм, который можно использовать при реализации обучения с подкреплением. Рассмотрим, как можно реализовать этот алгоритм в качестве независимой модели (а не просто механизма обучения) в советниках, собранных в Мастере, аналогично тому, как мы это делали в случаях с Q-обучением и DQN.
Упрощаем торговлю на новостях (Часть 4): Повышаем производительность Упрощаем торговлю на новостях (Часть 4): Повышаем производительность
В этой статье будут рассмотрены методы улучшения работы советника в тестере стратегий, будет написан код для разделения времени новостных событий на почасовые категории. Доступ к этим новостным событиям будет осуществляться в течение указанного для них часа. Это гарантирует, что советник может эффективно управлять сделками на основе событий как в условиях высокой, так и низкой волатильности.
Детерминированный осциллирующий поиск — Deterministic Oscillatory Search (DOS) Детерминированный осциллирующий поиск — Deterministic Oscillatory Search (DOS)
Алгоритм Deterministic Oscillatory Search (DOS) — инновационный метод глобальной оптимизации, сочетающий преимущества градиентных и роевых алгоритмов без использования случайных чисел. Механизм осцилляций и наклонов фитнеса позволяет DOS исследовать сложные пространства поиска детерминированным методом.
Как интегрировать концепцию Smart Money (OB) в сочетании с индикатором Фибоначчи для оптимального входа в сделку Как интегрировать концепцию Smart Money (OB) в сочетании с индикатором Фибоначчи для оптимального входа в сделку
SMC (Order Block) — это ключевые области, где институциональные трейдеры совершают значительные покупки или продажи. После значительного движения цены уровни Фибоначчи помогают определить потенциальный откат от недавнего максимума колебания (swing high) к минимуму колебания (swing low) для определения оптимальной точки входа в сделку.