2.4. Tratamiento de inconsistencias#

Introducción#

La consistencia de datos es un concepto MUY amplio ya que abarca diferentes niveles:

  • desde el diseño de los datos (BDs) en sí mismos (redundancia-inconsistencia)

  • desde el punto de vista de transacciones (principios ACID)

  • desde los tipos y formatos de los datos (mismas medidas de longitud, fechas fuera de rango, etc.)

  • desde la semántica y relación del dato (un abogado de 5 años)

Cualquiera sea el caso, se deben implementar mecanismos para detectarlas y luego eliminarlas o corregirlas. Dejar este tipo de valores en los datos pueden afectar los resultados de los análisis.

Identificación de inconsistencias#

El primer paso para analizar la existencia de las inscosistencias en los datos es la identificación de las mismos como tales.

Veamos algunos ejemplos, comenzando con el siguiente dataframe:

import pandas as pd

df_inconsistente = pd.DataFrame({
    'nombre': ['Ana', 'Luis', 'Carlos', 'Marta', 'Sofía', 'Juan', 'Andrea', 'Pedro'],
    'genero': ['F', 'masculino', 'M', 'femenina', 'F', 'H', 'no binario', 'M'],
    'peso': ['70 kg', '154 lbs', '65kg', 'setenta', '60 kilogramos', '0 kg', '52', '80kg'],
    'fecha_nacimiento': [
        '1990-05-01',
        '1880-01-01',     # Demasiado antigua
        '2025-01-01',     # Futuro
        '2000-06-15',
        '2015-10-10',
        '2050-12-31',     # Futuro
        '2002-02-29',     # Válida solo si es año bisiesto
        '1985-07-20'
    ],
    'fecha_registro': [
        '2021-01-01',
        '2022-01-01',
        '2020-01-01',
        '2019-01-01',
        '2017-01-01',
        '2010-01-01',
        '2020-05-05',
        '2021-12-31'
    ]
})
df_inconsistente
nombre genero peso fecha_nacimiento fecha_registro
0 Ana F 70 kg 1990-05-01 2021-01-01
1 Luis masculino 154 lbs 1880-01-01 2022-01-01
2 Carlos M 65kg 2025-01-01 2020-01-01
3 Marta femenina setenta 2000-06-15 2019-01-01
4 Sofía F 60 kilogramos 2015-10-10 2017-01-01
5 Juan H 0 kg 2050-12-31 2010-01-01
6 Andrea no binario 52 2002-02-29 2020-05-05
7 Pedro M 80kg 1985-07-20 2021-12-31

En df_inconsistente como vemos, tenemos diferentes clases de insconsistencias como:

  1. genero con diferentes formatos

  2. peso con diferente sistema de medidas

  3. fecha_nacimiento que tienen fechas en el futuro o inexistente

  4. fecha_nacimiento <–> fecha_registro erróneas ya que no puede haberse registrado sin haber nacido antes

Vamos primero a detectar los problemas que tenemos.

Para el caso 1 del genero:

# agrupamos por cada valor en el genero
df_inconsistente['genero'].value_counts()
genero
F             2
M             2
masculino     1
femenina      1
H             1
no binario    1
Name: count, dtype: int64

Librería re de expresiones regulares#

El segundo caso en mas complejo ya que debemos detectar medidas diferentes en un dato de peso o altura que esta definido como un string. Entonces debemos usar métodos que nos permitan trabajar sobre ellos.

Librería para operaciones con expresiones regulares

La librería re (Regular expression operations) permite definir expresiones regulares, las cuales especifican un conjunto de cadenas que coinciden con la misma. Los métodos de esta librería permiten comprobar si una determinada cadena coincide con una expresión regular dada, denominada patrón. Por ejemplo:

  • re.search() busca la primera coincidencia del patrón en un texto

  • re.findall() devuelve todas las coincidencias del patrón

  • re.split() divide un texto usando un patrón como separador

  • re.match() busca una coincidencia al principio del texto, es decir el patrón se encuentra en el primer carácter del texto

Para definir una expresión regula (patrón) tenemos también una cheatsheet de regex.

También pueden crear expresiones regulares mediante asistentes online como:

