Análise Exploratória de Dados com Javascript –  Parte 1: Manipulação de Dados

A linguagem Javascript é uma das que mais crescem no mundo, tanto em recursos quanto em relação à comunidade. O objetivo aqui não é apresentar profundidade teórica ou caracterizar Javascript como a melhor solução para a análise de dados, mas mostrar algumas das principais abordagens com o uso da linguagem Javascript e suas bibliotecas para a realização da análise exploratória de dados.

Antes de começarmos, quero ressaltar que esta é uma série de 3 artigos sobre análise de dados com Javascript, que seguirão a seguinte ordem:

  • Parte 1 – Manipulação de Dados
  • Parte 2 – Estatística Descritiva
  • Parte 3 – Visualização de Dados Online

Neste artigo você aprenderá:

  • Coleta e preparação de dados para torna-los acessíveis para a aplicação de técnicas estatísticas;
  • Manipulação e transformação de séries de dados;
  • Filtragem e Ordenação;
  • Concatenação de Séries de Dados.

Análise Exploratória de Dados

Este tipo de análise tem por objetivo descrever os dados de forma ampla e detalhada, usando métodos numéricos e gráficos, proporcionando uma visão aprofundada para a aplicação dos modelos e consequentemente a extração do conhecimento.

O código completo apresentado neste artigo pode ser encontrado no GitHub.

Manipulação de dados

Para a manipulação dos dados vamos utilizar a biblioteca data-forge, inspirada na famosa biblioteca Python para manipulação de dados e análise descritiva: o Pandas.

Para os exemplos apresentados neste capítulo, foi utilizada a famosa base Iris, provavelmente uma das bases mais conhecidas dentre os estudos de análise de dados e reconhecimento de padrões.

Para iniciar o projeto vamos executar três comandos, um para criar um novo arquivo package.json e outro para instalar nossa dependência:

$ npm init
$ npm install data-forge -save

Carregando dados

Vamos carregar o conjunto de dados Iris que está no formato .csv como um data frame. Os data frames são objetos usados para guardar tabelas com dados, formada por um conjunto de linhas e atributos. Para isso, vamos invocar a função readFileSync que irá realizar a leitura do arquivo e por fim parseCSV que converterá os dados do arquivo CSV para o formato de objeto DataFrame.

let dataFrame = dataForge.readFileSync('irisdata.csv').parseCSV();

Para extrair linhas como arrays de dados (ordenados por coluna):

let arrayOfArrays = dataFrame.toRows();

console.log(arrayOfArrays);

Também é possível obter um array de objetos, onde os nomes das colunas serão os atributos do objeto. Esta notação é a mais comum dentre o desenvolvimento de soluções em Javascript e sua utilização é muito simples:

let arrayOfObjs = dataFrame.toArray();

console.log(arrayOfObjects);

Todas as linhas de registro em nosso data frame possuem um índice, este é fundamental para o mapeamento e a manipulação dos dados. Por exemplo, em uma série temporalpoderíamos ter as informações temporais como um índice e isso seria a forma como nós iríamos manipular estas informações.

Para facilitar este trabalho, temos a função toPairs() que retorna um array de pares <Índice, Valor> para cada linha de registro.

let arrayOfPairs = dataFrame.toPairs();

console.log(arrayOfPairs);

O uso de índices é algo particularmente importante para a redução dos dados e a criação de um novo data frame. A função between(Início, fim) faz esse papel e sua utilização é bem simples, basta invocar a função repassando os índices de início e fim, retornando como resultado um novo data frame.

let rowSubset = dataFrame.between(10, 11);

console.log(rowSubset.toArray());

Como vimos anteriormente, podemos recuperar as informações do data frame como um conjunto de pares de dados Índice-Valor. Logo, é importante ter condições de obter todos índices do data frame, para isso, podemos invocar a função getIndex().

let index = dataFrame.getIndex();

console.log('Retornando os índices', index);

Os Data frames possuem uma função muito importante que possibilita a iteração linha a linha, para isto basta invocar a função forEach(callback). No callback desta função, temos um parâmetro que representa cada linha de dado como um objeto javascript.

dataFrame.forEach((row)=>{
     console.log(row);
});

Trabalhando com colunas e séries de dados

Para trabalhar individualmente com cada coluna, a biblioteca data-forge, nos provê algumas funções que facilitam muito este trabalho. Lembrando que em diversas operações, seja de transformação ou tratamento de dados, precisaremos trabalhar de forma direcionada com cada coluna e série de dados.

Para obter a lista de nomes de cada coluna, ou seja, os atributos do data frame, podemos invocar a função getColumnNames(), como mostrado abaixo:

let arrayOfColumnNames = dataFrame.getColumnNames();

console.log(arrayOfArrays);

Podemos obter cada série de dados exclusivamente, utilizando a função getSeries(), que nos retorna um vetor unidimensional com os dados da coluna selecionada.

var series_sepallength = dataFrame.getSeries('sepallength');

O resultado desta operação pode ser obtido como um array com os dados em cada posição ou como pares de dados, utilizando novamente a função toPairs():

result = series_sepallength.toArray();

console.log(series_sepallength.toPairs());

Assim como iteramos o data frame, é possível invocar a função forEach em apenas uma série de dados, neste caso o valor dentro do callback representa o valor da série.

series_sepallength.forEach(function(value) {
     console.log(value);
});

Deste modo, são comuns os casos que nem todos os atributos de um dataset serão usados para análise, neste caso, você poderá criar um subconjunto de dados, invocando a função subset().

Criar um novo quadro de dados a partir de um subconjunto de colunas:

var columnSubset = dataFrame.subset(['sepalwidth', 'petalwidth']);

console.log(columnSubset.toArray());

Assim, você poderá aplicar os estudos sobre este novo modelo, sem as colunas indesejadas interferindo em suas análises.

Ocasionalmente, de acordo com a necessidade de cada problema, novas colunas podem ser adicionadas a um data frame. Isso não altera o original, mas gera um novo com a coluna adicional. No exemplo abaixo, utilizamos a função withSeries(), onde em seu callback instanciamos um objeto do tipo Series passando o atributo values com um array de dados.

let newDf = dataFrame.withSeries('NEW_COLUMN', ()=>{
   return new dataForge.Series({
          values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
   })
});

console.log(newDf.toArray());

Caso este novo atributo tenha menos registros que os demais, ao ser iterado seu valor virá como undefined, quando for inexistente.

Se ao invocar a função withSeries(), o nome da coluna for o mesmo de uma já existente, esta será substituída, pela nova série que estará sendo passada como parâmetro:

var newDf = dataFrame.withSeries('sepalwidth', series_sepallength);

Uma dica importante para a criação de novas séries, é evitar o uso de traços, espaços ou caracteres especiais nos nomes dos atributos, isso facilita a manipulação e evita problemas de codificação de caracteres, dependendo do sistema operacional que esteja utilizando.

Uma outra forma bem interessante de criar novas séries de dados, é com uma sugestiva função chamada generateSeries(), onde de uma forma bem estruturada em objeto javascript podemos criar séries baseadas em séries já existentes manipulando os seus dados, o que pode ser uma alternativa bem simplificada para o tratamento dos dados.

No exemplo abaixo, estamos gerando uma coluna nova chamada de SomeNewColumn que conterá os dados da coluna sepalwidth.

var newDf = dataFrame.generateSeries({'SomeNewColumn': (row)=>{
    return row['sepalwidth']; 
  }
});

console.log(newDf.toString());

Transformação da série de dados

Até o momento trabalhamos apenas com a modificação direta sobre os data frames, entretanto, é possível que seja preciso trabalhar unicamente com as séries. Abaixo é apresentado uma forma de transformar os dados de uma série para a criação de outra.

Para isso utilizamos a função select(), que para cada valor, invocará a função transform(), por fim, esta nova série é adicionada com o uso da função withSeries.

function transformValue(value) {
   return value * 1000;
}

var newDf = dataFrame.withSeries('SomeNewColumn', df =>    
  dataFrame.getSeries('sepalwidth').select(value =>  
  transformValue(value))
);

console.log(newDf.toString());

Neste caso, os valores da nova coluna, serão o produto dos valores da coluna existente por 1000. Do mesmo modo, caso seja repassada uma coluna já existente, a mesma terá seus valores substituídos por esta transformação.

function transformValue(row) {
   return row * 1000;
}

var newDf = dataFrame.withSeries('sepalwidth', dataFrame =>    
    dataFrame.getSeries('sepalwidth').select(row =>    
   transformValue(row))
);

console.log(newDf.toString());

Há também uma conveniente função chamada transformSeries():

var newDf = dataFrame.transformSeries({'sepalwidth': row =>    
    transformValue(row)
});

console.log(newDf.toArray());

A função select() tem uma utilização mais ampla e pode ser usada para modificar mais de uma série, auxiliando na criação de colunas. Sua utilização segue o mesmo padrão anterior, entretanto, para diversificar a compreensão, vamos manter o callback padrão, retornando um objeto contendo dois atributos, ambos representado as novas colunas que serão criadas dentro do novo data frame.

