Como profesionales dedicados al mundo de los datos sabemos la importancia de cualidades como la cantidad, la diversidad, la fiabilidad y la calidad de los datos a la hora de abordar y solventar problemas.

Los datos son el “alimento” que nutre a los cuadros de mando, modelos dimensionales, modelos de Machine Learning, etc. Su objetivo final es el de facilitar la toma de decisiones o realizar predicciones para solventar algún problema en concreto. Si esos datos no tienen la calidad deseada es evidente que las decisiones, previsiones o predicciones a partir de los mismos no serán idóneas. De ahí la necesidad de que los datos tengan la calidad esperada para que la toma de decisiones sea acertada.

Por ello, si quieres maximizar la calidad de los datos con los que trabajas en el día a día entonces tienes que conocer Great Expectations (GX), una librería de Python para la creación y aplicación de validaciones de calidad sobre los datos.

En este post os contamos qué es GX y cómo se usa. En futuros artículos explicaremos cómo inferir las validaciones de calidad haciendo introspección sobre los datos y detallaremos cómo crear validaciones de calidad personalizadas, usando algunas de las características avanzadas que ofrece GX (que no son pocas).

¿Qué es GX?

Great Expectations (GX) es una librería open source, implementada en Python, para validar datos en formato tabular, representados en dataframes de Pandas y Spark, así como en Bases de datos SQL. Aplica sobre estos mismos un conjunto de validaciones de calidad mejor conocidas como Expectation Suite, las cuales se componen de varias validaciones denominadas Expectation, que realizan comprobaciones predefinidas por GX de diferente índole para validar si el dato de una columna cumple con las siguientes aserciones:

También puedes crear tus propias Expectation que se encargan de comprobar aspectos concretos de tus datos según el caso de uso e incluso compartirlas con la comunidad.

El hecho de que GX sea un proyecto Open Source nos permite crear nuevas validaciones de calidad para que otras personas puedan darle utilidad. Podemos también corregir bugs que hayamos detectado, con todas las ventajas que esto implica, por ejemplo, la de evitar esperar a que los propietarios de una librería o producto solventen el bug, lo cual es una desventaja de proyectos que no son Open Source. Si quieres ver las contribuciones de la comunidad puedes acceder al GitHub de GX aquí. También puedes plantear dudas, reportar errores o consultar temas de interés en torno a GX en su canal de Slack.

Podemos decir que GX permite ejecutar test unitarios sobre los datos y en caso de fallo llevar a cabo alguna acción, como puede ser el envío de notificaciones a los interesados o lo que consideres oportuno.

Cabe destacar que GX se puede conectar a diversas fuentes de datos tanto en On-premise como en Cloud, como por ejemplo: Athena, MySQL, MSSQL, S3, Redshift, Trino, Azure Blob, Google Cloud Storage, BigQuery y Snowflake.

Algunas fuentes de datos y plataformas que pueden integrarse con GX.
Algunas fuentes de datos y plataformas que pueden integrarse con GX.

Por otra parte, GX se puede integrar con otras plataformas externas como orquestadores tipo Airflow, del cual hablamos en este interesante post, o Prefect; clusters de procesamiento distribuido como EMR o Databricks; herramientas de ETL como Glue; frameworks de ML como ZenML, lo que nos permitirá aplicar uno o varios conjuntos de Expectation Suite sobre los datos que se están procesando, ya sea durante la ingesta, transformación o carga de los mismos.

Además, GX ofrece otra característica interesante como los Data Docs que no son más que la documentación en formato HTML acerca de los resultados de aplicar la Expectation Suite sobre los datos y que puede ser compartida con los consumidores de los mismos para que tengan una visión clara de los controles de calidad aplicados sobre los datos que consumen.

Si no tienes muy claro que validaciones de calidad aplicar a tus datos, GX ofrece un Profiler que al ser ejecutado sobre un conjunto de datos genera una serie de métricas y validaciones automáticamente, basándose en las características de los datos y que te pueden servir como punto de partida.

¿Cómo funciona GX?

Para entender cómo funciona GX, os contamos varios conceptos que conviene conocer, ya que son parte del core de GX:

Fundamentalmente, GX provee varias formas de crear Expectation que son:

En la siguiente imagen se representan las alternativas que GX nos ofrece para crear Expectation.

Pasos para configurar y usar GX interactivamente

Para empezar a usar GX de forma interactiva, lo podemos hacer mediante un Jupyter Notebook, donde podemos llevar a cabo una exploración sobre un conjunto de datos de ejemplo, cuya temática son las características y precios de coches, que puedes descargar de este enlace. Sobre este conjunto de datos podemos indagar acerca de sus características como por ejemplo: el número de registros, columnas, tipos de datos, si hay nulos, etc., todo ello mediante el uso de la librería great_expectations.

Como primer paso instalamos GX mediante pip usando Python 3.10. La versión más reciente de la librería a la fecha de elaboración de este post es 0.15.44.

pip install great_expectations

Una vez instalada, podemos usar GX como si de la librería Pandas se tratase, debido a que GX usa dicha librería “under the hood” como dialecto, aceptando también PySpark y SQL (a través de SQLAlchemy). Por tanto, podemos cargar los datos en un dataframe y explorarlos usando los métodos que usaríamos con Pandas y que puedes consultar en esta cheat sheet.

Cargamos en un dataframe los datos de ejemplo a través de la URL especificada previamente.

Cargamos en un dataframe los datos de ejemplo a través de la URL especificada previamente.

Tal como indicamos anteriormente, el dataframe generado por GX es una subclase de pandas.DataFrame.

Dataframe generado por GX es una subclase de pandas.DataFrame.

Obtenemos el tipo de dato de cada columna del dataframe.

Obtenemos el tipo de dato de cada columna del dataframe.

Identificamos si hay valores nulos presentes en alguna de las columnas del dataframe. Vemos que hay 5 columnas con valores nulos.

Identificamos si hay valores nulos presentes en alguna de las columnas del dataframe.

Extraemos las estadísticas del dataframe para ver los valores mínimos, máximos, percentiles, etc.

Extraemos las estadísticas del dataframe para ver los valores mínimos, máximos, percentiles,

Tal como hemos visto podemos usar muchos de los métodos que ofrece Pandas sobre un dataframe creado mediante GX.

Lo interesante de todo lo anterior es que podemos darle utilidad a las características de los datos para crear validaciones de calidad a partir de las mismas. Podemos, por ejemplo, controlar que una o varias columnas tengan un tipo de dato determinado, que no puedan tener valores nulos, que solo puedan tener ciertos valores o que estén dentro de un rango, etc. Para poder crear validaciones de calidad sobre los datos primero debes conocer sus características.

Para ello, GX ofrece las Expectation, que básicamente son aserciones o afirmaciones acerca de lo que esperamos encontrar en los datos. GX provee más de 50 Expectation predefinidas que pueden ser aplicadas a nivel de tabla o columna y que puedes consultar en detalle aquí.

A continuación, se listan algunas de las Expectation predefinidas de GX, con el correspondiente enlace a la documentación oficial donde se detalla su configuración:

Expectation a nivel de Tabla:

Expectation a nivel de Columna:

Para aplicar alguna una de las Expectation predefinidas es tan sencillo como ejecutar el código que mostramos a continuación y elegir aquella que nos venga bien.

Ejecucción del el código y elección de la expectaction.

Si ejecutamos la Expectation a nivel de tabla o dataframe, expect_table_columns_to_match_set, que comprueba que el dataframe tenga las columnas indicadas en el campo column_set, podemos ver que devuelve un JSON denominado ExpectationSuiteValidationResult.

JSON que nos devulve, denominado ExpectationSuiteValidationResult.

Si forzamos a que la Expectation falle añadiendo una columna que no existe este sería el resultado.

Si forzamos a que la Expectation falle añadiendo una columna que no existe este sería el resultado.

Como hemos visto anteriormente, cuando ejecutamos una Expectation sobre los datos y esta no se cumple, GX nos devuelve un JSON con los resultados de la validación indicando detalles que nos permiten identificar claramente el motivo del fallo.

Veamos que resultados obtenemos al aplicar la Expectation a nivel de columna, expect_column_values_to_be_in_set, sobre la columna Transmission Type. Pero antes vamos a ver que valores puede tomar dicha columna. El valor UNKNOWN vamos a considerarlo cómo no válido.