La documentación de esta librería puede encontrarse aca.

Entonces una expresión regular que busque números con decimales en un string podría ser:

Las expresiones regulares se pueden crear formando grupos, que se escriben con (x), que nos permiten separar diferentes partes de un string. Se obtienen con match.group(i).

Para el caso 2 del peso definimos la expresion regular para separar el numero de la medida en dos nuevas columnas:

import re

# separamos el número y unidad con una expresión regular 
# y la guardamos en una nueva tabla

# primero el numero con decimales
df_inconsistente['peso_num'] = df_inconsistente['peso'].str.extract(r'(\d+(?:[\.,]\d+)?)')  

#la medida
df_inconsistente['peso_unidad'] = df_inconsistente['peso'].str.extract(r'([a-zA-Z]+)')  

# Limpiamos para que quede lindo
df_inconsistente['peso_num'] = df_inconsistente['peso_num'].str.replace(',', '.').astype(float)
df_inconsistente['peso_unidad'] = df_inconsistente['peso_unidad'].str.lower().str.strip()
df_inconsistente
nombre genero peso fecha_nacimiento fecha_registro peso_num peso_unidad
0 Ana F 70 kg 1990-05-01 2021-01-01 70.0 kg
1 Luis masculino 154 lbs 1880-01-01 2022-01-01 154.0 lbs
2 Carlos M 65kg 2025-01-01 2020-01-01 65.0 kg
3 Marta femenina setenta 2000-06-15 2019-01-01 NaN setenta
4 Sofía F 60 kilogramos 2015-10-10 2017-01-01 60.0 kilogramos
5 Juan H 0 kg 2050-12-31 2010-01-01 0.0 kg
6 Andrea no binario 52 2002-02-29 2020-05-05 52.0 NaN
7 Pedro M 80kg 1985-07-20 2021-12-31 80.0 kg

Para las fechas, debemos también detectar casos inconsstentes. Para el caso de la fecha_nacimiento, no debería suceder que haya fechas demasiado antiguas (no estarían vivos), fechas en el futuro (no nacieron todavía) y fechas inválidas (como un 29 de febrero en un año no bisiesto).

Veamos todos estos casos:

#primero debemos pasar la fecha al tipo datetime
df_inconsistente['fecha_nacimiento'] = pd.to_datetime(df_inconsistente['fecha_nacimiento'], errors = 'coerce')

# definimos el rango válido
fecha_min = pd.Timestamp('1900-01-01')
fecha_max = pd.Timestamp.today()

# buscamos las condiciones de inconsistencia

#es nula
cond_fecha_nula = df_inconsistente['fecha_nacimiento'].isna()

#es antigua, no estarían vivos
cond_antigua = df_inconsistente['fecha_nacimiento'] < fecha_min

#fechas en el futuro (no nacieron todavía)
cond_futura = df_inconsistente['fecha_nacimiento'] > fecha_max

# combinamos todas las condiciones con OR lógico
# y creamos una nueva columna llamada fecha_nacimiento_invalida?
df_inconsistente['fecha_nacimiento_invalida?'] = cond_fecha_nula | cond_antigua | cond_futura

# filtramos los casos inválidos
fechas_invalidas = df_inconsistente[df_inconsistente['fecha_nacimiento_invalida?']]
print(f'Las fechas inválidas son: \n{fechas_invalidas.iloc[:,3]}')

df_inconsistente
Las fechas inválidas son: 
1   1880-01-01
5   2050-12-31
6          NaT
Name: fecha_nacimiento, dtype: datetime64[ns]
nombre genero peso fecha_nacimiento fecha_registro peso_num peso_unidad fecha_nacimiento_invalida?
0 Ana F 70 kg 1990-05-01 2021-01-01 70.0 kg False
1 Luis masculino 154 lbs 1880-01-01 2022-01-01 154.0 lbs True
2 Carlos M 65kg 2025-01-01 2020-01-01 65.0 kg False
3 Marta femenina setenta 2000-06-15 2019-01-01 NaN setenta False
4 Sofía F 60 kilogramos 2015-10-10 2017-01-01 60.0 kilogramos False
5 Juan H 0 kg 2050-12-31 2010-01-01 0.0 kg True
6 Andrea no binario 52 NaT 2020-05-05 52.0 NaN True
7 Pedro M 80kg 1985-07-20 2021-12-31 80.0 kg False

