QA: Informo dataset 🚦
INESDATA-MOV
Análisis de calidad¶
Este cuaderno analiza la calidad del dataset proveniente de la fuente de datos de Información de Movilidad de Madrid (Informo). La calidad del mismo se validará teniendo en cuenta los siguientes aspectos:
- Análisis de las variables
- Conversiones de tipos de datos
- Checks de calidad del dato
- Análisis Exploratorio de los datos (EDA)
La calidad del dato se refiere a la medida en que los datos son adecuados para su uso, por lo que es esencial para garantizar la confiabilidad y utilidad de los datos en diversas aplicaciones y contextos. Así, en este notebook se evaluarán también las cinco dimensiones de la calidad del dato:
- Unicidad: Ausencia de duplicados o registros repetidos en un conjunto de datos. Los datos son únicos cuando cada registro o entidad en el conjunto de datos es único y no hay duplicados presentes.
- Exactitud: Los datos exactos son libres de errores y representan con precisión la realidad que están destinados a describir. Esto implica que los datos deben ser correctos y confiables para su uso en análisis y toma de decisiones.
- Completitud: Los datos completos contienen toda la información necesaria para el análisis y no tienen valores faltantes o nulos que puedan afectar la interpretación o validez de los resultados.
- Consistencia: Los datos consistentes mantienen el mismo formato, estructura y significado en todas las instancias, lo que facilita su comparación y análisis sin ambigüedad.
- Validez: Medida en que los datos son precisos y representan con exactitud la realidad que están destinados a describir.
Nota
Este dataset ha sido creado ejecutando el comando create
del paquete de Python inesdata_mov_datasets
.
Para poder ejecutar este comando es necesario haber ejecutado antes el comando extract
, que realiza la extracción de datos de la API de Informo y los almacena en Minio. El comando create
se encargaría de descargar dichos datos y unirlos todos en un único dataset.
%matplotlib inline
import os
import re
from datetime import datetime
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from ydata_profiling import ProfileReport
sns.set_palette("deep")
import warnings
warnings.filterwarnings("ignore")
ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(os.getcwd())))
DATA_PATH = os.path.join(ROOT_PATH, "data", "processed")
INFORMO_DATA_PATH = os.path.join(DATA_PATH, "informo")
Cada fila de este dataset representa el estado del tráfico de Madrid, en una determinada zona (medida por un dispositivo concreto), para una fecha y hora concretos.
-
Vamos a analizar la calidad del dataset generado solamente para el día 13 de marzo, en el futuro dispondremos de más días.
df = pd.read_csv(
os.path.join(INFORMO_DATA_PATH, "2024", "03", "13", "informo_20240313.csv"),
parse_dates=["date", "datetime"],
)
df
idelem | descripcion | accesoAsociado | intensidad | ocupacion | carga | nivelServicio | intensidadSat | error | subarea | st_x | st_y | velocidad | datetime | date | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 6711 | NaN | NaN | 3840 | 30 | 105 | 1.0 | NaN | N | NaN | 442306,196584557 | 4481615,71687789 | 37.0 | 2024-03-13 07:55:08 | 2024-03-13 |
1 | 10112 | Arturo Soria - Pablo Vidal - Vicente Muzas | 4604002.0 | 740 | 4 | 29 | 1.0 | 2438.0 | N | 3203.0 | 443972,01821329 | 4478986,47739102 | NaN | 2024-03-13 07:55:08 | 2024-03-13 |
2 | 6038 | Torrelaguna - Arturo Baldasano-José Silva | 4627002.0 | 380 | 2 | 28 | 1.0 | 1390.0 | N | 3246.0 | 443981,955537857 | 4478451,45254494 | NaN | 2024-03-13 07:55:08 | 2024-03-13 |
3 | 6039 | Torrelaguna - Av. Ramón y Cajal-Acceso M30 | 4628002.0 | 840 | 6 | 24 | 0.0 | 3000.0 | N | 3215.0 | 443984,139107713 | 4478277,30226478 | NaN | 2024-03-13 07:55:08 | 2024-03-13 |
4 | 6040 | Torrelaguna - Sorzano-Acceso M30 | 4628001.0 | 520 | 5 | 31 | 1.0 | 2000.0 | N | 3215.0 | 444079,304201131 | 4478026,60397703 | NaN | 2024-03-13 07:55:08 | 2024-03-13 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
853109 | 10031 | Narcís Monturiol N-S - Sangenjo-Av. Ilustración | 4304002.0 | 20 | 0 | 2 | 0.0 | 775.0 | N | 314.0 | 440656,756311705 | 4481793,23972478 | NaN | 2024-03-13 22:50:11 | 2024-03-13 |
853110 | 10463 | Av. Ilustración O-E (Azobispo Morcillo - Nudo ... | NaN | 700 | 1 | 14 | 0.0 | 4420.0 | N | 314.0 | 440789,229442877 | 4481734,1912437 | NaN | 2024-03-13 22:50:11 | 2024-03-13 |
853111 | 3421 | Bravo Murillo E-O - Pl.Castilla-Conde Serrallo | 6201004.0 | 480 | 1 | 16 | 0.0 | 2900.0 | N | 304.0 | 441453,970035479 | 4479675,62306307 | NaN | 2024-03-13 22:50:11 | 2024-03-13 |
853112 | 3423 | Lateral Pº Castellana N-S - Pl.Castilla-Rosari... | 6003012.0 | 280 | 2 | 8 | 0.0 | 3200.0 | N | 301.0 | 441493,761559993 | 4479352,76508833 | NaN | 2024-03-13 22:50:11 | 2024-03-13 |
853113 | 10899 | Av. Entrevías - Mejorana-Sierra de Albarracín | 6805002.0 | 220 | 1 | 17 | 0.0 | 1800.0 | N | 3008.0 | 443058,894528119 | 4470747,45219509 | NaN | 2024-03-13 22:50:11 | 2024-03-13 |
853114 rows × 15 columns
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 853114 entries, 0 to 853113 Data columns (total 15 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 idelem 853114 non-null int64 1 descripcion 801383 non-null object 2 accesoAsociado 714568 non-null float64 3 intensidad 853114 non-null int64 4 ocupacion 853114 non-null int64 5 carga 853114 non-null int64 6 nivelServicio 852760 non-null float64 7 intensidadSat 801383 non-null float64 8 error 851431 non-null object 9 subarea 801383 non-null float64 10 st_x 853114 non-null object 11 st_y 853114 non-null object 12 velocidad 51731 non-null float64 13 datetime 853114 non-null datetime64[ns] 14 date 853114 non-null datetime64[ns] dtypes: datetime64[ns](2), float64(5), int64(4), object(4) memory usage: 97.6+ MB
df.columns
Index(['idelem', 'descripcion', 'accesoAsociado', 'intensidad', 'ocupacion', 'carga', 'nivelServicio', 'intensidadSat', 'error', 'subarea', 'st_x', 'st_y', 'velocidad', 'datetime', 'date'], dtype='object')
De acuerdo con la documentación obtenemos la siguiente información para la variable nivelServicio
:
- Fluido = 0
- Lento = 1
- Retenciones = 2
- Congestión = 3
- Sin datos = -1
Conversiones de tipos¶
num_cols = list(df.select_dtypes(include=np.number).columns)
cat_cols = list(df.select_dtypes(include=["object"]).columns)
date_cols = list(df.select_dtypes(exclude=[np.number, "object"]).columns)
print(f"Numeric cols: {num_cols}")
print(f"Categoric cols: {cat_cols}")
print(f"Date cols: {date_cols}")
Numeric cols: ['idelem', 'accesoAsociado', 'intensidad', 'ocupacion', 'carga', 'nivelServicio', 'intensidadSat', 'subarea', 'velocidad'] Categoric cols: ['descripcion', 'error', 'st_x', 'st_y'] Date cols: ['datetime', 'date']
# Convert nivelServicio, subarea and idelem to categoric
df["nivelServicio"] = df["nivelServicio"].astype("str")
df["idelem"] = df["idelem"].astype("str")
df["subarea"] = df["subarea"].astype("str")
QA checks ✅¶
Unicidad¶
Como hemos comentado anteriormente, cada fila de este dataset representa el estado del tráfico de Madrid, en una determinada zona (medida por un dispositivo concreto), para una fecha y hora concretos. Por tanto, las claves primarias de este dataset se conformarán teniendo en cuenta dichos atributos:
df['idelem'].nunique()
4766
df['datetime'].nunique()
179
# Create dataset primary key
df.insert(0, "PK", "")
df["PK"] = (
df["datetime"].astype(str)
+ "_I"
+ df["idelem"].astype(str)
+ "_S"
+ df['subarea'].astype(str)
)
df.head()
PK | idelem | descripcion | accesoAsociado | intensidad | ocupacion | carga | nivelServicio | intensidadSat | error | subarea | st_x | st_y | velocidad | datetime | date | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2024-03-13 07:55:08_I6711_Snan | 6711 | NaN | NaN | 3840 | 30 | 105 | 1.0 | NaN | N | nan | 442306,196584557 | 4481615,71687789 | 37.0 | 2024-03-13 07:55:08 | 2024-03-13 |
1 | 2024-03-13 07:55:08_I10112_S3203.0 | 10112 | Arturo Soria - Pablo Vidal - Vicente Muzas | 4604002.0 | 740 | 4 | 29 | 1.0 | 2438.0 | N | 3203.0 | 443972,01821329 | 4478986,47739102 | NaN | 2024-03-13 07:55:08 | 2024-03-13 |
2 | 2024-03-13 07:55:08_I6038_S3246.0 | 6038 | Torrelaguna - Arturo Baldasano-José Silva | 4627002.0 | 380 | 2 | 28 | 1.0 | 1390.0 | N | 3246.0 | 443981,955537857 | 4478451,45254494 | NaN | 2024-03-13 07:55:08 | 2024-03-13 |
3 | 2024-03-13 07:55:08_I6039_S3215.0 | 6039 | Torrelaguna - Av. Ramón y Cajal-Acceso M30 | 4628002.0 | 840 | 6 | 24 | 0.0 | 3000.0 | N | 3215.0 | 443984,139107713 | 4478277,30226478 | NaN | 2024-03-13 07:55:08 | 2024-03-13 |
4 | 2024-03-13 07:55:08_I6040_S3215.0 | 6040 | Torrelaguna - Sorzano-Acceso M30 | 4628001.0 | 520 | 5 | 31 | 1.0 | 2000.0 | N | 3215.0 | 444079,304201131 | 4478026,60397703 | NaN | 2024-03-13 07:55:08 | 2024-03-13 |
print("PK/Unique identifier check")
if df["PK"].nunique() == df.shape[0]:
print("✅ PK is unique")
# As we passed the PK quality check, we can set this PK as dataframe index
df.set_index("PK", inplace=True)
else:
print("❌ PK is not unique")
display(df[df["PK"].duplicated()][["idelem", "datetime"]])
PK/Unique identifier check
✅ PK is unique
Por tanto para esta PK
queda perfectamente idetificado el tráfico en un identificador concreto a una fecha concreta.
Exactitud y Completitud¶
En primer lugar comprobaremos que para cada hora tenemos el mismo número de datos:
A continuación comprobaremos los valores nulos para cada variable
for col in df.columns:
print(f"{col}: {df[col].isnull().sum()}/{df.shape[0]} valores nulos")
idelem: 0/853114 valores nulos descripcion: 51731/853114 valores nulos accesoAsociado: 138546/853114 valores nulos intensidad: 0/853114 valores nulos ocupacion: 0/853114 valores nulos carga: 0/853114 valores nulos nivelServicio: 0/853114 valores nulos intensidadSat: 51731/853114 valores nulos error: 1683/853114 valores nulos subarea: 0/853114 valores nulos st_x: 0/853114 valores nulos st_y: 0/853114 valores nulos velocidad: 801383/853114 valores nulos datetime: 0/853114 valores nulos date: 0/853114 valores nulos
La variable velocidad
posee un 93% de valores nulos, por lo cual no nos va a aportar apenas información. Por tanto, decidimos eliminarla.
df.drop(columns="velocidad", inplace=True)
Veamos si los valores nulos de descripcion
coinciden con los valores nulos de subarea
.
descr_null_pk = df[df['descripcion'].isnull()].index
subarea_null_pk = df[df['subarea']=='nan'].index
if (descr_null_pk == subarea_null_pk).all():
print('Los valores nulos coinciden en ambas columnas')
else:
print('Hay valores incompletos')
Los valores nulos coinciden en ambas columnas
Por tanto, eliminaremos estos valores
df=df[df['descripcion'].notnull()]
for col in df.columns:
print(f"{col}: {df[col].isnull().sum()}/{df.shape[0]} valores nulos")
idelem: 0/801383 valores nulos descripcion: 0/801383 valores nulos accesoAsociado: 86815/801383 valores nulos intensidad: 0/801383 valores nulos ocupacion: 0/801383 valores nulos carga: 0/801383 valores nulos nivelServicio: 0/801383 valores nulos intensidadSat: 0/801383 valores nulos error: 0/801383 valores nulos subarea: 0/801383 valores nulos st_x: 0/801383 valores nulos st_y: 0/801383 valores nulos datetime: 0/801383 valores nulos date: 0/801383 valores nulos
De esta forma no tenemos ningún valor nulo
PROFILING 📑¶
profile = ProfileReport(
df,
title="🚦 INFORMO QA",
dataset={
"description": "INFORMO - Estado del tráfico",
"url": "https://informo.madrid.es/informo/tmadrid/pm.xml",
},
variables={
"descriptions": {
"PK": "Identificador único (Primary Key) del dataset, compuesto por <datetime>_<idelem>",
"date": "Fecha de la petición a la API",
"datetime": "Fecha y hora de la petición a la API",
"idelem": "Identificador del punto de medida. Permite su posicionamiento sobre plano e identificación del vial y sentido de la circulación",
"descripcion": "Denominación del punto de medida",
"accesoAsociado": "Código de control relacionado con el control semafórico para la modificación de los tiempos",
"intensidad": "Intensidad de número de vehículos por hora. Un valor negativo implica la ausencia de datos",
"ocupacion": "Porcentaje de tiempo que está un detector de tráfico ocupado por un vehículo",
"carga": "Parámetro de carga del vial. Representa una estimación del grado",
"nivelServicio": "Parámetro calculado en función de la velocidad y la ocupación",
"intensidadSat": "Intensidad de saturación de la vía en veh/hora",
"error": "Código de control de la validez de los datos del punto de medida",
"subarea": "Identificador de la subárea de explotación de tráfico a la que pertenece el punto de medida",
"st_x": "Coordenada X UTM del centroide que representa al punto de medida en el fichero georreferenciado",
"st_y": "Coordenada Y UTM del centroide que representa al punto de medida en el fichero georreferenciado",
"velocidad": "Velocidad medida",
}
},
interactions=None,
explorative=True,
dark_mode=True,
)
profile.to_file(os.path.join(ROOT_PATH, "docs", "qa", "informo_report.html"))
# profile.to_notebook_iframe()
Summarize dataset: 0%| | 0/5 [00:00<?, ?it/s]
Generate report structure: 0%| | 0/1 [00:00<?, ?it/s]
Render HTML: 0%| | 0/1 [00:00<?, ?it/s]
Export report to file: 0%| | 0/1 [00:00<?, ?it/s]