Expectation a nivel de columna, expect_column_values_to_be_in_set, sobre la columna Transmission Type.

Cuando aplicamos la Expectation mencionada anteriormente, podemos ver que GX nos devuelve un JSON indicando el número de registros del dataframe que tiene asignado cada uno de los valores: AUTOMATIC, MANUAL, AUTOMATED_MANUAL, DIRECT_DRIVE y UNKNOWN. Esta información es muy útil para identificar que una de las columnas del dataframe tiene valores que nuestro caso de uso considera incorrectos.

JSON indicando el número de registros del dataframe que tiene asignado cada uno de los valores.

Adicionalmente, podemos aplicar una Expectation que tenga en cuenta el valor de varias columnas, por ejemplo: expect_column_pair_values_A_to_be_greater_than_B, que comprueba que el valor de la columna A debe ser mayor al valor de la columna B.

A modo de ejemplo, vamos a usar dicha Expectation sobre las columnas ‘highway MPG’ (Columna A) y ‘city mpg’ (Columna B) del dataframe, para validar que las MPG (millas por galón de combustible) transitando en autopista (highway MPG) debe ser mayor al MPG transitando en ciudad (city mpg).

Usamos la   Expectation sobre las columnas ‘highway MPG’ (Columna A) y ‘city mpg’ (Columna B).

Una vez aplicada la Expectation, GX nos devuelve el siguiente JSON, del cual podemos sacar las siguientes métricas, gracias al uso del atributo result_format, el cual establece el nivel de detalle de los resultados de la Expectation.

Una vez aplicada la Expectation, GX nos devuelve el siguiente JSON.

Dependiendo del nivel de detalle que queremos obtener de los resultados de una Expectation, podemos asignar uno de los siguientes valores: [‘BOOLEAN_ONLY’, ’BASIC’, ’SUMMARY’, ’COMPLETE’] al atributo result_format y que puedes ver en detalle aquí.

Otro atributo interesante que podemos configurar en la Expectation es el conocido como mostly, el cual puede tener valores del 0 al 1 y nos permite establecer un umbral de aceptación que representa el % de registros que cumplen la Expectation para darla por válida, aunque el 100% de los registros no la cumplan. Vamos a verlo en el siguiente ejemplo.

La columna Market Category del dataframe tiene registros nulos y queremos validar que dicha columna tenga solo cierto % de nulos, p.e. un 70%. Para lograr el objetivo usamos la Expectation expect_column_values_to_not_be_null, usando el atributo mostly.

Usamos la Expectation expect_column_values_to_not_be_null, usando el atributo mostly.

Si reducimos el umbral del 70% al 65%, por ejemplo, vemos que ahora sí se cumple la Expectation.

Si reducimos el umbral del 70% al 65%, se cumple la Expectation.

Mediante el atributo mostly podemos ajustar el nivel de aceptación de una Expectation, ya que en ciertos casos nuestro caso de uso puede tener un margen de tolerancia en registros que no se ajustan 100% a una validación de calidad determinada.

Hasta el momento ya sabemos en qué consisten las Expectation y para qué se utilizan, pero ¿cómo podemos agrupar varias Expectation, aplicarlas sobre un conjunto de datos y ver los resultados de la validación de una forma más centralizada y legible? GX posee un Data Context que nos ayuda a configurar todo lo que necesitamos para usar todas las características de GX y que se representa en la siguiente imagen.

A continuación, vamos a detallar los pasos para empezar a validar los datos con GX.

1 Configuración inicial

Asumiendo que ya hemos instalado great_expectations, procedemos a inicializar el Data Context ejecutando el siguiente comando en el directorio donde queremos crearlo.

great_expectations init

Después de ejecutar el comando anterior, se genera la siguiente estructura de directorios que conforman el Data Context, en donde se realiza la configuración de todo lo necesario para ejecutar las Expectation sobre los datos.

Estructura de directorios del Data Context.
Estructura de directorios del Data Context.

En el fichero great_expectations.yml se configuran los Data Sources, Data Docs, etc. En el directorio /expectations es donde se van creando los ficheros JSON con la definición de las Expectation que queremos usar.