Por último deberíamos ver la dependecia que existe entre fecha_nacimiento y fecha_registro donde la regla debería ser que fecha_nacimiento debe ser al menos 10 años menor que la fecha_registro.

Vamos a detectar los casos que no cumple la regla:

#pasamos la fecha_registro a datetime
df_inconsistente['fecha_registro'] = pd.to_datetime(df_inconsistente['fecha_registro'], errors='coerce')

#calculamos diferencia en días entre ambas fechas
diferencia = df_inconsistente['fecha_registro'] - df_inconsistente['fecha_nacimiento']

# 10 años ≈ 3650 días (aproximación simple)
diez_anios = pd.Timedelta(days=365 * 10)

# detectamos las inconsistencias
df_inconsistente['no_tiene_mas_de_10_años?'] = diferencia < diez_anios

# Filtramos los que violan la regla
violaciones_regla = df_inconsistente[df_inconsistente['no_tiene_mas_de_10_años?']]
print(f'Las relaciones invalídas son: \n{violaciones_regla.iloc[:,3:5]}')

df_inconsistente
Las relaciones invalídas son: 
  fecha_nacimiento fecha_registro
2       2025-01-01     2020-01-01
4       2015-10-10     2017-01-01
5       2050-12-31     2010-01-01
nombre genero peso fecha_nacimiento fecha_registro peso_num peso_unidad fecha_nacimiento_invalida? no_tiene_mas_de_10_años?
0 Ana F 70 kg 1990-05-01 2021-01-01 70.0 kg False False
1 Luis masculino 154 lbs 1880-01-01 2022-01-01 154.0 lbs True False
2 Carlos M 65kg 2025-01-01 2020-01-01 65.0 kg False True
3 Marta femenina setenta 2000-06-15 2019-01-01 NaN setenta False False
4 Sofía F 60 kilogramos 2015-10-10 2017-01-01 60.0 kilogramos False True
5 Juan H 0 kg 2050-12-31 2010-01-01 0.0 kg True True
6 Andrea no binario 52 NaT 2020-05-05 52.0 NaN True False
7 Pedro M 80kg 1985-07-20 2021-12-31 80.0 kg False False

Tratamiento de inconsistencias#

Una vez que se detectan todas las inconsistencias, deben tratar, que significa se deben colocar reglas para hacer los reemplazos.

Por ejemplo, vamos a reemplazar por pd.NaT las fechas de registro donde la persona tenía menos de 10 años.

Hacemos el reemplazo por nulo en una nueva columna:

#defino la variable donde NO cumple la edad de mayor a 10
condicion_menor_10 = diferencia < diez_anios

# creamos nueva columna que es copia de fecha_registro
df_inconsistente['fecha_registro_OKA'] = df_inconsistente['fecha_registro']


# reemplazamos la fecha de registro por NaT si no cumple la condicion de 10 años
df_inconsistente.loc[condicion_menor_10, 'fecha_registro_OKA'] = pd.NaT

df_inconsistente
nombre genero peso fecha_nacimiento fecha_registro peso_num peso_unidad fecha_nacimiento_invalida? no_tiene_mas_de_10_años? fecha_registro_OKA
0 Ana F 70 kg 1990-05-01 2021-01-01 70.0 kg False False 2021-01-01
1 Luis masculino 154 lbs 1880-01-01 2022-01-01 154.0 lbs True False 2022-01-01
2 Carlos M 65kg 2025-01-01 2020-01-01 65.0 kg False True NaT
3 Marta femenina setenta 2000-06-15 2019-01-01 NaN setenta False False 2019-01-01
4 Sofía F 60 kilogramos 2015-10-10 2017-01-01 60.0 kilogramos False True NaT
5 Juan H 0 kg 2050-12-31 2010-01-01 0.0 kg True True NaT
6 Andrea no binario 52 NaT 2020-05-05 52.0 NaN True False 2020-05-05
7 Pedro M 80kg 1985-07-20 2021-12-31 80.0 kg False False 2021-12-31