Na ciência de dados, o gerenciamento eficaz de consultas é fundamental. Mergulhe na estratégia do Nubank, onde o poder de Scala e Spark é aproveitado para garantir transformações de dados eficientes.

Descubra como a complexa arquitetura de dados do Nubank, que inclui extração, transformação e carregamento, trabalha perfeitamente com Scala para manter a consistência e governança dos dados.

Este guia oferece uma imersão nas nuances da codificação e exemplos do mundo real, demonstrando as imensas capacidades de Scala e Spark no manejo de consultas complexas.

Uma breve introdução à estrutura de dados do Nubank 

Antes de mergulharmos na parte de programação, é crucial entender a estrutura de dados do Nubank. Em resumo, nossa arquitetura de dados é composta por três partes principais:

  1. Extração: Os dados são obtidos de microsserviços ou outras fontes, como o Instituto Brasileiro de Geografia e Estatística (IBGE), Banco Central, etc. Esses dados são extraídos diariamente e armazenados na Amazon S3.
  2. Transformação e carregamento: Esse processo ocorre uma vez por dia. Aqui, gerenciamos nossas transformações usando um repositório de consultas. Cada ‘bloco’ em nossa estrutura de dados representa um conjunto de dados ou modelo. Para contextualizar, gerenciamos mais de 60.000 conjuntos de dados com contribuições de mais de mil pessoas todos os meses.
  3. Disponibilidade e uso: Após o processamento, os dados são carregados em uma nova S3 e no Google Cloud. Esses dados podem ser acessados através do Databricks, BigQuery, etc., oferecendo um ambiente de dados democratizado.

Check our job opportunies

O papel do Scala e Spark

Scala e Spark entram em cena durante a etapa de transformação. Com a grande quantidade de transformações ocorrendo, é essencial gerenciar nossas consultas de maneira eficaz, garantindo a consistência e governança dos dados. É aqui que Scala e Spark realmente se destacam.

Entrando na parte de codificação, é importante mencionar que utilizamos o Databricks, que é semelhante ao Jupyter. Este é um ambiente de execução no estilo de um notebook.

Um exemplo prático com Scala e Spark

 Vamos demonstrar uma consulta simples construída usando uma ‘Tabela Base’. Esta tabela consiste em detalhes de transações, como a data da solicitação e informações do cliente.

  1. Filtrando dados: Assim como no SQL, a linguagem do Spark permite filtrar dados de maneira eficiente. Por exemplo, usando a cláusula ‘where’, é possível filtrar as transações com status ‘concluído’.
  2. Adicionando colunas:Podemos adicionar uma coluna ‘dataSolicitacao’ aos nossos dados usando a função ‘withColumn’ no Spark. Essa coluna deriva dados de ‘timestampSolicitacao’, convertendo os timestamps em um formato de data.

Embora o código inicial consista em apenas algumas linhas, o poder do Scala e Spark nos permite reescrever as transformações como cadeias baseadas em funções puras. Essa abordagem estruturada ajuda a gerenciar consultas complexas de maneira mais eficiente.

Metadados: um ingrediente essencial e aproveitando o poder do Scala

Ao lidar com grandes volumes de dados, os metadados se tornam cruciais. Eles fornecem informações sobre a consulta, seu autor, a descrição e muito mais.

Embora seja possível manter um repositório de metadados separado, integrar a consulta com seus metadados é mais eficiente, simplificando o processo de transformação e gerenciamento de dados.

A grande vantagem do Scala é sua capacidade de combinar programação orientada a objetos com programação funcional de forma fluida.

Para estruturar melhor nossos metadados, introduzimos o ‘Spark Query’—um Trait em Scala (semelhante a uma interface no Java ou uma classe abstrata em outras linguagens). Isso nos permite definir uma consulta com seus metadados associados.

Por exemplo, um ‘Spark Query’ pode ter:

  • tableName:Define onde o resultado da consulta será armazenado.
  • description: Fornece um breve resumo sobre o propósito da consulta.

Preparando o terreno

Imagine o desafio: você tem uma consulta e precisa encaixá-la na sua estrutura predefinida. Isso exige a criação de um objeto para a consulta definida previamente. Para fins deste tutorial, podemos nomeá-lo de CompletedPixTransactions.

Criando nosso objeto

 Para implementar um trait em Scala, usamos a palavra-chave extends. Após estender nosso trait, definimos todos os elementos necessários dentro dele. Como o Scala possui capacidades impressionantes de inferência de tipos, não é necessário especificar o tipo explicitamente para tudo. Por exemplo, se tableName é uma string, basta definir nossa string.

Para este exercício, nosso tableName será nomeado CompletedPixMovements. É aqui que nossos dados serão armazenados. Para fornecer uma breve descrição, ou o que chamaríamos de ExampleDataset, considere-o como um instantâneo do nosso projeto em andamento. A equipe proprietária (vamos chamá-la de OwnerTeam) não precisa ser compreendida em profundidade para este contexto. Nossos dados dizem respeito ao Brasil, então, naturalmente, todo o nosso conteúdo está em português.

Após avaliar nossa consulta, concluímos que seu nível de QualityAssurance é alto, indicando um conceito de negócio bem escrito e preciso. Quanto aos nossos inputs, mantivemos tudo simples, usando apenas uma tabela: PixMovements.

Construindo metadados e a consulta

Agora vem a parte crucial. Como redigir nossa consulta? Vamos recuperar nossa consulta escrita anteriormente e incorporá-la dentro da nossa estrutura de encapsulamento. Com Scala, ao construir um objeto dessa maneira, temos a liberdade de adicionar detalhes adicionais. Se desejado, podemos acrescentar metadados de frequência para indicar a frequência de execução da consulta.

Funções exclusivas para essa consulta do Spark também podem ser aninhadas dentro dela. Isso melhora a clareza, pois quando a consulta é escrita, essas funções podem ser referenciadas diretamente.

O próximo passo é extrair nossos dados de entrada do dataframe, aplicar as transformações necessárias como antes, e evitar quaisquer funções de exibição, já que nossa principal preocupação é a execução da consulta, e não seu mecanismo de exibição ou salvamento.

Com a execução bem-sucedida, essa abordagem nos ajuda a encapsular tanto nossa consulta quanto seus metadados associados dentro de um único objeto padronizado.

Execução e resultados

O processo para executar essa consulta encapsulada é bastante intuitivo. Primeiro, especifique a entrada da sua consulta, que normalmente é um mapa (map). Em seguida, acesse os dados dentro da tabela usando o nome desse mapa. Esse objeto pode então chamar a consulta definida e inserir dados nela. Se executada corretamente, você deve ver resultados semelhantes aos anteriores.

Para salvar os resultados em uma tabela—um procedimento padrão em pipelines—você pode utilizar os comandos do Spark. O nome da tabela está predefinido dentro do nosso objeto como TableName. Ao executar, os resultados da consulta serão armazenados nessa tabela especificada.

Em resumo, encapsulamos com sucesso uma consulta básica e seus metadados em um objeto padronizado, oferecendo um método claro e robusto para gerenciar consultas Spark com Scala.

Um passo além: múltiplas consultas

Ambientes de produção do mundo real lidam não apenas com uma consulta, mas potencialmente com centenas. Gerenciar volumes desse tipo exige uma abordagem mais avançada.

No nosso segmento inicial, aprendemos como criar uma consulta Spark usando a API do Scala, com ênfase em funções puras e camadas de governança aprimoradas. À medida que avançamos, vamos explorar como aproveitar o poder combinado da programação orientada a objetos e da linguagem funcional com Scala.

Dentro da nossa Spark Query, possuímos um atributo chamado QueryInputs. Esse atributo contém uma lista detalhando as entradas para uma consulta específica.

Por exemplo, se quisermos determinar o número de entradas para uma dada consulta, podemos facilmente fazer isso chamando o método .size na nossa lista.

Criando a função getNumberOfInputs 

Para demonstrar, podemos criar uma função chamada getNumberOfInputs que busca o número de entradas. Quando essa função é aplicada a uma lista de consultas, ela retorna uma lista de inteiros, representando o número de entradas para cada consulta.

Essa transformação é possível usando o operador map. O operador map aplica uma função dada a cada item de uma lista de entrada, criando uma nova lista que contém os itens retornados pela função.

Entendendo a operação de reduce:

Outra operação crucial na programação funcional é o reduce. O objetivo do reduce é pegar uma lista de elementos e condensá-la em um único valor.

Por exemplo, dada uma lista de números, pode-se usar o reduce para calcular a média ou a soma dessa lista. A função aceita pelo método reduce normalmente recebe dois argumentos e retorna um único resultado.

Para visualizar, imagine aplicar uma função sequencialmente a pares de elementos em uma lista, reduzindo gradualmente até obter um único resultado acumulado.

Somando as entradas

Para ilustrar, considere um cenário onde temos uma lista de inteiros representando entradas de consultas. Podemos definir uma função chamada sumNumberOfInputs, que calcula a soma de dois inteiros.

Quando aplicada à nossa lista usando o reduce, essa função acumularia o total de entradas em todas as consultas.

Identificando consultas da equipe de engenheiros do PIX

Para isolar as consultas feitas pela equipe de engenheiros do PIX, utilizamos a operação de filtragem. Essa operação processa uma lista, retendo apenas os elementos que correspondem a uma determinada condição.

Para o nosso caso, definimos uma função chamada isFromTeamPixEngineers, que verifica se uma determinada consulta é originária dessa equipe específica.

Isolando consultas de baixa qualidade

A partir da nossa lista filtrada de consultas dos engenheiros do PIX, podemos querer identificar quais delas são de baixa qualidade. Podemos introduzir outra condição de filtragem, verificando o atributo QualityAssurance de cada consulta e comparando com o valor ‘low’.

Uma exigência comum em ambientes ETL é identificar dependências. Suponha que queremos determinar quais conjuntos de dados dependem do dataset Meetup PixMovements. Podemos criar outra função de filtro para examinar nossa lista e encontrar consultas que dependem desse dataset específico.

Esse exercício é crucial, especialmente ao considerar os efeitos em cadeia de alterar uma consulta fundamental.

Desafiando as normas: exigindo mais do Scala 

Depois de estabelecer um sistema robusto de consultas, o que vem a seguir? Aqui estão algumas provocações:

Implementando testes rigorosos

  1. Garantindo alta validade: Por exemplo, todos os conjuntos de dados da equipe PixEngineers devem manter alta validade. Assim, podemos aceitar apenas transformações que cumpram essa regra.
  2. Limitando entradas de consulta: Nenhuma consulta deve depender de mais de 10 entradas. A ideia é evitar dependências excessivamente complexas que poderiam ser difíceis de depurar ou manter.
  3. Evitando dependências cíclicas: Por exemplo, se o conjunto de dados A depende do conjunto de dados B, e o B depende do C, então o C não deve depender do A. Dependências cíclicas podem introduzir bugs sutis e problemas de desempenho.

Essas são práticas que incorporamos na nossa governança de dados no Nubank, garantindo controle e consistência.

Capacidades avançadas do Scala

A flexibilidade do Scala oferece possibilidades ainda mais avançadas:

  1. Visualização de dependências: Dada uma lista de todas as consultas, o Scala nos permite interpretar o grafo de dependências e visualizá-lo. Cada conjunto de dados é representado como um bloco, com cadeias mostrando suas dependências. Essa visualização pode ser enriquecida por meio de cores, codificando os conjuntos de dados com base na sua qualidade, permitindo insights rápidos sobre possíveis áreas de preocupação.
  2. Metadados como dados: Outra capacidade interessante envolve tratar metadados como dados reais. Ao capturar os metadados de transformações, podemos criar uma tabela e integrá-la nos nossos processos ETL. Esses dados podem então ser consumidos e monitorados, oferecendo insights em tempo real.

Por exemplo, com apenas algumas linhas de código, podemos criar um dataframe, que nos permite analisar a distribuição dos dados, como o número de conjuntos de dados por país ou equipe responsável, em tempo real.

Dominar Scala e Spark é crucial para otimizar as transformações e o gerenciamento de dados. Conforme ilustrado pela metodologia do Nubank, a integração de ambas as ferramentas oferece uma solução abrangente para os desafios da governança de dados.

Com recursos como encapsular consultas e metadados, visualizar dependências e tratar metadados como dados reais, o Scala se revela um ativo inestimável para os profissionais de dados. À medida que os ecossistemas de dados continuam a crescer e se tornar mais complexos, adotar tais estratégias se torna essencial para empresas que buscam eficiência, clareza e uma governança de dados robusta.

Confira o que compartilhamos sobre esse tema no Meetup a seguir:

Check our job opportunies