Dentro del directorio /uncommited se guardan ficheros que, a priori, no se deberían versionar por tener valores sensibles, como es el caso del fichero config_variables.yml, donde se podrían especificar credenciales para autenticarse contra una base de datos o un sistema de almacenamiento en la nube. Para entender con mayor detalle la función de cada directorio/fichero del Data Context puedes ir a la documentación oficial.

Una vez que ya tenemos configurado el Data Context, podemos usar la CLI o Python para ir creando los diferentes componentes de GX como el Data Source, Expectation Suite, Checkpoints, Data Docs, etc., y cuya definición hemos indicado al principio del post.

En este primer post vamos a configurar los componentes mencionados mediante Python, creando un Jupyter Notebook en el mismo directorio donde hemos inicializado el Data Context.

2 Conectar con la fuente de datos

Otro aspecto importante a tener en cuenta son los datos sobre los que vamos a trabajar. Para ello podemos descargar un dataset de ejemplo y almacenarlo en algún subdirectorio (p.e. /data), ejecutando el siguiente comando dentro del notebook que hemos creado antes.

!wget -q -P ../data https://raw.githubusercontent.com/alexeygrigorev/mlbookcamp-code/master/chapter-02-car-price/data.csv

Teniendo los datos que serán validados con GX, lo siguiente que debemos hacer es configurar un Data Source que apunte a los datos que acabamos de descargar. Para ello ejecutamos el siguiente código en el notebook para cumplir dicho cometido.

from ruamel.yaml import YAML

import great_expectations as gx
from great_expectations.core.batch import BatchRequest
from great_expectations.cli.datasource import sanitize_yaml_and_save_datasource
from great_expectations.core.expectation_configuration import ExpectationConfiguration

context = gx.data_context.DataContext()

my_datasource_name = 'my_datasource_local'
my_dataconnector_name = 'my_dataconnector_local'

my_datasource_config = f"""
    name: {my_datasource_name}
    class_name: Datasource
    execution_engine:
      class_name: PandasExecutionEngine
    data_connectors:
      {my_dataconnector_name}:
        class_name: InferredAssetFilesystemDataConnector
        base_directory: ../data
        default_regex:
          group_names:
            - data_asset_name
          pattern: (.*)
      default_runtime_data_connector_name:
        class_name: RuntimeDataConnector
        assets:
          my_runtime_asset_name:
            batch_identifiers:
              - runtime_batch_identifier_name
"""

context.test_yaml_config(yaml_config=my_datasource_config)
yaml = YAML()
context.add_datasource(**yaml.load(my_datasource_config))
sanitize_yaml_and_save_datasource(context, my_datasource_config, overwrite_existing=True)

Una vez se ha ejecutado el código, vemos que se añade la configuración del Data Source en el fichero great_expectations.yml que se encuentra dentro del Data Context. A continuación, se resaltan algunos de los items de configuración más relevantes del Data Source.

Items de configuración más relevantes del Data Source.

3 Crear las Expectation

Ya tenemos acceso a los datos mediante el Data Source, ahora necesitamos leerlos, esto se hace mediante el uso del Batch Request, cuya función es solicitar al Data Connector que le devuelva uno o varios Batch de datos. El siguiente código nos indica cómo hacerlo, donde el atributo data_asset_name corresponde al nombre del fichero que queremos leer.

}my_data_asset_name = 'data.csv'
batch_request = BatchRequest(
    datasource_name=f"{my_datasource_name}",
    data_connector_name=f"{my_dataconnector_name}",
    data_asset_name=f"{my_data_asset_name}",
    batch_spec_passthrough={
        "reader_method": "read_csv"
    },
)
validator = context.get_validator(
    batch_request=batch_request
)

validator.head(n_rows=5, fetch_all=False)

Al ejecutar el código anterior se visualiza una muestra de los datos contenidos dentro de un Batch usando el Validator.

Muestra de los datos contenidos dentro de un Batch usando el Validator.

El código anterior se resume en la siguiente imagen, donde vemos que el Batch Request solicita la lectura de datos usando el Execution Engine y mediante el Data Connector accede al Data Source para extraer uno o varios Batch de datos que luego son enviados al Validator que posteriormente se encargará de ejecutar la Expectation Suite sobre cada Batch de datos.

Para crear una Expectation Suite de manera interactiva podemos usar el Validator de GX, que nos permite probar las Expectation que GX provee, recordemos que son más de 50. Elegimos las Expectation que deseamos, las ejecutamos y vemos los resultados para establecer si es útil aplicarlas sobre los datos.

Por ejemplo, podemos configurar el Validator para ejecutar la Expectation expect_column_values_to_not_be_null que se encarga de comprobar que una columna del dataset, en nuestro caso Market Category, tenga un 80% de valores no nulos usando el atributo mostly explicado al inicio del post.

validator.expect_column_values_to_not_be_null('Market Category', mostly=0.80)

Añadimos la Expectation expect_column_pair_values_a_to_be_greater_than_b que también hemos explicado previamente usando las columnas highway MPG y city mpg.

validator.expect_column_pair_values_a_to_be_greater_than_b('highway MPG', 'city mpg', result_format='BASIC', mostly=0.99)

A continuación, obtenemos la Expectation Suite subyacente, a partir de las Expectation especificadas anteriormente en el Validator mediante el método validator.get_expectation_suite() y luego podemos persistir la Expectation Suite resultante usando el método context.save_expectation_suite() a la cual le asignamos el nombre: another_expectation_suite, tal como observamos en el siguiente código.

validator_expectation_suite = validator.get_expectation_suite(
  discard_failed_expectations=False
)
context.save_expectation_suite(expectation_suite=validator_expectation_suite, expectation_suite_name='another_expectation_suite')

Como se puede ver en la siguiente imagen la Expectation Suite obtenida del Validator se ha persistido en el fichero another_expectation_suite.json, ubicado en el directorio /expectations del Data Context.

la Expectation Suite obtenida del Validator se ha persistido en el fichero another_expectation_suite.json, ubicado en el directorio /expectations del Data Context.

Otra forma de definir Expectation es la que podemos ver en el siguiente código mediante el uso del objeto ExpectationConfiguration, donde definimos 2 Expectation de forma declarativa.

expectation_configuration_01 = ExpectationConfiguration(
   expectation_type="expect_column_values_to_not_be_null",
   kwargs={
      "column": "Engine Cylinders",
      "mostly": 0.75,
   },
   meta={
      "notes": {
         "format": "markdown",
         "content": "Expectation to validate column `Engine Cylinders` does not have null values."
      }
   }
)

expectation_configuration_02 = ExpectationConfiguration(
   expectation_type="expect_column_distinct_values_to_be_in_set",
   kwargs={
      "column": "Transmission Type",
      "value_set": ['AUTOMATIC', 'MANUAL', 'AUTOMATED_MANUAL', 'DIRECT_DRIVE'],
      "mostly": 1,
      "result_format": 'COMPLETE'
   },
   meta={
      "notes": {
         "format": "markdown",
         "content": "Expectation to validate column `Transmission Type` has values to be in set."
      }
   }
)

Creamos otra Expectation Suite con el nombre: my_expectation_suite y le añadimos las 2 Expectation creadas previamente y que se plasma en el siguiente código.

my_expectation_suite_name = "my_expectation_suite"
my_expectation_suite = context.create_expectation_suite(
    expectation_suite_name=my_expectation_suite_name, overwrite_existing=True
)

my_expectation_suite.add_expectation(expectation_configuration=expectation_configuration_01)

my_expectation_suite.add_expectation(expectation_configuration=expectation_configuration_02)

context.save_expectation_suite(expectation_suite=validator_expectation_suite, expectation_suite_name=my_expectation_suite_name)

Podemos crear tantas Expectation Suite como queramos y en cada una incluir Expectation que se apliquen sobre columnas diferentes del dataset o de otro dataset en caso de que tengamos varios y luego agrupar dichas suites dentro de un Checkpoint como explicamos en el siguiente apartado.

4 Validar los datos

Mediante un Checkpoint podemos especificar las Expectation Suite que queremos que sean aplicadas sobre nuestro conjunto de datos. El modo de hacerlo se detalla en el siguiente código, en donde especificamos las 2 suites que creamos previamente: my_expectation_suite y another_expectation_suite.

my_checkpoint_name = 'my_checkpoint_local'
config_checkpoint = f"""
    name: {my_checkpoint_name}
    config_version: 1
    class_name: SimpleCheckpoint
    validations:
      - batch_request:
          datasource_name: {my_datasource_name}
          data_connector_name: {my_dataconnector_name}
          data_asset_name: {my_data_asset_name}
          data_connector_query:
            index: -1
        expectation_suite_name: {my_expectation_suite_name}
      - batch_request:
          datasource_name: {my_datasource_name}
          data_connector_name: {my_dataconnector_name}
          data_asset_name: {my_data_asset_name}
          data_connector_query:
            index: -1
        expectation_suite_name: another_expectation_suite
"""

Usamos el método test_yaml_config() para validar que el YAML es correcto y a continuación añadimos el Checkpoint al contexto de GX mediante el comando add_checkpoint().

context.test_yaml_config(config_checkpoint)
context.add_checkpoint(**yaml.load(config_checkpoint))

Hasta aquí ya tenemos todo configurado para ejecutar las Expectation Suite definidas sobre nuestros datos, y para lograrlo ejecutamos el comando run_checkpoint(), como podemos ver en el siguiente código.

context.run_checkpoint(checkpoint_name=my_checkpoint_name)

Ahora bien, para ver los resultados de la ejecución del Checkpoint de forma centralizada y legible lo hacemos mediante la ejecución del método open_data_docs() encargado de abrir los Data Docs, que son documentos HTML que muestran los resultados generados por las validaciones definidas en las Expectation Suite y que por defecto se generan en formato JSON. GX mediante un Renderer transforma el JSON a formato HTML y, de este modo, se pueden identificar más fácilmente las Expectation fallidas y aquellas que han terminado satisfactoriamente.

context.open_data_docs()

Después de ejecutar el comando citado antes se abre automáticamente una ventana de nuestro navegador donde veremos 2 apartados: Validation Results y Expectation Suites. En el primero, podemos ver las ejecuciones de las Expectation Suite viendo si han terminado bien o mal.

Vemos si las ejecuciones de las Expectation Suite han terminado bien o mal.

Al acceder a una de las ejecuciones vemos el detalle de las validaciones, donde identificamos las que han fallado y el motivo del fallo, permitiendo depurar fácilmente y poder tomar las decisiones pertinentes.

Al acceder a una de las ejecuciones vemos el detalle de las validaciones.

Por otro lado, en el segundo apartado Expectation Suites podemos ver un listado con las 2 Expectation Suite que hemos creado.

Listado con las 2 Expectation Suite que hemos creado.

Si accedemos al detalle de la suite my_expectation_suite vemos la definición de cada una de las Expectation incluidas en dicha suite, donde identificamos que validaciones se realizan y sobre qué columna o tabla se aplican.

Si accedemos al detalle de la suite my_expectation_suite vemos la definición de cada una de las Expectation incluidas en dicha suite

Del mismo modo, si accedemos a la otra suite, another_expectation_suite, vemos la definición de la misma, tal como hemos explicado.

Suite another_expectation_suite

GX permite configurar Actions que en función de los resultados de las validaciones se pueden acometer acciones, como, por ejemplo, notificar vía Slack al equipo de datos que se han generado errores durante la ejecución del Checkpoint para tomar las medidas oportunas o mover los Data Docs a un almacenamiento en la nube. GX proporciona varias Actions predefinidas que puedes consultar aquí.

En el siguiente diagrama se puede ver la representación de lo que hemos explicado, haciendo uso de Bacth Request, Data Source, Valdiator, Checkpoint, etc.

Podríamos seguir añadiendo Expectation predefinidas para validar que los datos del dataset cumplan con otros criterios de calidad que necesitemos.

Hasta este punto, a grandes rasgos, hemos abarcado el flujo que sigue GX para validar un conjunto de datos de forma interactiva. En los post posteriores hablaremos del uso del Data Assistant de GX que nos permite inferir Expectation Suite sobre un conjunto de datos a través de la introspección y además hablaremos de las características avanzadas de GX que nos permiten crear Expectation Custom.

Conclusiones

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.