let transformedDataFrame = dataFrame.select((row)=>{
   return {
      NewColumn: row.sepalwidth * 2,
      AnotherNewColumn: Math.random().toFixed(2)
   };
});

console.log(transformedDataFrame.toString());

Removendo colunas

Como uma alternativa a criação de um subconjunto de dados, é a remoção das colunas. O data forge possui uma função apropriada para esta finalidade chamada dropSeries() que pode receber uma String com o nome da coluna ou um Array de Strings contendo os nomes das colunas que deverão ser removidas. O resultado desta função gera um novo data frame.

var newDf = dataFrame.dropSeries(['sepallength', 'sepalwidth']);

console.log(newDf.between(0, 2).toString());

Neste exemplo, utilizamos a função between(<start>,<end>), esta função nos retorna apenas os índices entre 2, e por fim invocamos a função toString() que retorna os dados organizados e tabulados, facilitando a visualização e a compreensão dos dados, como mostrado na figura abaixo:

Subconjuntos de dados

Criar subconjunto de dados, pode ser a chave para facilitar diversos tipos de análises que poderão ser feitas sobre os dados. Existem várias maneiras de extrair um subconjunto de dados de uma série ou de um data frame, uma dessas formas e a mais simplificada, é utilizando as funções skip() e take(), que servem para ignorar e obter uma certa quantidade de registros, respectivamente.

No exemplo abaixo, estamos ignorando os 10 primeiros registros e obtendo os 15 próximos, criando assim um subconjunto de dados.

var subset = dataFrame.skip(10).take(15);

console.log(subset.toString());

Além disso, em análise de dados, é comum verificarmos as amostras do início e fim do data frame, para isso podemos utilizar as funções head() e tail(), como mostrado no exemplo abaixo:

let head = dataFrame.head(10);
let tail = dataFrame.tail(5);

console.log(head.toString());
console.log(tail.toString());

Existem funções mais avançadas, que são: skipWhiletakeWhileskipUntil e takeUntil. Todas essas funções, podem ser utilizadas para filtragem e criação de subconjuntos, onde a condição fica dentro a funções de callback, que irá iterar cada linha de registro e precisa retornar um valor booleano.

No exemplo abaixo, criamos um novo subconjunto de dados que contém o atributo class igual a ‘Iris-virginica’.

var newSeries = dataFrame.skipUntil(row => (row) => {
    return row.class !== 'Iris-virginica';
});

Além disso, poder ter filtros ainda mais inteligentes se trabalharmos com índices, abaixo encontra-se uma tabela completa com as funções que nos ajudarão a realizar a filtragem, e o trabalho com subconjuntos de dados:

==============================================================
Function | Description
==============================================================
startAt | todos os valores a partir de um índice específico
endAt   | todos os valores terminando em um índice particular
after   | todos os valores após um índice particular.
before  | todos os valores antes de um índice particular.
between | todos os valores entre dois índices
==============================================================

Filtragem de Dados

Sem dúvida, a filtragem de dados será uma das funções mais utilizadas durante o processo de investigação e análise exploratória. Precisamos de respostas rápidas, imediatas, e nesse sentido a filtragem de dados é muito útil.

Além disso, como veremos posteriormente, na composição de gráficos, precisaremos muito desta filtragem. Já vimos anteriormente algumas estratégias para a realização da filtragem, na biblioteca data forge podemos contar também com a função where() que facilita o processo de filtragem e sua utilização é totalmente flexível para cada finalidade. No exemplo abaixo, filtramos os dados originais e criamos um subconjunto de dados apenas para os registros que o atributo class é igual a ‘Iris-versicolor’.

var newDf = dataFrame.where(function(row) {
    return row.class == ‘Iris-versicolor’
});

console.log(newDf.toString());

Distinct values

Outra funcionalidade muito comum e totalmente relacionada a filtragem é quando queremos obter os valores únicos de uma série de dados, neste caso usamos a função distinct(), em seu callback devemos retornar o atributo que será filtrado. Desta forma os valores duplicados serão totalmente removidos e o novo subconjunto será composto apenas por registros únicos do atributo selecionado. O exemplo abaixo, mostra como esta função é utilizada:

var distinctDataFrame = dataFrame.distinct(function(row) {
    return row.class;
});

console.log(distinctDataFrame.toString());

Existe uma forma mais simplificada de utilização, onde a função será aplicada diretamente sobre uma série de dados:

var distinctSeries = dataFrame.getSeries('sepallength').distinct();

console.log(distinctSeries.toString());

Agrupamentos

