English Русский 中文 Español Deutsch 日本語
preview
Informação mútua como critério para seleção progressiva de características

Informação mútua como critério para seleção progressiva de características

MetaTrader 5Estatística e análise |
36 0
Francis Dube
Francis Dube

Introdução

A informação mútua é uma ferramenta valiosa para identificar preditores eficazes, especialmente quando se trata de relações complexas e não lineares. Ela pode revelar dependências que outros métodos podem deixar passar, tornando-se especialmente adequada para modelos capazes de explorar tais complexidades. Neste artigo é analisada a aplicação da informação mútua na escolha de características, com foco principal no algoritmo proposto por Hanchuan Peng, Fuhui Long e Chris Ding em seu trabalho de pesquisa intitulado "Seleção de características baseada em informação mútua: Critérios de máxima dependência, máxima relevância e mínima redundância" ("Feature Selection Based on Mutual Information: Criteria of Max-Dependency, Max-Relevance, and Min-Redundancy".

Começamos discutindo a avaliação da informação mútua para variáveis contínuas e, em seguida, avançamos para o processo de seleção de características em si. Por fim, ilustramos a eficácia do algoritmo com exemplos que utilizam tanto conjuntos de dados sintéticos quanto reais.


Avaliação da informação mútua de variáveis contínuas

A informação mútua, indicada como I(X;Y), quantifica a informação compartilhada entre duas variáveis aleatórias, X e Y. Para variáveis discretas, seu cálculo é relativamente simples e envolve a soma das probabilidades conjuntas e marginais.

Discrete Mutual Information

No entanto, as variáveis contínuas representam um grande desafio devido à sua faixa infinita de valores possíveis. Uma abordagem comum para resolver esse problema é discretizá-las em intervalos e, então, aplicar a fórmula da informação mútua discreta. K infelizmente, a precisão desse método depende significativamente da largura ou do número de intervalos escolhidos, podendo levar a diferenças consideráveis no valor calculado da informação mútua.

Continuous Mutual Information

Para calcular a informação mútua de variáveis contínuas, precisamos passar das somas discretas para integrais contínuas. Isso exige conhecimento das funções de densidade de probabilidade conjunta e marginal (PDF), que muitas vezes não estão disponíveis devido à limitação dos dados. O método da janela de Parzen oferece uma solução ao aproximar a PDF diretamente a partir dos dados. Esse método consiste em posicionar uma função de janela sobre os dados e calcular a soma ponderada dos pontos contidos dentro dessa janela. O peso atribuído a cada ponto diminui à medida que sua distância em relação ao centro da janela aumenta. Movendo essa janela pelo espaço dos dados e calculando a soma ponderada em cada ponto, podemos construir uma estimativa da função de densidade de probabilidade.

Parzen Density Approximation

Esse método é especialmente útil em cenários em que a distribuição subjacente é desconhecida ou complexa. Ajustando a forma e o tamanho da janela, é possível aumentar a suavidade e a precisão da estimativa de densidade. No entanto, é importante destacar que a escolha dos parâmetros da janela pode afetar significativamente a qualidade da estimativa. A eficiência da estimativa de densidade de Parzen depende da escolha de uma função de peso adequada, que idealmente deve ser unitária e apresentar rápido decaimento conforme o argumento se afasta de zero. Uma escolha popular para essa função de peso é a função Gaussiana, caracterizada pelo parâmetro de escala sigma.

Gaussian Weight Function

Entretanto, determinar o valor ótimo de sigma é uma tarefa complexa, frequentemente resultando em grandes variações na densidade calculada. Essa sensibilidade à escolha de sigma é a principal desvantagem do método da janela de Parzen, tornando-o pouco ideal para tarefas como a avaliação de preditores candidatos, nas quais uma estimativa precisa da densidade é indispensável.

Uma alternativa ao método da janela de Parzen para estimar a informação mútua entre variáveis contínuas é a partição adaptativa. A partição adaptativa representa um avanço significativo em relação à abordagem ingênua de divisão fixa em intervalos. Por meio da divisão iterativa de regiões com alto conteúdo de informação, a partição adaptativa concentra de forma eficiente os recursos computacionais nas áreas mais relevantes do espaço dos dados. Esse abordagem direcionada resulta em estimativas mais precisas da informação mútua, principalmente quando se trabalha com distribuições complexas e irregulares. A principal vantagem da partição adaptativa está em sua capacidade de se ajustar à estrutura subjacente dos dados. O processo recursivo de divisão garante que regiões com dependências significativas sejam subdivididas, enquanto regiões com pouca informação permaneçam intactas. Esse método dinâmico permite evitar os erros comuns da divisão fixa, que podem levar a excesso de suavização ou agrupamento indevido dos dados.

Além disso, a partição adaptativa incorpora testes estatísticos para controlar o processo de divisão. Ao avaliar a significância estatística das subdivisões potenciais, a partição adaptativa permite distinguir informação verdadeira de ruído aleatório. Isso ajuda a evitar o sobreajuste, que de outra forma poderia levar a estimativas infladas da informação mútua. O principal problema do método ingênuo é que ele dá atenção excessiva a regiões do domínio bivariado com poucos ou nenhum caso, ao mesmo tempo em que pode negligenciar áreas densas onde se concentra a maior parte da informação.  

Adaptive Partitioning

A partição adaptativa começa com uma divisão de baixa granularidade do espaço dos dados. Para cada partição é calculada a informação mútua entre as variáveis. Se esse valor excede um limite pré-definido, a partição é subdividida em seções menores. Esse processo recursivo de divisão continua até que seja atingido um critério de parada. Se o processo de divisão for interrompido cedo demais, as partições resultantes podem ser muito grosseiras, causando perda de detalhes e viés para baixo na estimativa da informação mútua. Por outro lado, se a divisão prosseguir por muito tempo, as partições podem se tornar excessivamente pequenas, resultando em sobreajuste e estimativas infladas da informação mútua devido ao ruído.

Para determinar se uma partição deve ou não ser subdividida, utiliza-se o teste do qui-quadrado. Esse teste avalia a independência entre duas variáveis dentro da partição. A partição é dividida em quatro subpartições, formando uma tabela de contingência 2x2. Conta-se o número de pontos de dados que caem em cada uma das quatro subdivisões. Sob a hipótese nula de independência entre as duas variáveis, o número esperado de pontos em cada subdivisão é calculado com base nos totais marginais. Em seguida, a estatística qui-quadrado obtida é comparada com o valor crítico da distribuição qui-quadrado com 1 grau de liberdade. Se o valor calculado excede o valor crítico, a hipótese nula de independência é rejeitada, indicando uma relação significativa entre as duas variáveis dentro da partição.

Se o teste do qui-quadrado for significativo, a partição é subdividida em regiões menores. Esse processo continua de forma recursiva até que as partições sejam suficientemente homogêneas ou até que seja satisfeito o critério de parada. Se o teste do qui-quadrado não for significativo, a partição é considerada homogênea e não há necessidade de novas subdivisões.

Para lidar com a possibilidade de surgirem relações mais complexas dentro da partição, que poderiam ser ignoradas em uma simples divisão 2x2, o algoritmo inclui uma verificação adicional. Se o teste qui-quadrado 2x2 não indicar relação significativa, mas a partição ainda for relativamente grande, é feita uma divisão mais detalhada 4x4. Em seguida, o teste qui-quadrado é aplicado a essa divisão 4x4 para avaliar a presença de uma distribuição não aleatória. Se o teste 4x4 também não identificar dependência significativa, a partição é considerada homogênea e seu valor é incorporado ao cálculo da informação mútua total. Nesse caso, não é necessário prosseguir com novas divisões.

Se o teste qui-quadrado 2x2 ou 4x4 apontar uma distribuição desigual dentro da partição, esta é dividida em quatro subdivisões menores. Essas subdivisões são tratadas de forma diferenciada conforme seu tamanho. Se uma subdivisão for muito pequena, ela é considerada final. Seu valor é calculado e somado à informação mútua total, e a subdivisão deixa de ser analisada para futuras divisões. Caso contrário, se a subdivisão for suficientemente grande, ela é marcada como candidata para novas divisões posteriores.

A estatística padrão do teste qui-quadrado dois por dois é apresentada na seguinte equação.

2 by 2 Chi-Square test statistic


Partição adaptativa em MQL5

A classe Capm em mutualinfo.mqh implementa o algoritmo de partição adaptativa para estimar a informação mútua de variáveis contínuas. Para controlar o processo recursivo de divisão, ela utiliza a estrutura personalizada IntStack. Essa estrutura armazena informações sobre as partições e quaisquer subdivisões nelas contidas. A estrutura é composta por seis membros, descritos a seguir:

  • Xstart e Xstop: Definem o intervalo de índices de uma variável dentro da partição atual.
  • Ystart e Ystop: Definem o intervalo de índices da segunda variável dentro da partição atual.
  • DataStart e DataStop: Determinam os índices inicial e final dos pontos de dados contidos na partição, delimitando as subdivisões.
//+------------------------------------------------------------------+
//| int type stack structure                                         |
//+------------------------------------------------------------------+
struct IntStack
  {
   int Xstart ;     // X value (rank) at which this rectangle starts
   int Xstop ;      // And stops
   int Ystart ;     // Ditto for Y
   int Ystop ;
   int DataStart ;  // Starting index into indices for the cases in this
   int DataStop ;   // rectangle, and the (inclusive) ending index
  };

O construtor Capm recebe três argumentos:

  • no_split: Um sinalizador lógico que define como tratar valores aproximadamente iguais. Se definido como true, o algoritmo garante que esses valores sejam agrupados durante a divisão, evitando que sejam separados em diferentes partições.
  • dep_vals: Um vetor que contém os valores de uma das variáveis analisadas. Normalmente, essa é a variável cuja informação mútua será calculada em relação a um conjunto de outras variáveis.
  • chi_critical_value: O valor limite usado nos testes qui-quadrado aplicados durante o processo de divisão. Esse valor define o nível de significância dos testes e influencia a profundidade da divisão. Em geral, valores entre 4,0 e 8,0 são adequados, sendo 6,0 uma escolha comum com utilidade geral.
//+------------------------------------------------------------------+
//|  constructor                                                     |
//+------------------------------------------------------------------+
Capm::Capm(bool no_split,vector &dep_vals, double chi_critical_value = 6.0)
  {
   m_no_split = no_split;
   m_size = int(dep_vals.Size()) ;
   m_chi_crit = chi_critical_value;

   if(ArrayResize(m_indices,m_size)<0 || !m_tempvals.Resize(m_size) || ArrayResize(m_ranks,m_size)<0
      || (m_no_split && ArrayResize(m_tied_ranks,m_size)<0))
     {
      Print(__FUNCTION__, " Arrayresize error ", GetLastError());
      return;
     }

   for(int i=0 ; i<m_size ; i++)
     {
      m_tempvals[i] = dep_vals[i] ;
      m_indices[i] = i ;
     }

   qsortdsi(0, m_size-1, m_tempvals, m_indices) ;

   for(int i=0 ; i<m_size ; i++)
     {
      m_ranks[m_indices[i]] = i ;
      if(! m_no_split)
         continue ;
      if(i < m_size-1  &&
         m_tempvals[i+1] - m_tempvals[i] < 1.e-12 * (1.0+fabs(m_tempvals[i])+fabs(m_tempvals[i+1])))
         m_tied_ranks[i] = 1 ;
      else
         m_tied_ranks[i] = 0 ;
     }
  }

O construtor da classe Capm começa configurando o sinalizador no_split, que determina se devem ser consideradas as ligações nos dados, e o parâmetro chi_critical_value, que define o valor de corte para o teste qui-quadrado utilizado na detecção de dependências significativas entre as variáveis. Em seguida, o construtor aloca memória para os arrays de armazenamento de dados, ranks e valores associados. Ele copia os valores das variáveis para um vetor temporário e os ordena. Os índices originais dos pontos de dados são armazenados no array m_indices. Por fim, o construtor atribui ranks aos pontos de dados ordenados e identifica quaisquer valores ligados, marcando-os no array m_tied_ranks caso o sinalizador no_split esteja ativo.

//+------------------------------------------------------------------+
//| Mutual information using the Adaptive partitioning method        |
//+------------------------------------------------------------------+
class Capm
  {

public:
                     Capm(bool no_split,vector &dep_vals, double chi_critical_value = 6.0) ;
                    ~Capm(void) ;
   double            fit(vector &raw) ;

private:
   bool               m_no_split;  // respect ties
   int m_size ;                        // Number of cases
   int m_ranks[] ;                     // 'Dependent' variable ranks
   int m_tied_ranks[] ;                // tied[i] != 0 if case with rank i == case with rank i+1
   double m_chi_crit ;                 // Chi-square test criterion
   int               m_indices[];      // indices
   vector            m_tempvals;       // temp values
  } ;

O método fit() da classe Capm é projetado para estimar a informação mútua entre uma variável (fornecida ao construtor) e outra variável passada como argumento. Em seguida, recebe um vetor que representa os valores da segunda variável. Esse vetor deve ter o mesmo tamanho do vetor fornecido ao construtor. Chamando o método fit() várias vezes com vetores diferentes, é possível calcular de forma eficiente a informação mútua entre uma variável e diversas outras.

//+------------------------------------------------------------------+
//|  mutual information for continuous variables                     |
//+------------------------------------------------------------------+
double Capm::fit(vector &raw)
  {
   int i, k, ix, iy, nstack, splittable ;
   int current_indices[], x[], fullXstart, fullXstop, fullYstart, fullYstop, ipos ;
   int trialXstart[4], trialXstop[4], trialYstart[4], trialYstop[4] ;
   int ipx, ipy, xcut[4], ycut[4], iSubRec, x_tied[], ioff ;
   int X_AllTied, Y_AllTied ;
   int centerX, centerY, currentDataStart, currentDataStop ;
   int actual[4], actual44[16] ;
   vector expected(16), xfrac(4), yfrac(4) ;
   double diff, testval;
   double px, py, pxy, MI ;

   IntStack stack[];

   if(ArrayResize(stack,1,256)<0)
     {
      Print(__FUNCTION__," arrayresize error ", GetLastError());
      return EMPTY_VALUE;
     }

   if(ArrayResize(current_indices,m_size)<0 || ArrayResize(x,m_size)<0
      || (m_no_split && ArrayResize(x_tied,m_size)<0))
     {
      Print(__FUNCTION__, " Arrayresize error ", GetLastError());
      return EMPTY_VALUE;
     }

   for(i=0 ; i<m_size ; i++)
     {
      m_tempvals[i] = raw[i] ;
      m_indices[i] = i ;
     }

   if(!qsortdsi(0, m_size-1, m_tempvals, m_indices))
     {
      Print(__FUNCTION__, " Arrayresize error ", GetLastError());
      return EMPTY_VALUE;
     }

   for(i=0 ; i<m_size ; i++)
     {
      x[m_indices[i]] = i ;
      if(! m_no_split)
         continue ;
      if(i < m_size-1  &&
         m_tempvals[i+1] - m_tempvals[i] < 1.e-12 * (1.0+fabs(m_tempvals[i])+fabs(m_tempvals[i+1])))
         x_tied[i] = 1 ;
      else
         x_tied[i] = 0 ;
     }

   for(i=0 ; i<m_size ; i++)
     {
      m_indices[i] = i ;
     }

   stack[0].Xstart = 0 ;
   stack[0].Xstop = m_size-1 ;
   stack[0].Ystart = 0 ;
   stack[0].Ystop = m_size-1 ;
   stack[0].DataStart = 0 ;
   stack[0].DataStop = m_size-1 ;
   nstack = 1 ;

   MI = 0.0 ;
   while(nstack > 0)
     {

      --nstack ;
      fullXstart = stack[nstack].Xstart ;
      fullXstop = stack[nstack].Xstop ;
      fullYstart = stack[nstack].Ystart ;
      fullYstop = stack[nstack].Ystop ;
      currentDataStart = stack[nstack].DataStart ;
      currentDataStop = stack[nstack].DataStop ;


      centerX = (fullXstart + fullXstop) / 2 ;
      X_AllTied = (x_tied.Size())  && (x_tied[centerX] != 0) ;
      if(X_AllTied)
        {
         for(ioff=1 ; centerX-ioff >= fullXstart ; ioff++)
           {
            if(! x_tied[centerX-ioff])
              {
               X_AllTied = 0 ;
               centerX -= ioff ;
               break ;
              }
            if(centerX + ioff == fullXstop)
               break ;
            if(! x_tied[centerX+ioff])
              {
               X_AllTied = 0 ;
               centerX += ioff ;
               break ;
              }
           }
        }

      centerY = (fullYstart + fullYstop) / 2 ;
      Y_AllTied = (m_tied_ranks.Size())  && (m_tied_ranks[centerY] != 0) ;
      if(Y_AllTied)
        {
         for(ioff=1 ; centerY-ioff >= fullYstart ; ioff++)
           {
            if(! m_tied_ranks[centerY-ioff])
              {
               Y_AllTied = 0 ;
               centerY -= ioff ;
               break ;
              }
            if(centerY + ioff == fullYstop)
               break ;
            if(! m_tied_ranks[centerY+ioff])
              {
               Y_AllTied = 0 ;
               centerY += ioff ;
               break ;
              }
           }
        }

      if(X_AllTied  ||  Y_AllTied)
         splittable = 0 ;
      else
        {
         trialXstart[0] = trialXstart[1] = fullXstart ;
         trialXstop[0] = trialXstop[1] = centerX ;
         trialXstart[2] = trialXstart[3] = centerX+1 ;
         trialXstop[2] = trialXstop[3] = fullXstop ;
         trialYstart[0] = trialYstart[2] = fullYstart ;
         trialYstop[0] = trialYstop[2] = centerY ;
         trialYstart[1] = trialYstart[3] = centerY+1 ;
         trialYstop[1] = trialYstop[3] = fullYstop ;

         for(i=0 ; i<4 ; i++)
            expected[i] = (currentDataStop - currentDataStart + 1) *
                          (trialXstop[i]-trialXstart[i]+1.0) / (fullXstop-fullXstart+1.0) *
                          (trialYstop[i]-trialYstart[i]+1.0) / (fullYstop-fullYstart+1.0) ;

         actual[0] = actual[1] = actual[2] = actual[3] = 0 ;
         for(i=currentDataStart ; i<=currentDataStop ; i++)
           {
            k = m_indices[i] ;
            if(x[k] <= centerX)
              {
               if(m_ranks[k] <= centerY)
                  ++actual[0] ;
               else
                  ++actual[1] ;
              }
            else
              {
               if(m_ranks[k] <= centerY)
                  ++actual[2] ;
               else
                  ++actual[3] ;
              }
           }

         testval = 0.0 ;
         for(i=0 ; i<4 ; i++)
           {
            diff = fabs(actual[i] - expected[i]) - 0.5 ;
            testval += diff * diff / expected[i] ;
           }

         splittable = (testval > m_chi_crit)  ?  1 : 0 ;

O método começa inicializando a pilha que controla o processo de divisão. Todo o conjunto de dados é inicialmente colocado na pilha como a partição raiz. O algoritmo, então, extrai iterativamente uma partição da pilha e avalia sua homogeneidade. Para verificar a significância estatística de possíveis dependências entre as variáveis dentro da partição, aplica-se o teste qui-quadrado. Se o teste indicar uma dependência significativa, a partição é dividida em quatro subdivisões, e cada subdivisão é colocada de volta na pilha para análise posterior. Esse processo continua até que todas as partições satisfaçam o critério de parada ou sejam consideradas homogêneas.

if(! splittable && fullXstop-fullXstart > 30 && fullYstop-fullYstart > 30)
           {
            ipx = fullXstart - 1 ;
            ipy = fullYstart - 1 ;
            for(i=0 ; i<4 ; i++)
              {
               xcut[i] = (fullXstop - fullXstart + 1) * (i+1) / 4 + fullXstart - 1 ;
               xfrac[i] = (xcut[i] - ipx) / (fullXstop - fullXstart + 1.0) ;
               ipx = xcut[i] ;
               ycut[i] = (fullYstop - fullYstart + 1) * (i+1) / 4 + fullYstart - 1 ;
               yfrac[i] = (ycut[i] - ipy) / (fullYstop - fullYstart + 1.0) ;
               ipy = ycut[i] ;
              }
            for(ix=0 ; ix<4 ; ix++)
              {
               for(iy=0 ; iy<4 ; iy++)
                 {
                  expected[ix*4+iy] = xfrac[ix] * yfrac[iy] *
                                      (currentDataStop - currentDataStart + 1) ;
                  actual44[ix*4+iy] = 0 ;
                 }
              }
            for(i=currentDataStart ; i<=currentDataStop ; i++)
              {
               k = m_indices[i] ;
               for(ix=0 ; ix<3 ; ix++)
                 {
                  if(x[k] <= xcut[ix])
                     break ;
                 }
               for(iy=0 ; iy<3 ; iy++)
                 {
                  if(m_ranks[k] <= ycut[iy])
                     break ;
                 }
               ++actual44[ix*4+iy] ;
              }
            testval = 0.0 ;
            for(ix=0 ; ix<4 ; ix++)
              {
               for(iy=0 ; iy<4 ; iy++)
                 {
                  diff = fabs(actual44[ix*4+iy] - expected[ix*4+iy]) - 0.5 ;
                  testval += diff * diff / expected[ix*4+iy] ;
                 }
              }
            splittable = (testval > 3 * m_chi_crit)  ?  1 : 0 ;
           }
        }

      if(splittable)
        {

         for(i=currentDataStart ; i<=currentDataStop ; i++)
            current_indices[i] = m_indices[i] ;

         ipos = currentDataStart ;
         for(iSubRec=0 ; iSubRec<4 ; iSubRec++)
           {

            if(actual[iSubRec] >= 3)
              {
               stack[nstack].Xstart = trialXstart[iSubRec] ;
               stack[nstack].Xstop = trialXstop[iSubRec] ;
               stack[nstack].Ystart = trialYstart[iSubRec] ;
               stack[nstack].Ystop = trialYstop[iSubRec] ;
               stack[nstack].DataStart = ipos ;
               stack[nstack].DataStop = ipos + actual[iSubRec] - 1 ;
               ++nstack ;
               if(ArrayResize(stack,nstack+1,256)<0)
                 {
                   Print(__FUNCTION__," arrayresize error ", GetLastError());
                   return EMPTY_VALUE;
                 }

               if(iSubRec == 0)
                 {
                  for(i=currentDataStart ; i<=currentDataStop ; i++)
                    {
                     k = current_indices[i] ;
                     if(x[k] <= centerX  &&  m_ranks[k] <= centerY)
                        m_indices[ipos++] = current_indices[i] ;
                    }
                 }
               else
                  if(iSubRec == 1)
                    {
                     for(i=currentDataStart ; i<=currentDataStop ; i++)
                       {
                        k = current_indices[i] ;
                        if(x[k] <= centerX  &&  m_ranks[k] > centerY)
                           m_indices[ipos++] = current_indices[i] ;
                       }
                    }
                  else
                     if(iSubRec == 2)
                       {
                        for(i=currentDataStart ; i<=currentDataStop ; i++)
                          {
                           k = current_indices[i] ;
                           if(x[k] > centerX  &&  m_ranks[k] <= centerY)
                              m_indices[ipos++] = current_indices[i] ;
                          }
                       }
                     else
                       {
                        for(i=currentDataStart ; i<=currentDataStop ; i++)
                          {
                           k = current_indices[i] ;
                           if(x[k] > centerX  &&  m_ranks[k] > centerY)
                              m_indices[ipos++] = current_indices[i] ;
                          }
                       }
              }

            else
              {
               if(actual[iSubRec] > 0)
                 {
                  px = (trialXstop[iSubRec] - trialXstart[iSubRec] + 1.0) / m_size ;
                  py = (trialYstop[iSubRec] - trialYstart[iSubRec] + 1.0) / m_size ;
                  pxy = (double) actual[iSubRec] / m_size ;
                  MI += pxy * log(pxy / (px * py)) ;
                 }
              }
           }
        }
      else
        {
         px = (fullXstop - fullXstart + 1.0) / m_size ;
         py = (fullYstop - fullYstart + 1.0) / m_size ;
         pxy = (currentDataStop - currentDataStart + 1.0) / m_size ;
         MI += pxy * log(pxy / (px * py)) ;
        }
     }

   return MI;

Para as partições consideradas homogêneas, seja por causa de um teste qui-quadrado não significativo ou por conterem um número mínimo de pontos de dados, a informação mútua é calculada. Esse cálculo inclui a estimativa das distribuições conjuntas e marginais de probabilidade dentro da partição e a aplicação da seguinte fórmula.

Mutual Information Contribution

O processo continua de forma recursiva, em que as partições são divididas e avaliadas até que não seja mais possível realizar divisões significativas. A estimativa final da informação mútua é obtida somando as contribuições de todas as partições terminais, resultando em uma avaliação abrangente baseada na estrutura dos dados. Caso o método identifique um erro, ele retorna um valor equivalente à constante EMPTY_VALUE incorporada no MQL5. Na próxima seção, utilizaremos a classe Capm para implementar o algoritmo de seleção progressiva de características.


Seleção de preditores usando informação mútua

Ao aplicar a informação mútua à tarefa de seleção de características, nosso objetivo é identificar um subconjunto de preditores, a partir de um conjunto maior de candidatos, que maximize a dependência conjunta com a variável alvo. Essa dependência conjunta é uma generalização da informação mútua, em que uma das grandezas é um conjunto de variáveis em vez de uma única variável. O cálculo da dependência conjunta para grandes subconjuntos de preditores torna-se computacionalmente inviável devido à maldição da dimensionalidade. Isso ocorre porque a necessária partição multidimensional dos dados gera um número extremamente pequeno de células, especialmente à medida que o número de dimensões (preditores) aumenta.

Além disso, a enorme quantidade de combinações possíveis de características pode ser esmagadora mesmo em conjuntos de tamanho moderado. Esse explosão combinatória torna impraticável uma busca exaustiva por todos os subconjuntos possíveis, exigindo, portanto, o uso de estratégias de busca eficientes.

Uma abordagem comum para seleção de características é a seleção progressiva direta. Esse método começa com um modelo vazio e adiciona iterativamente a característica que proporciona a maior melhoria no desempenho do modelo, medida de acordo com o critério escolhido. Embora essa abordagem seja eficiente, ela sofre da limitação da "ganância". Isso significa que podem não ser encontradas combinações ótimas de características, como nos casos em que duas características juntas oferecem poder preditivo significativamente melhor do que qualquer uma delas isoladamente. Isso acontece porque o algoritmo se concentra em melhorias incrementais a cada etapa, podendo deixar de lado relações sinérgicas entre características.

Embora existam outros métodos, como os de ordem superior e o de seleção regressiva, a seleção progressiva direta continua sendo a escolha mais prática devido à sua eficiência computacional, especialmente quando se trabalha com grandes conjuntos de dados e recursos computacionais limitados. Além disso, quando aplicada ao contexto específico da dependência conjunta, a seleção progressiva direta apresenta uma propriedade notável: ela pode aproximar de forma eficaz o subconjunto ótimo de características, mesmo sem otimizar explicitamente a medida de dependência conjunta.

Peng, Long e Ding propuseram um algoritmo de seleção de características baseado em informação mútua, conhecido como critério de máxima relevância e mínima redundância (MRMR). Esse algoritmo aproxima de maneira eficiente o subconjunto ótimo de características sem a necessidade de calcular explicitamente a dependência conjunta, o que seria computacionalmente inviável. O critério MRMR é fundamentado nos conceitos de relevância e redundância. A relevância de um conjunto de características S em relação à variável alvo Y é definida como a média da informação mútua entre Y e cada característica em S.

Relevance

Aqui, ∣S∣ representa o número de características em S, e I(Xi,Y) é a informação mútua entre a característica Xi e Y.

Embora a maximização da relevância seja intuitiva, ela pode resultar em um conjunto não ótimo devido à redundância. Se selecionarmos apenas com base na relevância individual de cada característica em relação à variável alvo, poderemos acabar com um conjunto de características altamente correlacionadas, que fornecem pouca informação adicional. Para superar esse problema, é necessário considerar não apenas a relevância de uma característica, mas também sua redundância em relação às características já selecionadas.

A redundância de um preditor potencial Xi em relação ao conjunto de características já selecionado S é definida como a média da informação mútua entre Xi e cada característica em S:

Redundancy

Onde I(Xi,Xj) representa a informação mútua entre o preditor potencial Xi e a característica Xj já presente em S.

É importante destacar que a formulação matemática para redundância é idêntica à de relevância. A única diferença está na interpretação. Quando calculamos a informação mútua entre um preditor potencial e a variável alvo, chamamos isso de relevância. Entretanto, quando a informação mútua é calculada entre um preditor potencial e uma característica já pertencente ao conjunto selecionado, chamamos isso de redundância.

Essencialmente, ambos os conceitos envolvem medir a quantidade de informação compartilhada entre duas variáveis, mas o contexto define se ela será considerada relevância ou redundância. No caso da relevância, observamos a relação entre a característica e a variável alvo, enquanto no caso da redundância o foco está no grau de sobreposição entre uma característica potencial e as já selecionadas.

Resumindo, o algoritmo MRMR funciona da seguinte forma:

  1. Como primeira característica, é selecionada aquela com a maior informação mútua em relação à variável alvo Y.
  2. Em cada etapa seguinte, o algoritmo escolhe a característica que maximiza o seguinte critério:

MRMR Criterion

Esse critério equilibra a relevância da característica em relação à variável alvo Y (ou seja, I(X_i, Y)) com sua redundância em relação às características já selecionadas. Um aspecto notável do algoritmo MRMR é que ele consegue aproximar de forma eficiente o subconjunto ótimo de características, mesmo que a otimização direta da dependência conjunta seja computacionalmente inviável. Esse resultado, embora não seja imediatamente evidente, foi matematicamente demonstrado no artigo original.

Para avaliar a significância estatística das características selecionadas, podem ser aplicados dois testes de permutação de Monte Carlo (MCP):

  • Significância individual da característica: Esse teste avalia a importância de cada característica individual ao comparar sua relevância em relação à variável alvo com uma distribuição nula obtida por meio de permutações. Ao embaralhar os valores das características, criamos uma distribuição nula que assume que a característica não tem relação com o alvo. Se a relevância observada for significativamente maior do que os valores permutados, podemos concluir que a característica provavelmente é de fato informativa.
  • Significância global do modelo: Esse teste avalia a importância do conjunto completo de características selecionadas ao comparar a soma agregada da relevância individual com uma distribuição nula obtida por permutação. Ao embaralhar os valores da variável alvo, criamos uma distribuição nula que assume que as características não têm relação com o alvo. Se a relevância observada do conjunto for significativamente maior do que os valores permutados, podemos concluir que o subconjunto de características selecionadas provavelmente terá poder preditivo sobre a variável alvo.


Implementação da seleção progressiva em MQL5 baseada em informação mútua

A classe Cmrmr em mutualinfo.mqh implementa o algoritmo MRMR e em seu construtor recebe os seguintes parâmetros:
  • num_reps: Número de repetições para o teste de permutação de Monte Carlo. Definir esse parâmetro como um valor menor ou igual a 1 desativa os testes.
  • max_preds: Número máximo de características a serem selecionadas.
  • chisquare_thresh: Valor limite do teste qui-quadrado usado no método de partição adaptativa para estimar a informação mútua.
  • m_verbose: Um sinalizador lógico que define se a saída detalhada deve ser exibida durante a execução do algoritmo.
//+-----------------------------------------------------------------------+
//|Relevance minus redundancy for building an optimal subset of predictors|
//+-----------------------------------------------------------------------+
class Cmrmr
  {
private:
   int               m_max_preds;
   int               m_reps;
   bool              m_verbose;
   int               m_samples;
   int               m_vars;
   matrix            m_preds;
   vector            m_target;
   vector            m_crits;
   double            m_chisquare_thresh;
   ulong             m_selected_vars[];
   matrix            m_critical_values;
   vector            mutualinfo(int which_size,int &which[],vector &targs);
public:
                     Cmrmr(int num_reps,int max_preds, double chisquare_thresh,bool verbose);
                    ~Cmrmr(void);
   bool              StepWise(matrix &predictors, vector &targets);
   matrix            GetCriticalValues(void);
   bool              GetSelectedVars(ulong &output[]);
  };

Para iniciar o processo de seleção de características com a classe Cmrmr, é necessário instanciar um objeto dessa classe e, em seguida, chamar seu método StepWise(). O método StepWise() utiliza dois insumos principais:

  • Matriz de variáveis preditoras potenciais: Essa matriz deve conter os dados de todas as características potenciais que o praticante deseja considerar.
  • Vetor da variável alvo: Esse vetor deve conter os valores da variável alvo.

O método retorna um valor lógico indicando o sucesso ou falha do processo de seleção de características. O método StepWise() na classe Cmrmr inicializa as estruturas de dados necessárias e, em seguida, entra no ciclo de testes MCP. Se os testes MCP não forem solicitados (`num_reps` menor ou igual a 1), o ciclo é executado apenas uma vez. Dentro do ciclo, o método calcula a informação mútua entre cada preditor potencial e a variável alvo utilizando uma instância da classe Capm.

//+------------------------------------------------------------------+
//|  Stepwise feature selection based on mutual information          |
//+------------------------------------------------------------------+
bool Cmrmr::StepWise(matrix &predictors,vector &targets)
  {
   if(m_selected_vars.Size())
      ArrayFree(m_selected_vars);

   m_preds = predictors;
   m_samples = int(m_preds.Rows());
   m_vars = int(m_preds.Cols());
   m_max_preds = m_max_preds>=m_vars?m_vars:m_max_preds;
   m_target = targets;
   
   
   int i, j, k, ivar, irep;
   int index[], stepwise_mcpt_count[], solo_mcpt_count[], stepwise_ivar[], which_preds[],original_stepwise_ivar[] ;

   int nkept,best_ivar;
   vector casework(m_samples), sorted(m_samples), mutual(m_vars);
   vector crit(m_vars), relevance(m_vars), original_relevance(m_vars), current_crits(m_vars), sorted_crits(m_vars);
   double best_crit, dtemp, group_pvalue,solo_pvalue;
   vector stepwise_crit(m_vars), original_stepwise_crit(m_vars);
   double sum_relevance;
   vector original_sum_relevance(m_vars), sum_redundancy(m_vars);
   vector target = m_target;

   best_crit = -DBL_MAX;
   best_ivar = -1;
   nkept = m_max_preds;

   if(ArrayResize(index,m_vars)<0 || ArrayResize(stepwise_mcpt_count,m_vars)<0 ||
      ArrayResize(solo_mcpt_count,m_vars)<0 || ArrayResize(which_preds,m_vars)<0 || ArrayResize(stepwise_ivar,m_vars)<0 ||
      ArrayResize(original_stepwise_ivar,m_vars)<0)
     {
      Print(__FUNCTION__," array resize error ", GetLastError());
      return false;
     }

   int unif_error;
   for(irep=0 ; irep<m_reps ; irep++)
     {
      if(irep)
        {
         i = m_samples ;
         while(i > 1)
           {
            j = (int)(MathRandomUniform(0.0,1.0,unif_error) * i);
            if(unif_error)
              {
               Print(__FUNCTION__," Mathrandomuniform error ", GetLastError());
               return false;
              }
            if(j >= i)
               j = i - 1 ;
            dtemp = target[--i] ;
            target[i] = target[j] ;
            target[j] = dtemp ;
           }
        }

      for(i=0 ; i<m_vars ; i++)
         which_preds[i] = i ;

      crit = mutualinfo(m_vars,which_preds,target);

      for(ivar=0 ; ivar<m_vars ; ivar++)
        {
         relevance[ivar] = crit[ivar] ;
         if(ivar == 0  ||  crit[ivar] > best_crit)
           {
            best_crit = crit[ivar] ;
            best_ivar = ivar ;
           }
        }

      stepwise_crit[0] = best_crit ;
      stepwise_ivar[0] = best_ivar ;
      sum_relevance = best_crit ;
      if(irep == 0)
        {

         original_stepwise_crit[0] = best_crit ;
         original_stepwise_ivar[0] = best_ivar ;
         original_sum_relevance[0] = sum_relevance ;
         stepwise_mcpt_count[0] = 1 ;

         for(ivar=0 ; ivar<m_vars ; ivar++)
           {
            index[ivar] = ivar ;
            original_relevance[ivar] = sorted_crits[ivar] = current_crits[ivar] = crit[ivar] ;
            solo_mcpt_count[ivar] = 1 ;
           }

         if(!qsortdsi(0, m_vars-1, sorted_crits, index))
           {
            Print(__FUNCTION__, " failed qsort ");
            return false;
           }

         if(m_verbose)
            Print("       Variable         MI") ;

         for(i=m_vars-1 ; i>=0 ; i--)
           {
            k = index[i] ;

            if(m_verbose)
               PrintFormat("%15s %12.4lf",string(k), current_crits[k]) ;
           }
        }

      else
        {
         if(sum_relevance >= original_sum_relevance[0])
            ++stepwise_mcpt_count[0] ;
         for(ivar=0 ; ivar<m_vars ; ivar++)
           {
            if(relevance[ivar] >= original_relevance[ivar])
               ++solo_mcpt_count[ivar] ;
           }
        }

      for(i=0 ; i<m_vars ; i++)
         sum_redundancy[i] = 0.0 ;

      for(nkept=1 ; nkept<m_max_preds ; nkept++)
        {

         if(irep == 0 && m_verbose)
           {
            Print("Predictors so far   Relevance   Redundancy   Criterion") ;
            for(i=0 ; i<nkept ; i++)
              {
               k = stepwise_ivar[i] ;
               PrintFormat("%15s %12.4lf %12.4lf %12.4lf",string(k), relevance[k], relevance[k] - stepwise_crit[i], stepwise_crit[i]) ;
              }
           }

         k = 0 ;
         for(i=0 ; i<m_vars ; i++)
           {
            for(j=0 ; j<nkept ; j++)
              {
               if(stepwise_ivar[j] == i)
                  break ;
              }
            if(j == nkept)
               which_preds[k++] = i ;
           }
         if(k != (m_vars - nkept))
           {
            Print(__FUNCTION__, " failed assertion ", __LINE__);
            return false;
           }

         k = stepwise_ivar[nkept-1] ;

         casework = m_preds.Col(k);

         crit = mutualinfo(m_vars-nkept,which_preds,casework);

         for(i=0 ; i<(m_vars-nkept) ; i++)
           {
            k = which_preds[i] ;
            sum_redundancy[k] += crit[i] ;
            index[i] = k ;
            sorted_crits[i] = current_crits[i] = ((relevance[k] - sum_redundancy[k]) / double(nkept)) ;
            if(i == 0  ||  current_crits[i] > best_crit)
              {
               best_crit = current_crits[i] ;
               best_ivar = k ;
              }
           }

         stepwise_crit[nkept] = best_crit ;
         stepwise_ivar[nkept] = best_ivar ;
         sum_relevance += relevance[best_ivar] ;

         if(irep == 0)
           {
            original_stepwise_crit[nkept] = best_crit ;
            original_stepwise_ivar[nkept] = best_ivar ;
            original_sum_relevance[nkept] = sum_relevance ;
            stepwise_mcpt_count[nkept] = 1 ;

            if(!qsortdsi(0, m_vars-nkept-1, sorted_crits, index))
              {
               Print(__FUNCTION__, " failed qsort ");
               return false;
              }

            if(m_verbose)
              {
               Print("Additional candidates, in order of decreasing relevance minus redundancy") ;
               Print("       Variable     Relevance   Redundancy   Criterion") ;

               for(i=m_vars-nkept-1 ; i>=0 ; i--)
                 {
                  k = index[i] ;
                  PrintFormat("%15s %12.4lf %12.4lf %12.4lf",
                              string(k), relevance[k], sum_redundancy[k] / nkept,
                              relevance[k] - sum_redundancy[k] / nkept) ;
                 }
              }
           }

         else
           {
            if(sum_relevance >= original_sum_relevance[nkept])
               ++stepwise_mcpt_count[nkept] ;
           }

        }

     }
//---
   m_critical_values = matrix::Zeros(nkept,m_reps>1?5:3);
//---
   if(ArrayResize(m_selected_vars,nkept)<0)
     {
      Print(__FUNCTION__, " failed array resize ", GetLastError());
      return false;
     }
//---
   if(m_verbose)
     {
      if(m_reps > 1)
         Print("Final predictors ||  Relevance ||  Redundancy || Criterion  ||  Solo pval || Group pval") ;
      else
         Print("Final predictors  ||  Relevance ||  Redundancy ||  Criterion") ;
     }
//---
   for(i=0 ; i<nkept ; i++)
     {
      k = original_stepwise_ivar[i] ;
      m_selected_vars[i] = ulong(k);

      m_critical_values[i][0] = original_relevance[k];
      m_critical_values[i][1] = original_relevance[k] - original_stepwise_crit[i];
      m_critical_values[i][2] = original_stepwise_crit[i];

      if(m_critical_values.Cols()>3)
        {
         group_pvalue = (double) stepwise_mcpt_count[i] / (double) m_reps;
         solo_pvalue = (double) solo_mcpt_count[k] / (double) m_reps;
         m_critical_values[i][3] = solo_pvalue;
         m_critical_values[i][4] = group_pvalue;
        }

      if(m_verbose)
        {
         if(m_reps > 1)
            PrintFormat("%15s %12.4lf %12.4lf %12.4lf    %8.3lf    %8.3lf",string(k), m_critical_values[i][0], m_critical_values[i][1], m_critical_values[i][2],solo_pvalue,group_pvalue) ;
         else
            PrintFormat("%15s %12.4lf %12.4lf %12.4lf",string(k), m_critical_values[i][0], m_critical_values[i][1], m_critical_values[i][2]);
        }
     }

   return true;

  }

O método mutualinfo(), que é um método privado da classe Cmrmr, é responsável por estimar a informação mútua entre um determinado preditor e a variável alvo por meio do método de partição adaptativa. Ele retorna um vetor com as aproximações da informação mútua para os preditores selecionados, especificados como índices de colunas no mutualinfo().

//+------------------------------------------------------------------+
//| continuous mutual infomation                                     |
//+------------------------------------------------------------------+
vector Cmrmr::mutualinfo(int which_size,int &which[],vector &targs)
  {
   vector res = vector::Zeros(m_vars);

   res.Fill(-DBL_MAX);

   vector vars;

   Capm mia(true,targs,m_chisquare_thresh);

   for(int i = 0; i<which_size; i++)
     {
      vars = m_preds.Col(which[i]);
      res[i] = mia.fit(vars);
     }

   return res;

  }

O método StepWise() armazena os valores calculados de informação mútua em um vetor de "relevância", já que esses valores serão usados nas iterações seguintes para calcular o coeficiente de redundância. A característica com a maior informação mútua é escolhida como a primeira, e suas informações são armazenadas nas variáveis stepwise_crit e stepwise_ivar. Além disso, a variável sum_relevance é inicializada para acompanhar a relevância acumulada das características selecionadas, que mais tarde será utilizada nos testes MCP.

Na iteração inicial, sem permutações, o algoritmo guarda os valores originais de relevância e do critério para a primeira característica selecionada. Ele também inicializa os contadores para o teste global de significância do modelo e para os testes de significância individuais das características. Além disso, é exibida uma tabela mostrando a informação mútua de cada característica potencial em relação à variável alvo, a fim de fornecer uma visão do ranqueamento inicial das características. Essa tabela ajuda a visualizar o processo inicial de seleção e serve como referência para comparação com os valores permutados na etapa de testes de significância.

Se não estivermos na iteração inicial sem permutação, o algoritmo passa a executar os dois testes MCP, projetados para avaliar a significância estatística do conjunto de características selecionado pelo algoritmo MRMR. Para gerenciar de forma eficiente a crescente redundância à medida que novas características são adicionadas ao conjunto selecionado, o algoritmo utiliza o array `sum_redundancy`. Esse array é inicializado com zeros. Esse array é usado para armazenar a redundância acumulada de cada característica potencial em relação às já selecionadas.

No início, como nenhuma característica foi escolhida, todos os valores desse array permanecem zerados. Quando uma nova característica é adicionada ao conjunto selecionado, é necessário atualizar sua redundância em relação a cada uma das características potenciais restantes. A redundância de uma característica em relação ao conjunto selecionado em crescimento é definida como a média da informação mútua entre a característica potencial e as já escolhidas. Calcula-se a informação mútua entre a nova característica e cada uma das restantes, o que quantifica a quantidade de informação que a nova característica compartilha com cada uma delas. O valor de redundância de cada característica restante é atualizado somando-se a informação mútua entre a característica recém-adicionada e a já existente.

Após a atualização dos valores de redundância, o algoritmo calcula o critério MRMR (Máxima relevância, mínima redundância) para cada uma das características potenciais ainda não selecionadas. O critério MRMR é obtido subtraindo-se a redundância média de cada característica de sua relevância. Assim que o critério MRMR é calculado para todas as características restantes, é escolhida aquela com o maior valor de MRMR. O algoritmo repete esse processo de forma iterativa, atualizando o array `sum_redundancy` e recalculando o critério MRMR a cada nova adição. À medida que mais características são selecionadas, a redundância das restantes em relação ao conjunto escolhido é continuamente atualizada, garantindo que as relações dinâmicas entre características sejam consideradas durante todo o processo de seleção.

Se estivermos na replicação inicial, sem permutação, os valores originais dessas métricas são armazenados para comparação posterior durante os testes de permutação. Caso contrário, os contadores dos testes de permutação "globais" e "individuais" são incrementados. Após completar o número necessário de iterações, o algoritmo finaliza exibindo uma tabela resumida com as características selecionadas, sua relevância e os valores-p obtidos nos testes de permutação. Depois da chamada e execução bem-sucedida de `StepWise()`, é possível usar `GetCriticalValues()` para obter a matriz da tabela de valores exibida no modo detalhado. Já `GetSelectedVars()` extrai os índices das variáveis selecionadas (colunas), correspondentes à propriedade `m_max_preds` da classe.

//+------------------------------------------------------------------+
//| get the matrix of decision variables                             |
//| matrix rows arranged in terms of descending relevance of each    |
//| candidate predictor. Most relevant predictors critical values are|
//| in the first row, etc.                                           |
//| Column 0 is the calculated relevance                             |
//| Column 1 is the calculated redundancy                            |
//| Column 2 is the calculated Stepwise Mutual information           |
//| Column 3 and 4 will exist only if MCP test is specified          |
//| Column 3 is the Solo probability value                           |
//| Column 4 are the Group probability values                        |
//+------------------------------------------------------------------+
matrix Cmrmr::GetCriticalValues(void)
{
  return m_critical_values;
}
//+--------------------------------------------------------------------------+
//|get the indices of the most relevant predictors as determined by algorithm|
//+--------------------------------------------------------------------------+
bool Cmrmr::GetSelectedVars(ulong &output[])
{
 return (ArrayCopy(output,m_selected_vars)>0);
}

In the next section, we will see how the Cmrmr class is applied using synthetic and real-world datasets.


Exemplos de relevância menos redundância

Para ilustrar de forma clara a aplicação do algoritmo MRMR, começamos aplicando-o a um conjunto de dados sintético. Esse conjunto é composto por 100 amostras e 10 variáveis preditoras potenciais (características). A variável alvo Y é calculada pela soma das quatro primeiras colunas da matriz, de modo que a relação entre a variável alvo e os preditores seja determinística nesses casos. No entanto, as colunas de 4 a 7 não contêm nenhuma informação real sobre a variável alvo e funcionam apenas como características irrelevantes. Já as variáveis nas colunas 8 e 9 representam combinações das variáveis com índices de colunas {1,3} e {0,2}, respectivamente.

//---
   MathSrand(125);

   matrix rdata(100,10);

   rdata.Random(0.0,1.0);

   vector dep = rdata.Col(0) + rdata.Col(1) + rdata.Col(2) + rdata.Col(3);

   vector sum02 = rdata.Col(0) + rdata.Col(2);

   vector sum13 = rdata.Col(1) + rdata.Col(3);

   if(!rdata.Col(sum13,8) || !rdata.Col(sum02,9))
     {
      Print(" Failed column insertion ", GetLastError());
      return;
     }

O objetivo é usar o algoritmo MRMR para identificar quais colunas são os preditores mais relevantes. Esse experimento foi implementado no script RelevanceMinusRedundancy.mq5, que permite configurar diferentes hiperparâmetros, incluindo o número de permutações no método de Monte Carlo e o número máximo de características a serem selecionadas. O script foi configurado para ser executado em modo detalhado, fornecendo saídas completas durante o processo de seleção de características.

//inputs
input int NumReplications = 100;
input int MaxPredictors = 10;
input double ChiSquareThreshold = 6.0;

Os dados de entrada do script RelevanceMinusRedundancy.mq5 são os valores individuais da informação mútua entre cada preditor potencial e a variável alvo. Esses valores de informação mútua são organizados em ordem decrescente, facilitando a identificação das características mais relevantes.

Variable         MI
      9       0.2675
      8       0.1696
      0       0.1662
      3       0.1336
      2       0.0823
      1       0.0645
      6       0.0000
      7       0.0000
      4       0.0000
      5       0.0000

Como esperado, a primeira característica selecionada pelo algoritmo MRMR é aquela com a maior informação mútua individual em relação à variável alvo. Neste caso, trata-se da variável localizada na coluna de índice 9.

Predictors so far   Relevance   Redundancy   Criterion
            9       0.2675       0.0000       0.2675

A segunda característica selecionada é a variável da coluna de índice 8. Pelos resultados da análise, essa variável apresenta alta relevância, além de baixa redundância em relação à primeira característica selecionada.

Predictors so far   Relevance   Redundancy   Criterion
            9       0.2675       0.0000       0.2675
            8       0.1696       0.0000       0.1696

A terceira característica selecionada mostra como a redundância influencia o processo de escolha. Embora as variáveis das colunas 0, 1, 2 e 3 estejam diretamente relacionadas à variável alvo, sua alta redundância em relação às já selecionadas (colunas 8 e 9) reduz o valor do critério MRMR. Assim, o algoritmo passa a selecionar características menos diretamente associadas, mas com menor redundância em relação ao conjunto existente. Somente após a inclusão de algumas variáveis não relacionadas é que o algoritmo começa a priorizar as características das colunas 0, 1, 2 e 3, uma vez que sua redundância em relação ao conjunto selecionado diminui.

Additional candidates, in order of decreasing relevance minus redundancy
Variable     Relevance   Redundancy   Criterion
    5       0.0000       0.0000       0.0000
    4       0.0000       0.0000       0.0000
    7       0.0000       0.0000       0.0000
    6       0.0000       0.0000       0.0000
    1       0.0645       0.0600       0.0044
    0       0.1662       0.1351       0.0311
    2       0.0823       0.1426      -0.0603
    3       0.1336       0.1781      -0.0445

Enquanto as variáveis irrelevantes neste exemplo sintético apresentaram relevância nula, é importante destacar que, em cenários reais, características irrelevantes ainda podem mostrar algum grau de correlação com a variável alvo, resultando em redundância diferente de zero. Nessas situações, o algoritmo MRMR equilibra de forma eficaz relevância e redundância, priorizando as características que oferecem informações únicas.

Final predictors ||  Relevance ||  Redundancy || Criterion  || Solo pval|| Group pval
9                     0.2675        0.0000       0.2675       0.010       0.010
8                     0.1696        0.0000       0.1696       0.010       0.010
4                     0.0000        0.0000       0.0000       1.000       0.010
5                     0.0000        0.0000       0.0000       1.000       0.010
6                     0.0000        0.0000       0.0000       1.000       0.010
7                     0.0000        0.0000       0.0000       1.000       0.010
1                     0.0645        0.0738      -0.0093       0.010       0.010
0                     0.1662        0.1811      -0.0149       0.010       0.010
2                     0.0823        0.1077      -0.0254       0.010       0.010
3                     0.1336        0.1584      -0.0247       0.010       0.010

Os valores-p obtidos por meio dos testes de permutação de Monte Carlo fornecem informações valiosas sobre a significância estatística das características selecionadas. Valores-p mais baixos indicam evidências mais fortes de que a característica é de fato informativa e não fruto do acaso. Para determinar significância estatística, utiliza-se comumente o limiar de 0,05.

A seguir, é apresentada uma demonstração do algoritmo em um conjunto de dados mais realista. O script StepWiseFeatureSelectionByMutualInformation.mq5 mostra a aplicação prática do algoritmo MRMR em dados financeiros. Ele parte da premissa de prever a rentabilidade futura de um determinado ativo com base em um conjunto de indicadores técnicos. O script calcula quatro indicadores: Índice de Fluxo de Dinheiro (MFI), Média Móvel (MA), índice de força dos ursos e índice de força dos touros em diferentes janelas de retrospectiva. Esses indicadores funcionam como características potenciais no processo de seleção. Aplicando o algoritmo MRMR, o script busca identificar a combinação mais informativa de indicadores capazes de prever de forma eficaz a rentabilidade futura. Isso envolve selecionar os indicadores que melhor se relacionam com a variável alvo (rentabilidade futura), ao mesmo tempo em que se minimiza a redundância em relação às demais características escolhidas.

//+------------------------------------------------------------------+
//|                  StepWiseFeatureSelectionByMutualInformation.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
#resource "\\Indicators\\LogReturns.ex5"
#include<mutualinfo.mqh>
#include<ErrorDescription.mqh>
//+------------------------------------------------------------------+
//|indicator type                                                    |
//+------------------------------------------------------------------+
enum SELECT_INDICATOR
  {
   MFI=0,//MFI
   MA,//MA
   BEARS,//BEARS
   BULLS//BULLS
  };
//--- input parameters
input int NumReplications = 100;
input int MaxPredictors = 10;
input double ChiSquareThreshold = 6.0;
input bool VerboseOutPut = false;
input uint     period_inc=2;//lookback increment
input uint     max_lookback=6;
input ENUM_MA_METHOD         AppliedMA = MODE_SMA;
input ENUM_APPLIED_PRICE     AppliedPrice = PRICE_CLOSE;
input datetime SampleStartDate=D'2019.12.31';
input datetime SampleStopDate=D'2022.12.31';
input string   SetSymbol="BTCUSD";
input ENUM_TIMEFRAMES SetTF = PERIOD_D1;
//----
string predictor_names[];                      // variable names
int size_sample,                      //training set size
    size_observations,                //size of of both training and testing sets combined
    maxperiod,                        //maximum lookback
    indicator_handle=INVALID_HANDLE;  //long moving average indicator handle
//---
vector indicator[];                   //indicator indicator values;
//---
matrix feature_matrix;          //full matrix of features;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---get relative shift of sample set
   int samplestart,samplestop,num_features;
   samplestart=iBarShift(SetSymbol!=""?SetSymbol:NULL,SetTF,SampleStartDate);
   samplestop=iBarShift(SetSymbol!=""?SetSymbol:NULL,SetTF,SampleStopDate);
   num_features = int((max_lookback/period_inc)*4);
//---check for errors from ibarshift calls
   if(samplestart<0 || samplestop<0)
     {
      Print(ErrorDescription(GetLastError()));
      return;
     }
//---set the size of the sample sets
   size_observations=(samplestart - samplestop) + 1 ;
   maxperiod=int(max_lookback);
//---check for input errors
   if(size_observations<=0 || maxperiod<=0)
     {
      Print("Invalid inputs ");
      return;
     }
//---allocate memory
   if(ArrayResize(indicator,num_features+1)<0)
     {
      Print(ErrorDescription(GetLastError()));
      return;
     }
//----get the full collection of indicator values
   int period_len;
   int k=0;
//---
   for(SELECT_INDICATOR select_indicator = 0; select_indicator<4; select_indicator++)
     {
      for(int iperiod=0; iperiod<int((indicator.Size()-1)/4); iperiod++)
        {
         period_len=int((iperiod+1) * period_inc);
         int try
               =10;
         predictor_names.Push(EnumToString(select_indicator)+"_"+string(period_len));
         while(try)
           {
            switch(select_indicator)
              {
               case MFI:
                  indicator_handle=iMFI(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len,VOLUME_TICK);
                  break;
               case MA:
                  indicator_handle=iMA(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len,0,AppliedMA,AppliedPrice);
                  break;
               case BEARS:
                  indicator_handle=iBearsPower(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len);
                  break;
               case BULLS:
                  indicator_handle=iBullsPower(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len);
                  break;
              }

            if(indicator_handle==INVALID_HANDLE)
               try
                  --;
            else
               break;
           }

         if(indicator_handle==INVALID_HANDLE)
           {
            Print("Invalid indicator handle ",EnumToString(select_indicator)," ", GetLastError());
            return;
           }

         Comment("copying data to buffer for indicator ",period_len);
         try
               = 0;
         while(!indicator[k].CopyIndicatorBuffer(indicator_handle,0,samplestop,size_observations) && try
                  <10)
              {
               try
                  ++;
               Sleep(5000);
              }

         if(try
               <10)
               ++k;
         else
           {
            Print("error copying to indicator buffers ",GetLastError());
            Comment("");
            return;
           }

         if(indicator_handle!=INVALID_HANDLE && IndicatorRelease(indicator_handle))
            indicator_handle=INVALID_HANDLE;
        }
     }

//---resize matrix
   if(!feature_matrix.Resize(size_observations,indicator.Size()-1))
     {
      Print(__LINE__);
      Print(ErrorDescription(GetLastError()));
      Comment("");
      return;
     }
//---copy collected data to matrix
   for(ulong i = 0; i<feature_matrix.Cols(); i++)
      if(!feature_matrix.Col(indicator[i],i))
        {
         Print(__LINE__);
         Print(ErrorDescription(GetLastError()));
         Comment("");
         return;
        }
//---
   int try
         = 10;
   while(try
            >-1 && indicator_handle == INVALID_HANDLE)
        {
         indicator_handle=iCustom(SetSymbol!=""?SetSymbol:NULL,SetTF,"\\Indicators\\LogReturns",0,1,1);
         try
            --;
        }
//---
   if(try
         <0)
        {
         Print("Could not initialize returns indicator ");
         Print(ErrorDescription(GetLastError()));
         Comment("");
         return;
        }
   else
     {
      try
            = 10;
     }
//---
   while(try
            >-1 && !indicator[indicator.Size()-1].CopyIndicatorBuffer(indicator_handle,0,samplestop-1,size_observations))
        {
         Sleep(2000);
         try
            --;
        }
//---
   if(try
         <0)
        {
         Print("Could not collect returns indicator info ");
         Print(ErrorDescription(GetLastError()));
         Comment("");
         return;
        }
   else
     {
      IndicatorRelease(indicator_handle);
      Comment("");
     }
//--- display the dataset
   string console;
   for(uint i = 0; i<predictor_names.Size(); i++)
      console+=StringFormat(" %12s ",predictor_names[i]);
   console+=" NextBar Returns (Target) ";
   Print(console);
   for(ulong i = 0; i<feature_matrix.Rows(); i++)
     {
      console = "";
      for(ulong j = 0; j<feature_matrix.Cols(); j++)
         console += StringFormat(" %12.6lf ",feature_matrix[i][j]);
      console+=StringFormat(" %12.6lf ",indicator[indicator.Size()-1][i]);
      Print(console);
     }
//---
   Cmrmr mstep(NumReplications,MaxPredictors,ChiSquareThreshold,VerboseOutPut);
//---
   if(!mstep.StepWise(feature_matrix,indicator[indicator.Size()-1]))
      return;
//---
   Print(" Final predictor labels ");
   ulong variables[];
   if(mstep.GetSelectedVars(variables))
     {
      for(uint i = 0; i<variables.Size(); i++)
         Print(predictor_names[variables[i]]);
     }
   return;
  }
//+------------------------------------------------------------------+
Abaixo está um trecho do conjunto de dados de indicadores coletados no terminal MetaTrader 5, juntamente com a variável alvo na última coluna. No total, existem 12 preditores potenciais (indicadores).
MFI_2         MFI_4         MFI_6          MA_2          MA_4          MA_6       BEARS_2       BEARS_4       BEARS_6       BULLS_2       BULLS_4       BULLS_6  NextBar Returns(Target)
0.000000     53.151442     46.921608   7213.654000   7279.552750   7260.007333    -76.371267   -101.867797   -107.113146     87.265733     61.769203     56.523854     -0.003564
0.000000     26.316595     34.451328   7180.962000   7244.558000   7255.420333    -41.795089    -70.889078    -83.462247     42.344911     13.250922      0.677753     -0.032447
0.000000      0.000000     36.720977   7053.740000   7133.697000   7204.281833   -127.731696   -210.813447   -251.244462    153.948304     70.866553     30.435538      0.054562

A seguir estão os resultados da análise dos preditores usando os parâmetros padrão do script. Primeiro, observamos a tabela de valores de informação mútua. Aqui, vemos que alguns indicadores apresentam informação mútua nula em relação ao alvo. O indicador de média móvel com período igual a 6 possui a maior informação mútua em relação ao alvo.
PS          Variable         MI
ND                 5       0.0308
LG                 4       0.0293
MJ                 3       0.0279
GM                 6       0.0227
JP                 9       0.0182
IS                 1       0.0165
MF                 8       0.0135
JI                 7       0.0126
HL                 0       0.0000
JO                 2       0.0000
GS                10       0.0000
HD                11       0.0000

Naturalmente, o MA_6 é a primeira escolha feita pelo algoritmo. Como essa é a primeira seleção, torna-se interessante verificar se existem outros preditores potenciais que compartilham informações com ele (MA_6). Isso pode ser feito analisando os valores de redundância mostrados a seguir.

ME   Predictors so far   Relevance   Redundancy   Criterion
GH                 5       0.0308       0.0000       0.0308
II   Additional candidates, in order of decreasing relevance minus redundancy
FE          Variable     Relevance   Redundancy   Criterion
LE                 0       0.0000       0.0095      -0.0095
ED                 1       0.0165       0.0869      -0.0704
FD                 2       0.0000       0.1149      -0.1149
PE                11       0.0000       0.2768      -0.2768
ID                 8       0.0135       0.3251      -0.3116
NS                 7       0.0126       0.3492      -0.3366
CR                10       0.0000       0.3484      -0.3484
ES                 6       0.0227       0.4030      -0.3802
IS                 9       0.0182       0.4285      -0.4103
CR                 3       0.0279       2.5363      -2.5084
DR                 4       0.0293       3.0096      -2.9803
A próxima seleção do algoritmo é o MFI_2, já que ele apresenta a menor redundância entre todos os preditores potenciais restantes, apesar de sua relevância nula em relação à variável alvo. Também vale destacar que os preditores nas colunas de índice 3 e 4 (MA_2 e MA_4, respectivamente) possuem os maiores níveis de redundância com o MA_6. Isso é esperado, pois trata-se do mesmo indicador calculado com janelas menores. Abaixo está o preditor escolhido após mais duas seleções, ambas também com relevância nula em relação à variável alvo. É importante observar o preditor principal no conjunto de candidatos que não foi selecionado. O algoritmo passa a considerar os preditores relevantes em virtude do efeito de diluição causado pelas duas últimas seleções no conjunto escolhido como um todo.
PQ   Predictors so far   Relevance   Redundancy   Criterion
FL                 5       0.0308       0.0000       0.0308
JK                 0       0.0000       0.0095      -0.0095
DK                 2       0.0000       0.1796      -0.1796
EE  Additional candidates, in order of decreasing relevance minus redundancy
JH         Variable     Relevance   Redundancy   Criterion
CH                6       0.0227       0.1469      -0.1241
PH                9       0.0182       0.1465      -0.1283
GI               10       0.0000       0.2024      -0.2024
PG                7       0.0126       0.2133      -0.2007
PF               11       0.0000       0.2308      -0.2308
GG                8       0.0135       0.2664      -0.2529
QG                1       0.0165       0.4679      -0.4514
KF                3       0.0279       0.8840      -0.8561
LF                4       0.0293       1.0372      -1.0079
Encerramos nosso comentário sobre os resultados avançando até o conjunto final de preditores selecionados pelo algoritmo.
OQ  Final predictors ||  Relevance ||  Redundancy || Criterion  ||  Solo pval || Group pval
HN                5       0.0308       0.0000       0.0308       0.010       0.010
DJ                0       0.0000       0.0095      -0.0095       1.000       0.010
JG                2       0.0000       0.1796      -0.1796       1.000       0.010
HD                6       0.0227       0.1620      -0.1393       0.010       0.010
FQ                9       0.0182       0.2023      -0.1842       0.010       0.010
DL               11       0.0000       0.2798      -0.2798       1.000       0.010
KJ                1       0.0165       0.2932      -0.2767       0.010       0.010
OG                7       0.0126       0.3298      -0.3172       0.010       0.010
OE               10       0.0000       0.4151      -0.4151       1.000       0.010
JP                8       0.0135       0.4585      -0.4450       0.010       0.010
RN   Final predictor labels
NO  MA_6
DE  MFI_2
NN  MFI_6
KP  BEARS_2
DJ  BULLS_2
FL  BULLS_6
PQ  MFI_4
MK  BEARS_4
FM  BULLS_4
OF  BEARS_6


Considerações finais

Neste artigo, foi analisada a aplicação da informação mútua na seleção de características. O foco principal foi o algoritmo MRMR (máxima dependência, máxima relevância, mínima redundância), proposto por Peng, Long e Ding. Iniciamos com a discussão sobre a avaliação da informação mútua para variáveis contínuas, ressaltando sua capacidade de capturar dependências não lineares complexas entre características e a variável alvo. Ao longo da investigação do algoritmo MRMR, mostramos como ele equilibra de maneira eficaz as tarefas concorrentes de maximizar a relevância em relação à variável alvo e, ao mesmo tempo, minimizar a redundância entre as características já selecionadas.

Por meio da adição iterativa de características com base em seus valores MRMR e da avaliação de sua significância estatística usando os testes de permutação de Monte Carlo, o algoritmo garante que o conjunto selecionado forneça os preditores mais informativos e únicos para o modelo. Os exemplos sintéticos e reais demonstraram a utilidade prática do algoritmo MRMR em cenários reais de processamento de dados, nos quais características irrelevantes e multicolinearidade frequentemente dificultam o processo de seleção. A capacidade do algoritmo MRMR de priorizar características relevantes e evitar redundância garante que ele continue sendo uma ferramenta valiosa na seleção de características, especialmente em situações em que as relações entre variáveis são complexas e não lineares.

Todo o código citado neste artigo está incluído abaixo. A tabela a seguir descreve todos os arquivos de código-fonte que acompanham o artigo.

Nome do arquivo
Descrição
MQL5/include/np.mqh
Arquivo de cabeçalho com funções utilitárias de matrizes e vetores.
MQL5/include/mutualinfo.mqh
Arquivo de cabeçalho que contém a definição da classe Capm, que implementa o método de partição adaptativa para estimar a informação mútua de variáveis contínuas. E a definição da classe Cmrmr, que implementa a seleção progressiva de características baseada em informação mútua.
MQL5/scripts/RelevanceMinusRedundancy.mq5
Script que demonstra o uso da classe Cmrmr com dados sintéticos.
MQL5/scripts/StepWiseFeatureSelectionByMutualInformation.mq5
Outro script que demonstra a aplicação da classe Cmrmr em um conjunto de dados mais realista.

Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/16416

Criando um Painel de Administração de Trading em MQL5 (Parte VI): Interface de Funções Múltiplas (I) Criando um Painel de Administração de Trading em MQL5 (Parte VI): Interface de Funções Múltiplas (I)
O papel do Administrador de Trading vai além das comunicações via Telegram; ele também pode realizar várias atividades de controle, incluindo gerenciamento de ordens, acompanhamento de posições e personalização da interface. Neste artigo, compartilharemos insights práticos sobre como expandir nosso programa para suportar múltiplas funcionalidades em MQL5. Esta atualização tem como objetivo superar a limitação atual do Painel de Administração de se concentrar principalmente na comunicação, permitindo que ele lide com uma gama mais ampla de tarefas.
Negociando com o Calendário Econômico MQL5 (Parte 2): Criando um Painel de Notícias Negociando com o Calendário Econômico MQL5 (Parte 2): Criando um Painel de Notícias
Neste artigo, criamos um painel prático de notícias usando o Calendário Econômico MQL5 para aprimorar nossa estratégia de negociação. Começamos projetando o layout, focando em elementos-chave como nomes dos eventos, importância e horário, antes de avançar para a configuração dentro do MQL5. Por fim, implementamos um sistema de filtragem para exibir apenas as notícias mais relevantes, dando aos traders acesso rápido a eventos econômicos impactantes.
Redes Generativas Adversariais (GANs) para Dados Sintéticos em Modelagem Financeira (Parte 1): Introdução às GANs e Dados Sintéticos em Modelagem Financeira Redes Generativas Adversariais (GANs) para Dados Sintéticos em Modelagem Financeira (Parte 1): Introdução às GANs e Dados Sintéticos em Modelagem Financeira
Este artigo introduz os traders às Redes Generativas Adversariais (GANs) para geração de dados financeiros sintéticos, abordando limitações de dados no treinamento de modelos. Ele cobre os fundamentos das GANs, implementações em Python e MQL5, e aplicações práticas em finanças, capacitando traders a aumentar a precisão e a robustez dos modelos por meio de dados sintéticos.
Desenvolvimento de ferramentas para análise do movimento de preços (Parte 2): Script de comentários analíticos Desenvolvimento de ferramentas para análise do movimento de preços (Parte 2): Script de comentários analíticos
Dando continuidade ao nosso trabalho para simplificar a interação com o comportamento do preço, temos o prazer de apresentar mais uma ferramenta que pode melhorar significativamente sua análise de mercado e ajudar na tomada de decisões bem fundamentadas. Esta ferramenta exibe indicadores técnicos importantes, como os preços do dia anterior, níveis significativos de suporte e resistência, além do volume de negociação, gerando automaticamente dicas visuais no gráfico.