Expresiones regulares en R para revisar columnas de archivos de texto

"Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now they have two problems" (Tomado del libro "Regular Expressions Cookbook")

En relación al artículo anterior una de las razones por las cuales es necesario hacer manipulación de las consultas devueltas por Apache Spark es sin duda la presentación de resultados.

En este artículo vamos a presentar una forma de obtener y analizar las columnas de un archivo delimitado por algún carácter (en sus inicios la definición era Comma Separated Values -CSV- sin embargo debido a que la coma pudiera ser parte del contenido de una columna pues ahora se separan por cualquier otro caracter) y un acercamiento a sus tipos de datos.

Cómo sería muy azaroso el revisar millones de registros con las herramientas estándar del lenguaje y citando nuevamente a la que pareciera es la biblia de los usuarios de R que utilizan Spark (Mastering Spark with R) en la sección 8.2.2 nos muestra un código para realizar esto, así como el siguiente comentario:

"Cuando leemos datos, Spark es capaz de determinar el nombre de las columnas de una fuente de datos y sus tipos, a este se le conoce como el esquema. Aunque, el obtener el esquema viene con un costo; Spark necesita hacer un pase inicial a los datos para determinar que contienen. Para un conjunto de datos grande, esto puede llevar una cantidad significativa de tiempo en el proceso de ingesta de datos, lo cual puede volverse 'costoso'  incluso para conjuntos de datos (datasets) de tamaño mediano. Para archivos que se leen una y otra vez, el tiempo adicional de lectura se acumula.

Para evitar esto, Spark te permite proporcionar una definición de columna proporcionando un argumento de columnas para describir tu conjunto de datos (dataset). Puedes crear este esquema por muestreo de una pequeña porción de tu archivo." [1]

En el mismo texto viene la instrucción y para ejemplificar aquí vamos a descargar los datos abiertos de la página del IMSS para ver las columnas y sus tipos tomando una muestra de 10 registros.

spec_with_r <- sapply(read.csv("ruta\\asg-2022-06-30.csv", sep = '|', nrows = 10), class)

Este archivo tiene más de 4 millones de registros, por lo que está técnica parecería ser una opción viable , sin embargo en la práctica llegan los archivos y no podemos fiarnos de las pequeñas o grandes muestras que pudiéramos analizar, sin embargo vamos aprovechar esta aproximación en lo referente a tipos de datos, que al final del día es el contenido de la columnas y que debiera ser la primera validación por lo que deberían pasar estos archivos.

Cada estructura de datos y más formalmente  los manejadores de bases de datos definen tipos distintos de acuerdo al software que manejan, lo recomendable sería lograr un consenso y definir un estándar para la definición de dichos tipos que entiendan tanto las personas que manejan los datos de forma conceptual así como los que la implementan en una plataforma de software.

Una buena referencia sería el esfuerzo que hace el Banco Mundial y adoptar ese sistema para definir los tipos de los metadatos. Esta iniciativa es el estándar DDI (Data Documentation Initiative por sus siglas en inglés) y en este estándar se manejan cuatro tipos de datos (que en nuestro caso serían solo 3).

  • Numérica
  • Alfanumérica fija.
  • Alfanumérica variable.
  • Fecha

Entonces vamos a pasar un tema que es casi obligado en todos los lenguajes de programación como lo son las expresiones regulares.

En este ejercicio hubo mucho aprendizaje y ahonde más a profundidad en el tema logrando investigar, adecuar, probar y en el caso de los números desarrollar las expresiones regulares siguientes:

Detectar números:

expreg_num <- '^([0-9])*(\\.){0,1}([0-9])*$'

Explicación:

  • Los caracteres ^ y $ significan el inicio y final de la cadena.
  • La expresión ([0-9])* significa que tenemos sucesión de dígitos del 0 al 9, 0 o n veces (*).
  • La expresión (\\.){0,1} significa que puede ir un punto o ninguno (en R hay que poner doble diagonal inversa ya que esta expresión la van a encontrar en otros lenguajes simplemente con una diagonal inversa)
  • La expresión ([0-9])* misma explicación 

Aquí dejamos un pequeño script con un vector que es analizado por la expresión regular a través de la función grepl.


Detectar fechas:

Aquí fue el gran obstáculo y aunque hubo que investigar algo más a profundidad se pudieron implementar dos expresiones regulares que se encuentra quizás en el foro donde más respuestas se encuentran (stackoverflow) [2], sin embargo al querer verificar el contenido de una columna usando Spark, la expresión regular no pudo detectar dichas fechas.

La primera expresión regular es muy sencilla son fechas del tipo día-mes-año.

expreg_fec_dma <- '^([0-2][0-9]|3[0-1])(-|\\/)(0[1-9]|1[0-2])\\2(\\d{4})$'