A construção de agrupamento é uma ferramenta útil para a análise de dados em muitas situações diferentes. Esta técnica pode ser usada para reduzir a dimensão de um conjunto de dados, reduzindo uma ampla gama de objetos à informação do centro do seu conjunto. Uma vantagem que facilita o agrupamento de dados no data forge é o fato de podermos aplicar a mesma função seja um data frame ou uma série de dados.

Observe que agrupar os dados, também é um processo de filtragem, ou seja, você tem a sua disposição diversas alternativas para fazer uma filtragem, fica a critério do tipo de análise que deseja realizar. No exemplo abaixo, estamos agrupando o atributo sepallength:

var sepallengthGroup = dataFrame.groupBy(function(row) {
    return row.sepallength;
});

console.log(sepallengthGroup.toString());

Isso também pode ser feito com séries de dados, neste caso em específico podemos incrementar a função de callback para adicionar uma nova implementação ao agrupamento.

var outputSeries = dataFrame.getSeries('sepallength')
.groupBy(function(value) {
      return value;
});

console.log(outputSeries.toString());

Ordenação

A ordenação de elementos em uma lista ou vetor é uma atividade fundamental em vários problemas computacionais. Quanto mais facilitada for esta atividade, maior será nossa capacidade de gerar análises e identificar padrões a priori sobre os dados.

Para isso, temos algumas funções muito simples que são: orderBy e orderByDescending que ordenam de forma ascendente e descendente, respectivamente. Além destas, temos as funções thenBy e thenByDescending para especificar critérios de classificação adicionais, como mostrado no exemplo abaixo:

let sortedAscending = dataFrame.orderBy(row => row.sepallength);
let sortedDescending = dataFrame.orderByDescending(row => row.sepalwidth);

console.log(sortedAscending);
console.log(sortedDescending);

let sorted = dataFrame
  .orderBy(row => row.sepallength)
  .thenByDescending(row => row.sepalwidth)
  .orderBy(row => row.petallength);

console.log(sorted.between(20, 40).toString());

Concatenando Séries e Data frames

Existe um trabalho árduo para os analistas de dados, quando estamos diante de bases diferentes que precisam ser unidas, seja em volume de registros ou na inclusão de novos atributos provenientes de uma outra base de dados. Para este objetivo, temos na biblioteca data forge, recursos muito interessantes para facilitar este trabalho.

Vamos começar com a junção de dois data frames, formando um só, para isso vamos utilizar a função concat() perceba que inicialmente criamos os dois data frames e em seguida realizamos a concatenação:

/** Create data frame */
var df_a = new dataForge.DataFrame({
    columnNames: ['id','name','email'],
    values: [[1, 'Alana', '[email protected]'], 
             [2, 'Aline', '[email protected]']],
});

/** Create data frame */
var df_b = new dataForge.DataFrame({
    columnNames: ['id','name','email'],
    values: [[3, 'Celia', '[email protected]'],
             [4, 'Alaide', '[email protected]']]
});

/** concat two data frames */
var df_new = df_a.concat(df_b);

console.log(df_new.toString());

Podemos agora, criar um novo data frame, por exemplo, para guardar as idades de cada usuário do novo conjunto de dados criado a partir da concatenação.

/** Create data frame */
var df_ages = new dataForge.DataFrame({
columnNames: ['id','age'],
values: [[1, 25],
         [2, 28],
         [3, 50],
         [4, 62]]
});

Em seguida vamos juntar as tabelas, para que o conjunto de dados receba uma nova coluna, vamos utilizar a função join(), que de forma análoga aos bancos de dados, aqui também possuirá a direção dos dados, seja à direita ou à esquerda. Mapeamos então os registros pelos seus respectivos Ids, e a última função de callback irá receber via parâmetro. Por fim, basta invocar os atributos a partir de cada parâmetro e criar o objeto que representará o novo registro de dados.

var df_merged = df_new.join(df_ages,
    left => left.id,
    right => right.id,
    (left, right) => {
         return {
             subject_id: left.id,
             name: left.name,
             email: left.email,
             age: right.age
    };
});

Tendo assim, como resultado, a adição de mais um atributo neste data frame, como mostrado na figura abaixo:

Resultado após a concatenação

Considerações Finais

Como você pôde ver ao longo deste artigo, as principais funções para manipulação de dados que você encontra em outras plataformas, também encontrará em Javascript. Existem inúmeras formas de otimizar os códigos aqui expostos, mas a intenção é exatamente deixar da forma mais simples possível.

No proximo, veremos com a mesma riqueza de detalhes sobre como fazer análises baseadas em estatísticas descritivas utilizando Javascript.

Enjoy! 😉

Dúvidas e sugestões, fique à vontade para deixar um recado.

About João Gabriel Lima

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *