Node.js - Aprendendo um pouco de Funcional com Estatística

Sim! Isso mesmo que você leu!

Isso aconteceu pois tenho um aluno co-orientado, William Dias, que está fazendo um TCC sobre Ensino de Estatística aplicada com Tecnologia e como eu gosto muitchoo.

Cumpadi Whashington eu gosto muito

Logo sempre ajudo ele a refatorar seus códigos, o último foi sobre MODA!

Não essa!

Essa moda - wikipedia.org/wiki/Moda_(estat%C3%ADstica).

Segundo a Wikipedia:

Em estatística descritiva, a moda é o valor que detém o maior número de observações, ou seja, "o valor que ocorre com maior frequência num conjunto de dados, isto é, o valor mais comum".

Basicamente é achar o elemento com maior ocorrência no Array, se não existir nenhum que tenha mais ocorrências então a moda é 0 ou amodal.

O código em questão é esse e ele não funcionava:

'use strict'
 /**/

 function moda (arr) {
   return ((arr.sort((a, b) =>
   (arr.filter(v => v === a).length) - (arr.filter(v => v === b).length))
 ).pop())
 }
module.exports = moda  
console.log(moda([1,2,3,4,5])) //amodal nao tem moda(nao deveria aparecer nada)  
console.log(moda([1,2,3,4,5,5])) //modal tem moda(5) pois aparece mais vezes  
console.log(moda([1,2,3,3,4,4,5,5])) //plurimodal tem mais de 1 moda(3,4,5)pois aparecem mais vezes  

Porém dessa forma não dá para entendermos o que está acontecendo, então eu separei para:

const achaMaior = (counter) => Math.max.apply(null, counter)  
const ordenacao = (a, b) => a - b  
const ordenar = (arr, ordenacao) => arr.sort(ordenacao)  
const mapear = (name) => {  
  return {count: 1, name: name}
}
const reduzir = (a, b) => {  
  a[b.name] = (a[b.name] || 0) + b.count
  return a
}
const mapearParaArray = (contagem) => {  
  const counter = []
  Object.keys(contagem).filter((a) => {
    counter.push(contagem[a])
  })
  return counter
}
const filtraModa = (contagem, MAX) => Object.keys(contagem).filter((a) => {  
  return (contagem[a] === MAX) ? contagem[a] : null
})

function moda(arr) {  
  const ordenado =  ordenar(arr, ordenacao)
  let contagem = ordenado.map(mapear).reduce(reduzir, {}) 
  const counter = mapearParaArray(contagem)
  const MAX = achaMaior(counter) 
  return filtraModa(contagem, MAX)
}
module.exports = moda  

Dessa vez percebemos que a função moda possui 4 fases:

  • ordenar do menor para o maior
  • contar cada ocorrência
  • achar o maior
  • filtrar o maior

Vamos só refatorar um pouco para melhorar ela para:

function moda(arr) {  
  const contagem = arr.sort(ordenacao).map(mapear).reduce(reduzir, {}) 
  return filtraModa(contagem, achaMaior(mapearParaArray(contagem)))
}

Nesse momento ela ainda não retorna 0 se não tiver moda e ainda não está muito Funcional, então vamos melhorar isso..

Inicialmente ordenamos o array, exemplo [2, 1, 3, 3] do menor para o maior para facilitar a busca das mesmas ocorrências no Array, a função ordenarMenorParaMaior será chamada na função sort

const ordenarMenorParaMaior = (a, b) => a - b  

A função sort precisa receber uma funcaoDeComparacao para ordenar:

  • Se funcaoDeComparacao(a, b) for menos que 0, ordena a para um índice anterior a b, i.e. a vem primeiro.
  • Se funcaoDeComparacao(a, b) retornar 0, deixa a e b inalterados em relação um ao outro, mas ordenado em relação a todos os outros elementos. Nota: O padrão ECMAscript não garante este comportamento, e, portanto, nem todos os navegadores (e.g. Versões do Mozilla anteriores a 2003) respeitarão isto.
  • Se funcaoDeComparacao(a, b) é maior que 0, ordena b para um índice anterior que a.
  • funcaoDeComparacao(a, b) sempre deve retornar o mesmo valor dado um par específico de elementos a e b como seus dois parametros. Se resultados inconsistentes são retornados, então a ordenação é indefinida.

