En Ciencia de Datos, la gestión efectiva de consultas es fundamental. Sumérgete en la estrategia de Nubank, donde se aprovecha el poder de Scala y Spark para asegurar transformaciones de datos eficientes.

Descubre cómo la intrincada arquitectura de datos de Nubank, que incluye extracción, transformación y carga, funciona de manera fluida con Scala para mantener la consistencia y gobernanza de los datos.

Esta guía ofrece una inmersión profunda en los matices de la codificación y ejemplos del mundo real, mostrando las enormes capacidades de Scala y Spark para manejar consultas complejas.

Una breve introducción a la estructura de datos de Nubank

Antes de profundizar en la parte de programación, es crucial entender la estructura de datos de Nubank. En resumen, nuestra arquitectura de datos se compone de tres partes estándar:

  1. Extracción: Los datos se obtienen de microservicios u otras fuentes de datos, como el Instituto Brasileño de Geografía y Estadística (IBGE), el Banco Central, etc. Luego, los datos se extraen diariamente y se almacenan en Amazon S3.
  2. Transformación y carga: Este proceso ocurre una vez al día. Aquí, gestionamos nuestras transformaciones utilizando un repositorio de consultas. Cada ‘bloque’ en nuestra estructura de datos representa un dataset o modelo. Para darte un contexto, gestionamos más de 60,000 datasets con contribuciones de más de mil personas cada mes.
  3. Disponibilidad y uso: Después del procesamiento, los datos se cargan en un nuevo S3 y Google Cloud. Estos datos pueden ser accesados a través de Databricks, BigQuery, etc., ofreciendo un entorno de datos democratizado.

Descubre las oportunidades

El papel de Scala y Spark

Scala y Spark entran en juego durante la etapa de transformación. Con la gran cantidad de transformaciones que ocurren, es imperativo gestionar nuestras consultas de manera efectiva, asegurando la consistencia y gobernanza de los datos. Aquí es donde Scala y Spark brillan.

Profundizando en el aspecto de la codificación, es importante mencionar que utilizamos Databricks, que es similar a Jupyter. Es un entorno de ejecución con el formato de un cuaderno de notas.

Un ejemplo práctico con Scala y Spark

Vamos a demostrar una consulta simple construida utilizando una ‘Tabla Base’. Esta tabla consta de detalles de transacciones, como la fecha de solicitud y la información del cliente.

  1. Filtrado de datos: Al igual que en SQL, el lenguaje de Spark permite filtrar datos de manera eficiente. Por ejemplo, utilizando la cláusula ‘where’, se pueden filtrar las transacciones con un estado ‘completado’.
  2. Añadir columnas: Podemos añadir una columna ‘requestDate’ a nuestros datos utilizando la función ‘withColumn’ en Spark. Esta columna deriva su información de ‘requestTimestamp’, convirtiendo las marcas de tiempo en un formato de fecha.

Aunque el código inicial consta de solo unas pocas líneas, el poder de Scala y Spark nos permite reescribir las transformaciones como cadenas basadas en funciones puras. Este enfoque estructurado ayuda a gestionar consultas complejas de manera más eficiente.

MetaDatos: un ingrediente esencial y aprovechando el poder de Scala

Cuando se manejan grandes cantidades de datos, los metadatos se vuelven cruciales. Estos proporcionan información sobre la consulta, su autor, la descripción y más.

Aunque es posible mantener un repositorio de metadatos separado, integrar la consulta con sus metadatos es más eficiente, simplificando el proceso de transformación y gestión de datos.

La propuesta única de Scala es su capacidad para combinar la programación orientada a objetos con la programación funcional de manera fluida.

Para estructurar mejor nuestros metadatos, introducimos ‘Spark Query’, un Trait en Scala (similar a una interfaz en Java o una clase abstracta en otros lenguajes). Esto nos permite definir una consulta con sus metadatos asociados.

Por ejemplo, una ‘Spark Query’ puede tener:

  • tableName: Define dónde se almacenará el resultado de la consulta.
  • description: Ofrece una breve descripción del propósito de la consulta.

Preparando el escenario

Imagina el desafío: tienes una consulta y necesitas integrarla en tu estructura predefinida. Esto requiere que crees un objeto para la consulta previamente definida. Para efectos de este tutorial, podemos llamarlo CompletedPixTransactions..

Creando nuestro objeto

Para implementar un hilo en Scala, usamos la palabra clave extends. Después de extender nuestro hilo, definiremos todos los elementos necesarios dentro. Dado que Scala cuenta con capacidades impresionantes de inferencia de tipos, no es necesario especificar explícitamente el tipo para todo. Por ejemplo, si tableName es una cadena de texto, simplemente definimos nuestra cadena.

Para este ejercicio, nuestro tableName se llamará CompletedPixMovements. Aquí es donde residirán nuestros datos. Para proporcionar una breve descripción o lo que llamaríamos un ExampleDataset, considérelo como una instantánea de nuestro proyecto en curso. El equipo propietario (llamémoslo OwnerTeam) no necesita ser entendido en profundidad para este contexto. Nuestros datos pertenecen a Brasil, por lo que, naturalmente, todo nuestro contenido está en portugués.

Después de evaluar nuestra consulta, concluimos que su nivel de QualityAssurance es alto, lo que indica un concepto de negocio bien escrito y preciso. En cuanto a nuestras entradas, lo hemos mantenido simple utilizando solo una tabla: PixMovements.

Construyendo metadatos y la consulta

Ahora viene la parte crucial. ¿Cómo redactamos nuestra consulta? Recuperaremos nuestra consulta previamente escrita y la integraremos dentro de nuestra estructura de encapsulación. Con Scala, al construir un objeto de esta manera, hay libertad para añadir detalles suplementarios. Si se desea, se puede agregar un metadato de frecuencia para indicar la frecuencia de ejecución de la consulta.

Funciones únicas exclusivas de esta consulta de Spark también pueden estar anidadas dentro. Esto mejora la claridad, ya que al escribir la consulta, estas funciones pueden ser referenciadas directamente.

El siguiente paso es extraer nuestros datos de entrada del dataframe, aplicar las transformaciones necesarias como antes, y evitar cualquier función de visualización ya que nuestra principal preocupación es la ejecución de la consulta, no su visualización o mecanismo de guardado.

Con una ejecución exitosa, este enfoque nos ayuda a encapsular tanto nuestra consulta como sus metadatos asociados dentro de un solo objeto estandarizado.

Ejecución y resultados

El proceso para ejecutar esta consulta encapsulada es bastante intuitivo. Primero, especifica la entrada de tu consulta, que típicamente es un mapa, luego accede a los datos dentro de la tabla usando el nombre del mapa. Este objeto puede entonces llamar a la consulta definida e introducir los datos en ella. Si se ejecuta correctamente, deberías ver resultados similares a los de antes.

Para guardar los resultados en una tabla—un procedimiento estándar en las pipelines—puedes utilizar los comandos de Spark. El nombre de la tabla está predefinido dentro de nuestro objeto como TableName. Al ejecutar, los resultados de la consulta serán almacenados en esta tabla especificada.

En resumen, hemos encapsulado exitosamente una consulta básica y sus metadatos en un objeto estandarizado, ofreciendo un método claro y robusto para gestionar consultas de Spark con Scala.

Un paso más allá: múltiples consultas

Los entornos de producción reales no lidian con una sola consulta, sino potencialmente con cientos. Manejar tal volumen requiere un enfoque más avanzado.

En nuestro segmento inicial, aprendimos cómo elaborar una consulta de Spark utilizando la API de Scala, enfatizando funciones puras y capas de gobernanza mejoradas. A medida que avanzamos, profundizaremos en aprovechar el poder combinado de la programación orientada a objetos y el lenguaje funcional con Scala.

Dentro de nuestra consulta de Spark, poseemos un atributo llamado QueryInputs. Este atributo contiene una lista que detalla las entradas para una consulta específica.

Por ejemplo, si queremos determinar el número de entradas para una consulta dada, podríamos hacerlo fácilmente llamando al método .size en nuestra lista.

Creando la función getNumberOfInputs

Para demostrar, podemos crear una función llamada getNumberOfInputs que obtenga el número de entradas. Cuando esta función se aplica a una lista de consultas, produciría una lista de enteros que representa la cantidad de entradas para cada consulta.

Esta transformación es posible utilizando el operador map. El operador map aplica una función dada a cada elemento de una lista de entrada, creando una nueva lista que contiene los elementos devueltos por la función.

Entendiendo la operación reduce:

Otra operación crucial en la programación funcional es reduce. El objetivo de reduce es tomar una lista de elementos y condensarla en un solo valor.