Explicación:

  • Los caracteres ^ y $ significan el inicio y final de la cadena.
  • ([0-2][0-9]|3[0-1]) aquí algo nuevo e interesante que inclusive tiene ya algo de validación el primer digito esta entre 0 y 2 acompañado del segundo dígito que puede ir de 0 a 9, con esta primera parte comprende desde el día 00 (el cual sería incorrecto) hasta el día 29 "o"  - que se representa con el pipeline (|) - el dígito 3 acompañado del dígito 0 o 1.
  • (-|\\/) representa el separador entre los días, meses y años el cual puede ser el guion de en medio (-) "o" (|)  la diagonal (/).
  • (0[1-9]|1[0-2]) la validación del mes que es el 0 acompañado del 1 al 9 para formar la serie 01 al 09, o (|) el dígito 1 acompañado del dígito 1 o 2.
  • \\2 otra cosa que aprendí, al anotar esta expresión significa que el caracter que sigue tiene que ser igual este caso a la segunda expresión que sería el separador (-|\\/).
  • (\\d{4}) para la última expresión es el año que nos dice que tiene que ser un dígito \\d de cuatro posiciones {4}, lo da la posibilidad de la serie 0000 a 9999.
Nuevamente el código para probar la anterior expresión regular con un vector con ejemplos de fechas.


Como no me gustó la fecha doble 00 en día, modifiqué la expresión regular para que esto no ocurriera quedando de la siguiente manera:
expreg_fec_dma <- ^(0[1-9]|[1-2][0-9]|3[0-1])(-|\\/)(0[1-9]|1[0-2])\\2(\\d{4})$'

Es importante señalar que solo checa el formato, ya verificar años bisiestos, día 31 en ciertos meses requeriría de otra validación.

Detectar fecha con hora.

Por último y dado que un archivo que se recibe viene atado la hora nos encontramos con la siguiente expresión regular que verifica esto.

expreg_fec_amdh <- '^(\\d{4})(-|\\/)(0[1-9]|1[0-2])\\2(0[1-9]|[1-2][0-9]|3[0-1])(\\s)([0-1][0-9]|[2][0-3])(:)([0-5][0-9])(:)([0-5][0-9])$'

Explicación:

  • La explicación es la misma que la anterior solo que agrego la nueva expresión para evitar el 00 y el orden como vienen estas fechas que es año, mes y día.
  • \\s es un espacio
  • ([0-1][0-9]|[2][0-3]) esta es la hora que iría de las 12am (00 horas) hasta las 7pm (19 horas) o (|) bien de las 8pm (20 horas) a las 11pm (23 horas)
  • (:)  el caracter que divide a las horas minutos y segundos
  • ([0-5][0-9]) los minutos y sus segundos que van desde 00 a 59.
Podemos nuevamente ejemplificar el funcionamiento con el siguiente script:



Y parecía que con esto podíamos verificar y reconocer los tres tipos de datos que maneja el estándar DDI pero hay un detalle con los archivos de Spark en lo referente a las fechas ya que la expresión regular funciona cuando el archivo es leído como CSV, sin embargo pasa esto cuando se lee mediante Spark (o registros RDD) como podemos ver en estos ejemplos donde se verifica una pequeña muestra de un archivo muy grande que maneja fechas, a continuación la vista de este pequeño archivo:

Archivo muestra.



Código para verificar con una expresión regular - Spark



Salida del script



Código para verificar con expresión regular  usando read.csv

Salida del script


Otra cosa a notar es que el uso de sdf_nrow es de Spark por lo que para usar dlpyr usamos tally() cuando leemos el archivo como CSV.

Como se puede observar las expresiones regulares funcionan sin embargo parece que Spark al convertirlas en estructuras RDD hace un "parseo" sobre datos que en estructura cumplen con el formato de fecha, como lo dice la guía de Spark 3.3.0 "las fuentes de datos CSV/JSON usan el patrón 'string' para parsear y formatear contenido de fecha-hora"  [3]

Aun así en el siguiente artículo veremos como todo este intento no es necesario ya que existe una función para extraer el esquema de Spark, sin embargo este camino nos sirvió mucho de enseñanza.

Seguimos leyendo en los próximos artículos.

Miguel Araujo

Webgrafía

[1] J. Lurashi, K. Kuo,  y E. Ruiz. Mastering Spark with R. O'Reilly. Nov 19, 2019. Accesado el 9/08/2022) Online: https://therinspark.com/

[2] Xerif, "Expresión regular para validar fecha y hora (dd/mm/yyyy hh:mm)". https://es.stackoverflow.com/questions/73759/expresi%C3%B3n-regular-para-validar-fecha-y-hora-dd-mm-yyyy-hhmm (accesada el 9/08/2022)

[3] "Datetime Patterns for Formatting and Parsing". https://spark.apache.org/docs/latest/sql-ref-datetime-pattern.html (accesada el 9/08/2022)


Comentarios

Entradas populares de este blog

Librería de REDATAM para R

Conectar bases de datos Oracle con R vía JDBC

Red 7 Admin