OU SEJA, quando comparamos com nossa função a - b:

  • 2 e 1: 2 - 1 = 1
    • se é maior que 0 então a, 2, vem DEPOIS que b, 1
  • 2 e 3: 2 - 3 = -1
    • se é menor que 0 então a, 2, vem ANTES que b, 3
  • 3 e 3: 3 - 3 = 0
    • se é igual 0 então permanecem inalterados

Assim por diante.

Depois mapeamos a ocorrência de cada número no Array com count = 1 e seu valor como name, deixando essa função assim:

const mapearOcorencias = (name) => ({count: 1, name: name})  

Vamos ver como fica o resultado de {count: 1, name: name}:

{ count: 1, name: 1 }
{ count: 1, name: 2 }
{ count: 1, name: 3 }
{ count: 1, name: 3 }

Perceba então que apenas mapeamos cada ocorrência encontrada no Arra, aí sim iremos reduziremos esse mapeamento para um objeto a que vai adicionando cada elemento mapeado, a[b.name], com a sua quantidade de ocorrências encontradas no Array, (a[b.name] || 0) + b.count, essa função contarOcorrencia será chamada no reduce:

const contarOcorrencia = (anterior, atual) => {  
  anterior[atual.name] = (anterior[atual.name] || 0) + atual.count
  return anterior
}

Tendo como retorno:

{ '1': 1 }
{ '1': 1, '2': 1 }
{ '1': 1, '2': 1, '3': 1 }
{ '1': 1, '2': 1, '3': 2 }

Pois caso exista anterior[atual.name], ou seja, '3': 1 ela irá retornar anterior[atual.name], porém esse valor é resultado de:

anterior[atual.name] + atual.count  
// atual.name = 3 // '3':1
// anterior[atual.name] = 1
// 1 + 1
// anterior[atual.name] = 2