Por ejemplo, dada una lista de números, uno podría usar reduce para calcular el promedio o la suma de la lista. La función que acepta el método reduce típicamente toma dos argumentos y retorna un solo resultado.

Para visualizarlo, imagina aplicar una función secuencialmente a pares de elementos en una lista, reduciéndola gradualmente hasta obtener un único resultado acumulado.

Sumando las entradas

Para poner esto en perspectiva, consideremos un escenario donde tenemos una lista de enteros que representa las entradas de consultas. Podríamos definir una función, sumNumberOfInputs, que calcule la suma de dos enteros.

Cuando esta función se aplica sobre nuestra lista utilizando reduce, acumularía el número total de entradas a través de todas las consultas.

Identificando consultas del equipo de ingenieros PIX

Para aislar las consultas creadas por el equipo de Ingenieros PIX, empleamos la operación filter. Esta operación procesa una lista, reteniendo solo los elementos que cumplen con una condición dada.

En nuestro caso, definimos una función, isFromTeamPixEngineers, que verifica si una consulta dada se origina de este equipo específico.

Aislamiento de consultas de baja calidad

De nuestra lista filtrada de consultas de los Ingenieros PIX, podríamos querer discernir cuáles de ellas son de baja calidad. Podemos introducir otra condición de filtrado, verificando el atributo QualityAssurance de cada consulta en busca del valor ‘bajo’.

Un requisito común en los entornos ETL es identificar dependencias. Supongamos que queremos identificar qué conjuntos de datos dependen del conjunto de datos Meetup PixMovements. Podemos crear otra función de filtro para examinar nuestra lista y encontrar consultas que dependan de este conjunto de datos específico.

Este ejercicio es fundamental, especialmente al considerar los efectos en cadena de alterar una consulta fundamental.

Desafiando las normas: exigiendo más de Scala

Después de establecer un sistema de consultas robusto, ¿qué sigue? Aquí hay algunas provocaciones:

Implementación de pruebas rigurosas

  1. Garantizando alta validez:Por ejemplo, todos los conjuntos de datos bajo el equipo de PixEngineers deben mantener una alta validez. Solo podemos aceptar transformaciones que se adhieran a esta regla.
  2. Limitando las entradas de consultas: Ninguna consulta debería depender de más de 10 entradas. La idea es prevenir dependencias excesivamente complejas que podrían ser difíciles de depurar o mantener.
  3. Evitando dependencias cíclicas: Por ejemplo, si el conjunto de datos A depende del conjunto de datos B, y B depende de C, entonces C no debería depender de A. Tales dependencias cíclicas pueden introducir errores sutiles y problemas de rendimiento.

Estas son prácticas que hemos integrado en nuestra gobernanza de datos en Nubank, asegurando control y consistencia.

Capacidades avanzadas de Scala

La flexibilidad de Scala ofrece posibilidades aún más avanzadas:

  1. Visualización de dependencias: Dada una lista de todas las consultas, Scala nos permite interpretar el grafo de dependencias y visualizarlo. Cada conjunto de datos se representa como un bloque, con cadenas que muestran sus dependencias. Esta visualización puede enriquecerse con codificación de colores según la calidad de los conjuntos de datos, lo que permite obtener rápidamente ideas sobre posibles áreas de preocupación.
  2. Metadatos como datos: Otra capacidad intrigante es tratar los metadatos como datos reales. Al capturar los metadatos de transformación, podemos crear una tabla e integrarla en nuestros procesos ETL. Estos datos pueden ser consumidos y monitoreados, ofreciendo información en tiempo real.

Por ejemplo, con solo unas pocas líneas de código, podemos crear un dataframe, que nos permite analizar la distribución de datos, como el número de conjuntos de datos por país o por equipo propietario, en tiempo real.

Dominar Scala y Spark resulta fundamental para optimizar las transformaciones y la gestión de datos. Como ilustra la metodología de Nubank, integrar ambas herramientas ofrece una solución integral a los desafíos de la gobernanza de datos.

Con características como la encapsulación de consultas y metadatos, la visualización de dependencias y el tratamiento de metadatos como datos reales, Scala se convierte en un activo invaluable para los profesionales de datos. A medida que los ecosistemas de datos continúan creciendo y volviéndose más complejos, adoptar estas estrategias se vuelve crucial para las empresas que buscan eficiencia, claridad y una gobernanza de datos robusta.

Mira lo que compartimos sobre este tema en Meetup a continuación:

Descubre las oportunidades