`

Sim isso aqui é o famoso map/reduce!

Então o retorno de:

const contagem = arr  
    .sort(ordenarMenorParaMaior)
    .map(mapearOcorencias)
    .reduce(contarOcorrencia)

É:

{ '1': 1, '2': 1, '3': 2 }

Para filtrar a moda precisamos pegar as chaves da contagem que é o retorno do reduce(contarOcorrencia) e com isso testarmos se o seu valor é MAIOR QUE 1 E se é IGUAL a MAX que é o valor resultante da função acharMaior(mapearParaArray(contagem)).

Se passar pelo teste retornará o mesmo valor contagem[a], se não passar não retornará nada, null, pois como é uma função filter o resultado final é apenas o verdadeiro, nesse caso ? contagem[a]:

const filtrarModa = (contagem, MAX) =>  
  Object.keys(contagem).filter((a) => 
    ((contagem[a] > 1 && contagem[a] === MAX) 
      ? contagem[a] 
      : null
  ))

Depois disso vamos mapear o objeto contagem para Array para depois chamarmos a função acharMaior que irá receber esse Array e retornará apenas qual o maior valor encontrado no Array, usei a função mapearParaArray refatorada pelo Marcelo Camargo:

const mapearParaArray = (obj, inputKeys, acc = []) => {  
  const keys = (undefined === inputKeys)
    ? Object.keys(obj)
    : inputKeys
  const [head, ...tail] = keys

  return (0 === keys.length)
    ? acc
    : mapearParaArray(obj, tail, acc.concat([obj[head]]))
}

Parece Klingon para um pobre humano como eu, então eu dexavei essa função para explicá-la para vocês!

Na primeira chamada da função mapearParaArray = (obj, inputKeys, acc = []) nós temos a seguinte entrada:

  • obj: { '2': 1, '3': 2, count: 1, name: 1 }
  • inputKeys: undefined
  • acc: []

O obj possui essa estrutura pois ele é o resultado do nosso reduce, o inputKeys vem como undefined pois não passamos seu valor na primeira chamada, assim como acc vem como [] pois definimos ela via Default Parameters

Inicialmente pegamos as chaves do objeto com Object.keys(obj) que retornará o Array [ '2', '3', 'count', 'name' ] para keys, apenas na primeira chamada e depois sempre retornará o valor de inputKeys, isso será muito importante para a próxima chamada dessa função em mapearParaArray(obj, tail, acc.concat([obj[head]])).

Onde inputKeys será o valor de tail, lembrando que o tail sempre será o Array de keys menos o primeiro elemento head.

Por isso separamos a cabeça/head do Array, head = 2, do seu rabo/tail, [ '3', 'count', 'name' ] usando Destructuring:

  const [head, ...tail] = keys

Agora vamos analisar o retorno dessa função:

return (0 === keys.length)  
    ? acc
    : mapearParaArray(obj, tail, acc.concat([obj[head]]))

Se o tamanho do Array keys for igual a 0, (keys.length === 0 sim eu a inverti pra ficar mais fácil de visualizar, retornamos o acc se não retornamos a chamada para a mesma função passando os seguintes parâmetro (obj, tail, acc.concat([obj[head]])):

  • obj: nosso objeto mapeado e reduzido
  • tail: a cada iteração retorna 1 Array sem o primeiro elemento, head
  • acc.concat([obj[head]])): retorna 1 Array sempre adicionando o valor de obj[head]

SIM! Isso é uma função recursiva!

A maior dúvida deve estar na funcionalidade desse acc.concat([obj[head]])), correto?

Então vamos ver o que acontece na última chamada, utilizando o Array já ordenado [1,2,3,3]:

obj { '2': 1, '3': 2, count: 1, name: 1 }  
Object.keys(obj) [ '2', '3', 'count', 'name' ]  
acc [ 1, 2, 1, 1 ]  

Pois quando chegarmos na última iteração, onde (0 === keys.length) a função mapearParaArray irá retornar [ 1, 2, 1, 1 ] que será usado na função acharMaior em onst filtrada = filtrarModa(contagem, acharMaior(mapearParaArray(contagem))), bom depois de achar o maior, 2, precisamos chamar a função filtrarModa passando como primeiro parâmetro que é nosso objeto mapeado contagem e o maior valor encontrado para as ocorrências para que essa função filtrarModa retorne o valor correto, contagem[a], quando (contagem[a] > 1 && contagem[a] === MAX), lembrando que contagem e obj serão os mesmos vamos ver o teste de mesa dessa função:

contagem { '2': 1, '3': 2, count: 1, name: 1 }  
a 2  
MAX 2  
contagem[a] 1  
contagem[a] > 1 && contagem[a] === MAX false  
a 3  
MAX 2  
contagem[a] 2  
contagem[a] > 1 && contagem[a] === MAX true  
a count  
MAX 2  
contagem[a] 1  
contagem[a] > 1 && contagem[a] === MAX false  
a name  
MAX 2  
contagem[a] 1  
contagem[a] > 1 && contagem[a] === MAX false  

Logo o único valor retornado no Array novo é o a = 3 onde a sua quantidade de ocorrências é contagem[a] = 2.

Então o retorno para const filtrada é [ '3' ], sendo o retorno da função moda:

return filtrada.length ? filtrada : 0  

Então como filtrada.length é MAIOR QUE 0, por isso é verdadeiro, retornará [ '3' ], se não fosse retornaria 0 que é o valor amodal.

Depois de separarmos cada função agora iremos compor a função moda com as funções pré-criada:

function moda(arr) {  
  const contagem = arr
    .sort(ordenarMenorParaMaior)
    .map(mapearOcorencias)
    .reduce(contarOcorrencia) 
  const filtrada = filtrarModa(contagem, acharMaior(mapearParaArray(contagem)))
  return filtrada.length ? filtrada : 0
}
module.exports = moda  

Mesmo assim não está muito funcional e eu só vi a resposta, BEM FUNCIONAL, da Jú Gonçalves depois que já tinha escrito essa explicação hehehehehe então fica aí para vocês mais 1 código para se comparar e tentar entender como as coisas acontecem de modo puro e funcional.

Fico por aqui deixando 1 convite para vocês, entrem no nosso Grupo de Programação Funcional no Facebook e no Grupo de Programação Funcional no Telegram

Além disso ainda teremos O PRIMEIRO meetup sobre Programação Funcional da Webschool em Sampa!

Se você for de Sampa e AMA <3 Funcional e JavaScript NÃO PODE PERDER!
Increva-se em https://www.eventick.com.br/meetup-fp-sampa-setembro-2016

Referências:

BONUS TIME

Para facilitar o entendimento de não-programadores vou descrever aqui o algoritmo para a solução desse problema:

  1. Ordene o Array do menor para o maior.
  2. Mapeie a quantidade de ocorrências de cada elemento do Array.
  3. Reduza esse mapeamento para 1 objeto que contenha como nome do atributo o valor do elemento e como valor desse atributo a quantidade de vezes que ele aparece no Array.
  4. Filtre esse Array para achar qual elemento teve o maior número de ocorrências.
    4.1 Antes descubra qual o maior valor das ocorrências.
  5. Caso não exista nenhum elemento que tenha mais ocorrências que outro então retorne 0, pois é amodal.
  6. Caso tenha 1 elemento que possui mais ocorrências que os outros retorne 1 Array com esse elemento, o qual indica qual é a moda.
  7. Caso tenha mais de 1 elemento que possua a mesma quantidade de ocorrências, sendo essa quantidade igual, então retorne 1 Array com esses elementos, pois pode ser bimodal ou multimodal.

E para melhorar ainda o entendimeto da função mapearParaArray vamos ver o teste de mesa dela:

contagem { '2': 1, '3': 2, count: 1, name: 1 }

// PRIMEIRA CHAMDA
obj { '2': 1, '3': 2, count: 1, name: 1 }  
inputKeys undefined  
acc []  
keys [ '2', '3', 'count', 'name' ]  
head 2  
tail [ '3', 'count', 'name' ]  
acc.concat([obj[head]]) [ 1 ]

// SEGUNDA CHAMDA
obj { '2': 1, '3': 2, count: 1, name: 1 }  
inputKeys [ '3', 'count', 'name' ]  
acc [ 1 ]  
keys [ '3', 'count', 'name' ]  
head 3  
tail [ 'count', 'name' ]  
acc.concat([obj[head]]) [ 1, 2 ]

// TERCEIRA CHAMDA
obj { '2': 1, '3': 2, count: 1, name: 1 }  
inputKeys [ 'count', 'name' ]  
acc [ 1, 2 ]  
keys [ 'count', 'name' ]  
head count  
tail [ 'name' ]  
acc.concat([obj[head]]) [ 1, 2, 1 ]

// QUARTA CHAMDA
obj { '2': 1, '3': 2, count: 1, name: 1 }  
inputKeys [ 'name' ]  
acc [ 1, 2, 1 ]  
keys [ 'name' ]  
head name  
tail []  
acc.concat([obj[head]]) [ 1, 2, 1, 1 ]

// QUINTA CHAMDA
obj { '2': 1, '3': 2, count: 1, name: 1 }  
inputKeys []  
acc [ 1, 2, 1, 1 ]  
keys []  
head undefined  
tail []  
acc.concat([obj[head]]) [ 1, 2, 1, 1, undefined ]  

Então ele irá retornar [ 1, 2, 1, 1, undefined ] para a função acharMaior que irá apenas retornar o valor que é o maior entre os elementos do Array, no caso o 2, e será esse valor usado em filtrarModa(contagem, acharMaior(mapearParaArray(contagem))).

Então ficando:

filtrarModa({ '2': 1, '3': 2, count: 1, name: 1 }, 2)  

E o retorno dessa função será a chave que possui o valor achado como máximo, o 2 e PRONTO!

Simples né?

Comentários

comments powered by Disqus